Hacking QR code design

By Marien Raat

Check out My-QR.art for the final result!

QR codes provide a clear and approachable way of translating the physical world to the digital world. When I see a QR code, I always want to scan it to find out what is behind it, somehow they always fascinate me. But QR codes always look very similar, could there be a way to make them look more cool? Make them a certain specific shape or contain pixel art?

People already (ab)use the error correction build into QR codes to embed their logo in it, they simply put their logo over a part of the QR code and let the error correction handle the missing data. However, you can never obscure more than 30% of the code (and usually a lot less) if you choose this approach. But QR codes are mostly a visualisation of data. Can we approach the problem from the other side? What if we don't try to design a QR code to fit the data, but fit the data to the design of the QR code that we want.

Spoiler, we can. Check out My-QR.art. All the code is totally free and open source, so feel free to contribute on GitHub

Say, we want to promote our website. And we have a nice grayscale logo. Now we want to have an engaging QR code that looks like our logo.

My-QR.art logo

And we have an URL that we want the QR code to lead us too, for example: https://my-qr.art.

We set up a web server that can redirect certain URLs to other URLs. Then we can make the beginning of the data of our QR code the URL to that web-server and the rest of the QR's data can be anything. So we can choose the rest of the data in any way that makes our QR code look most like our design!

For example the QR code could send you to https://my-qr.art/r/arbitrary-super-long-string. On the back-end we tell the server to redirect any request with the URL https://my-qr.art/r/arbitrary-super-long-string to https://my-qr.art. And tada! We have a functional QR code that sends us to the address we want, while having way more control over how the QR code looks! Up to 80% of the code can be freely designed this way!

When we have this all working, we could even make nice scannable QR gifs. We could make each frame separately and let them all redirect to the same page. I admit, most QR codes are printed, so gifs might not make the most sense, but they look pretty cool! To keep you excited for the rest of the article, here is world's first ever working QR gif (as far as I know)!

Animated QR code that shows a running horse

The running horse might remind you of some other movie...

Now that we know the concept, lets get started. First we need to know how QR codes actually work.

QR codes encode data by making some blocks dark and other light. The QR scanner can then read which are dark and which are light and reconstruct the original data. QR codes can have 40 different sizes, from version 1 (very small, 17x17) to version 40 (very big, 177x177). QR codes are made of black and white 'modules' and each module has one of the following 4 functions:

  1. Help the scanner recognise and orient the QR code
  2. Give information about the QR code size, type and other internal QR stuff
  3. Encode the actual data that the QR code represents
  4. Encode error correction info about the QR code

Our plan is to allow the user to choose the colour of any module that has function 3. So the first step is to let the user know which pixels they can control and which pixels they can't control for the QR code size they chose. For now lets try to create an image on which the modules with function 3 are coloured white and all other modules are coloured grey.

For this we can colour all the modules and error data. To get this right you really need to dive into how QR codes work. We need to think about the data encoding, interleaving, extra eyes, error words and more. We'll also need to take into account that we'll need to reserve first few characters of our data for the start of our URL. Luckily there is a great source on QR codes on the internet, the QR code tutorial from Thonky.com. Putting the work in, we get something like this, for version 30.

A template for a QR design

Now let's work on the other side of the puzzle, when we have a design that a user has drawn on our template, how do we convert it to a working QR code? QR codes have 4 possible types of encoding: numeric, alphanumeric, binary and kanji. We need something that we can create valid URLs with, so numeric and kanji are out. Secondly, we need the random string at the end of our URL to be normal enough so that QR scanners will still recognise our code as an URL. The binary encoding will create characters that aren't ordinarily found in URLs ('\0' for example), which means lots of QR scanners won't let their users open our URL. Luckily the alphanumeric encoding has just enough characters to let us make valid URLs (although they'll have to be all caps).

A design for a QR code that contains the My-QR.art logo An illustration from Thonky.com that shows how data is encoded in a QR code

So with that figured out, lets read that design the user made. First we'll extract a bit string from all the data modules in the design. For small QR codes this is quite straight forward, the QR code is simply read from right to left, bottom to top and back again in double columns, see the illustration from Thonky above. However, when the QR code size passes 5, we'll have to take into account interleaving. Then the data words are scattered around the QR code, presumably to make the data pattern more random. So we'll have to reverse that interleaving procedure. Luckily how it works is explained by Thonky.com. At last we get a bit string from our design, here we only show the first 80 character, the real bit string for this design is 13863 bits long.

01100011010101001100101010001100011110111010100000000001110100111110011101001001

Decoding the bitstring

Now we'll need to decode the bits to an alphanumeric string. Here we group 11 bits together every time and then convert that into two alphanumeric characters using this table. We get the following data string (again truncated).

KS$#LKAHRDF9H8XQI9AL HRD$OIWHSRE94QX95IFR*IK9HF/ 5NM+/AMIA99LB I5R II98ULR JRD/AR1 IRG8...

We'll just replace the first 20 characters with our URL prefix and we get:

HTTPS://MY-QR.ART/R/ HRD$OIWHSRE94QX95IFR*IK9HF/ 5NM+/AMIA99LB I5R II98ULR JRD/AR1 IRG8...

Generating the QR code

Now we can use any QR library to create the QR code! The QR library will handle adding all the difficult error codes and other boring stuff for us.

A random looking QR code

But what is that? Why does it still look all random? The problem is masking. The QR code spec includes 5 different masks and the mask that makes the QR code look most random is chosen, which is almost certainly not our design. This is done to make the code easier to read for scanners. However, in my experience, QR codes scan just fine with more structured data. So we don't have to feel too bad about modifying our QR library to always use the same mask. Then after we apply the same mask to our design before processing it, we finally get the desired image.

A QR code with the design in it

Now to redirect the QR code to the right domain. This should be easy, put the QR URL in a database and link it to the redirect URL. When we receive a request, simply look up the corresponding URL in a database. However, if we want to use a modern web server, we'll have to jump through some hoops... Apache doesn't like most of our URLs, since they are technically malformed (ugh, spoil the fun much Apache?). So it throws an Error 400: Bad request. We can't disable this error anywhere it seems. However, luckily there is a workaround, we can set a custom error page for our Error 400, so if we just set the redirect page as the error page for Error 400, we are home free! Luckily Apache is not so much of a spoilsport that it throws a Bad Request error while trying to handle a Bad Request...

For the back-end I've been using Django, and it has another fun bug that shows up with these really weird URLs. It tries to decode the URL as an ISO-8859-1 string, but some of the characters passed by Apache aren't in ISO-8859-1. So we have to derive our own WSGI handler to workaround this, see here. But now it works, try to scan the QR we made, it redirects where we want it to redirect!

One small caveat though, QR codes themselves are standardized and we create standard compliant QR codes with correct URLs in them, but QR scanners aren't standardized and some might not recognize our code as a URL and will see it as plain text instead.

Just outputting and parsing images isn't very user friendly, so I made a nice web app at My-QR.art using Django. It has a friendly editor where you can draw, fill or upload black and white images to make your QR code. It also has some explanations and the redirecting code we made. And it is completely free and open source, see exactly how it is made at the GitHub page.

A screenshot of the homepage of My-QR.art A screenshot of the editor of My-QR.art A screenshot of the success screen of My-QR.art

Alas, the shackles of purely random QR codes has been lifted. Go ahead and try to make your own cool QR codes on My-QR.art or check out the code on Github. Think this is cool and want to help? Share this blog post or the website with friends or family and star the repository on Github. If you can spare the money, a donation using the button below would be greatly appreciated, especially since hosting My-QR.art costs money. Did you make a cool QR code using My-QR.art? Please email it to me!