AI cannot draw a QR code that works
What you will learn
- Why an image generator can draw a perfect looking square code that never scans
- How to make a real one and put it into artwork without breaking it
- The four ways a technically perfect code still fails on a real phone
I asked an image generator for a poster with a square scannable code on it. It produced a handsome poster with a convincing code: the right shape, the right texture, the little target squares in three corners. It does not scan. None of them ever scan.
The reason tells you where the boundary of this whole class of tool is
An image generator is drawing a picture of a code. It has seen thousands of them and learned what they look like. Dense black and white blocks, those three distinctive corner markers, a quiet border. It reproduces the appearance faithfully.
But one of these codes is not a texture. It is an encoding. Every little square is the result of a specific calculation over your specific text, wrapped in mathematics that lets a scanner recover from smudges. The pattern is not decoration. The pattern is the data.
Drawing something that resembles it is like writing a paragraph in convincing handwriting using letter shapes that are not letters. It looks like writing from across the room, and it says nothing.
A painter can paint a barcode onto a tin perfectly and the till will never beep. The stripes are not a style. They are a number.
So the method is boring and completely reliable
Make the code with a real encoder, then place it into whatever artwork you want. Let the generator do the thing it is good at, which is the picture. Let a proper piece of software do the thing it is good at, which is being correct.
What surprised me more than the generator part
While testing, I collected the ways a technically valid code still fails in the real world. Codes printed light on dark are rejected by a lot of phone cameras. Too little clear space around the edge and the scanner cannot find where the code starts. A code sized for a page, printed on a business card, becomes unreadable.
Each of those produces a code that is mathematically perfect and practically useless. Which is why the last step is not optional, and it is the step people skip because the image looks right: scan it yourself, with a phone, from the distance a real person will stand.
The stripes are not a style. They are a number.
Making a code with a real encoder, the settings that decide whether it scans in the real world, placing it into artwork, and the test that has to happen before you publish anything.
1. Make it with proper software, never with an image generator
What this does: produces a real, correct code from your text.
Why there is nothing to think about here: every programming language has a tool for this and they are all fine. This problem is solved and you should not be interesting about it.
# Python
pip install "qrcode[pil]"
python3 - <<'PY'
import qrcode
q = qrcode.QRCode(
version=None, # smallest that fits
error_correction=qrcode.constants.ERROR_CORRECT_Q,
box_size=10, # size of each little square
border=4, # clear space. do not reduce.
)
q.add_data("https://example.com/your-target")
q.make(fit=True)
q.make_image(fill_color="black", back_color="white").save("code.png")
PY
# or straight from the command line
qrencode -o code.png -s 10 -m 4 -l Q 'https://example.com/your-target'
2. Choose the smudge tolerance on purpose
What this does: sets how much damage or covering up the code can survive and still be read.
The tradeoff: higher tolerance survives more and holds less text in the same size.
# L about 7% smallest code, least forgiving. fine on a clean screen.
# M about 15% the usual default
# Q about 25% good for print, or if a logo will sit on top of it
# H about 30% most forgiving, largest code
# printing, or putting anything on top -> Q or H
3. Keep the clear space, and keep it dark on light
What this does: protects the two things that cause most real world failures.
Why the border is not padding: it is how the scanner finds the edges of the code. Four blocks of clear background on every side, minimum.
Why polarity matters: light code on a dark background is rejected by many phone cameras. If the design wants dark, put a light panel behind the code.
# clear space : at least 4 blocks of plain background on every side
# polarity : DARK blocks on a LIGHT background
# contrast : real black on real white. not charcoal on cream.
4. Keep the target short, and never point at something you cannot change
What this does: keeps the code small enough to still read when printed badly or reproduced small.
Why: less text means fewer little squares. And a code printed on a physical object is permanent, so point it at an address you control and can redirect later.
# prefer
# https://yourdomain.tld/x
# over
# https://yourdomain.tld/some/deep/path?lots=of&tracking=junk
# point it somewhere YOU can redirect, so the destination can move
# without reprinting anything
5. Place the real code on top, do not regenerate it
What this does: makes the artwork however you like, including with a generator, and then puts the real code onto it as a separate layer.
Why the light panel: it guarantees contrast whatever the artwork underneath is doing.
If you want a logo in the middle, that is what the higher smudge tolerance was for. Keep it small, keep it centred, and test after adding it rather than assuming the margin covers it.
python3 - <<'PY'
from PIL import Image
art = Image.open("poster.png").convert("RGB")
code = Image.open("code.png")
panel = Image.new("RGB", (code.width + 40, code.height + 40), "white")
panel.paste(code, (20, 20))
art.paste(panel, (art.width - panel.width - 60, art.height - panel.height - 60))
art.save("poster-final.png")
PY
6. Scan it, from the real distance, with a real phone
What this does: the only test that counts.
When: before publishing, not after somebody tells you.
And check where it goes, not just that something happened. A code that scans to the wrong place has still failed.
# the checklist
# a plain phone camera, not a specialist scanning app
# at the size and distance a real person will use
# printed, if it is going to be printed
# one dark room and one bright room
# and confirm it opens the RIGHT place
If you are making many, have a program read each finished image back, so a broken one cannot go out quietly. Read back the finished artwork, not the bare code. The bare code passing tells you nothing about what happened when you put it on the poster.
python3 -c "
from PIL import Image
from pyzbar.pyzbar import decode
print(decode(Image.open('poster-final.png')))"
A code that scans on your phone may still fail on an older camera or a cheap scanner. Keep it generous. The only thing you gain by making a code smaller or prettier is a higher chance that it does not work.
Related reading: my assistant told me it had done the job, and it had not is the same lesson about confident output that nobody checked.
This page in the original Kyber Cypher voice: AI cannot make a working QR code