Skip to content

Instantly share code, notes, and snippets.

@Winterhuman
Last active March 4, 2025 22:20
Show Gist options
  • Save Winterhuman/21d7b148db40ff041f397b07a7aafb83 to your computer and use it in GitHub Desktop.
Save Winterhuman/21d7b148db40ff041f397b07a7aafb83 to your computer and use it in GitHub Desktop.
A script for finding the smallest lossless encoding of a PNG or GIF input image (that I know of).
#!/usr/bin/env -S unshare --pid --mount-proc --kill-child --map-root-user /bin/sh
# Licensed under the Zero-Clause BSD terms: https://opensource.org/license/0bsd
## Requires pngquant, oxipng, gifsicle, gif2apng, and optionally libwebp &
## perl-image-exiftool
## -C: Fail if redirects try to overwrite an existing file.
## -e: Fail if any command fails (with exceptions).
## -u: Fail if an unset variable tries to be expanded.
## -f: No glob expansion.
set -Ceuf
perr() { printf "\033[1m\033[31m%b\033[0;39m" "$1" >&2; }
pquit() { perr "$1"; exit 1; }
pstat() { printf "\033[1m\033[34m%b\033[0;39m\033[1m%b\033[0;39m" "$1" "$2"; }
## `kill` handles background jobs, but `exit` is required for normal processes
trap 'kill "$$"; exit 1' INT
help() {
cat << "HELP"
Usage: sisyphus [OPTION]... SRC DEST
Losslessly optimise PNGs & GIFs by all known means.
If DEST is omitted, DEST is set to '{SRC wo/ext}.new.ext'. Possible output formats
include: PNG, GIF, WEBP, and APNG.
Options:
-a, --all-oxi <bool> Optionally takes a boolean value. Controls whether to always,
or never, try all OxiPNG variations. If unset, heuristically
determine whether to try every OxiPNG variation.
-f, --force Overwrite existing destination files.
-n, --no-webp Never optimise with CWebP.
-s, --size <int> Takes an integer (bytes). Sets the minimum qualifying size.
If this size isn't met or surpassed, DEST is not created.
-h, --help Display this help message, and then exit.
Warning:
HDR SRC images will NOT be losslessly optimised!
HELP
exit 0
}
# Setup a private tmpfs for this script to use, which is what 'unshare' is for.
## 'nr_inodes' must be >= to 'max number of files + 1'
mount -t tmpfs -o nosuid,nodev,noexec,size=50%,nr_inodes=1349 sisyphus /tmp ||
pquit "Failed to overmount '/tmp/'!\n"
# Argument parsing
ALL_OXI=""
SIZE=""
FORCE=""
NO_WEBP=""
SKIP=""
ARG_COUNT="0"
while [ "$ARG_COUNT" -lt "$#" ]; do
if [ -z "$SKIP" ]; then
case "$1" in
-a|--all-oxi)
case "${2:-""}" in
0|no|false) ALL_OXI="0"; shift ;;
1|yes|true) ALL_OXI="1"; shift ;;
*) ALL_OXI=1 ;;
esac
;;
-s|--size)
case "${2:-""}" in
[0-9]*) SIZE="$2"; shift ;;
*) pquit "No positive integer was given for '-s|--size'!\n" ;;
esac
;;
-f|--force) FORCE="1" ;;
-n|--no-webp) NO_WEBP="1" ;;
-h|--help) help ;;
--) SKIP="1" ;;
-*) pquit "'$1' is not a known option!\n" ;;
*) set -- "$@" "$1"; ARG_COUNT="$(( ARG_COUNT + 1 ))" ;;
esac
else set -- "$@" "$1"; ARG_COUNT="$(( ARG_COUNT + 1 ))"
fi
shift
done
## Validate arguments
# `[ cond ] && cmd` will carry over non-zero exit codes, so always use `||`
[ "$#" -le 2 ] || pquit "Too many arguments given!\n"
# Don't try WebP if `libwebp` isn't installed
command -v cwebp >/dev/null || NO_WEBP=1
### SRC
src_real="${1:?$(pquit "No source image file given!\n")}"
[ -s "$src_real" ] || pquit "'$src_real' is not a non-empty file!\n"
## Assign the SRC a file descriptor
exec 3<"$src_real"
src="/proc/self/fd/3"
src_real="$(realpath -- "$src_real")"
## Size detection
find_size() {
if [ -f "$1" ]; then
wc -c <"$1" || pquit "Failed to find size of '$1'!\n"
else
printf "99999999999999"
fi
}
src_size="$(find_size "$src")"
if [ -n "$SIZE" ]; then
OLD_SIZE="$SIZE"
## Strip leading zeros, and avoid integer overflow
SIZE="$(printf "%s" "$SIZE" | sed "s/^0*//")"
## '0' would be trimmed to nothing by `sed`, so reset it
SIZE="${SIZE:-0}"
printf "%d" "$SIZE" >/dev/null 2>&1 ||
pquit "The value given for '-s|--size', '$OLD_SIZE', is not an integer!\n"
[ "$SIZE" -eq "$(printf "%s" "$SIZE" | cut -c -12)" ] ||
pquit "The value given for '-s|--size', '$OLD_SIZE', is over the integer limit!\n"
fi
## Mimetype detection
mime="$(
file -Lib -- "$src" ||
pquit "Couldn't determine mimetype of '$src_real'!\n"
)"
mime="${mime%;*}"
### DEST
dest_path="${2:-""}"
dest_path="${dest_path%.*}"
dest_path="${dest_path:-${src_real%.*}.new}"
dest_path="$(realpath -- "$dest_path")"
dest_exists() {
[ -z "$FORCE" ] || return 0
## Allow globbing & unset-variables for just this for-loop
set +fu
for similar in "$1"*; do
if [ -e "$similar" ] && [ "${similar%.*}" = "$1" ]; then
perr "'$similar' shares a filename with '$1'!\n"
return 1
fi
done
set -fu
}
dest_exists "$dest_path" || exit 1
pstat "Input:\t\t\t" "$src_real \033[37m($mime)\033[0;39m\n"
pstat "Output template:\t" "${dest_path}.???\n"
[ -z "$SIZE" ] || pstat "Size target:\t\t" "${SIZE} bytes\n"
## Misc variables
delimiter="> "
# Optimisation
keep_if_smaller() {
## Truncate '$file' to retain its perceived size, without allocating
## any real blocks to it, if it can't meet the requirement
file="$1"
req1="$2"
req2="$3"
if [ ! -f "$file" ]; then
perr "'$file' was truncated, but, it didn't originally exist!\n"
return 1
fi
if [ "$req1" -lt "$req2" ]; then
file_size="$(find_size "$file")"
truncate --size=0 "$file"
truncate --size="$file_size" "$file"
fi
}
## In case of PNG
run_oxi() {
## The '$oxi_safe' variable controls APNG mode
oxi_src="$oxi_perm_src"
oxi_ext="$1"
oxi_dest="${2}.${3:+"a"}png"
oxi_safe="${3:-""}"
## Pass arguments 4 and beyond as-is to `oxipng` to avoid word splitting
shift 3
if ! oxipng "$@" --opt max --strip "${oxi_safe:-"all"}" --alpha \
--out "$oxi_dest" -- "$oxi_src" >/dev/null 2>&1; then
perr "Passing '$oxi_src' through 'oxipng' ($oxi_ext) failed!\n"
return 1
fi
oxi_dest_size="$(find_size "$oxi_dest")"
keep_if_smaller "$oxi_dest" \
"$smallest_known" "$oxi_dest_size"
}
oxi_permutations() {
# Try every permutation of `--[nb,nc,ng,np]`
oxi_perm_src="$1"
oxi_perm_ext="$2"
oxi_perm_dest="$3"
oxi_perm_safe="${4:-""}"
shift 4
## Skip `--zc=12` without extra options (it already exists for the
## heuristic that was tested earlier)
if [ "$*" != "--zc=12" ] ||
[ "$oxi_perm_src" = "${src_quant:-""}" ]; then
run_oxi "$oxi_perm_ext" \
"${oxi_perm_dest}${oxi_perm_ext}" \
"$oxi_perm_safe" \
"$@"
fi
run_oxi "$oxi_perm_ext + nb" \
"${oxi_perm_dest}${oxi_perm_ext} + nb" \
"$oxi_perm_safe" \
"$@" --nb
run_oxi "$oxi_perm_ext + nb + nc" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + nc" \
"$oxi_perm_safe" \
"$@" --nb --nc
run_oxi "$oxi_perm_ext + nb + ng" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + ng" \
"$oxi_perm_safe" \
"$@" --nb --ng
run_oxi "$oxi_perm_ext + nb + np" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + np" \
"$oxi_perm_safe" \
"$@" --nb --np
run_oxi "$oxi_perm_ext + nb + nc + ng" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + nc + ng" \
"$oxi_perm_safe" \
"$@" --nb --nc --ng
run_oxi "$oxi_perm_ext + nb + nc + np" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + nc + np" \
"$oxi_perm_safe" \
"$@" --nb --nc --np
run_oxi "$oxi_perm_ext + nb + ng + np" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + ng + np" \
"$oxi_perm_safe" \
"$@" --nb --ng --np
run_oxi "$oxi_perm_ext + nb + nc + ng + np" \
"${oxi_perm_dest}${oxi_perm_ext} + nb + nc + ng + np" \
"$oxi_perm_safe" \
"$@" --nb --nc --ng --np
run_oxi "$oxi_perm_ext + nc" \
"${oxi_perm_dest}${oxi_perm_ext} + nc" \
"$oxi_perm_safe" \
"$@" --nc
run_oxi "$oxi_perm_ext + nc + ng" \
"${oxi_perm_dest}${oxi_perm_ext} + nc + ng" \
"$oxi_perm_safe" \
"$@" --nc --ng
run_oxi "$oxi_perm_ext + nc + np" \
"${oxi_perm_dest}${oxi_perm_ext} + nc + np" \
"$oxi_perm_safe" \
"$@" --nc --np
run_oxi "$oxi_perm_ext + nc + ng + np" \
"${oxi_perm_dest}${oxi_perm_ext} + nc + ng + np" \
"$oxi_perm_safe" \
"$@" --nc --ng --np
run_oxi "$oxi_perm_ext + ng" \
"${oxi_perm_dest}${oxi_perm_ext} + ng" \
"$oxi_perm_safe" \
"$@" --ng
run_oxi "$oxi_perm_ext + ng + np" \
"${oxi_perm_dest}${oxi_perm_ext} + ng + np" \
"$oxi_perm_safe" \
"$@" --ng --np
run_oxi "$oxi_perm_ext + np" \
"${oxi_perm_dest}${oxi_perm_ext} + np" \
"$oxi_perm_safe" \
"$@" --np
}
oxi_all() {
tmp_dest_path="/tmp/${2:-""}"
tmp_safe="${4:-""}"
## Keep `--zopfli` separate from the other permutations due to its cost
if [ -z "${3:-""}" ]; then
zc=0
while [ "$zc" -le 12 ]; do
oxi_permutations \
"$1" \
"zc${zc}" \
"$tmp_dest_path" \
"$tmp_safe" \
--zc="$zc" &
zc="$(( zc + 1 ))"
done
wait
else
## Use 'zop' as the extension so 'zcN' is ordered before
oxi_permutations "$1" "zop" "$tmp_dest_path" "$tmp_safe" \
--zopfli --zi=255
fi
}
src_is_png() {
# Create CWebP and baseline OxiPNG outputs, for use as a heuristic
cwebp_dest="/tmp/cwebp.webp"
oxi_baseline_dest="/tmp/zc12.png"
if [ "$NO_WEBP" != 1 ]; then
## Passing `-q` or `-m` disables lossless mode
cwebp -quiet -mt -z 9 -alpha_filter best -o "$cwebp_dest" \
-- "$src" ||
perr "Passing '$src' through 'cwebp' failed!\n"
fi &
oxipng --opt max --strip all --alpha --out "$oxi_baseline_dest" \
-- "$src" >/dev/null 2>&1 ||
perr "Passing '$src' through 'oxipng' (zc12) failed!\n" &
wait
# Skip trying the OxiPNG variants if explicitly requested
[ "$ALL_OXI" != "0" ] || return 0
oxi_baseline_size="$(find_size "$oxi_baseline_dest")"
cwebp_size="$(find_size "$cwebp_dest")"
fraction="$(( ( oxi_baseline_size * 9 ) / 10 ))"
smallest_heuristic="$((
oxi_baseline_size < cwebp_size
? oxi_baseline_size
: cwebp_size
))"
## '$smallest_known' is read by `run_oxi()`
if [ -n "$SIZE" ]; then
smallest_known="$((
smallest_heuristic < SIZE
? smallest_heuristic
: SIZE
))"
else
smallest_known="$((
smallest_heuristic < src_size
? smallest_heuristic
: src_size
))"
fi
# Try the OxiPNG variants if:
# 1. Explicitly requested.
# 2. Else, if the baseline OxiPNG's size is less than 1KiB.
# 3. Else, if the input image is still the smallest known encoding.
# 4. Else, if the CWebP size isn't smaller than 90% of the baseline.
if [ "$ALL_OXI" = 1 ] ||
[ "$oxi_baseline_size" -le 1024 ] ||
[ "$src_size" -le "$smallest_heuristic" ] ||
[ "$fraction" -le "$cwebp_size" ]; then
oxi_all "$src" &
src_quant="/tmp/quant.png"
if pngquant --quality 100-100 --speed 1 --strip \
--output "$src_quant" -- "$src"; then
oxi_all "$src_quant" "quant $delimiter" &
fi
wait
## Execute the `--zopfli` permutations last & sequentially
pstat "" "\tBeginning Zopfli trials, \033[33mthis may take a while...\n"
oxi_all "$src" "" "zop"
[ ! -f "$src_quant" ] ||
oxi_all "$src_quant" "quant $delimiter" "zop"
fi
}
## In case of GIF
run_apng() {
apng_src="$1"
apng_ext="$2"
apng_dest="$3"
## Pass arguments 4 and beyond as-is to `gif2apng` to avoid word splitting
shift 3
apng_dest_noext="${apng_dest} + ${apng_ext}"
apng_dest_ext="${apng_dest_noext}.apng"
gif2apng "$@" -i20 -- "$apng_src" "$apng_dest_ext" \
>/dev/null 2>&1 ||
perr "Passing '$apng_src' through 'gif2apng' ($apng_ext) failed!\n"
## `gif2apng` doesn't remove all unnecessary metadata, so do that here
exiftool -overwrite_original_in_place -all= "$apng_dest_ext" \
>/dev/null 2>&1 ||
perr "Failed to remove EXIF metadata from '$apng_dest_ext'!\n"
## Convert the relative paths back to absolute paths, for neatness
apng_oxi_src="$(realpath -- "$apng_dest_ext")"
apng_oxi_src_size="$(find_size "$apng_oxi_src")"
apng_oxi_dest_noext="$(realpath -- "$apng_dest_noext")"
smallest_heuristic="$apng_oxi_src_size"
## '$smallest_known' is read by `run_oxi()`
if [ -n "$SIZE" ]; then
smallest_known="$((
smallest_heuristic < SIZE
? smallest_heuristic
: SIZE
))"
else
smallest_known="$((
smallest_heuristic < src_size
? smallest_heuristic
: src_size
))"
fi
# Skip trying the OxiPNG variants if explicitly requested, and create a
# basic OxiPNG output instead
if [ "$ALL_OXI" = 0 ]; then
baseline_apng="${apng_oxi_dest_noext} ${delimiter}zc12.apng"
oxipng --opt max --strip safe --alpha --out "$baseline_apng" \
-- "$apng_oxi_src" >/dev/null 2>&1 ||
perr "Passing '$apng_oxi_src' through 'oxipng' (zc12) failed!\n"
baseline_apng_size="$(find_size "$baseline_apng")"
keep_if_smaller "$baseline_apng" \
"$smallest_known" "$baseline_apng_size"
return 0
fi
# Try the OxiPNG variants if:
# 1. Explicitly requested.
# 2. Else, if the APNG SRC image is less than 1KiB.
# 3. Else, if the input image is still the smallest known encoding.
## A heuristic for when to try the OxiPNG variants, based on a size
## comparison between a baseline OxiPNG output and the WebP output size,
## hasn't been determined for animated images yet
if [ "$ALL_OXI" = 1 ] ||
[ "$apng_oxi_src_size" -le 1024 ]
[ "$src_size" -le "$smallest_heuristic" ]; then
## Passing "safe" here sets `--strip safe` for `oxipng`,
## which preserves APNG animation frames in the metadata
oxi_all \
"$apng_oxi_src" \
"${apng_oxi_dest_noext##*/} ${delimiter}" \
"" "safe"
## Execute the `--zopfli` permutations last
pstat "" "\tBeginning Zopfli trials for '${apng_oxi_dest_noext##*/}'...\n"
oxi_all \
"$apng_oxi_src" \
"${apng_oxi_dest_noext##*/} ${delimiter}" \
"zop" "safe"
else
keep_if_smaller "$apng_oxi_src" \
"$smallest_known" "$apng_oxi_src_size"
fi
}
toapng() {
# GIF2APNG can't handle absolute paths, so make them relative
# Source: https://sourceforge.net/p/gif2apng/discussion/1022150/thread/8ec5e7e288
apng_src="$(realpath --relative-to="$PWD" -- "$src")"
tmp_dest="$(realpath --relative-to="$PWD" -- "/tmp/gif2apng")"
## Try every permutation of `-[z[0-2],kc,kp]`, and their oxi-variants
run_apng "$apng_src" "z0" "$tmp_dest" -z0
run_apng "$apng_src" "z1" "$tmp_dest" -z1
run_apng "$apng_src" "z2" "$tmp_dest" -z2
run_apng "$apng_src" "z0 + kp" "$tmp_dest" -z0 -kp
run_apng "$apng_src" "z1 + kp" "$tmp_dest" -z1 -kp
run_apng "$apng_src" "z2 + kp" "$tmp_dest" -z2 -kp
}
togif() {
gifsicle_dest="/tmp/gifsicle + o${1}.gif"
gifsicle --optimize="$1" --optimize=keep-empty --threads="$(nproc)" \
-o "$gifsicle_dest" -- "$src" ||
pquit "Passing '$src' through 'gifsicle --optimize=\"${1}\"' failed!\n"
gifsicle_dest_size="$(find_size "$gifsicle_dest")"
keep_if_smaller "$gifsicle_dest" "$smallest_known" "$gifsicle_dest_size"
}
towebp() {
[ "$NO_WEBP" != 1 ] || return 0
webp_dest="/tmp/cwebp.webp"
gif2webp -quiet -mt -min_size -m 6 -q 100 -metadata none \
-o "$webp_dest" -- "$src" 2>/dev/null ||
pquit "Passing '$src' through 'gif2webp' failed!\n"
webp_dest_size="$(find_size "$webp_dest")"
keep_if_smaller "$webp_dest" "$smallest_known" "$webp_dest_size"
}
src_is_gif() {
if [ -n "$SIZE" ]; then
smallest_known="$SIZE"
else
smallest_known="$src_size"
fi
togif 3 &
togif 2 &
togif 1 &
towebp &
wait
pstat "" "\tBeginning 'gif2apng' trials, \033[33mthis may take a while...\n"
toapng
}
# File selection
case "$mime" in
"image/png") src_is_png ;;
"image/gif") src_is_gif ;;
*) pquit "Mimetype of '$src_real' is neither PNG nor GIF!\n" ;;
esac
## Allow globbing & unset-variables for just this for-loop
set +fu
for tmp_file in /tmp/*; do
[ ! -f "$tmp_file" ] ||
sizepathlist="$(find_size "$tmp_file")\t${tmp_file}\n$sizepathlist"
done
set -fu
## 1. Prefix lines with their length followed by a space (e.g. '20 8 bytes ...').
## 2. Sort the lines by their length values (aka. `sort -n`).
## 3. Use space as the delimiter (aka. '-d " "'), and remove everything from
## before the second field (e.g. '8 bytes ...').
## 4. Sort numerically, but for cases where the byte count is the same, preserve
## the ordering from the previous steps (aka. `sort -sn`).
orderedlist="$(
printf "%b" "$sizepathlist" |
awk '{ print length, $0 }' |
sort -n |
cut -d " " -f 2- |
sort -sn
)"
## The second sed pattern uses '|' delimiters since '/' is used in the path
fancy_orderedlist="$(
printf "%b" "$orderedlist" |
sed -e "s/\t/ bytes\t/g;s|/tmp/||g;s/^/\t/g"
)"
pstat "Size order:\n" "$fancy_orderedlist\n\n"
# Output selection
smallest="$(
printf "%b" "$orderedlist" |
head -n1 |
cut -d " " -f 2-
)"
smallest_size="$(find_size "$smallest")"
if [ -n "$SIZE" ]; then
[ "$smallest_size" -le "$SIZE" ] ||
pquit "The smallest output was greater than the minimum size target.\n"
else
[ "$smallest_size" -lt "$src_size" ] ||
pquit "The smallest output was greater or equal in size to the input.\n"
fi
[ "$(stat --format "%b" -- "$smallest")" -gt 0 ] ||
pquit "The smallest output was truncated at some point!\n"
final_dest="${dest_path}.${smallest##*.}"
if [ -e "$final_dest" ] && [ -z "$FORCE" ]; then
pquit "'$final_dest' already exists!\n"; fi
mv -- "$smallest" "$final_dest" ||
pquit "Failed to move '$smallest' to '$final_dest'!\n"
delimiter="${delimiter:-""}"
method="${smallest##*"$delimiter"}"
method="${smallest##*"/"}"
method="${method%.*}"
pstat "Final output:\t" "$final_dest \033[37m($method)\033[0;39m\n"
pstat "Size diff:\t" \
"$src_size bytes \033[37m->\033[0;39m \033[1m$smallest_size bytes\n"
@TPS
Copy link

TPS commented Feb 26, 2025

@Winterhuman How does this compare to, e.g., FileOptimizer?

@Winterhuman
Copy link
Author

I'm not able to test it since it's an EXE, but, since I think it's still using OptiPNG under the hood, I would guess my script is better for now (OptiPNG always faired worse than OxiPNG from my testing). The script currently tries every valid, unordered permutation of:

[pngquant --quality 100-100 --speed 1 --strip |,] oxipng [--zopfli --zi=255,--zc={0..12}] --n[bcgp] --opt max --strip all --alpha

If, when FileOptimizer switches to OxiPNG, it additonally executes oxipng with some other options beyond these (which I'm not aware of currently), or it implements a palette randomiser itself (which is what pngquant functions as here), it could beat this script.

@TPS
Copy link

TPS commented Feb 26, 2025

Per the current source @ https://sourceforge.net/p/nikkhokkho/code/HEAD/tree/trunk/FileOptimizer/Source/cppMain.cpp#l2087, FO currently uses "apngopt, pngquant, PngOptimizer, TruePNG, pngout, oxipng, pngwolf, Leanify, ect, pingo, advpng, deflopt, defluff, deflopt" (not a typo, runs deflopt 2× based on testing) in the PNG toolchain, w/ configurable options via GUI & INI.

FO reportedly works fine under WINE & emulators. Perhaps you could compare that way?

@Winterhuman
Copy link
Author

Winterhuman commented Feb 26, 2025

Managed to use Bottles to run FO, and... inconclusive? Here's what I'm noticing:

  1. Sisyphus can beat FO, and vice-versa, so neither is guaranteed to win unfortunately.
  2. From that cppMain.cpp file you linked, I'm noticing that oxipng -Z is always being used, however, Zopfli can actually lose to non-Zopfli compression (and also, there's no option permutation like I'm doing here, which can also help).
  3. pingo is able to beat cwebp sometimes, which is unfortunate since pingo is also a Windows-only executable, so this script is definitely missing out in those cases.

I haven't tested GIFs or APNGs yet, and I also need to update this script with my new APNG brute-forcing pipeline, but from the above results, I'm guessing that's going to be a mixed bag as well.

EDIT: I've updated Sisyphus with the new APNG stuff, along with some other changes.

@TPS
Copy link

TPS commented Feb 26, 2025

Thanks for the detailed comparison! If you've test files you'd be willing to make available that Sisyphus beats FO, that'd be much appreciated. 🙇🏾‍♂️

Pinging @javiergutierrezchamorro (the FO dev) to take note of above feedback.

@Winterhuman
Copy link
Author

Winterhuman commented Feb 26, 2025

Here's some samples:


decipher-sisyphus

Sisyphus: 1824 bytes (zop.png. I threw these first two samples in to show that FO can beat Sisyphus)

decipher-fo

FO: 1823 bytes


dd-sisyphus

Sisyphus: 479 bytes (quant > zop + nb.png)

dd-fo

FO: 474 bytes


thisline-sisyphus

[FO >] Sisyphus: 413 bytes (zop.png. It's probably --opt max that makes the difference with these zop.png results)

thisline-fo

FO: 414 bytes


disc-sisyphus

Sisyphus: 534 bytes (zop.png)

disc-fo

FO: 537 bytes

disc-fo-sisyphus

FO > Sisyphus: 536 bytes (zop.png)


seat-sisyphus

[FO >] Sisyphus: 246 bytes (quant > zop.png. Regular zop.png matched FO, so pngquant did something here)

seat-fo

FO: 247 bytes


space-sisyphus

Sisyphus: 3396 bytes (zop.png)

space-fo

FO: 3398 bytes

@Winterhuman
Copy link
Author

Winterhuman commented Feb 26, 2025

I forgot to set the optimisation level to 9 in FO... one moment.

EDIT: Done

@TPS
Copy link

TPS commented Feb 26, 2025

Just to make sure I understand, just the last example beats FO (by 1 byte)? 🤔

@Winterhuman
Copy link
Author

Winterhuman commented Feb 26, 2025

Yeah, I had some other images going before I realised I forgot the level 9 setting; will edit with some of the other examples when I have time.

EDIT: Okay, so bad news. Pretty much every time Sisyphus yields a smaller result, it's because it uses --zopfli and --opt max, which FO doesn't. Except for that quant > zop.png result, where regular zop.png matched FO, so the extra palette randomisation that pngquant introduces can help on rare occasions.

But yeah, ever since I set the level in FO's settings correctly, I've been struggling to beat it; it's usually a tie or loss save for these samples. Welp.

@TPS
Copy link

TPS commented Feb 27, 2025

1 tip: I think you're pretty used to long processing times b/c of your script, but, if you've the spare processing power, multiple instances of FO work quite well on split file lists to speed up overall processing.

@TPS
Copy link

TPS commented Feb 27, 2025

Also, does running Sisyphus before/after FO-processed images improve anything?

@Winterhuman
Copy link
Author

Winterhuman commented Feb 27, 2025

Found these (I'll edit the previous samples if I find anything for them too):

general-fo

FO: 388 bytes

general-fo-sisyphus

FO > Sisyphus: 387 bytes (zop + nb.png. Regular zop.png matches FO, and every permutation of zop + *.png beats it by a single byte, so it's not just --opt max here)


balloon-fo

FO: 2987 bytes (FO > FO: Same size)

balloon-fo-sisyphus

FO > Sisyphus: 2956 bytes (zop.png. Probably --opt max again)

balloon-fo-sisyphus-fo

FO > Sisyphus > FO: 2955 bytes (???)

@TPS
Copy link

TPS commented Feb 27, 2025

Sometimes running FO multiple times on the same file (Shift+F5,▶️) back-to-back gives better results, too, so that's kind-of expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment