hexatess-code

Hexatess Code — JavaScript encoder, decoder & image scanner

Pure-JavaScript, zero-dependency encoder, decoder and image scanner for Hexatess Code (specification v0.3). All are 1:1 ports of the reference Python implementation: the encoder produces byte-identical symbols for uncompressed payloads (compressed payloads use a self-contained fixed-Huffman DEFLATE compressor that any spec-conforming inflator, including Python’s zlib, decodes), the decoder reads back any conforming symbol — including those written by Python’s zlib -9 (dynamic-Huffman DEFLATE) — and the scanner locates and reads symbols inside clean renders and photographs.

The browser playground demo.html lives at the repository root (one level up) — double-click it, no server needed: live encoding with SVG/PNG export, instant decode round trip, and decoding of uploaded images (PNG fully in JavaScript, so it also works on file:// pages; photos with uneven lighting, noise, mild tilt and 60°-step rotations are handled).

Files

File Purpose
hexatess-encoder.js Encoder library (UMD: browser global Hexatess + Node require)
hexatess-decoder.js Decoder library (browser global HexatessDecode, merged into Hexatess; Node require)
hexatess-scanner.js Image scanner + pure-JS PNG decoder (browser global HexatessScan, merged into the others; Node require)
../demo.html Playground — encode + decode, at the repository root
test_encoder.js Encoder conformance tests (node test_encoder.js)
test_decoder.js Decoder conformance tests (node test_decoder.js)
test_scanner.js Scanner tests, incl. photo-like conditions (node test_scanner.js)
package.json Metadata for a future npm release

Usage — browser

<script src="javascript/hexatess-encoder.js"></script>
<script src="javascript/hexatess-decoder.js"></script>
<script src="javascript/hexatess-scanner.js"></script>
<script>
  var out = Hexatess.encode("Hello, Hexatess!", { ecPct: 30 });
  document.body.innerHTML = Hexatess.renderSVG(out.grid);

  var dec = Hexatess.decode(out.grid);          // merged from HexatessDecode
  console.log(dec.text);                        // "Hello, Hexatess!"
  console.log(dec.stats);                       // { rmax, mask, ec, blocks,
                                                //  dataLen, compressed, repairBits }
</script>

Usage — Node.js

const Hexatess = require("./hexatess-encoder.js");
const HexatessDecode = require("./hexatess-decoder.js");

const { grid, params } = Hexatess.encode("Zdravo 🐝", { ecPct: 25 });
const { text, stats } = HexatessDecode.decode(grid);
console.log(text === "Zdravo 🐝", stats.repairBits);   // true 0

Encoder API

encode(input, options?) → { grid, params }

Option Default Meaning
input string (UTF-8 encoded) or Uint8Array of bytes
options.ecPct 30 Error-correction budget, 5–90 in steps of 5
options.mask "auto" Force mask 0–7, or let the encoder pick
options.minRings Force a minimum symbol radius (1–31)
options.compress "auto" "auto" = deflate when strictly smaller; true = force; false = raw UTF-8 (byte-identical to spec v0.2 symbols)

grid is a Map keyed by axial coordinates "q,r" with values 0|1 (1 = dark module). params reports rmax, mask, ec, blocks ([dataBytes, eccBytes] pairs), dataLen (stored bytes) and compressed.

renderSVG(grid, options?) → string

Options: size (module radius px, default 18), quiet (quiet zone in modules, default 1.5), dark, light (CSS colors), background (false disables the light rectangle).

Helpers

canonicalHex(grid, rmax) — canonical bitstream hex (the conformance format used by test_vectors/vectors_v0.3.json). gridToJSON(grid) — plain object {"q,r": bit}. Hexatess.internals — GF(256), Reed-Solomon, header packing, masks, geometry and the DEFLATE compressor, exposed for testing and for third-party implementations.

Decoder API

decode(grid, options?) → { text, stats }

grid is the encoder’s Map or a plain object with "q,r" keys. Throws on uncorrectable damage, damaged zlib streams or invalid UTF-8. stats mirrors the Python decoder: rmax, mask, ec, blocks, dataLen, compressed and repairBits (total Hamming distance the Reed–Solomon layer had to correct inside the data bytes; header and ECC repairs are absorbed silently).

Erasure decoding (v0.4.3): pass options.erasures — an array of "q,r" keys (or a Set) marking cells the sampler could not read confidently. Every payload byte that contains an erased cell is then corrected as an erasure, which costs 1 EC symbol instead of 2 (2e + v ≤ ecc per block; Forney syndromes + erasure-aware Berlekamp–Massey). Header bytes can be erased as well (RS(10,5)). Unknown keys are ignored; stats.erasedBits counts the erased bits that landed inside header/payload bytes. Without the option the behaviour is identical to earlier releases.

decodeHex(hex, rmax?), gridFromHex(hex, rmax?), payloadToText(bytes, compressed)

gridFromHex parses the canonical conformance hex (rmax inferred from the length when omitted); decodeHex = decode(gridFromHex(...)). payloadToText inflates (when compressed) and strictly validates UTF-8. HexatessDecode.internals exposes the RS decoder (syndromes → Berlekamp–Massey → Chien → Forney), the full zlib/DEFLATE inflator and the strict UTF-8 decoder.

Scanner API

scanImageData(img, options?) → { text, stats, scan }

img is { width, height, data } with data a RGBA Uint8Array (a canvas ImageData works as-is). Returns the decoder result plus a scan report: { cx, cy, s, rmax, rotation, candidate, homo, finder, sampling, erasures, … }homo: true marks a photo-mode decode, sampling is plain | jitter | poly. Options: normalize (default true), rotations (default true), onAttempt (progress callback). Throws when no candidate decodes.

Robust to uneven illumination, camera noise, blur, JPEG artefacts, glare, busy backgrounds, arbitrary in-plane rotation and moderate perspective: photo mode sweeps the full circle with a 91-cell margin objective, fits an 8-DOF homography (pattern search + DLT refinement), samples cells through its Jacobian with median-reading discs, re-samples through a quadratic correction surface fitted on the finder + RS-verified header cells, marks unresolvable cells as RS erasures, and retries tiny prints on a bilinear zoom crop. Very small prints (a few pixels per cell) and extreme angles remain the domain of the Python hexatess decode camera pipeline.

decodePNG(bytes) → { width, height, data }

Pure-JS PNG decoder (8/16-bit; gray, RGB, palette, gray+alpha, RGBA; non-interlaced). Lets a browser decode PNG uploads without a canvas — no file:// tainted-canvas SecurityError — and works in Node for testing.

downscaleImageData(img, maxDim) → img

Integer box downscale used to cap huge photos before scanning.

Usage — Node

const Scan = require("./hexatess-scanner.js");
const png = Scan.decodePNG(fs.readFileSync("koda.png"));
const { text, scan } = Scan.scanImageData(Scan.downscaleImageData(png, 2000));
console.log(text, scan.rotation, scan.s);

Conformance status

Rendering notes

The SVG renderer draws the same pointy-top hexagonal lattice as the Python renderer (x = s·√3·(q + r/2), y = s·1.5·r), with the same default colors (dark #181612 = RGB 24,22,18) and a 1.5-module quiet zone. demo.html additionally exports PNG via canvas with 3× supersampling, mirroring the Python PNG pipeline, and its Decode panel runs the full image scanner from hexatess-scanner.js.