Skip to main content

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.

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.

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

polyqr-1.1.3.tar.gz (18.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

polyqr-1.1.3-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

File details

Details for the file polyqr-1.1.3.tar.gz.

File metadata

  • Download URL: polyqr-1.1.3.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

Hashes for polyqr-1.1.3.tar.gz
Algorithm Hash digest
SHA256 6fe4a13c464b725589d3c5775c42cf8470d299a2f4f8f9b24b794a561585a86f
MD5 db132071ae06198ce4bc83a2a0f4605c
BLAKE2b-256 82f2fa269144e866aa7679db2a4775915f51a012a63aa14b22babaa72b5715ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for polyqr-1.1.3.tar.gz:

Publisher: publish-to-pypi.yml on KurtBoehm/polyqr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file polyqr-1.1.3-py3-none-any.whl.

File metadata

  • Download URL: polyqr-1.1.3-py3-none-any.whl
  • Upload date:
  • Size: 14.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for polyqr-1.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 f829a649b8abdfba870cbcd9cbc96d2f594ed6e358a61c5f4c7a8e819be9eb46
MD5 930618679c9fe42c378311464a2432e9
BLAKE2b-256 c1354761fa92a2bf8265d789841046493236f65821c2c7d5285d5e54afcfb92d

See more details on using hashes here.

Provenance

The following attestation bundles were made for polyqr-1.1.3-py3-none-any.whl:

Publisher: publish-to-pypi.yml on KurtBoehm/polyqr

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page