A 128KB Export Pipeline
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.
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 (
convert,identify) - gifsicle
- ffmpeg
- gum (for the interactive UI)
How this works
- Checks dependencies: Verifies that
convert,gifsicle, andffmpegare installed. - Accepts input: Takes either a
.giffile or a folder containing apng/subdirectory with PNG frames. - 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.
- If input is a folder with PNGs:
- Combines PNG frames into a GIF.
- Runs the same post-processing steps as above.
- 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}"
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 […]
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 […]
Do you want to share the creations and sketches you’ve developed for the 128KB challenge on Instagram or other social […]