-
-
Save Winterhuman/21d7b148db40ff041f397b07a7aafb83 to your computer and use it in GitHub Desktop.
#!/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" |
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.
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.
Also, does running Sisyphus before/after FO-processed images improve anything?
Found these (I'll edit the previous samples if I find anything for them too):
FO: 388 bytes
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)
FO: 2987 bytes (FO > FO: Same size)
FO > Sisyphus: 2956 bytes (zop.png
. Probably --opt max
again)
FO > Sisyphus > FO: 2955 bytes (???)
Sometimes running FO multiple times on the same file (Shift+F5,
Just to make sure I understand, just the last example beats FO (by 1 byte)? 🤔