A 128KB Export Pipeline • tim rodenbröker creative coding

A 128KB Export Pipeline

Published by Tim on Monday September 8, 2025

Last modified on November 20th, 2025 at 22:14

With some help by CoPilot I have coded a Bash script that you can use to convert your animations for the 128KB challenge into a wide range of video formats. It’s not completely finished yet, but it does a good job for 72×128 pixel gifs.

What is 128KB?

My goal is to cover all edge cases for converting submissions to the 128KB challenge with a single script and to make the tiny animations ready for sharing on the internet in high resolution.

This post will be updated with new versions of the script. And of course, constructive feedback is welcome!

Dependencies

  • ImageMagick (convertidentify)
  • gifsicle
  • ffmpeg
  • gum (for the interactive UI)

How this works

  1. Checks dependencies: Verifies that convertgifsicle, and ffmpeg are installed.
  2. Accepts input: Takes either a .gif file or a folder containing a png/ subdirectory with PNG frames.
  3. If input is a GIF:
    • Enlarges the GIF by 400%.
    • Creates a looped version (3x longer, infinite loop).
    • Converts the looped GIF to MP4.
    • Creates a padded (passepartout) MP4 with a black background.
  4. If input is a folder with PNGs:
    • Combines PNG frames into a GIF.
    • Runs the same post-processing steps as above.
  5. Outputs:
    • Enlarged GIF
    • Looped GIF
    • MP4
    • Padded MP4
      (All named after the input file or folder.)
#!/bin/bash
###############################################################################
#                                                                             #
#                        128KB MEDIA ENHANCEMENT SCRIPT                       #
#                        128kb.timrodenbroeker.de                             #
#                                                                             #
#                        Version: 2025-09-09.2                                 #
###############################################################################

clear

###############################################################################
#                        CLEANUP OLD OUTPUT FILES                             #
###############################################################################
find . -maxdepth 1 -type f -name "*processed*" -exec rm -f {} \;

# Exit immediately if a command exits with a non-zero status
set -e

###############################################################################
#                        COLOR CODES FOR TERMINAL                             #
###############################################################################

GREEN="\033[42;30m"
AQUA="\033[38;2;0;255;255m" # #00ffff
CYAN="\033[36m"
YELLOW="\033[33m"
RESET="\033[0m"

###############################################################################
#                                FANCY BANNER                                 #
###############################################################################
gum style --foreground "#00ffff" --border double --align center --margin "1 2" --padding "1 4" "
 ██╗██████╗  █████╗ ██╗  ██╗██████╗ 
███║╚════██╗██╔══██╗██║ ██╔╝██╔══██╗
╚██║ █████╔╝╚█████╔╝█████╔╝ ██████╔╝
 ██║██╔═══╝ ██╔══██╗██╔═██╗ ██╔══██╗
 ██║███████╗╚█████╔╝██║  ██╗██████╔╝
 ╚═╝╚══════╝ ╚════╝ ╚═╝  ╚═╝╚═════╝ "
gum style --foreground "#00ffff" --align center "a 128KB media enhancement script"
gum style --foreground "#00ffff" --align center "128kb.timrodenbroeker.de"
echo

###############################################################################
#                        PASSEPARTOUT COLOR SELECTION UI                      #
###############################################################################

ALL_COLORS=("#000000" "#aaaaaa" "#f1f1f1" "#ffffff" "#ff0000" "#00ff00" "#0000ff" "#ffff00")
color_choice=$(gum choose --header "Choose a background color for the passepartout:" \
  "1) #000000 (black)" \
  "2) #aaaaaa (gray)" \
  "3) #f1f1f1 (light gray)" \
  "4) #ffffff (white)" \
  "5) #ff0000 (red)" \
  "6) #00ff00 (green)" \
  "7) #0000ff (blue)" \
  "8) #ffff00 (yellow)")
color_choice=${color_choice:0:1}

if [[ "$color_choice" =~ ^[1-8]$ ]]; then
  case "$color_choice" in
    1) PASSEPARTOUT_COLOR="#000000" ;;
    2) PASSEPARTOUT_COLOR="#aaaaaa" ;;
    3) PASSEPARTOUT_COLOR="#f1f1f1" ;;
    4) PASSEPARTOUT_COLOR="#ffffff" ;;
    5) PASSEPARTOUT_COLOR="#ff0000" ;;
    6) PASSEPARTOUT_COLOR="#00ff00" ;;
    7) PASSEPARTOUT_COLOR="#0000ff" ;;
    8) PASSEPARTOUT_COLOR="#ffff00" ;;
  esac
  echo -e "${CYAN}Passepartout color set to: $PASSEPARTOUT_COLOR${RESET}"
  echo
else
  echo -e "${CYAN}Rendering with all colors...${RESET}"
  for PASSEPARTOUT_COLOR in "${ALL_COLORS[@]}"; do
    echo -e "${CYAN}Passepartout color set to: $PASSEPARTOUT_COLOR${RESET}"
    ARGS=("$@")
    PASSEPARTOUT_COLOR="$PASSEPARTOUT_COLOR" bash "$0" --color-loop "${ARGS[@]}"
  done
  exit 0
fi

###############################################################################
#                        CHECK FOR REQUIRED TOOLS                             #
###############################################################################
for tool in convert gifsicle ffmpeg; do
  if ! command -v $tool &> /dev/null; then
    echo -e "${YELLOW}Error: $tool is not installed. Please install it first.${RESET}"
    exit 1
  fi
done

###############################################################################
#                                CONFIGURATION                                #
###############################################################################
OUTPUT_DIR="output"

###############################################################################
# Function: generate1080x1920
# Enlarges, loops, and generates 1080x1920 or 1080x1080 MP4
###############################################################################
generate1080x1920() {
  INPUT_GIF="$1"
  BASENAME="$2"
  local SCALE_FACTOR=400           # For scaling up GIFs
  local LOOP_COUNT=5               # How many times to loop the GIF
  local MP4_WIDTH=1080             # Standard MP4 width
  local MP4_HEIGHT=1920            # Standard MP4 height
  local MP4_SQUARE=1080            # Square MP4 size for 128x128 GIFs
  mkdir -p "$OUTPUT_DIR"
  OUTPUT_GIF="${OUTPUT_DIR}/${BASENAME}_processed.gif"
  LOOPED_GIF="${OUTPUT_DIR}/${BASENAME}_processed_looped.gif"
  OUTPUT_MP4_1080x1920="${OUTPUT_DIR}/${BASENAME}_processed_1080x1920.mp4"
  OUTPUT_MP4_1080x1080="${OUTPUT_DIR}/${BASENAME}_processed_1080x1080.mp4"
  ORIG_SIZE=$(identify "$INPUT_GIF" | head -n1 | awk '{print $3}')
  ORIG_WIDTH=$(echo $ORIG_SIZE | cut -dx -f1)
  ORIG_HEIGHT=$(echo $ORIG_SIZE | cut -dx -f2)
  echo -e "${CYAN}[*] [1080x1920] Original GIF size: ${ORIG_WIDTH} x ${ORIG_HEIGHT} ${RESET}"
  echo -e "${CYAN}[*] [1080x1920] Processing GIF (scaling up)...${RESET}"
  convert "$INPUT_GIF" -filter point -resize ${SCALE_FACTOR}% "$OUTPUT_GIF"
  echo "    Created processed GIF: $OUTPUT_GIF"
  echo -e "${CYAN}[*] [1080x1920] Looping GIF...${RESET}"
  gifsicle --loopcount=0 $(for i in $(seq 1 $LOOP_COUNT); do echo "$OUTPUT_GIF"; done) > "$LOOPED_GIF"
  echo "    Created looped GIF: $LOOPED_GIF"
  if [ "$ORIG_WIDTH" -eq 128 ] && [ "$ORIG_HEIGHT" -eq 128 ]; then
    echo -e "${CYAN}[*] [1080x1920] Converting to MP4 (1080x1080, no passepartout)...${RESET}"
    ffmpeg -y -i "$LOOPED_GIF" -vf "scale=${MP4_SQUARE}:${MP4_SQUARE}:flags=neighbor" -movflags faststart -pix_fmt yuv420p "$OUTPUT_MP4_1080x1080" &> /dev/null
    echo "    Created 1080x1080 MP4: $OUTPUT_MP4_1080x1080"
  else
    echo -e "${CYAN}[*] [1080x1920] Converting to MP4 (1080x1920, no passepartout)...${RESET}"
    ffmpeg -y -i "$LOOPED_GIF" -vf "scale=${MP4_WIDTH}:${MP4_HEIGHT}:flags=neighbor" -movflags faststart -pix_fmt yuv420p "$OUTPUT_MP4_1080x1920" &> /dev/null
    echo "    Created 1080x1920 MP4: $OUTPUT_MP4_1080x1920"
  fi
  rm "$OUTPUT_GIF" "$LOOPED_GIF"
}

