OVERHEAD

Optimising pixel art

Design Meta

TLDR

POSIX shell script to generate the smallest possible PNG output from a PNG input.

#!/bin/sh

# Licensed under the Zero-Clause BSD terms: https://opensource.org/license/0bsd
# Requires pngquant and optipng

## Arguments
# 1: /path/to/input
# 2: /path/to/output

pquit() { printf "\033[1m\033[31m%b\033[0;39m" "$1"; exit 1; }

input="$1"
output="$2"

if [ "$#" -lt 2 ]; then
	pquit "Not enough arguments given!\n"; fi

if [ ! -f "$input" ]; then
	pquit "'$input' does not exist, or isn't a file!\n"; fi

if [ -e "$output" ]; then
	pquit "'$output' already exists!\n"; fi

# 'pngquant' will remove the 'A' out of APNG otherwise
if grep "acTL" "$input" > /dev/null 2>&1; then
		pquit "'$input' contains an Animation Control Chunk (acTL), refusing operation on APNG image.\n"
fi

# Accounts for the lack of output if 'pngquant' fails, and optimises the image with 'optipng' either way.
if ! pngquant --quality 100-100 --speed 1 --strip "$input" --skip-if-larger --output "$output"; then
		printf "\033[1m\033[31m'pngquant' failed! Likely due to suboptimal output, such as:\033[0;39m\n"
		printf "\033[1m\033[31m\tThe input image having more than 256 colours.\n\tThe output was larger than the input.\n\033[0;39m"
		printf "\033[1mTrying OptiPNG...\033[0;39m\n"
		if ! optipng -quiet -o7 -strip all "$input" -out "$output"; then
			pquit "'optipng' failed!\n"; fi
	else
		if ! optipng -quiet -o7 -strip all "$output"; then
			pquit "'optipng' failed!\n"; fi			
fi

Occasionally, pngquant will create an output larger than the input, but then, optipng brings the final size down below what optipng alone can do. This script doesn’t handle those cases for the sake of simplicity.

…Oh, you actually want to know what the pngquant and optipng commands do? Very well.

pngquant

pngquant is intended to be a lossy PNG optimiser, which typically doesn’t go well with pixel art due to the visual significance of every pixel, however, we can still take advantage of it for lossless optimisations when using the right options:

pngquant --quality 100-100 --speed 1 --strip --skip-if-larger /path/to/input --output /path/to/output

  • --quality/-Q: Sets the quality target for the output relative to the input. If pngquant can’t meet the target without changing the image, like if more than 256 colours are present, the input will be returned instead.
  • --speed/-s: Spend as much time optimising the image as needed.
  • --strip: Strip out unnecessary metadata from the PNG, saving even more bytes.
  • --skip-if-larger: Will cause the command to fail if the output image is larger than the input image. Negates the --quality option’s behaviour of returning the input image as the output, and will instead cause no output to be returned.
  • --output/-o: The output path for the image.
  • --ext: Instead of defining an explicit output path, you can instead set an extension, which places the output next to the input (e.g. pngquant --ext -quant.png /path/to/name.png -> /path/to/name-quant.png).

pngquant is very effective at shrinking most images, but, the output isn’t actually as small as it could be and it can’t losslessly handle images with more than 256 colours. For that, you’ll need to pass the output through the next tool as well…

OptiPNG

OptiPNG is a lossless PNG optimiser capable of brute-forcing the smallest possible output, with techniques including:

  • Indexed colours for images with less than 256 colours, and only if it saves bytes. It’s supposed to do this optimisation, but from what I can tell, it never tries this (hence the need for pngquant beforehand).
  • Minimising the number of bits used for the colour mode, such as encoding each colour channel with only 4 bits where losslessly feasible.
  • Optimising the input for maximum compressibility, as well as upping the compression level in general.
  • And many other enhancements which are detailed here.

Both OptiPNG and pngquant will remove the animation frames from APNGs.

Here’s a look at the command:

optipng -quiet -o7 -strip all /path/to/input -out /path/to/output

  • -quiet: Silences the output of optipng for use in scripts.
  • -o: Sets the optimisation level to use. 0 being minimal effort and 7 being the most. See man optipng for the exact options each optimisation level entails, as well as additional options for toggling individual optimisations.
  • -strip: Similar to the --strip option for pngquant.
  • -out: The output path for the image. If omitted, the original PNG will be overwritten (if the input is a PNG that is, as optipng supports other input formats too, see man optipng for more information).

Be warned, optipng will spend as much time as necessary to optimise whatever input image you give it, which for high optimisation levels can take a long time (up to ten minutes for very large images). For pixel art though, it usually finishes in sub-second time.

Or, if you want the smallest possible PNG without needing to think about all this, just use the script from the beginning of the post.