Fancy QR Codes

Other languages

You may have seen sylnsfar's "Artistic QR Code" library. It produces non-standard QR codes that incorporate arbitrary pictures, but are nonetheless machine-readable [5].

Of course, I had to understand how it worked ☺

It doesn't take much to find that the magic happens in the combine function in myqr.py:

if ver > 1:
  aloc = alig_location[ver-2]
  for a in range(len(aloc)):
    for b in range(len(aloc)):
      if not ((a==b==0) or (a==len(aloc)-1 and b==0) or (a==0 and b==len(aloc)-1)):
        for i in range(3*(aloc[a]-2), 3*(aloc[a]+3)):
          for j in range(3*(aloc[b]-2), 3*(aloc[b]+3)):
            aligs.append((i,j))

for i in range(qr.size[0]-24):
  for j in range(qr.size[1]-24):
    if not ((i in (18,19,20)) or (j in (18,19,20)) or (i<24 and j<24) or (i<24 and j>qr.size[1]-49) or (i>qr.size[0]-49 and j<24) or ((i,j) in aligs) or (i%3==1 and j%3==1) or (bg0.getpixel((i,j))[3]==0)):
      qr.putpixel((i+12,j+12), bg.getpixel((i,j)))

That code is full of magic numbers, and the multiple nested loops don't really help legibility much. What's going on in there?

To understand where those numbers came from, I studied a QR code tutorial. The most useful page for our purposes is the one that explains "function patterns".

Everything in that function assumes a 3-pixel QR module, so:

  • the first block collects the placements of the alignment patterns
    • but only if the code is large enough to need them (ver > 1)
    • and ignoring those that would collide with finder patterns ((a==b==0) or (a==len(aloc)-1 and b==0) or (a==0 and b==len(aloc)-1) means "top-left, top-right, bottom-left corners")
  • the second block replaces pixels in the QR code (qr.putpixel) with pixels from the fancy picture (bg.getpixel) if:
    • they're not in timing patterns ((i in (18,19,20)) or (j in (18,19,20)))
    • they're not in finder patterns ((i<24 and j<24) or (i<24 and j>qr.size[1]-49) or (i>qr.size[0]-49 and j<24))
    • they're not in alignment patterns ((i,j) in aligs)
    • and two more conditions I'll explain later

So all those magic numbers define the placements of finder / timing / alignment patterns: those really have to be there, otherwise the scanning / recognition code won't even realise it's looking at a QR code.

The last two conditions are:

(i%3==1 and j%3==1) or (bg0.getpixel((i,j))[3]==0)

The second one skips transparent pixels in the picture (bg0 is a ARGB-version of the input picture bg, and the pixel value at position 3 is the alpha value).

The first one makes sure to leave the central pixel of each 3×3 QR module untouched: we want the QR data to remain there!

So, another way to get the same result would be:

  • paint a normal black & white QR code
  • layer the fancy picture on top of it, honouring the alpha channel
  • paint all non-data modules on top
  • paint the data modules on top, but only their central pixels

Of course, it's not trivial to know whether a module is part of the data or of a function pattern, but we're in luck: the libqrencode C library provides us with that information! It generates the QR code as a matrix of bytes, and each byte is a bitfield:

MSB 76543210 LSB
    |||||||`- 1=black/0=white
    ||||||`-- data and ecc code area
    |||||`--- format information
    ||||`---- version information
    |||`----- timing pattern
    ||`------ alignment pattern
    |`------- finder pattern and separator
    `-------- non-data modules (format, timing, etc.)

The Perl modules Text::QRCode and Imager::QRCode wrap that library, but do not expose the internal byte matrix. So of course I wrapped it again myself ☺

Alien::QREncode uses Alien::Base to install libqrencode, then Data::QRCode binds it via Inline::Module and provides a Perlish API (see the test file for an example).

Now, we only need to deal with the actual images: let's use Imager, which seems simple and flexible enough. The actual code is maybe too flexible, but the core of the logic is:

my $back_image = qr_image($qr_code);

return $back_image unless $fancy_picture;

my $front_image = qr_dots_image($qr_code);

my $target = Imager->new(xsize=>$size,ysize=>$size);
$target->paste(src => $back_image);
$target->compose(src => $fancy_picture);
$target->compose(src => $front_image);

return $target;

which is a direct translation of the procedure outlined before.

Once we've installed Alien::QREncode, Data::QRCode, and Imager::QRCode::Fancy, we can say:

perl overlay-qr.pl 'https://pokketmowse.deviantart.com/art/In-the-Kawaii-of-the-Beholder-177076230' kawaii-beholder.png kawaii-beholder-qr.png

and get this:

Or maybe:

perl overlay-qr.pl 'Squirrel Girl is the best' squirrel-girl.png squirrel-girl-qr.png

and get this:

Notice how the size of the "dots" adapts to the resolution of the picture (although it could be smarter), and how transparency is maintained.

[5]

more or less: I think you need a high-enough-resolution camera and some pretty forgiving recognition code

DatesCreated: 2016-11-10 17:06:20 Last modification: 2023-02-10 12:45:24