Skip to content

[Draft/RFC] Experimental Micro QR Code support (M2-M4 numeric)#426

Draft
nssmd wants to merge 1 commit into
lincolnloop:mainfrom
nssmd:feature/micro-qr-experimental
Draft

[Draft/RFC] Experimental Micro QR Code support (M2-M4 numeric)#426
nssmd wants to merge 1 commit into
lincolnloop:mainfrom
nssmd:feature/micro-qr-experimental

Conversation

@nssmd
Copy link
Copy Markdown

@nssmd nssmd commented May 10, 2026

[Draft / RFC] Experimental Micro QR Code support (M2-M4 numeric)

Branch: feature/micro-qr-experimental
Base: lincolnloop/python-qrcode:main
Type: New feature, experimental
Status: Draft — opened for design feedback before scope expansion.

What this adds

A new module qrcode.micro that encodes data as a Micro QR Code
per ISO/IEC 18004:2006 Annex F. The first iteration covers the
practically-most-useful subset:

Item Supported Notes
Versions M2 (13×13), M3 (15×15), M4 (17×17) M1 left for a follow-up
Modes Numeric Alphanumeric / byte queued
ECC L, M, plus Q on M4 Matches Annex F Table 7
Output matrix (list of int) and to_ascii() PIL / SVG factories TBD
from qrcode.micro import MicroQRCode, MICRO_ECC_L

mqr = MicroQRCode("01234567", error_correction=MICRO_ECC_L)
print(mqr.to_ascii())
matrix = mqr.matrix          # 13×13 grid of 0/1 ints
print(f"M{mqr.version}, mask {mqr._mask_used}")

Why side-by-side, not bolted onto QRCode

Micro QR has structurally different module placement, format-info
encoding, mask functions, and capacity tables than full QR. Forcing
those branches into QRCode would introduce many if micro: ... else:
arms in code that today is straight-line. Instead this PR introduces a
sibling class MicroQRCode in a separate module, reusing qrcode.base
for the GF(256) arithmetic and Reed-Solomon polynomial machinery.

If you'd prefer a factory entry point on QRCode, that's a small
follow-up — the implementation here doesn't preclude it.

Verification

13 unit tests generate symbols, render to PIL, and round-trip through
zxing-cpp
which natively
supports Micro QR. Every variant tested decodes back to the original
payload byte-for-byte:

✓ M2  input='01234567'                          -> decoded='01234567'
✓ M2  input='12345'                             -> decoded='12345'
✓ M2  input='0123456789'                        -> decoded='0123456789'
✓ M3  input='012345678901234567'                -> decoded='012345678901234567'
✓ M4  input='01234567890123456789012345678901234' -> decoded=… (35 digits)

Tests are gated with pytest.importorskip("zxingcpp") so CI runners
without the optional dependency simply skip them.

Implementation notes

  • GF(256): Reuses EXP_TABLE, LOG_TABLE, gexp, glog, and the
    Polynomial class from qrcode.base. No duplicated arithmetic.
  • Format info: Implements BCH(15,5) with the Micro-QR-specific
    generator G(x) = x^10 + x^4 + x^3 + x^2 + x + 1 (= 0x537) and
    mask 0x4445 (Annex F.5). This is different from the full-QR
    BCH used by BCH_type_info — they are not interchangeable.
  • Masks: Implements all four Micro-QR mask patterns from Annex F.4
    Table F.4 and the Annex F.5 mask-selection score.
  • Version selection: _best_fit() picks the smallest M2/M3/M4
    whose data capacity (after mode indicator + character count
    indicator + terminator) fits the payload. Raises ValueError
    with "capacity" in the message on overflow — distinct from
    DataOverflowError to avoid implying full-QR semantics.

Backward compatibility

Zero. New module, new class, new tests. Touches no existing public
API. Optional zxing-cpp test dependency is not added to
pyproject.toml — it's only used when present.

What's not in this PR

Intentionally out of scope, queued as follow-ups:

  • M1 (3-bit CCI, no mode indicator, 4-bit terminator)
  • Alphanumeric and byte modes
  • PIL / pypng / SVG image factories
  • Additional ISO 18004 Annex F worked-vector tests
  • A factory_kind="micro" entry on the existing qrcode.make /
    QRCode if the maintainers prefer one entry point.

Why this is a Draft / RFC

I'd like maintainer feedback before expanding scope. Specifically:

  1. Is qrcode.micro the right placement, or should this live under
    qrcode.experimental.micro (or similar) until promoted?
  2. API shape: MicroQRCode(data, version, error_correction) vs.
    reusing QRCode with a format= flag. I picked the former because
    the symbology really is different; happy to refactor.
  3. zxing-cpp for tests: optional dependency vs. a dependency-free
    reference checker (the round-trip is the convincing test, but
    committing a fixture-based byte-level check against ISO Annex F
    examples is also feasible).

Discovered & built by the python-qrcode testing study group as part
of the same engagement that produced the bug-fix PR (#425). The
Micro QR work was driven by user demand — a few payloads in the
test corpus are short enough that M2/M3 would yield substantially
smaller symbols than v1 full-QR.

Adds a new module `qrcode.micro` providing experimental Micro QR Code
support per ISO/IEC 18004:2006 Annex F. The first iteration covers
the most common subset:

  - Versions M2, M3, M4
  - Numeric mode end-to-end
  - Error-correction levels L and M (plus Q on M4)
  - 0/1 matrix and ASCII rendering (image factories TBD)

Implementation
--------------
* Reuses the existing GF(256) arithmetic and `Polynomial` class from
  `qrcode.base` for Reed-Solomon parity computation.
* Implements format-info BCH(15,5) with the Micro-QR generator
  (G(x) = x^10+x^4+x^3+x^2+x+1, mask 0x4445) — distinct from the
  full-QR BCH used elsewhere in the library.
* Implements the four Micro-QR mask patterns from Annex F.4 and the
  Annex F.5 mask-evaluation score.
* Auto-version selection: smallest M2/M3/M4 that fits the payload
  for the requested ECC level.

Verification
------------
13 round-trip tests (`qrcode/tests/test_micro.py`) generate a symbol,
render it as a PIL image, and decode it back through `zxing-cpp`,
which natively supports Micro QR. All five payload sizes covering
M2/M3/M4 round-trip identically. `zxing-cpp` is gated with
`pytest.importorskip` so the tests are skipped on CI runners that
don't have it installed.

Public API
----------
    from qrcode.micro import MicroQRCode, MICRO_ECC_L
    mqr = MicroQRCode("01234567", error_correction=MICRO_ECC_L)
    print(mqr.to_ascii())
    matrix = mqr.matrix  # list[list[int]] of 0/1

Status: EXPERIMENTAL
--------------------
The module is intentionally side-by-side with `qrcode.QRCode` rather
than altering it, so we can iterate on the API in `qrcode.micro`
without affecting the stable codepath. Follow-ups before promoting:

  - M1 (3-bit CCI, no mode indicator, 4-bit terminator)
  - Alphanumeric and byte modes
  - PIL / SVG image factories (currently ASCII / matrix only)
  - Additional spec-vector tests against ISO/IEC 18004 Annex F figures

This PR is opened as a Draft / RFC: happy to split, scope down, or
land behind a `qrcode.experimental` namespace per maintainer
preference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant