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.
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.
mkdir animatedqr
cd animatedqr
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)
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.
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
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.
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.
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.