Skip to main content

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

Tests with 100% coverage

📦 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
Basic TikZ PolyQR TikZ Rounded PolyQR TikZ

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 using fill-rule="evenodd" to handle holes.
  • All segments are axis-aligned and therefore encoded using only M, H, V, and Z commands.
  • 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:

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:

  1. QR code generation:
    • Uses qrcode to build a Boolean module matrix for the input message.
  2. 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 Counter in canonical (sorted-endpoint) form.
    • Any edge seen exactly once lies on the region’s boundary (outer boundary or hole).
  3. 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.

Metadata

Release files for polyqr 1.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for polyqr 1.1.3
File Size Uploaded
polyqr-1.1.3.tar.gz 18.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for polyqr 1.1.3
File Interpreter ABI Platform
polyqr-1.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 32.2 kB

Release files / polyqr-1.1.3.tar.gz

Download URL polyqr-1.1.3.tar.gz
Size 18.0 kB
Tags Source
SHA-256 checksum
How to use checksums
6fe4a13c464b725589d3c5775c42cf8470d299a2f4f8f9b24b794a561585a86f
BLAKE2b-256 checksum
How to use checksums
82f2fa269144e866aa7679db2a4775915f51a012a63aa14b22babaa72b5715ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jan 11, 2026.

Transparency log

Release files / polyqr-1.1.3-py3-none-any.whl

Download URL polyqr-1.1.3-py3-none-any.whl
Size 14.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f829a649b8abdfba870cbcd9cbc96d2f594ed6e358a61c5f4c7a8e819be9eb46
BLAKE2b-256 checksum
How to use checksums
c1354761fa92a2bf8265d789841046493236f65821c2c7d5285d5e54afcfb92d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jan 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.3 This release

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page