What is this?
QRtsy is an open source QR code generator with a bunch of knobs to make pretty pictures. It's pronounced "cue-artsy" which I think captures the spirit of the project pretty well.
QRtsy is meant to be a reusable python library which also includes an example web application to help with experimentation. The source code for the project can be found at https://codeberg.org/newbery/qrtsy. See the project README for installation and developer instructions. The document you are reading here is more of a user guide and a high-level discussion of how this works.
QRtsy is an experiment. Every artistic choice spends some of the visual redundancy that normally makes QR codes stubbornly resilient for scanning with less than optimal lighting or equipment or even if printed on a crumpled sheet of paper that you tossed in the trash (don't ask). Be cautious with the output from this application. Make something nice, then scan it from the devices, distances, print sizes, and lighting conditions that make sense for your use case.
Go ahead and play with it. You won't break anything. Probably.
Acknowledgements
QRtsy owes its biggest debt to Andrew Taylor's Dithered QR Code Generator and his very readable technical explanation. In particular, the idea of shrinking QR samples and diffusing the luminance error caused by forced QR pixels sounded very cool and made me wonder how well that technique would work with full color images (Andrew's implementation converted all images into a dithered greyscale). My first try at this looked promising but, at some point, I decided it would be more fun to lean into my Python strengths and just recreate it in Python.
There already exist plenty of QR code generators written in Python but none of them seemed to check off all the boxes I wanted which gave me enough of an excuse to reinvent this particular wheel (Not that I need much of an excuse). So that's how I ended up building this app that no one asked for. QRtsy is mostly just a tool for me to explore what can be done with QR codes and features are being added as I learn more. I borrowed Andrew's luminance correction idea but QRtsy doesn't yet fully implement his greyscale dithering. I plan to. I also plan to add a few more QR design options that I see in some other implementations. Coming soon :)
I didn't try to reinvent everything. QRtsy itself doesn't actually create the base QR code onto which most of these artistic effects are applied as post-processing steps. QRtsy is designed instead to make it easy to integrate with existing third-party python libraries that already do the basic QR code generation pretty well, like qrcode, qrcodegen, or segno. QRtsy currently only includes example integration for the Segno library (since I was kind of interested in playing with Segno's plugin architecture and Segno's output gets me very close to a semantic model of the QR code matrix that makes it easier to target the various parts of the QR code in post-processing).
The QRtsy project icon was generated by the QRtsy server app using Painter Artist by Gan Khoon Lay from Noun Project (licensed under CC BY 3.0) as the background image.
The QR Code standard itself comes from DENSO WAVE, whose public documentation is an good first stop to learn about the terminology and the constraints that we are trying to work around to get pretty pictures that still scan.
Settings tour
The application is intentionally a workshop for experimentation, not a polished QR code vending machine. Here is what every current control does, with links into the technical section when you are curious about the machinery underneath.
Content
Setting |
What it does |
Details |
|---|---|---|
Background image |
The picture QRtsy fits behind the QR matrix. With no upload, the server uses the selected solid background color. |
|
Background color |
Chooses the solid fallback background used when no image is uploaded. The default is white. |
|
QR code text |
The text, URL, or other content passed to the QR encoder. |
|
Error correction |
Selects L, M, Q, or H. More correction adds redundancy, usually at the cost of a larger symbol for the same payload. |
|
Version |
Selects QR versions 1–40, or lets Segno choose automatically. Higher versions have more modules and more capacity. |
|
Mask |
Selects one of the eight QR data masks, or lets Segno pick one. Different masks rearrange the visible data pattern without changing the decoded content. |
Image
Setting |
What it does |
Details |
|---|---|---|
Image mode |
Uses the source as full color, posterized color, or grayscale/monochrome input. |
|
Posterize levels |
Reduces each color channel to a smaller set of values when Posterized mode is active. |
|
Dither |
Enables constrained Floyd–Steinberg dithering in Monochrome mode. |
|
Dither threshold |
Moves the black/white decision point used by conventional monochrome dithering. |
|
Serpentine diffusion |
Alternates the scan direction on successive rows, reducing one-way artifacts in directional diffusion modes. |
|
Fit |
Chooses whether the background covers the QR area, fits inside it, or stretches to it. |
QR rendering
Setting |
What it does |
Details |
|---|---|---|
Module size |
Sets how many output pixels represent one QR module. |
|
Core size |
Sets the central square that QRtsy forces to the required QR value for shrinkable modules. |
|
Quiet-zone modules |
Adds clear margin around the rendered symbol. QRtsy allows experimentation here, although the QR specification normally calls for four modules. |
|
Module grid overlay |
Displays a separate transparent one-pixel grid over the preview without modifying the rendered QR image. QRtsy chooses black or white according to which gives greater contrast with the fitted background. The overlay is retained when hidden, so it can be toggled back on instantly. |
|
Shrink positioning blocks |
Shrinks finder and separator modules to cores, exposing much more image around the three big corner patterns. |
|
Shrink alignment markers |
Shrinks alignment-pattern modules to cores. |
|
Shrink timing patterns |
Shrinks timing-row and timing-column modules to cores. |
Free-pixel texture
Setting |
What it does |
Details |
|---|---|---|
Texture |
Chooses None, ordered dither, seeded noise, randomized error diffusion, or directional Flow diffusion. |
|
Texture strength |
Controls how strongly a visible texture block moves toward the current dark or light QR endpoint color. |
|
Texture density |
Sets the target population of visible contrasting texture blocks. |
|
Texture fade |
Controls how strongly texture retreats from colored, dark, or detailed image regions. Values near 1 deliberately have fine-grained response. |
|
Texture seed |
Makes noise and randomized error diffusion deterministic while letting you try another arrangement. |
Advanced compensation
Setting |
What it does |
Details |
|---|---|---|
Compensation |
None leaves the rendered pixels alone. Local compensation adjusts free pixels within each module to recover luminance changed by QR cores and texture. |
|
Strength |
Controls how much of the measured local luminance error QRtsy attempts to correct. |
Presets & browser features
Built-in and custom presets capture renderer settings without baking in the payload, image or fallback background color, QR version, mask, or error-correction choice. More on presets.
Browser persistence stores custom presets in version-specific browser storage. “Clear all” removes QRtsy presets from every saved version.
Per-field undo appears only when a field differs from the page default and restores that one setting without disturbing its neighbors.
Live preview rerenders as settings change and reports the actual QR version, mask, and error-correction level used.
Download naming derives a friendly PNG filename from the uploaded image, while leaving the name editable before saving.
Technical tour
This is an attempt to explain what all these knobs do. It is intentionally modest with just enough detail to describe the features at a high-level and where some of the tradeoffs come from. (Yeah, I know there is maybe too much technical jargon below. I'll work on cleaning this up. Another work-in-progress 🤓)
The pipeline
Encode first, then add art.
The core QRtsy renderer does not encode payloads. It accepts a semantic ModuleMatrix: a square grid whose cells know both their required dark/light value and their role: data, finder, timing, alignment, format, version, separator, and so on. The included Segno integration creates that semantic matrix from a Segno QR code. The renderer then combines the matrix with an image and RenderOptions.
Keeping those jobs separate is useful. Segno can stay very good at QR encoding and QRtsy can stay focused on the pretty post-processing. It also means another QR encoder can be adapted later without rewriting the artistic renderer.
QR anatomy
Not all squares have the same job.
The semantic matrix helps QRtsy to keep track of the different roles for each square (aka module). The large corner finders help a scanner locate and orient the symbol. Timing patterns establish the module grid. Alignment patterns help compensate for distortion. Format modules identify the error-correction level and mask, and version modules appear on larger versions. Data modules carry the encoded payload and its error-correction codewords.
QRtsy treats these roles differently. Data, format, version, and the fixed dark module use central sampling cores by default. Finder, separator, timing, and alignment modules remain full-sized unless their corresponding shrink switch is enabled.
Sampling cores
The center of the square does the heavy lifting.
A QR module occupies module size × module size output pixels. For shrinkable modules, QRtsy forces only a centered core size × core size square to the required QR endpoint color and leaves the surrounding pixels available for the image and optional texture. Smaller cores reveal more image; larger cores give the scanner a more conventional target.
Some of these techniques push the boundaries of what is allowed by the standard and what is reliably scanned in the real world. Scanner sampling behavior varies, so the right core size is an empirical choice for the way the final image will actually be used.
Image preparation
Fit it, and maybe simplify.
When no source image is uploaded, the server starts with a solid white background; the Background color control can replace that with another solid color. A supplied image is first resized to the QR content area (as governed by the "Fit" settings: cover, contain, or stretch). Full-color mode keeps the original RGB values. Posterized mode quantizes each channel to the requested number of levels. Monochrome mode converts the image to grayscale. Note that the monochrome/grayscale processing option is not yet optimized and is currently a work-in-progress currently producing some visual artifacts that I haven't yet debugged.
Monochrome dithering.
Fuzzy pictures can be pretty too.
Conventional Floyd–Steinberg dithering normally chooses black or white for a pixel, measures the resulting luminance error, and distributes that error to pixels that have not been processed yet. In QRtsy, the required QR sample pixels are fixed to their QR value and, when appropriate, the resulting error is allowed to diffuse into neighboring free pixels.
The threshold moves the black/white decision point. Serpentine mode alternates row direction so the error does not always march across the image from the same side.
Free-pixel texture.
Noise can also be pretty.
Free-pixel texture operates on core-sized blocks that do not overlap required QR samples. The goal is to make the regular sampling-core pattern less obvious by adding some visually pleasing texture to the background.
More detail...
Modes
Ordered dither uses a deterministic Bayer threshold pattern. Noise dither uses a deterministic pseudo-random threshold. Error diffusion uses a seeded randomized processing order and diffuses quantization error into nearby unprocessed blocks, avoiding a strong left-to-right grain. Flow diffusion deliberately keeps a directional Floyd–Steinberg-like traversal and some pixel color assignment tricks that can produce a cloud-and-tendril texture that looks vaguely like flowing fluid.
Polarity & strength
In the default texture modes, local background luminance decides which contrasting QR endpoint is useful: pale regions lean toward visible dark texture, dark regions toward visible light texture. Texture strength controls how far a rendered block moves toward the selected endpoint luminance.
Density
Texture density controls the target population of visible contrasting blocks. It is deliberately separate from strength: density controls “how many” while strength controls “how far from the original luminance”.
Fade
QRtsy samples a small neighborhood around each candidate block and scores how smooth, neutral, and near-white that neighborhood is. Texture fade blends between unrestricted texture and that adaptive background mask.
Seeds & QR cores
The seed makes randomized layouts repeatable. The required QR cores are protected from being overwritten when these randomized layouts are added in post-processing.
Advanced compensation
Also known as luminance correction or local compensation. I haven't decided on the best terminology for this.
A forced dark core in a pale module removes luminance; a forced light core in a dark module adds it. Texture can change the module's average luminance too. Local compensation runs after the required cores and texture have been rendered, compares each module against the prepared source image, and nudges the remaining free pixels toward the original total luminance. The effect can be subtle and not always so pretty. I'm still experimenting with this algorithm so it might improve in later iterations.
The luminance compensation correction is the slowest part of many high-resolution renders, which is why it is disabled by default. It's generally best to experiment with the other settings first and then try adding luminance compensation as a final tweak before generating the final image to download.
Presets
It's annoying to keep track of all your favorite settings.
Most of the rendering options are serialized into RenderOptions which we can save as presets. Presets intentionally do not capture the uploaded image, fallback background color, payload, QR version, mask, or error-correction level (I might change some of this later).
Built-in presets are immutable package data. Custom presets created in the server UI are application-owned state and live entirely in browser storage under a key containing the QRtsy version. The server validates and migrates preset data but does not maintain a mutable custom-preset registry. Custom presets will therefore seem to disappear whenever the server version is updated. Browser storage is a convenience, not long-term storage, so export any custom presets you want to save reliably for later use.
Scannability
QR error correction can recover damaged codewords, but artistic rendering is still spending robustness. Higher error-correction levels provide more recovery capacity, while also reducing payload capacity for a given version.
The standard QR quiet zone around the margin is four modules wide. QRtsy defaults to a smaller value because it is an experimentation tool, but reducing the margin is one more thing that can make real-world scanning less forgiving. A code that scans from your monitor under office lighting is just a first test. To test it robustly, print it, shrink it, tilt it, crumple the paper a bit, and maybe borrow somebody else's phone.
More QR resources
Still curious? Go ahead, dive into this rabbit hole. I dare you...
DENSO WAVE — What is a QR Code? — friendly official overview.
DENSO WAVE — Versions and capacity — versions 1 through 40 and how size grows.
DENSO WAVE — Error correction — the L/M/Q/H tradeoff.
DENSO WAVE — Symbol area and quiet zone — including the standard four-module margin.
DENSO WAVE — QR Code standardization — standards history and specification pointers.
ISO/IEC 18004:2024 — the current published QR Code symbology standard.
Segno documentation — the Python encoder used by QRtsy's optional integration.
Thonky QR Code Tutorial — a detailed programmer-friendly walk through encoding, masking, placement, and error correction.
Nayuki QR Code generator library — compact multi-language implementations plus a useful technology overview.
Andrew Taylor — How to make error-diffused QR codes — the direct inspiration for much of QRtsy's image/QR experimentation.
What's next?
There are so many ideas out there on how to make aesthetically pleasing QR codes. My research is just beginning and I plan to document my findings for the recreational reading by all my fellow QR code enthusiasts out there. If you're impatient, here is a sampling of some of the more interesting bits,
Papers
Chu, H.-K., Chang, C.-S., Lee, R.-R., & Mitra, N. J. (2013). Halftone QR codes. ACM Transactions on Graphics, 32(6), Article 217, 1–8. DOI: 10.1145/2508363.2508408
Lin, S.-S., Hu, M.-C., Lee, C.-H., & Lee, T.-Y. (2015). Efficient QR code beautification with high quality visual content. IEEE Transactions on Multimedia, 17(9), 1515–1524. DOI: 10.1109/TMM.2015.2437711
Xu, M., Su, H., Li, Y., Li, X., Liao, J., Niu, J., Lv, P., & Zhou, B. (2019). Stylized aesthetic QR code. IEEE Transactions on Multimedia, 21(8), 1960–1970. DOI: 10.1109/TMM.2019.2891420
Xu, M., Li, Q., Niu, J., Su, H., Liu, X., Xu, W., Lv, P., Zhou, B., & Yang, Y. (2021). ART-UP: A novel method for generating scanning-robust aesthetic QR codes. ACM Transactions on Multimedia Computing, Communications, and Applications, 17(1), Article 3418214. DOI: 10.1145/3418214
Su, H., Niu, J., Liu, X., Li, Q., Wan, J., Xu, M., & Ren, T. (2021). ArtCoder: An end-to-end method for generating scanning-robust stylized QR codes. Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 2277–2286
Su, H., Niu, J., Liu, X., Li, Q., Wan, J., & Xu, M. (2021). Q-Art Code: Generating scanning-robust art-style QR codes by deformable convolution. Proceedings of the 29th ACM International Conference on Multimedia, 722–730. DOI: 10.1145/3474085.3475239
Wu, G., Liu, X., Jia, J., Cui, X., & Zhai, G. (2024). Text2QR: Harmonizing aesthetic customization and scanning robustness for text-guided QR code generation. Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 8456–8465.
preprint: https://arxiv.org/abs/2403.06452
project page: https://mulns.github.io/Text2QR/
source code: https://github.com/mulns/Text2QR
Liao, J.-W., Wang, W., Wang, T.-S., Peng, L.-X., Weng, J.-H., Chou, C.-F., & Chen, J.-C. (2025). DiffQRCoder: Diffusion-based aesthetic QR code generation with scanning robustness guided iterative refinement. Proceedings of the IEEE/CVF Winter Conference on Applications of Computer Vision (WACV), 5916–5925.
preprint: https://arxiv.org/abs/2409.06355
project page: https://jwliao1209.github.io/DiffQRCoder/
source code: https://github.com/jwliao1209/DiffQRCoder
Cox, R. (2012). QArt codes. Constrained QR encoding.
source code: https://github.com/rsc/qr
The QArt implementation is in the qart portion of this repository. QArt uses the degrees of freedom in Reed-Solomon encoding and Gauss-Jordan elimination to produce valid QR matrices whose module patterns better approximate a target image.
Demaine, E. D., & Demaine, M. L. (2025). 3D QR codes. The Demaine Laboratory, Massachusetts Institute of Technology.
The "Layered Paper QR Codes with Images" section describes dividing each QR module into a 6 x 6 grid, preserving the center 2 x 2 submodules for QR information and using the remaining submodules for the image.
Interactive QR Designer: https://erikdemaine.org/prints/QR/layers/
Taylor, A. (2025). Making your own dithered QR codes.
online app: https://www.andrewt.net/dithered-qr-codes/
The technical article describes the QR-aware two-pass error-diffusion technique: first diffuse the visual error caused by forced QR data pixels, then perform ordinary error-diffusion dithering on the free image pixels.
Floyd, R. W., & Steinberg, L. (1976). An adaptive algorithm for spatial grey scale. Proceedings of the Society for Information Display, 17(2), 75–77.
https://isgwww.cs.uni-magdeburg.de/~stefans/npr/entry-Floyd-1976-AAS.html
This is the original Floyd-Steinberg error-diffusion algorithm on which Taylor's QR-aware error-diffusion technique is based.
Software
dithered-qr-codes
source code: https://codeberg.org/andrew-t/dithered-qr-codes
segno
source code: https://github.com/heuer/segno
documentation: https://segno.readthedocs.io/
qrcode-artistic (segno)
source code: https://github.com/heuer/qrcode-artistic
documentation: https://segno.readthedocs.io/en/stable/artistic-qrcodes.html
python-qrcode
source code: https://github.com/lincolnloop/python-qrcode
The StyledPilImage subsystem provides module shapes such as rounded, circular, and gapped modules; color masks and gradients; separate finder-pattern rendering; and embedded images.
qrcodegen
project page: https://www.nayuki.io/page/qr-code-generator-library
source code: https://github.com/nayuki/QR-Code-generator
fuqr
source code: https://github.com/zhengkyl/fuqr
Includes TypeScript and Rust implementations and examples involving image overlays, dithering, constrained pixel-art generation, and logo fitting.
go-qrcode
source code: https://github.com/yeqown/go-qrcode
Includes customizable module shapes, colors, icons, and halftone QR-code rendering.
“QR Code” is a registered trademark of DENSO WAVE INCORPORATED.