When QOI meets XZ

Table of Contents

QOI, the Quite OK Image format, has been gaining in popularity. Chris Wellons offers a great analysis.

QOI's key advantages is its simplicity. Being a byte-oriented format without entropy encoding, it can be further compressed with generic data compression programs like LZ4, XZ, and zstd. PNG, on the other hand, uses DEFLATE compression internally and is typically resistant to further compression. By applying a stronger compression algorithm on QOI output, you can often achieve a smaller file size compared to PNG.

XZ

Lasse Collin has shared some effective options for compressing uncompressed BMP/TIFF files. I tested them on the QOI benchmark images.

When the color table (palette) is used, a delta filter would increase the compressed size and should be disabled.

% cat ~/tmp/b.sh
#!/bin/zsh -ue
f() {
  pngcrush -fix -m 1 -l 0 $1 ${1/.png/.uncompressed.png}
  [[ -f ${1/.png/.uncompressed.png} ]] || cp $1 ${1/.png/.uncompressed.png}
  /tmp/p/qoi/qoiconv $1 ${1/.png/.qoi}
  convert $1 ${1/.png/.bmp}
  convert $1 -compress none ${1/.png/.tiff}
  xz --lzma2=pb=0 -fk ${1/.png/.qoi}
  if [[ $(file $1) =~ RGBA ]]; then
    pnm=${1/.png/.pam}
    convert $1 $pnm
    xz --delta=dist=4 --lzma2=lc=4 -fk $pnm
    xz --delta=dist=4 --lzma2=lc=4 -fk ${1/.png/.bmp}
    xz --delta=dist=4 --lzma2=lc=4 -fk ${1/.png/.tiff}
  elif [[ $(file $1) =~ 'colormap' ]]; then
    pnm=${1/.png/.ppm}
    convert $1 $pnm
    xz --lzma2=pb=0 -fk $pnm
    xz --lzma2=pb=0 -fk ${1/.png/.bmp}
    xz --lzma2=pb=0 -fk ${1/.png/.tiff}
  else
    pnm=${1/.png/.ppm}
    convert $1 $pnm
    xz --delta=dist=3 --lzma2=pb=0 -fk $pnm
    xz --delta=dist=3 --lzma2=pb=0 -fk ${1/.png/.bmp}
    xz --delta=dist=3 --lzma2=pb=0 -fk ${1/.png/.tiff}
  fi
  stat -c '%n %s' $1 ${1/.png/.qoi.xz} $pnm.xz ${1/.png/.bmp.xz} ${1/.png/.tiff.xz}
}

f $1
cd /tmp/dc-img/images/
ls -1 **/*.png | rush ~/tmp/b.sh '"{}"'
ls -1 **/*.uncompressed.png | rush 'xz -fk --lzma2=pb=0 "{}"'

ruby -e 'puts "directory\t.png\t.png.xz\t.qoi.xz\t.bmp.xz\t.tiff.xz\t.p[ap]m.xz"; Dir.glob("*").each{|dir| next unless File.directory? dir;
  png=pngxz=qoi=bmp=pnm=tiff=0; Dir.glob("#{dir}/*.qoi.xz").each{|f|
  png+=File.size(f.sub(/\.qoi.xz/,".png"));
  pngxz+=File.size(f.sub(/\.qoi.xz/,".uncompressed.png.xz"));
  qoi+=File.size(f); bmp+=File.size(f.sub(/\.qoi/,".bmp")); ppm=f.sub(/\.qoi/,".ppm");  pnm+=File.exists?(ppm) ? File.size(ppm) : File.size(f.sub(/\.qoi/,".pam")); tiff+=File.size(f.sub(/\.qoi/,".tiff"));
};
  puts "#{dir}\t#{png}\t#{pngxz}\t#{qoi}\t#{bmp}\t#{tiff}\t#{pnm}"
}'

While DEFLATE-compressed PNG files can hardly be further compressed, we can convert these PNG files to uncompressed ones then apply xz. The .png.xz results below do not apply a filter, and the files are generally larger than .qoi.xz.

