DEV Community

hao jia
hao jia

Posted on

A bash loop that logs bytes and seconds for each PNG optimization step

When you chain two PNG tools, the usual question is which one saves more. The question I care about more is whether I can see, per step, how many bytes and how many seconds each step cost, so a build review has something to argue with. A shell loop that writes one line per file is enough for that, and it reruns the same way on any machine that has bash, perl, pngquant and oxipng.

One loop records bytes and seconds per step

The script takes a folder of PNGs. For each file it runs pngquant with a quality range of 65-80 (speed 4 is its default), copies the result, runs oxipng on the copy, and prints the sizes and three timestamps. awk then groups the lines by the filename prefix, so icon- and photo- become two classes. The timestamps come from perl's Time::HiRes, so the format is the same wherever perl is installed.

now() { perl -MTime::HiRes=time -e 'printf "%.3f", time'; }
tmp=$(mktemp -d)
for f in "$1"/*.png; do
  c=$(basename "$f" | cut -d- -f1)
  t0=$(now); pngquant --quality=65-80 --force --output "$tmp/q.png" "$f"
  t1=$(now); cp "$tmp/q.png" "$tmp/o.png"; oxipng -o 4 --strip safe -q "$tmp/o.png"
  echo "$c $(wc -c <"$f") $(wc -c <"$tmp/q.png") $(wc -c <"$tmp/o.png") $t0 $t1 $(now)"
done | awk '{o[$1]+=$2; q[$1]+=$3; x[$1]+=$4; tq[$1]+=$6-$5; tx[$1]+=$7-$6}
  END {for (c in o) printf "%-13s %8d %8d %8d %5.2fs %5.2fs\n", c, o[c], q[c], x[c], tq[c], tx[c]}' | sort
Enter fullscreen mode Exit fullscreen mode

Drop the awk stage and the per-file lines show which single image moved a class total. --strip safe makes oxipng remove non-critical chunks but keep the color-profile ones (iCCP, sRGB, cICP) and pHYs, according to its own help text, which matters if your pipeline checks color profiles.

I ran it on 12 PNGs from Wikimedia Commons (CC licensed, noted at the end): three screenshots, three icons, three illustrations, three photos stored as PNG. Tools were pngquant 3.0.3 and oxipng 10.2.1 with -o 4 --strip safe, on an Apple M4. Sizes below are bytes, and the two time columns are pngquant and oxipng, summed over the three files of each class, one run.

icon            184137    37173    31496  0.19s  0.95s
illustration    316993    78209    73211  0.20s  1.43s
photo          2063192   694735   612968  0.42s  0.90s
screenshot     1890089   601760   565455  0.44s  1.67s
Enter fullscreen mode Exit fullscreen mode

The columns are class, original, after pngquant, after oxipng, pngquant time, oxipng time. I ran the loop three times and the byte counts never changed. Only the decimals of the times moved. The loop never touches the input files, so rerunning it is safe.

The second step still takes bytes off

Summed over all 12 files, 4,454,411 bytes became 1,411,877 after pngquant, which is 68.3% smaller. After oxipng the total was 1,283,130, which is 71.2% smaller. The second step removed another 128,747 bytes, or 2.9 percentage points of the original total.

Per class the extra points, measured against that class's original size, were 4.0 for photos, 3.1 for icons, 1.9 for screenshots and 1.6 for illustrations. I did not look into why photos gained the most, so I would not turn that order into a rule.

The cost is a few seconds. Across the whole table pngquant took about 1.25 seconds and oxipng about 4.95 seconds, so the second step is the slower of the two here. In a separate run, zopflipng added at most 0.5 points on top of pngquant and took 3.5 to 19.5 seconds per class, so I did not put it in the loop. That run used zopflipng's quick mode (-q), so it says nothing about its full mode, which I did not time here.

The order of the two steps changes the result

Swapping the two commands is a quick test. In an earlier run with the same tools, when oxipng ran first and pngquant second, the photo class ended at 694,253 bytes, against 694,735 for pngquant alone. That is 482 bytes of difference. My reading, which I have not verified, is that pngquant writes a fresh file and the earlier deflate work is thrown away. The numbers do say the same thing: on these files the lossless pass belongs after the palette step.

A tool's own palette output can go through the same loop

I also ran the same 12 files through the batch compression in ImgIng (https://imging.ai/) on desktop Chromium 149, with the output format set to PNG. Seven of the 12 came out larger than the original, and one was kept as the original, so I left those eight out. The other four were palette PNGs, and I fed them to the same oxipng command.

Those four shrank by another 28.6% to 45.5% relative to the tool's output, for example 43,216 bytes down to 23,571 for the blue star icon. Four files is a small sample and I did not compare image quality, so the only conclusion is that palette PNGs like these four are worth a second look in the loop.

What this loop does not prove

Twelve files, one machine, one run per step is the whole evidence. The photos are PNGs of photographs, not camera JPEGs. I did not compare image quality between the settings, so a smaller file here only means smaller. And oxipng at -o 4 is a middle setting, not the slowest one.

To reuse it, point the script at your own folder, keep the output as a CSV if you want a history, and diff the numbers when a tool version changes. If the byte counts differ between two runs on the same input, something in the chain is not deterministic, and that is worth knowing before the step goes into CI.

Sample images are from Wikimedia Commons under CC licenses (CC0, CC BY, CC BY-SA); author credits are on each file's Commons page.

Top comments (0)