###############################################################################
# Function: generatePadded
# Enlarges, loops, and generates padded MP4 (passepartout)
###############################################################################
generatePadded() {
  INPUT_GIF="$1"
  BASENAME="$2"
  local SCALE_FACTOR=400           # For scaling up GIFs
  local LOOP_COUNT=5               # How many times to loop the GIF
  local PAD_WIDTH=800              # Passepartout pad width
  local PAD_HEIGHT=1000            # Passepartout pad height
  mkdir -p "$OUTPUT_DIR"
  COLOR_SUFFIX="$(echo "$PASSEPARTOUT_COLOR" | tr -d '#')"
  OUTPUT_GIF="${OUTPUT_DIR}/${BASENAME}_processed.gif"
  LOOPED_GIF="${OUTPUT_DIR}/${BASENAME}_processed_looped.gif"
  OUTPUT_PADDED_MP4="${OUTPUT_DIR}/${BASENAME}_processed_padded_${COLOR_SUFFIX}.mp4"
  ORIG_SIZE=$(identify "$INPUT_GIF" | head -n1 | awk '{print $3}')
  ORIG_WIDTH=$(echo $ORIG_SIZE | cut -dx -f1)
  ORIG_HEIGHT=$(echo $ORIG_SIZE | cut -dx -f2)
  echo -e "${CYAN}[*] [Padded] Original GIF size: ${ORIG_WIDTH} x ${ORIG_HEIGHT} ${RESET}"
  echo -e "${CYAN}[*] [Padded] Processing GIF (scaling up)...${RESET}"
  convert "$INPUT_GIF" -filter point -resize ${SCALE_FACTOR}% "$OUTPUT_GIF"
  echo "    Created processed GIF: $OUTPUT_GIF"
  echo -e "${CYAN}[*] [Padded] Looping GIF...${RESET}"
  gifsicle --loopcount=0 $(for i in $(seq 1 $LOOP_COUNT); do echo "$OUTPUT_GIF"; done) > "$LOOPED_GIF"
  echo "    Created looped GIF: $LOOPED_GIF"
  if [ "$ORIG_WIDTH" -eq 128 ] && [ "$ORIG_HEIGHT" -eq 128 ]; then
    echo -e "${CYAN}[*] [Padded] Adding passepartout (padded MP4, 1080x1920 with 1080x1080 content)...${RESET}"
    ffmpeg -y -i "$LOOPED_GIF" \
      -vf "scale=1080:1080:flags=neighbor,pad=1080:1920:0:420:${PASSEPARTOUT_COLOR}" \
      -movflags faststart -pix_fmt yuv420p "$OUTPUT_PADDED_MP4" &> /dev/null
    echo "    Created padded MP4: $OUTPUT_PADDED_MP4"
  else
    echo -e "${CYAN}[*] [Padded] Adding passepartout (padded MP4)...${RESET}"
    ffmpeg -y -i "$LOOPED_GIF" \
      -vf "format=rgba,pad=${PAD_WIDTH}:${PAD_HEIGHT}:((${PAD_WIDTH}-iw)/2):((${PAD_HEIGHT}-ih)/2):${PASSEPARTOUT_COLOR}" \
      -movflags faststart -pix_fmt yuv420p "$OUTPUT_PADDED_MP4" &> /dev/null
    echo "    Created padded MP4: $OUTPUT_PADDED_MP4"
  fi
  rm "$OUTPUT_GIF" "$LOOPED_GIF"
}


###############################################################################
# Function: process_single_gif
# Processes an existing GIF file
###############################################################################
process_single_gif() {
  INPUT="$1"
  BASENAME="${INPUT%.*}"
  echo -e "${YELLOW}Processing existing GIF: $INPUT${RESET}"
  generate1080x1920 "$INPUT" "$BASENAME"
  generatePadded "$INPUT" "$BASENAME"
}


###############################################################################
# Function: process_all_gifs_in_folder
# Processes all GIF files in the current directory
###############################################################################
process_all_gifs_in_folder() {
  shopt -s nullglob
  files=(*.gif)
  if [ ${#files[@]} -eq 0 ]; then
    echo -e "${YELLOW}No GIF files found in current directory.${RESET}"
    exit 1
  fi
  for gif in "${files[@]}"; do
    process_single_gif "$gif"
  done
}

###############################################################################
#                                   MAIN                                      #
###############################################################################
if [ $# -eq 0 ]; then
  echo -e "${YELLOW}Usage:${RESET}"
  echo "  $0 input.gif"
  echo "  $0 input_folder/"
  echo "  $0 *.gif   # process all GIFs in current folder"
  exit 1
fi

if [ $# -gt 1 ]; then
  # Multiple arguments, likely *.gif expansion
  for gif in "$@"; do
    if [[ "$gif" == *.gif && -f "$gif" ]]; then
      process_single_gif "$gif"
    else
      echo -e "${YELLOW}Skipping non-GIF: $gif${RESET}"
    fi
  done
elif [ "$1" == "*.gif" ]; then
  process_all_gifs_in_folder
elif [ -f "$1" ] && [[ "$1" == *.gif ]]; then
  process_single_gif "$1"
elif [ -d "$1" ]; then
  echo -e "${YELLOW}Error: PNG-to-GIF conversion has been moved to 128kbGifMaker. Please use that script first.${RESET}"
  exit 1
else
  echo -e "${YELLOW}Error: Input must be a .gif file, '*.gif', or a directory containing a png/ subfolder.${RESET}"
  exit 1
fi

echo -e "${GREEN}All done!${RESET}"

Bi-Weekly Update

Active patrons in a paid tier get regular updates on new content, lessons and courses. For learners. On Fridays, 4pm CET.

Monthly Newsletter

Personal Reflections on Creative Coding, Design and life with Technology, every first Friday of the month, directly to your inbox.

Related

The Story of 128KB

One day in January 2024, I was lethargically scrolling through my Instagram feed on my laptop. And, as so often […]

Kris de Decker on Low Technology

In the two years I lived in Barcelona, one person in particular fascinated and inspired me. His name is Kris […]

Lo-Fi Collage Machine

Click here to login or connect!To view this content, you must be a member of Tim's Patreon at €10 or […]

Raquel Meyers – The Tool is the Message

Let’s begin here: A myriad of new technologies is accelerating our world at a breathtaking pace. I’m not interested in […]

Diogenes meets Demo Festival

Below is the written version of my talk at DEMO Festival in Amsterdam, January 2025. I’ve also recorded an audio […]

It’s Nice That POV: What happens when design ditches big tech?

I’ve had the great pleasure to chat with It’s Nice That editor Lucy Bourton about some of the aspects of […]

Back to the Future of the Internet

About a year ago, in February 2024, I was a invited speaker at an event at the Akasha Hub in […]

A bash script to convert 128KB gifs to mp4

Do you want to share the creations and sketches you’ve developed for the 128KB challenge on Instagram or other social […]