QR Codes as Polygons (TikZ/SVG)
Project description
🧩 PolyQR: QR Codes as Polygons
PolyQR is a small library that turns a message into a QR code where each contiguous black region is drawn as one merged polygon, not a grid of tiny squares. This eliminates the hideous hairline gaps between modules that appear with naïve approaches (an example is shown later) and also minimizes the number of points per polygon to keep the output compact.
PolyQR can generate:
- TikZ code with full styling support (e.g. rounded corners)
- SVG paths that are fully minimized to save space
The tests directory contains pytest-based tests for both TikZ and SVG.
🖼️ TikZ Output
PolyQR provides the command-line tool polyqr_tikz, which can be called like this:
polyqr_tikz "1mm" "rounded corners=0.25mm" "https://github.com/KurtBoehm/polyqr"
This prints a tikzpicture environment of the following form to stdout:
\begin{tikzpicture}[x=1mm, y=1mm, qrpoly/.style={fill=black, draw=none, even odd rule, rounded corners=0.25mm}]
% \draw commands to draw a QR code representing https://github.com/KurtBoehm/polyqr
\end{tikzpicture}
Because each connected component is rendered as a single polygon, TikZ styles such as rounded corners apply only to the outer boundary of each contiguous region.
This also eliminates visible gaps between modules, which you can see when comparing to a basic version that draws each module as a separate rectangle:
| Basic | PolyQR | PolyQR with rounded corners |
|---|---|---|
The LaTeX file used to generate these examples is at docs/tikz.tex.
You can also generate TikZ code directly from Python:
from polyqr import QrCodePainter
painter = QrCodePainter("https://github.com/KurtBoehm/polyqr")
print(painter.tikz(size="1mm", style="rounded corners=0.25mm"))
🖼️ SVG Output
PolyQR can also generate highly minimized SVG paths:
- Either the entire QR code or each contiguous area becomes a single
<path>element usingfill-rule="evenodd"to handle holes. - All segments are axis-aligned and therefore encoded using only
M,H,V, andZcommands. - For every move/line, absolute vs. relative commands are chosen based on which textual form is shorter.
SVG generation is available programmatically:
from polyqr import QrCodePainter
painter = QrCodePainter("https://github.com/KurtBoehm/polyqr")
# Full SVG document as a string
svg_doc = painter.svg
# Single <path> element covering the full QR code
svg_path = painter.svg_path
# Generator over <path> elements, one per contiguous region
for path in painter.svg_paths:
print(path)
qrcode, which PolyQR uses to generate the underlying module matrix, can also output SVG via qrcode.svg.SvgPathImage (among others).
SvgPathImage avoids gaps by collecting all modules into a single <path>, but its output is much larger.
For the message https://github.com/KurtBoehm/polyqr:
SvgPathImageoutput: ≈ 6.4 kB (docs/svg-qrcode.svg)QrCodePainter.svgoutput: ≈ 1.7 kB (docs/svg-polyqr.svg)
That is a size reduction of more than 70% with identical geometry.
🧠 Algorithm Overview
PolyQR converts a message into merged polygons in three main stages:
- QR code generation:
- Uses
qrcodeto build a Boolean module matrix for the input message.
- Uses
- Connected components and boundary extraction:
- Runs a 4-neighbour BFS flood fill on the module grid to find connected black regions.
- For each module in a component, its four unit-square edges are added to a
Counterin canonical (sorted-endpoint) form. - Any edge seen exactly once lies on the region’s boundary (outer boundary or hole); interior edges shared by two modules cancel out.
- Cycle tracing and polygon simplification:
- Builds an undirected adjacency graph from the remaining boundary edges.
- For each connected component of this boundary graph, traces a single “wall-hugging” cycle:
- At each step, the walk prefers non-collinear turns over going straight, producing visually pleasing outlines around holes when rounded corners are used.
- If the initial cycle does not visit every vertex of the component, it is iteratively extended by following any remaining unused edges (again preferring turns) until the component is fully covered.
- Each resulting cycle is simplified by removing collinear vertices.
The result is a small set of rectilinear polygons that exactly cover the QR modules.
🧪 Testing
PolyQR includes pytest-based tests for TikZ and SVG output, covering QR code generation, polygon extraction and simplification, and output formatting.
The development dependencies can be installed via the dev optional group:
pip3 install .[dev]
All tests can then be run from the project root:
pytest
The TikZ tests are relatively slow, as they require pdflatex to compile a LaTeX document to PDF, which is then rasterized via PyMuPDF.
To keep test times reasonable, the dev dependencies include pytest-xdist, so tests can be executed in parallel:
pytest -n 8 # or any other number of workers
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file polyqr-1.0.0.tar.gz.
File metadata
- Download URL: polyqr-1.0.0.tar.gz
- Upload date:
- Size: 10.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3395d2ca799b34008e2fcec69f21fa60b1edbc85aa2e5769600354dfe07bbb19
|
|
| MD5 |
1f6e751808a339cc8ddbe55649bb4154
|
|
| BLAKE2b-256 |
551592cca460c41b778226fb990d806fffa006888cf8b61e8b00615ae8f40347
|
File details
Details for the file polyqr-1.0.0-py3-none-any.whl.
File metadata
- Download URL: polyqr-1.0.0-py3-none-any.whl
- Upload date:
- Size: 8.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f91b1154038fb7b326bc55ac51592dbf7b776960db93ea06c3731a708b7db235
|
|
| MD5 |
7ea8cef419260314a18cd587d9c95521
|
|
| BLAKE2b-256 |
3bb67db36ea107e8a1eb690cc19206b3ecc0f7c18919a48cdcdea6edae6bde5a
|