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 hideous hairline gaps between modules that appear with naive 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 pytest-based test suite with 100% coverage (for both TikZ and SVG) is defined in the tests directory.
📦 Installation
This package is available on PyPI and can be installed with pip:
pip install polyqr
🖼️ TikZ Output
PolyQR provides the command-line tool polyqr_tikz, which can be called as follows:
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}
If the optional argument --full-size is added, the size parameter (1mm in the previous example) does not specify the size of each module (as it does by default), but the size of the entire QR code.
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 can be seen when comparing to a basic version that draws each module as a separate rectangle (full-screen viewing is advised):
| Basic | PolyQR | PolyQR with rounded corners |
|---|---|---|
The LaTeX file used to generate these examples is at docs/tikz.tex.
TikZ code can also be generated directly from Python:
from polyqr import QrCodePainter
painter = QrCodePainter("https://github.com/KurtBoehm/polyqr")
print(painter.tikz(size="1mm", style="rounded corners=0.25mm", full_size=False))
🖼️ SVG Output
PolyQR can also generate highly minimized SVG paths:
- 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).
- 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 making a turn 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 that cover the entire code base with a 100% code coverage.
The development dependencies can be installed via the dev optional group:
pip install .[dev]
All tests (including coverage reporting using pytest-cov) can then be run from the project root:
pytest --cov
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 --cov -n auto # or a fixed number of workers
📜 License
This library is licensed under the terms of the Mozilla Public License 2.0, provided in License.
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.1.2.tar.gz.
File metadata
- Download URL: polyqr-1.1.2.tar.gz
- Upload date:
- Size: 18.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62297a75157d89e45734b5a7d88895879a17da474e2c8f7bd89eada37e72a255
|
|
| MD5 |
6b7d97f9a93449b32312ac475260a652
|
|
| BLAKE2b-256 |
6172e38df1382eb9b7de583248be26c44c2e5820eed22b0b01172a15390b8c1f
|
Provenance
The following attestation bundles were made for polyqr-1.1.2.tar.gz:
Publisher:
publish-to-pypi.yml on KurtBoehm/polyqr
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polyqr-1.1.2.tar.gz -
Subject digest:
62297a75157d89e45734b5a7d88895879a17da474e2c8f7bd89eada37e72a255 - Sigstore transparency entry: 792325218
- Sigstore integration time:
-
Permalink:
KurtBoehm/polyqr@f5f1e3e902bbfc37050264bd23a39355ca230114 -
Branch / Tag:
refs/tags/1.1.2 - Owner: https://github.com/KurtBoehm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@f5f1e3e902bbfc37050264bd23a39355ca230114 -
Trigger Event:
release
-
Statement type:
File details
Details for the file polyqr-1.1.2-py3-none-any.whl.
File metadata
- Download URL: polyqr-1.1.2-py3-none-any.whl
- Upload date:
- Size: 14.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2eb65b6f1624b6d4516a56529851fa315637e6a6443bdfecf5fe776a7ad78a25
|
|
| MD5 |
543bc32b2f10c653b4a87621e7f19187
|
|
| BLAKE2b-256 |
e00018b9b2748b08a54d42191ad0d42fa8f3546e96dd6179fc14c66506fcd105
|
Provenance
The following attestation bundles were made for polyqr-1.1.2-py3-none-any.whl:
Publisher:
publish-to-pypi.yml on KurtBoehm/polyqr
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polyqr-1.1.2-py3-none-any.whl -
Subject digest:
2eb65b6f1624b6d4516a56529851fa315637e6a6443bdfecf5fe776a7ad78a25 - Sigstore transparency entry: 792325267
- Sigstore integration time:
-
Permalink:
KurtBoehm/polyqr@f5f1e3e902bbfc37050264bd23a39355ca230114 -
Branch / Tag:
refs/tags/1.1.2 - Owner: https://github.com/KurtBoehm
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@f5f1e3e902bbfc37050264bd23a39355ca230114 -
Trigger Event:
release
-
Statement type: