How big is a WebP of an animated QR code, relative to the original file?

For this experiment we'll use the TXQR format, and gif2webp from the libwebp library. We use a random file to simulate data that is not further compressible.

Experiment

I'm assuming you are on a Linux (or sufficiently Linux-like) system and have installed typical build tools, Git, and CMake. If they're not installed then your distribution will have them as packages, e.g. build-essential, git, and cmake on Debian.

Directory hygiene

mkdir animatedqr
cd animatedqr

Build txqr

git clone https://github.com/divan/txqr.git
cd txqr/cmd/txqr-gif
go build
./txqr-gif --help
Usage of ./txqr-gif:
  -fps int
    	Animation FPS (default 5)
  -o string
    	Output animated gif file (default "out.gif")
  -size int
    	QR code size (default 300)
  -split int
    	Chunk size for data split per frame (default 100)

Create a random 10000-byte file and encode it as a TXQR GIF

I'm just going to use the defaults for frame rate, QR size, etc. I don't think that matters very much.

cd ../../..
head --bytes=10000 /dev/urandom >random
txqr/cmd/txqr-gif/txqr-gif -o random.gif random
ls -l random*
-rw-rw-r-- 1 daira daira  10000 Aug 17 15:28 random
-rw-rw---- 1 daira daira 999929 Aug 17 15:28 random.gif

So at this size, an animated GIF is about 100 times the size of the original file. It is roughly consistent across runs. For a 1000-byte file the overhead is about 105 times, for larger files slightly less than 100.

Build libwebp

git clone https://chromium.googlesource.com/webm/libwebp.git
cd libwebp
cmake .
make
./gif2webp -h
Usage:
 gif2webp [options] gif_file -o webp_file
Options:
  -h / -help ............. this help
  -lossy ................. encode image using lossy compression
  -mixed ................. for each frame in the image, pick lossy
                           or lossless compression heuristically
  -q <float> ............. quality factor (0:small..100:big)
  -m <int> ............... compression method (0=fast, 6=slowest)
  -min_size .............. minimize output size (default:off)
                           lossless compression by default; can be
                           combined with -q, -m, -lossy or -mixed
                           options
  -kmin <int> ............ min distance between key frames
  -kmax <int> ............ max distance between key frames
  -f <int> ............... filter strength (0=off..100)
  -metadata <string> ..... comma separated list of metadata to
                           copy from the input to the output if present
                           Valid values: all, none, icc, xmp (default)
  -loop_compatibility .... use compatibility mode for Chrome
                           version prior to M62 (inclusive)
  -mt .................... use multi-threading if available

  -version ............... print version number and exit
  -v ..................... verbose
  -quiet ................. don't print anything

Use gif2webp to convert to WebP

cd ..
libwebp/gif2webp -min_size -metadata none random.gif -o random.webp
ls -l random*
-rw-rw-r-- 1 daira daira  10000 Aug 17 15:28 random
-rw-rw---- 1 daira daira 999929 Aug 17 15:28 random.gif
-rw-rw-r-- 1 daira daira 203632 Aug 17 15:28 random.webp

Without the -min_size option it is around 250000 bytes. Lossy compression doesn't help.

Addendum: BC-UR

The format that Keystone actually uses is BC-UR, which is widely adopted in the Bitcoin world. Let's repeat the experiment with that instead of TXQR.

git clone https://github.com/Foundation-Devices/foundation-ur-py.git
cd foundation-ur-py
python -m venv venv
venv/bin/python -m pip install qrcode webp

Now create a file encode.py with the following contents:

#!/bin/env python

import sys

from ur.ur_encoder import UREncoder
from ur.ur_decoder import URDecoder
from ur.ur import UR
import qrcode
import webp

def encode(ur, max_fragment_len):
    first_seq_num = 100
    encoder = UREncoder(ur, max_fragment_len, first_seq_num)
    decoder = URDecoder()
    while True:
        part = encoder.next_part()
        decoder.receive_part(part)
        yield part
        if decoder.is_complete():
            break

    if decoder.is_success():
        assert(decoder.result == ur)
    else:
        print('{}'.format(decoder.result))
        assert(False)

def main(args):
    if len(args) < 3:
        print("Usage: encode.py <inputfile> <outputfile-stem>")
        return

    inputfile = args[1]
    outputfile = args[2]

    with open(args[1], mode='rb') as f:
        data = f.read()

    # TODO: assign code for PCZT
    # I don't know why some fragment lengths don't work.
    for max_fragment_len in [100, 125, 200, 250, 400, 500, 650, 1000]:
        ur = UR('pczt', data)
        print(f"max_fragment_len = {max_fragment_len}")
        imgs = (qrcode.make(part).convert('RGB') for part in encode(ur, max_fragment_len))
        try:
            webp.save_images(imgs, f"{outputfile}-{max_fragment_len}.webp", fps=5, lossless=True)
        except AttributeError:
            print("doesn't work")  

if __name__ == "__main__":
    main(sys.argv)

and run:

venv/bin/python encode.py ../random ../random-ur
ls -l ../random*
-rw-rw-r-- 1 daira daira  10000 Aug 17 15:28 ../random
-rw-rw---- 1 daira daira 999929 Aug 17 15:28 ../random.gif
-rw-rw-r-- 1 daira daira 227280 Aug 19 15:11 ../random-ur-1000.webp
-rw-rw-r-- 1 daira daira 399558 Aug 19 15:10 ../random-ur-100.webp
-rw-rw-r-- 1 daira daira 329692 Aug 19 15:10 ../random-ur-125.webp
-rw-rw-r-- 1 daira daira 285868 Aug 19 15:10 ../random-ur-200.webp
-rw-rw-r-- 1 daira daira 236648 Aug 19 15:10 ../random-ur-250.webp
-rw-rw-r-- 1 daira daira 154786 Aug 19 15:10 ../random-ur-400.webp
-rw-rw-r-- 1 daira daira 164376 Aug 19 15:10 ../random-ur-500.webp
-rw-rw-r-- 1 daira daira 115544 Aug 19 15:10 ../random-ur-650.webp
-rw-rw-r-- 1 daira daira 203632 Aug 17 15:28 ../random.webp

Here the optimal fragment size of 650 produces random-ur-650.webp which is ~11.6 times larger than the input file.

Conclusion

For an incompressible input file of 10000 bytes, a WebP-encoded TXQR is roughly 20.4 times larger than the original file. This does not change very much across input sizes. Using WebP saves a factor of ~5 relative to animated GIF.

A WebP-encoded BC-UR can be roughly 11.6 times larger than the original file if the max_fragment_size is chosen optimally.

Select a repo