QRtsy

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.

Image preparation

Background color

Chooses the solid fallback background used when no image is uploaded. The default is white.

Image preparation

QR code text

The text, URL, or other content passed to the QR encoder.

The pipeline

Error correction

Selects L, M, Q, or H. More correction adds redundancy, usually at the cost of a larger symbol for the same payload.

Scannability

Version

Selects QR versions 1–40, or lets Segno choose automatically. Higher versions have more modules and more capacity.

QR anatomy

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.

QR anatomy

Image

Setting

What it does

Details

Image mode

Uses the source as full color, posterized color, or grayscale/monochrome input.

Image preparation

Posterize levels

Reduces each color channel to a smaller set of values when Posterized mode is active.

Image preparation

Dither

Enables constrained Floyd–Steinberg dithering in Monochrome mode.

Monochrome dithering

Dither threshold

Moves the black/white decision point used by conventional monochrome dithering.

Monochrome dithering

Serpentine diffusion

Alternates the scan direction on successive rows, reducing one-way artifacts in directional diffusion modes.

Monochrome dithering

Fit

Chooses whether the background covers the QR area, fits inside it, or stretches to it.

Image preparation

QR rendering

Setting

What it does

Details

Module size

Sets how many output pixels represent one QR module.

Sampling cores

Core size

Sets the central square that QRtsy forces to the required QR value for shrinkable modules.

Sampling cores

Quiet-zone modules

Adds clear margin around the rendered symbol. QRtsy allows experimentation here, although the QR specification normally calls for four modules.

Scannability

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.

QR anatomy

Shrink positioning blocks

Shrinks finder and separator modules to cores, exposing much more image around the three big corner patterns.

Function patterns

Shrink alignment markers

Shrinks alignment-pattern modules to cores.

Function patterns

Shrink timing patterns

Shrinks timing-row and timing-column modules to cores.

Function patterns

Free-pixel texture

Setting

What it does

Details

Texture

Chooses None, ordered dither, seeded noise, randomized error diffusion, or directional Flow diffusion.

Free-pixel texture

Texture strength

Controls how strongly a visible texture block moves toward the current dark or light QR endpoint color.

Polarity & strength

Texture density

Sets the target population of visible contrasting texture blocks.

Density

Texture fade

Controls how strongly texture retreats from colored, dark, or detailed image regions. Values near 1 deliberately have fine-grained response.

Fade

Texture seed

Makes noise and randomized error diffusion deterministic while letting you try another arrangement.

Seeds & diffusion

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.

Compensation

Strength

Controls how much of the measured local luminance error QRtsy attempts to correct.

Compensation

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...

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.

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.

Cox, R. (2012). QArt codes. Constrained QR encoding.

  • https://research.swtch.com/qart

  • 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.

Taylor, A. (2025). Making your own dithered QR codes.

Floyd, R. W., & Steinberg, L. (1976). An adaptive algorithm for spatial grey scale. Proceedings of the Society for Information Display, 17(2), 75–77.

Software

dithered-qr-codes

segno

qrcode-artistic (segno)

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

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

“QR Code” is a registered trademark of DENSO WAVE INCORPORATED.