AI cannot make a working QR code
I asked an image generator for a poster with a QR code on it. It produced a handsome poster with a convincing QR code, the right shape, the right texture, corner squares in all three corners. It does not scan. None of them ever scan.
The reason is worth understanding, because it tells you where the boundary is for this whole class of tool. An image generator is drawing a picture of a QR code. It has seen thousands of them and learned what they look like: dense black and white modules, those three distinctive corner markers, a quiet border. It reproduces the appearance faithfully.
But a QR code is not a texture. It is an encoding. Every module is the output of a specific calculation over your specific text, wrapped in error correction mathematics. The pattern is not decorative, it 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: generate the code with a real encoder, then composite it into whatever artwork you want. Let the generator do the thing it is good at, which is the picture, and let a library do the thing it is good at, which is being correct.
While testing I collected the ways a technically valid code still fails in the world, and they surprised me more than the AI part. Inverted codes, light on dark, are rejected by a lot of phone cameras. Too little margin around the edge and the scanner cannot find the boundary. A code sized for a page, printed on a 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.
Generating a code with a real encoder, the settings that decide whether it scans in the real world, compositing it into artwork, and the test that has to happen before you publish.
1. Generate with a library, never with an image model
Every language has one and they are all fine. This is solved and you should not be interesting about it.
# Python
pip install "qrcode[pil]"
python3 - <<'PY'
import qrcode
q = qrcode.QRCode(
version=None, # let it pick the smallest that fits
error_correction=qrcode.constants.ERROR_CORRECT_Q,
box_size=10, # module size in pixels
border=4, # quiet zone, in modules. 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
# command line alternative
qrencode -o code.png -s 10 -m 4 -l Q 'https://example.com/your-target'
2. Choose error correction on purpose
Error correction lets a damaged or partly obscured code still decode. Higher levels survive more and hold less data in the same size.
# L ~7% smallest code, least tolerant. fine for a clean screen.
# M ~15% the usual default
# Q ~25% good for print, or if a logo will overlap it
# H ~30% most tolerant, largest code
# printing, or putting anything on top of it -> Q or H
3. Keep the quiet zone, dark on light
These two cause most real world failures and both are easy to get wrong for aesthetic reasons.
# quiet zone: at least 4 modules of clear background on every side.
# it is not padding, it is how the scanner finds the edges.
# polarity: DARK modules on a LIGHT background.
# inverted codes are rejected by many phone cameras.
# if the design wants dark, put a light panel behind the code.
# contrast: real black on real white. not charcoal on cream.
4. Shorten the target, and never encode anything you cannot change later
Less data means fewer modules, which means a code that still reads when it is small or printed badly. And a code on a printed object is permanent, so point it at something you control.
# prefer
# https://yourdomain.tld/x
# over
# https://yourdomain.tld/some/deep/path?utm_source=...&utm_campaign=...
# and point at a path YOU can redirect, so the destination can move
# without reprinting anything
5. Composite, do not regenerate
Make the artwork however you like, including with a generator. Then place the real code on top of it as a separate layer.
# paste the real code onto the artwork, preserving its quiet zone
python3 - <<'PY'
from PIL import Image
art = Image.open("poster.png").convert("RGB")
code = Image.open("code.png")
# a light panel behind it guarantees contrast whatever the art is doing
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
If you want a logo in the middle, that is what the higher error correction was for. Keep it small, centred, and test after adding it rather than assuming the margin covers it.
6. Scan it, from the real distance, with a real phone
The only test that counts. Do it before publishing, not after someone tells you.
# the checklist
# plain phone camera, not a specialist scanner 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 destination, not just that it beeps
Also decode it programmatically if you are generating many, so a broken one cannot ship silently.
# verify the finished composite, not the bare code
python3 -c "
from PIL import Image; from pyzbar.pyzbar import decode
print(decode(Image.open('poster-final.png')))"
Decoding the composite is the real check. The bare code passing tells you nothing about what happened when you put it on the poster.
The honest catch: 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: my assistant said it did something, it hadn't is the same lesson about confident output that was never checked.