An experimental 2D barcode on a hexagonal grid β with a hexagonal bullseye finder, spiral serialization and a continuously selectable Reed-Solomon error-correction budget of 5β90 %.

from hexatess import encode, decode, render
grid, params = encode("Hello, Hexatess!", ec_pct=30)
render(grid, "hello.png")
text, stats = decode(grid) # ('Hello, Hexatess!', {...})

bit = 1 β ring mod 2),
alternating dark/light rings, and the key β the first two canonical
ring-5 cells set dark, breaking the 60-fold symmetry and marking the
spiral start direction;(β6, +6)),
rendered from the actual reference encoder output.Status: experimental. This is a young format: the symbol specification and reference implementation are solid and heavily tested (2,500+ tests, conformance vectors). A camera decoder (
hexatess.camera, optional[camera]extra) already reads symbols from real photographs in about a second β printed labels, foil transparencies, tilted and rotated shots. Since spec v0.3 payload text is zlib-compressed automatically, so long texts fit into considerably smaller symbols. See the roadmap below. Adopting a young format is a deliberate bet; the full format specification is the insurance.
pip install hexatess-code # from PyPI (once published)
pip install "hexatess-code[camera]" # + photo decoding (numpy, opencv, scipy)
# or from a source checkout:
pip install -e .
Requires Python β₯ 3.8; Pillow for rendering, numpy + OpenCV + SciPy for the optional camera decoder.
hexatess "Hello world" -o koda.png --ec 30
hexatess "Important URL https://example.org" -o url.png --ec 55
hexatess --demo # demo symbol + robustness statistics
hexatess decode photo1.jpg photo2.jpg # read symbols from images/photos
hexatess decode-photo photo1.jpg # same as `decode`
Payload text is zlib-compressed automatically when that saves space
(--no-compress disables it; the header flag keeps decoders fully
backward compatible).
One header bit marks the payload as a zlib stream. The encoder applies it only when it strictly helps, and decoders inflate transparently β symbols without the flag are byte-identical to v0.2. What that means in practice (EC 30 unless noted):
| payload | raw | stored | symbol |
|---|---|---|---|
| 80 digits | 80 B | 21 B | rmax 17 β 11 |
"X" Γ 250 |
250 B | 12 B | rmax 30 β 10 |
| 849-byte Slovene paragraph | 849 B | 203 B | would not fit β rmax 28 |
| short strings (β€ ~30 B) | β | unchanged | overhead wins |
The maximum stored capacity is unchanged (329 bytes at EC 5), so incompressible data behaves exactly as before.
| Function | Description |
|---|---|
encode(text, ec_pct=30, mask_id="auto", min_rings=None, compress="auto") |
UTF-8 text β (grid, params); grid maps axial (q, r) to 0/1 |
decode(grid) |
grid β (text, stats); RS-corrects and inflates transparently |
render(grid, path, size_px=18, ...) |
grid β PNG (pointy-top hexagons, quiet zone, supersampling) |
sample_grid_from_image(path, rmax, ...) |
ideal re-sampling of a rendered PNG (self-test helper) |
run_tests(...) |
noise/blob robustness statistics |
hexatess.camera.decode_photo(path) |
photograph β (text, stats); finder detection, perspective handling, adaptive sampling (optional [camera] extra) |
params / stats contain rmax (radius in rings), mask, ec,
blocks (list of (data_bytes, ecc_bytes)), data_len (stored
length) and compressed; stats also reports repair_bits (the RS
correction ledger) and, for camera decodes, sector and
finder_hits.
Choose any multiple of 5 between 5 and 90:
| EC | Character |
|---|---|
| 5β15 | maximum capacity, clean environments |
| 25β40 | general use (default 30) |
| 50β70 | industrial / outdoor |
| 80β90 | extreme damage tolerance |
Physical behaviour (measured on the reference implementation): one
flipped module is one RS symbol error, so uniform-noise tolerance is
roughly EC / 16 percent of modules, while clustered (smudge/blob)
damage survives several times higher area fractions because flips
concentrate inside whole bytes.
A pure-JavaScript encoder, decoder and image scanner already ship in
this repository β see javascript/ (zero dependencies,
byte-identical to the Python reference for uncompressed symbols) and
the browser playground demo.html at the repository root.
The playground encodes and decodes: it reads clean renders and
real photographs β uneven lighting, camera noise, blur, JPEG
artefacts, glare, arbitrary in-plane rotation and moderate perspective
are absorbed in pure JS (full-circle finder sweep, homography fit,
quadratic correction surface, RS erasure decoding). PNG files are
decoded without a canvas, so the page also works from file://.
Very small prints (a few pixels per cell) and extreme angles remain
with the Python camera pipeline.
The format is deliberately specification-first: everything needed
for an independent implementation is in
SPECIFICATION.md, and
test_vectors/vectors_v0.3.json
contains fixed inputs/outputs (grids, headers, damaged symbols, expected
results) to verify conformance. If your Rust/Go/JS decoder passes the
vectors, it speaks Hexatess Code.
hexatess.camera
reads symbols from photographs β bullseye detection, homography +
correction-field warp handling, adaptive sampling; validated on
printed foil with curl and glare. v0.3.1: β10Γ faster
(a typical 12 MP photo now takes about a second) plus stable
outer-ring sampling and mis-decode-proof pose selection.javascript/ +
demo.html; hosted playground (GitHub Pages) next.Contributions welcome β see CONTRIBUTING.md.
Hexatess Code stands on the shoulders of giants: Aztec Code (bullseye + spiral), MaxiCode (hexagonal lattice), QR Code and Data Matrix (Reed-Solomon practice).