directory.png.png.xz.qoi.xz.bmp.xz.tiff.xz.p[ap]m.xz
icon_5121115442478616527476640804203280644768039192
icon_64828119750836708480730472757760735296
photo_kodak153943051446450412902852136124401361614013610844
photo_tecnick237834256254803292213268188210591724210508596210468412
photo_wikipedia8833975110044999686679696863801248627448086241296
pngimg229608249134233476193382668186389368186654256186420564
screenshot_game266238855237976536218915316216626004216847500216765748
screenshot_web402726782469036021321460214584962153236021533432
textures_photo378546343639334028967008300549683006423630059784
textures_pk435234934086803654117600419905964063291646695172
textures_pk01189467691573434814950836148356481485342014839312
textures_pk021029629358603700082279000793741127934876879336276
textures_plants517653295304404443681548449132604502199645048652

While compressing QOI with XZ (.qoi.xz) can achieve good results, using a delta filter directly on the uncompressed BMP format (.bmp.xz) can sometimes lead to even smaller files. (TIFF and PPM/PAM, when compressed, can achieve similar file sizes to .bmp.xz.) This suggests that QOI is probably not better than a plain delta filter.

It's important to note that uncompressed BMP/TIFF files are huge. This can be problematic if the decompressed data can't be streamed directly into the program's internal structures. In such cases, a large temporary buffer would be needed, wasting memory.

Drop LZ match finders

QOI_OP_INDEX essentially does length-1 LZ77 using a conceptual window that contains 64 unique pixels. When further compressed, another match finder seems to help very little.

Lasse Collin mentioned that the LZ layer cannot be disabled but it can be made really weak using xz --lzma2=dict=4KiB,mode=fast,nice=2,mf=hc3,depth=1. Let's try it.

% =time -f '%e' xz -fk Prune_video_game_screenshot_2.qoi && stat -c %s Prune_video_game_screenshot_2.qoi.xz
0.76
2462360
% =time -f '%e' xz --lzma2=dict=4KiB,mode=fast,nice=2,mf=hc3,depth=1 -fk Prune_video_game_screenshot_2.qoi && stat -c %s Prune_video_game_screenshot_2.qoi.xz
0.27
2526664

Indeed, weakening the LZ layer improves compression speed signicantly. Now, let's test all benchmark images.

% cat ~/tmp/qoi-weak-xz.sh
#!/bin/zsh
/tmp/p/qoi/qoiconv $1 ${1/.png/.qoi}
xz --lzma2=pb=0 -fk ${1/.png/.qoi}
xz --lzma2=dict=4KiB,mode=fast,nice=2,mf=hc3,depth=1 -c ${1/.png/.qoi} > ${1/.png/.qoi.weak-lz.xz}
% cd /tmp/dc-img/images
% ls -1 **/*.png | rush ~/tmp/qoi-weak-xz.sh '"{}"'

ruby -e 'puts "directory\tstrong\tweak\tincrease"; Dir.glob("*").each{|dir| next unless File.directory? dir;
  strong=weak=0; Dir.glob("#{dir}/*.qoi.weak-lz.xz").each{|f| weak+=File.size(f); strong+=File.size(f.sub(/\.weak-lz/,""));};
  puts "#{dir}\t#{strong}\t#{weak}\t#{(100.0*weak/strong-100).round(2)}%"
}'
directorystrongweakincrease
icon_5127476640862990015.42%
icon_647084807350363.75%
photo_kodak12902852134640724.35%
photo_tecnick2132681882174603921.97%
photo_wikipedia86679696886097162.23%
pngimg1933826682066792246.88%
screenshot_game2189153162348890607.3%
screenshot_web213214602482002016.41%
textures_photo28967008312494927.88%
textures_pk54117600579561687.09%
textures_pk0114950836157495565.34%
textures_pk0282279000877475766.65%
textures_plants43681548454940844.15%

This size increase is small for certain directories but quite large for the others. For the directories with small size increases, relying purely on delta coding and a fast entropy encoder will give a strong competitor.

The Dark Horse of the Image Codec World: Near-Lossless Image Formats Using Ultra-Fast LZ Codecs remarks that fast LZ can make strong contenders.

PNG

The PNG International Standard defines the compression method 0 as DEFLATE with a sliding window of at most 32768 bytes. Technically new compression methods can be defined, but that would break compatibility of existing decoders and stakeholders would just resort to new image formats. However, it would be a nice experiment to check that after the compression part is improved, how PNG compares with newer image formats.