hexatess-code 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hexatess/__init__.py +83 -0
- hexatess/cli.py +65 -0
- hexatess/decoder.py +75 -0
- hexatess/encoder.py +122 -0
- hexatess/galois.py +82 -0
- hexatess/geometry.py +69 -0
- hexatess/header.py +120 -0
- hexatess/masks.py +48 -0
- hexatess/reedsolomon.py +161 -0
- hexatess/render.py +75 -0
- hexatess/resilience.py +95 -0
- hexatess_code-0.1.0.dist-info/METADATA +192 -0
- hexatess_code-0.1.0.dist-info/RECORD +17 -0
- hexatess_code-0.1.0.dist-info/WHEEL +5 -0
- hexatess_code-0.1.0.dist-info/entry_points.txt +2 -0
- hexatess_code-0.1.0.dist-info/licenses/LICENSE +30 -0
- hexatess_code-0.1.0.dist-info/top_level.txt +1 -0
hexatess/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Hexatess Code - an experimental 2D barcode on a hexagonal grid.
|
|
2
|
+
|
|
3
|
+
Reference implementation of specification v0.1. The symbol is a
|
|
4
|
+
hexagonal lattice with a hexagonal bullseye finder, an
|
|
5
|
+
orientation key ring, spiral serialization from the centre outwards,
|
|
6
|
+
a Reed-Solomon protected header and a continuously selectable
|
|
7
|
+
error-correction budget of 5-90 percent (Aztec-style).
|
|
8
|
+
|
|
9
|
+
Quick start
|
|
10
|
+
-----------
|
|
11
|
+
>>> from hexatess import encode, decode, render
|
|
12
|
+
>>> grid, params = encode("Hello, Hexatess!", ec_pct=30)
|
|
13
|
+
>>> params["rmax"], params["mask"], params["data_len"]
|
|
14
|
+
(11, 6, 16)
|
|
15
|
+
>>> render(grid, "hello.png")
|
|
16
|
+
'hello.png'
|
|
17
|
+
>>> decode(grid)[0]
|
|
18
|
+
'Hello, Hexatess!'
|
|
19
|
+
|
|
20
|
+
See SPECIFICATION.md in the repository for the full format
|
|
21
|
+
specification, and test_vectors/vectors_v0.1.json for conformance
|
|
22
|
+
data usable by independent implementations.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .decoder import decode
|
|
28
|
+
from .encoder import encode
|
|
29
|
+
from .geometry import (
|
|
30
|
+
DIRS,
|
|
31
|
+
hex_corner,
|
|
32
|
+
hex_distance,
|
|
33
|
+
hex_ring,
|
|
34
|
+
hex_to_pixel,
|
|
35
|
+
ring_capacity,
|
|
36
|
+
)
|
|
37
|
+
from .header import (
|
|
38
|
+
BULLSEYE_RINGS,
|
|
39
|
+
BLOCK_DATA_MAX,
|
|
40
|
+
DATA_RING0,
|
|
41
|
+
KEY_RING,
|
|
42
|
+
MAX_DATA_BYTES,
|
|
43
|
+
MAX_EC_PCT,
|
|
44
|
+
MAX_RINGS,
|
|
45
|
+
MIN_EC_PCT,
|
|
46
|
+
MODE_BITS,
|
|
47
|
+
MODE_BYTES,
|
|
48
|
+
MODE_ECC,
|
|
49
|
+
bits_to_bytes,
|
|
50
|
+
bytes_to_bits,
|
|
51
|
+
pack_mode,
|
|
52
|
+
plan_blocks,
|
|
53
|
+
unpack_mode,
|
|
54
|
+
)
|
|
55
|
+
from .masks import evaluate_mask, mask_bit, mask_payload, select_mask
|
|
56
|
+
from .reedsolomon import rs_correct_msg, rs_encode_msg
|
|
57
|
+
from .render import render, sample_grid_from_image
|
|
58
|
+
from .resilience import add_blob_damage, add_random_noise, run_tests
|
|
59
|
+
|
|
60
|
+
__version__ = "0.1.0"
|
|
61
|
+
SPEC_VERSION = "0.1"
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
# high-level API
|
|
65
|
+
"encode", "decode", "render", "sample_grid_from_image",
|
|
66
|
+
"run_tests", "add_random_noise", "add_blob_damage",
|
|
67
|
+
# geometry
|
|
68
|
+
"DIRS", "hex_ring", "hex_distance", "hex_to_pixel", "hex_corner",
|
|
69
|
+
"ring_capacity",
|
|
70
|
+
# framing
|
|
71
|
+
"bytes_to_bits", "bits_to_bytes", "pack_mode", "unpack_mode",
|
|
72
|
+
"plan_blocks",
|
|
73
|
+
# masks
|
|
74
|
+
"mask_bit", "mask_payload", "evaluate_mask", "select_mask",
|
|
75
|
+
# error correction
|
|
76
|
+
"rs_encode_msg", "rs_correct_msg",
|
|
77
|
+
# constants
|
|
78
|
+
"BULLSEYE_RINGS", "KEY_RING", "DATA_RING0", "MAX_RINGS",
|
|
79
|
+
"BLOCK_DATA_MAX", "MODE_BYTES", "MODE_ECC", "MODE_BITS",
|
|
80
|
+
"MIN_EC_PCT", "MAX_EC_PCT", "MAX_DATA_BYTES",
|
|
81
|
+
# meta
|
|
82
|
+
"__version__", "SPEC_VERSION",
|
|
83
|
+
]
|
hexatess/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Command-line interface for Hexatess Code.
|
|
2
|
+
|
|
3
|
+
Examples
|
|
4
|
+
--------
|
|
5
|
+
Encode text to PNG::
|
|
6
|
+
|
|
7
|
+
hexatess-code "Hello world" -o koda.png --ec 30
|
|
8
|
+
|
|
9
|
+
Render a demo symbol and run robustness tests::
|
|
10
|
+
|
|
11
|
+
hexatess-code --demo
|
|
12
|
+
hexatess-code --test
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
|
|
19
|
+
from .decoder import decode
|
|
20
|
+
from .encoder import encode
|
|
21
|
+
from .render import render, sample_grid_from_image
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv=None):
|
|
25
|
+
ap = argparse.ArgumentParser(
|
|
26
|
+
prog="hexatess",
|
|
27
|
+
description="Hexatess Code - hexagonal-grid 2D barcode "
|
|
28
|
+
"(reference implementation, spec v0.1)")
|
|
29
|
+
ap.add_argument("text", nargs="?", help="text to encode (UTF-8)")
|
|
30
|
+
ap.add_argument("-o", "--output", default="hexatess.png",
|
|
31
|
+
help="output PNG path (default: %(default)s)")
|
|
32
|
+
ap.add_argument("--ec", type=int, default=30,
|
|
33
|
+
help="error-correction %% (5-90, step 5; default 30)")
|
|
34
|
+
ap.add_argument("--mask", type=int, default=None, metavar="0-7",
|
|
35
|
+
help="force a mask instead of automatic selection")
|
|
36
|
+
ap.add_argument("--size", type=int, default=18,
|
|
37
|
+
help="hex radius in pixels (default %(default)s)")
|
|
38
|
+
ap.add_argument("--test", action="store_true",
|
|
39
|
+
help="run robustness tests")
|
|
40
|
+
ap.add_argument("--demo", action="store_true",
|
|
41
|
+
help="render a demo symbol and run robustness tests")
|
|
42
|
+
args = ap.parse_args(argv)
|
|
43
|
+
|
|
44
|
+
if args.test or args.demo:
|
|
45
|
+
from .resilience import run_tests
|
|
46
|
+
run_tests()
|
|
47
|
+
|
|
48
|
+
if args.demo or args.text:
|
|
49
|
+
text = args.text or "Hexatess Code v0.1 - " * 6
|
|
50
|
+
grid, params = encode(text, ec_pct=args.ec, mask_id="auto"
|
|
51
|
+
if args.mask is None else args.mask)
|
|
52
|
+
render(grid, args.output, size_px=args.size)
|
|
53
|
+
print("\nRendered: %s" % args.output)
|
|
54
|
+
print(" version (rings): %d, EC: %d%%, mask: %d, bytes: %d"
|
|
55
|
+
% (params["rmax"], params["ec"], params["mask"],
|
|
56
|
+
params["data_len"]))
|
|
57
|
+
grid2 = sample_grid_from_image(args.output, params["rmax"],
|
|
58
|
+
size_px=args.size)
|
|
59
|
+
t, _st = decode(grid2)
|
|
60
|
+
print(" self-decode from image: %s"
|
|
61
|
+
% ("OK" if t == text else "FAILED"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
hexatess/decoder.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Hexatess Code decoder (ideal-sampling reference).
|
|
2
|
+
|
|
3
|
+
Accepts a module grid ``{(q, r): 0|1}`` (as produced by the encoder or
|
|
4
|
+
sampled from an image) and returns the decoded UTF-8 text together with
|
|
5
|
+
the header parameters. Any symbol error correctable by Reed-Solomon is
|
|
6
|
+
absorbed transparently.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .geometry import hex_distance, hex_ring
|
|
12
|
+
from .header import (
|
|
13
|
+
DATA_RING0,
|
|
14
|
+
MODE_BITS,
|
|
15
|
+
bits_to_bytes,
|
|
16
|
+
plan_blocks,
|
|
17
|
+
unpack_mode,
|
|
18
|
+
)
|
|
19
|
+
from .masks import mask_bit
|
|
20
|
+
from .reedsolomon import rs_correct_msg
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def decode(grid, params=None):
|
|
24
|
+
"""Decode a module grid into text.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
grid : dict
|
|
29
|
+
Mapping ``{(q, r): 0|1}``.
|
|
30
|
+
params : dict, optional
|
|
31
|
+
Ignored; kept for API symmetry with v0.1 of the prototype.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
(text, stats)
|
|
36
|
+
``text`` is the decoded UTF-8 string; ``stats`` reports the
|
|
37
|
+
parameters read from the header (``rmax``, ``mask``, ``ec``,
|
|
38
|
+
``blocks``, ``data_len``).
|
|
39
|
+
"""
|
|
40
|
+
cells = sorted(grid.keys(), key=lambda c: hex_distance(*c))
|
|
41
|
+
rmax = max(hex_distance(*c) for c in cells)
|
|
42
|
+
data_cells = [c for k in range(DATA_RING0, rmax + 1)
|
|
43
|
+
for c in hex_ring(k)]
|
|
44
|
+
bits = [grid[c] for c in data_cells]
|
|
45
|
+
|
|
46
|
+
# --- protected header
|
|
47
|
+
mode_bits = bits[:MODE_BITS]
|
|
48
|
+
mode = unpack_mode(bits_to_bytes(mode_bits))
|
|
49
|
+
rmax_m, mask_id, ec_pct, block_count, data_len = mode
|
|
50
|
+
|
|
51
|
+
# --- masked payload
|
|
52
|
+
payload_bits = bits[MODE_BITS:MODE_BITS + (data_len + sum(
|
|
53
|
+
e for _, e in plan_blocks(data_len, ec_pct))) * 8]
|
|
54
|
+
payload_bits = [b ^ mask_bit(i, mask_id)
|
|
55
|
+
for i, b in enumerate(payload_bits)]
|
|
56
|
+
stream = bits_to_bytes(payload_bits)
|
|
57
|
+
|
|
58
|
+
# --- per-block Reed-Solomon correction
|
|
59
|
+
blocks = plan_blocks(data_len, ec_pct)
|
|
60
|
+
out = bytearray()
|
|
61
|
+
pos_d = 0
|
|
62
|
+
pos_e = 0
|
|
63
|
+
ecc_total = sum(e for _, e in blocks)
|
|
64
|
+
data_bytes = stream[:data_len]
|
|
65
|
+
ecc_bytes = stream[data_len:data_len + ecc_total]
|
|
66
|
+
for size, ecc in blocks:
|
|
67
|
+
cw = list(data_bytes[pos_d:pos_d + size]) + \
|
|
68
|
+
list(ecc_bytes[pos_e:pos_e + ecc])
|
|
69
|
+
out += bytes(rs_correct_msg(cw, ecc))
|
|
70
|
+
pos_d += size
|
|
71
|
+
pos_e += ecc
|
|
72
|
+
text = bytes(out).decode("utf-8")
|
|
73
|
+
stats = {"rmax": rmax_m, "mask": mask_id, "ec": ec_pct,
|
|
74
|
+
"blocks": block_count, "data_len": data_len}
|
|
75
|
+
return text, stats
|
hexatess/encoder.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Hexatess Code encoder.
|
|
2
|
+
|
|
3
|
+
Turns UTF-8 text into a hexagonal module grid ``{(q, r): 0|1}``.
|
|
4
|
+
|
|
5
|
+
Layout summary (specification v0.1):
|
|
6
|
+
|
|
7
|
+
* rings 0..4 -- hexagonal bullseye finder, ring ``k`` filled with
|
|
8
|
+
``k mod 2`` (bit 1 = dark module; the centre module is LIGHT in v0.1);
|
|
9
|
+
* ring 5 -- orientation key: all modules light except the first two
|
|
10
|
+
cells of ``hex_ring(5)`` (``(-5, 5)`` and ``(-4, 5)``) which are dark;
|
|
11
|
+
* rings 6..rmax -- payload bits in spiral order: header (80 bits,
|
|
12
|
+
unmasked) followed by the masked payload stream; unused tail cells
|
|
13
|
+
are padded with an alternating 0,1,... pattern.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .geometry import hex_ring, ring_capacity
|
|
19
|
+
from .header import (
|
|
20
|
+
BULLSEYE_RINGS,
|
|
21
|
+
DATA_RING0,
|
|
22
|
+
KEY_RING,
|
|
23
|
+
MAX_DATA_BYTES,
|
|
24
|
+
MAX_EC_PCT,
|
|
25
|
+
MAX_RINGS,
|
|
26
|
+
MIN_EC_PCT,
|
|
27
|
+
MODE_BITS,
|
|
28
|
+
bytes_to_bits,
|
|
29
|
+
pack_mode,
|
|
30
|
+
plan_blocks,
|
|
31
|
+
)
|
|
32
|
+
from .masks import select_mask
|
|
33
|
+
from .reedsolomon import rs_encode_msg
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def encode(text: str, ec_pct: int = 30, mask_id="auto", min_rings=None):
|
|
37
|
+
"""Encode UTF-8 ``text`` into a hexagonal grid.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
text : str
|
|
42
|
+
Payload, up to 4095 UTF-8 bytes.
|
|
43
|
+
ec_pct : int
|
|
44
|
+
Error-correction budget as a percentage of data bytes,
|
|
45
|
+
5..90 in steps of 5 (continuous Aztec-style choice).
|
|
46
|
+
mask_id : int | "auto"
|
|
47
|
+
Force a mask (0..7) or let the encoder pick the best.
|
|
48
|
+
min_rings : int | None
|
|
49
|
+
Force a minimum symbol radius (1..31), e.g. for uniform look.
|
|
50
|
+
|
|
51
|
+
Returns
|
|
52
|
+
-------
|
|
53
|
+
(grid, params)
|
|
54
|
+
``grid`` maps axial coordinates ``(q, r)`` to 0/1.
|
|
55
|
+
``params`` is a dict with keys ``rmax``, ``mask``, ``ec``,
|
|
56
|
+
``blocks`` (list of ``(size, ecc)``) and ``data_len``.
|
|
57
|
+
"""
|
|
58
|
+
if not MIN_EC_PCT <= ec_pct <= MAX_EC_PCT or ec_pct % 5:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"ec_pct must be %d..%d in steps of %d"
|
|
61
|
+
% (MIN_EC_PCT, MAX_EC_PCT, 5))
|
|
62
|
+
data = text.encode("utf-8")
|
|
63
|
+
if len(data) > MAX_DATA_BYTES:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"v0.1 supports up to %d bytes of data" % MAX_DATA_BYTES)
|
|
66
|
+
blocks = plan_blocks(len(data), ec_pct)
|
|
67
|
+
ecc_total = sum(e for _, e in blocks)
|
|
68
|
+
need_bits = MODE_BITS + (len(data) + ecc_total) * 8
|
|
69
|
+
|
|
70
|
+
rmax = max(DATA_RING0 + 1, min_rings or 0)
|
|
71
|
+
while ring_capacity(DATA_RING0, rmax) < need_bits:
|
|
72
|
+
rmax += 1
|
|
73
|
+
if rmax > MAX_RINGS:
|
|
74
|
+
raise ValueError("data does not fit (symbol too large)")
|
|
75
|
+
|
|
76
|
+
# --- payload (unmasked at this point): data of all blocks in order,
|
|
77
|
+
# then ECC of all blocks in order.
|
|
78
|
+
payload = []
|
|
79
|
+
pos = 0
|
|
80
|
+
for size, _ in blocks:
|
|
81
|
+
for b in data[pos:pos + size]:
|
|
82
|
+
payload += bytes_to_bits([b])
|
|
83
|
+
pos += size
|
|
84
|
+
pos = 0
|
|
85
|
+
for size, ecc in blocks:
|
|
86
|
+
cw = rs_encode_msg(list(data[pos:pos + size]), ecc)
|
|
87
|
+
payload += bytes_to_bits(cw[size:])
|
|
88
|
+
pos += size
|
|
89
|
+
pad = ring_capacity(DATA_RING0, rmax) - MODE_BITS - len(payload)
|
|
90
|
+
payload += [i % 2 for i in range(pad)]
|
|
91
|
+
|
|
92
|
+
# --- mask selection (payload only; the header carries the mask id
|
|
93
|
+
# and stays unmasked -- same approach as Aztec Code).
|
|
94
|
+
if mask_id == "auto":
|
|
95
|
+
payload, best_mask = select_mask(payload)
|
|
96
|
+
else:
|
|
97
|
+
from .masks import mask_payload
|
|
98
|
+
if not 0 <= mask_id <= 7:
|
|
99
|
+
raise ValueError("mask_id must be 0..7 or 'auto'")
|
|
100
|
+
payload = mask_payload(payload, mask_id)
|
|
101
|
+
best_mask = mask_id
|
|
102
|
+
|
|
103
|
+
# --- header with final mask id, then the full bitstream.
|
|
104
|
+
mode = pack_mode(rmax, best_mask, ec_pct, len(blocks), len(data))
|
|
105
|
+
bits = bytes_to_bits(mode) + payload
|
|
106
|
+
|
|
107
|
+
# --- place modules: bullseye + key + spiral payload.
|
|
108
|
+
data_cells = [c for k in range(DATA_RING0, rmax + 1)
|
|
109
|
+
for c in hex_ring(k)]
|
|
110
|
+
grid = {}
|
|
111
|
+
for k in range(BULLSEYE_RINGS + 1):
|
|
112
|
+
for c in hex_ring(k):
|
|
113
|
+
grid[c] = k % 2 # centre LIGHT in v0.1, rings alternate
|
|
114
|
+
for c in hex_ring(KEY_RING):
|
|
115
|
+
grid[c] = 0
|
|
116
|
+
key = hex_ring(KEY_RING)
|
|
117
|
+
grid[key[0]] = 1 # orientation key cell 1
|
|
118
|
+
grid[key[1]] = 1 # orientation key cell 2 (adjacent)
|
|
119
|
+
for idx, c in enumerate(data_cells):
|
|
120
|
+
grid[c] = bits[idx] if idx < len(bits) else 0
|
|
121
|
+
return grid, {"rmax": rmax, "mask": best_mask, "ec": ec_pct,
|
|
122
|
+
"blocks": blocks, "data_len": len(data)}
|
hexatess/galois.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""GF(256) Galois field arithmetic for Hexatess Code.
|
|
2
|
+
|
|
3
|
+
The field uses the primitive polynomial x^8 + x^4 + x^3 + x^2 + 1
|
|
4
|
+
(0x11D) -- the same choice as QR Code and Aztec Code -- with generator
|
|
5
|
+
alpha = 2. Polynomials are represented as lists of integer coefficients
|
|
6
|
+
in descending order of degree (coefficient list ``[a, b, c]`` means
|
|
7
|
+
a*x^2 + b*x + c), except inside the Forney algorithm where an ascending
|
|
8
|
+
order convention is used explicitly and documented.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
GF_EXP = [0] * 512
|
|
14
|
+
GF_LOG = [0] * 256
|
|
15
|
+
|
|
16
|
+
_x = 1
|
|
17
|
+
for _i in range(255):
|
|
18
|
+
GF_EXP[_i] = _x
|
|
19
|
+
GF_LOG[_x] = _i
|
|
20
|
+
_x <<= 1
|
|
21
|
+
if _x & 0x100:
|
|
22
|
+
_x ^= 0x11D
|
|
23
|
+
for _i in range(255, 512):
|
|
24
|
+
GF_EXP[_i] = GF_EXP[_i - 255]
|
|
25
|
+
|
|
26
|
+
PRIMITIVE_POLY = 0x11D
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def gf_mul(x: int, y: int) -> int:
|
|
30
|
+
"""Multiply two field elements."""
|
|
31
|
+
if x == 0 or y == 0:
|
|
32
|
+
return 0
|
|
33
|
+
return GF_EXP[GF_LOG[x] + GF_LOG[y]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def gf_div(x: int, y: int) -> int:
|
|
37
|
+
"""Divide two field elements (raises ZeroDivisionError on y == 0)."""
|
|
38
|
+
if y == 0:
|
|
39
|
+
raise ZeroDivisionError("division by zero in GF(256)")
|
|
40
|
+
if x == 0:
|
|
41
|
+
return 0
|
|
42
|
+
return GF_EXP[(GF_LOG[x] + 255 - GF_LOG[y]) % 255]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def gf_inv(x: int) -> int:
|
|
46
|
+
"""Multiplicative inverse of a non-zero field element."""
|
|
47
|
+
return GF_EXP[(255 - GF_LOG[x]) % 255]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def gf_poly_scale(p, x):
|
|
51
|
+
"""Multiply every coefficient of polynomial ``p`` by ``x``."""
|
|
52
|
+
return [gf_mul(c, x) for c in p]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def gf_poly_add(p, q):
|
|
56
|
+
"""Add two polynomials (descending coefficient order)."""
|
|
57
|
+
r = [0] * max(len(p), len(q))
|
|
58
|
+
for i, c in enumerate(p):
|
|
59
|
+
r[i + len(r) - len(p)] = c
|
|
60
|
+
for i, c in enumerate(q):
|
|
61
|
+
r[i + len(r) - len(q)] ^= c
|
|
62
|
+
return r
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def gf_poly_mul(p, q):
|
|
66
|
+
"""Multiply two polynomials (descending coefficient order)."""
|
|
67
|
+
r = [0] * (len(p) + len(q) - 1)
|
|
68
|
+
for j, qj in enumerate(q):
|
|
69
|
+
for i, pi in enumerate(p):
|
|
70
|
+
r[i + j] ^= gf_mul(pi, qj)
|
|
71
|
+
return r
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def gf_poly_eval(p, x):
|
|
75
|
+
"""Evaluate polynomial ``p`` at ``x`` using Horner's scheme.
|
|
76
|
+
|
|
77
|
+
``p[0]`` is the highest-degree coefficient.
|
|
78
|
+
"""
|
|
79
|
+
y = p[0]
|
|
80
|
+
for c in p[1:]:
|
|
81
|
+
y = gf_mul(y, x) ^ c
|
|
82
|
+
return y
|
hexatess/geometry.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Hexagonal grid geometry for Hexatess Code.
|
|
2
|
+
|
|
3
|
+
The symbol is a hexagonal lattice of "pointy-top" hexagonal modules.
|
|
4
|
+
Module positions use integer *axial coordinates* ``(q, r)``. Rings are
|
|
5
|
+
sets of cells at constant hex distance ``k`` from the origin; the cells
|
|
6
|
+
within ring ``k`` total ``6k`` for ``k >= 1`` and the region within
|
|
7
|
+
radius ``n`` contains ``3n(n+1) + 1`` cells (centered hexagonal numbers).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
# Neighbour directions in axial coordinates, fixed traversal order.
|
|
15
|
+
DIRS = [(1, 0), (1, -1), (0, -1), (-1, 0), (-1, 1), (0, 1)]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def hex_ring(k: int):
|
|
19
|
+
"""All cells at hex distance ``k`` from the origin, in canonical order.
|
|
20
|
+
|
|
21
|
+
The ring starts at cell ``(-k, +k)`` and then takes ``k`` steps in
|
|
22
|
+
each direction of ``DIRS`` in order, appending each visited cell.
|
|
23
|
+
"""
|
|
24
|
+
if k == 0:
|
|
25
|
+
return [(0, 0)]
|
|
26
|
+
cells = []
|
|
27
|
+
q, r = -k, k # start: direction 4 * k
|
|
28
|
+
for d in range(6):
|
|
29
|
+
for _ in range(k):
|
|
30
|
+
cells.append((q, r))
|
|
31
|
+
dq, dr = DIRS[d]
|
|
32
|
+
q += dq
|
|
33
|
+
r += dr
|
|
34
|
+
return cells
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def hex_distance(q: int, r: int) -> int:
|
|
38
|
+
"""Hex distance of ``(q, r)`` from the origin."""
|
|
39
|
+
return max(abs(q), abs(r), abs(q + r))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def hex_to_pixel(q, r, size):
|
|
43
|
+
"""Pointy-top conversion: centre of a module in pixel coordinates.
|
|
44
|
+
|
|
45
|
+
``x = size * sqrt(3) * (q + r/2)``, ``y = size * 1.5 * r``.
|
|
46
|
+
"""
|
|
47
|
+
x = size * math.sqrt(3.0) * (q + r / 2.0)
|
|
48
|
+
y = size * 1.5 * r
|
|
49
|
+
return x, y
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def hex_corner(cx, cy, size, i):
|
|
53
|
+
"""The i-th corner (i = 0..5) of the hexagon centred at (cx, cy).
|
|
54
|
+
|
|
55
|
+
Corners lie at angles 60*i - 30 degrees (pointy-top orientation).
|
|
56
|
+
"""
|
|
57
|
+
ang = math.pi / 180.0 * (60.0 * i - 30.0)
|
|
58
|
+
return (cx + size * math.cos(ang), cy + size * math.sin(ang))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ring_capacity(r_from: int, r_to: int) -> int:
|
|
62
|
+
"""Number of cells in rings ``r_from .. r_to`` inclusive.
|
|
63
|
+
|
|
64
|
+
Uses the centered-hexagonal-number identity:
|
|
65
|
+
cells within radius n = 3n(n+1) + 1.
|
|
66
|
+
"""
|
|
67
|
+
if r_to < r_from:
|
|
68
|
+
return 0
|
|
69
|
+
return (3 * r_to * (r_to + 1) + 1) - (3 * (r_from - 1) * r_from + 1)
|
hexatess/header.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Symbol constants, header (mode message) handling and block planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
from .reedsolomon import rs_encode_msg, rs_correct_msg
|
|
8
|
+
|
|
9
|
+
# ----------------------------------------------------------------------
|
|
10
|
+
# Symbol constants (fixed in specification v0.1)
|
|
11
|
+
# ----------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
BULLSEYE_RINGS = 4 # rings 0..4 form the hexagonal bullseye finder
|
|
14
|
+
KEY_RING = 5 # ring 5 is the orientation key
|
|
15
|
+
DATA_RING0 = 6 # payload serialization starts at ring 6
|
|
16
|
+
MAX_RINGS = 31 # largest permitted symbol radius
|
|
17
|
+
BLOCK_DATA_MAX = 50 # max data bytes per independent RS block
|
|
18
|
+
MODE_BYTES = 5 # header payload length in bytes
|
|
19
|
+
MODE_ECC = 5 # header ECC symbols (corrects up to 2 byte errors)
|
|
20
|
+
MODE_BITS = (MODE_BYTES + MODE_ECC) * 8 # 80 bits
|
|
21
|
+
|
|
22
|
+
MIN_EC_PCT = 5 # minimum error-correction percentage
|
|
23
|
+
MAX_EC_PCT = 90 # maximum error-correction percentage
|
|
24
|
+
EC_STEP = 5 # EC percentage must be a multiple of 5
|
|
25
|
+
MAX_DATA_BYTES = 4095 # 12-bit length field
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ----------------------------------------------------------------------
|
|
29
|
+
# Bit/byte helpers
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def bytes_to_bits(data) -> list:
|
|
33
|
+
"""MSB-first expansion of bytes into a list of 0/1 bits."""
|
|
34
|
+
bits = []
|
|
35
|
+
for b in data:
|
|
36
|
+
for i in range(7, -1, -1):
|
|
37
|
+
bits.append((b >> i) & 1)
|
|
38
|
+
return bits
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bits_to_bytes(bits) -> bytes:
|
|
42
|
+
"""Pack a list of bits (MSB first) into bytes.
|
|
43
|
+
|
|
44
|
+
Trailing bits fewer than 8 are ignored.
|
|
45
|
+
"""
|
|
46
|
+
out = bytearray()
|
|
47
|
+
for i in range(0, len(bits) - 7, 8):
|
|
48
|
+
v = 0
|
|
49
|
+
for j in range(8):
|
|
50
|
+
v = (v << 1) | bits[i + j]
|
|
51
|
+
out.append(v)
|
|
52
|
+
return bytes(out)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ----------------------------------------------------------------------
|
|
56
|
+
# Mode message (header): 5 data bytes + 5 ECC bytes, never masked
|
|
57
|
+
#
|
|
58
|
+
# byte 0: rmax (5 bits) | mask_id (3 bits)
|
|
59
|
+
# byte 1: ec_pct/5 (5 bits) | block_count >> 5 (3 bits)
|
|
60
|
+
# byte 2: block_count & 0x1F (5) | data_len >> 9 (3 bits)
|
|
61
|
+
# byte 3: data_len >> 1 & 0xFF (8 bits)
|
|
62
|
+
# byte 4: data_len & 1 (1 bit, leftmost) | padding 0x00 (7 bits)
|
|
63
|
+
# ----------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def pack_mode(rmax, mask_id, ec_pct, block_count, data_len) -> bytes:
|
|
66
|
+
"""Encode symbol parameters into a 10-byte protected mode message.
|
|
67
|
+
|
|
68
|
+
The returned bytes are the 5 data bytes followed by 5 Reed-Solomon
|
|
69
|
+
ECC bytes. The mode message is NEVER masked (it contains the mask
|
|
70
|
+
identifier itself -- the same design decision as Aztec Code).
|
|
71
|
+
"""
|
|
72
|
+
if rmax > 31 or mask_id > 7 or ec_pct % 5 or ec_pct // 5 > 31 \
|
|
73
|
+
or block_count > 255 or data_len > 4095:
|
|
74
|
+
raise ValueError("parameters out of mode-message range")
|
|
75
|
+
ecq = ec_pct // 5
|
|
76
|
+
b0 = (rmax << 3) | mask_id
|
|
77
|
+
b1 = (ecq << 3) | (block_count >> 5)
|
|
78
|
+
b2 = ((block_count & 0x1F) << 3) | ((data_len >> 9) & 0x7)
|
|
79
|
+
b3 = (data_len >> 1) & 0xFF
|
|
80
|
+
b4 = (data_len & 1) << 7
|
|
81
|
+
payload = bytes([b0, b1, b2, b3, b4])
|
|
82
|
+
return rs_encode_msg(list(payload), MODE_ECC)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def unpack_mode(mode_bytes):
|
|
86
|
+
"""RS-correct the mode message and decode symbol parameters.
|
|
87
|
+
|
|
88
|
+
Returns ``(rmax, mask_id, ec_pct, block_count, data_len)``.
|
|
89
|
+
"""
|
|
90
|
+
data = rs_correct_msg(list(mode_bytes), MODE_ECC)
|
|
91
|
+
b0, b1, b2, b3, b4 = data
|
|
92
|
+
rmax = b0 >> 3
|
|
93
|
+
mask_id = b0 & 0x7
|
|
94
|
+
ecq = b1 >> 3
|
|
95
|
+
block_count = ((b1 & 0x7) << 5) | (b2 >> 3)
|
|
96
|
+
data_len = ((b2 & 0x7) << 9) | (b3 << 1) | (b4 >> 7)
|
|
97
|
+
return rmax, mask_id, ecq * 5, block_count, data_len
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ----------------------------------------------------------------------
|
|
101
|
+
# Block planning
|
|
102
|
+
# ----------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def plan_blocks(data_len: int, ec_pct: int):
|
|
105
|
+
"""Split ``data_len`` bytes into independent RS blocks.
|
|
106
|
+
|
|
107
|
+
Returns a list of ``(data_bytes, ecc_bytes)`` pairs. Blocks carry at
|
|
108
|
+
most ``BLOCK_DATA_MAX`` data bytes each. ECC overhead per block is
|
|
109
|
+
``ceil(size * ec_pct / 100)``, clamped to a minimum of 2 symbols.
|
|
110
|
+
"""
|
|
111
|
+
if data_len == 0:
|
|
112
|
+
return [(0, 2)]
|
|
113
|
+
bc = max(1, math.ceil(data_len / BLOCK_DATA_MAX))
|
|
114
|
+
base, extra = divmod(data_len, bc)
|
|
115
|
+
blocks = []
|
|
116
|
+
for i in range(bc):
|
|
117
|
+
size = base + (1 if i < extra else 0)
|
|
118
|
+
ecc = max(2, math.ceil(size * ec_pct / 100.0))
|
|
119
|
+
blocks.append((size, min(ecc, 255 - size)))
|
|
120
|
+
return blocks
|
hexatess/masks.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Mask bit generation and automatic mask selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def mask_bit(index: int, mask_id: int) -> int:
|
|
7
|
+
"""Deterministic pseudo-random mask bit (32-bit LCG + xorshift).
|
|
8
|
+
|
|
9
|
+
The mask stream is a function of the payload bit position and the
|
|
10
|
+
mask identifier (0..7). Masking applies to the payload ONLY; the
|
|
11
|
+
mode message is never masked (it carries the mask id itself).
|
|
12
|
+
"""
|
|
13
|
+
x = (index * 1103515245 + 12345 + mask_id * 2654435761) & 0x7FFFFFFF
|
|
14
|
+
x ^= x >> 13
|
|
15
|
+
return (x >> 19) & 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def mask_payload(payload, mask_id: int):
|
|
19
|
+
"""XOR ``payload`` bit-by-bit with mask stream ``mask_id``."""
|
|
20
|
+
return [b ^ mask_bit(i, mask_id) for i, b in enumerate(payload)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def evaluate_mask(masked) -> int:
|
|
24
|
+
"""Penalty score of an already-masked bit list (lower is better).
|
|
25
|
+
|
|
26
|
+
Score = |2 * dark - total| (balance penalty)
|
|
27
|
+
+ count of adjacent equal-bit pairs (repetition penalty).
|
|
28
|
+
"""
|
|
29
|
+
score = abs(2 * sum(masked) - len(masked))
|
|
30
|
+
for i in range(1, len(masked)):
|
|
31
|
+
if masked[i] == masked[i - 1]:
|
|
32
|
+
score += 1
|
|
33
|
+
return score
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def select_mask(payload):
|
|
37
|
+
"""Evaluate all 8 masks and return ``(masked_payload, mask_id)``.
|
|
38
|
+
|
|
39
|
+
Ties resolve to the lowest mask id, mirroring the reference
|
|
40
|
+
implementation.
|
|
41
|
+
"""
|
|
42
|
+
best, best_id, best_score = None, 0, None
|
|
43
|
+
for m in range(8):
|
|
44
|
+
masked = mask_payload(payload, m)
|
|
45
|
+
score = evaluate_mask(masked)
|
|
46
|
+
if best_score is None or score < best_score:
|
|
47
|
+
best, best_id, best_score = masked, m, score
|
|
48
|
+
return best, best_id
|
hexatess/reedsolomon.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Systematic Reed-Solomon codec over GF(256) for Hexatess Code.
|
|
2
|
+
|
|
3
|
+
Encoder: polynomial long division by the generator polynomial, producing
|
|
4
|
+
``nsym`` trailing ECC symbols (systematic code, identical to the scheme
|
|
5
|
+
used by QR Code and Aztec Code).
|
|
6
|
+
|
|
7
|
+
Decoder: syndromes -> Berlekamp-Massey error locator -> Chien search for
|
|
8
|
+
error positions -> Forney algorithm for error magnitudes. The Forney
|
|
9
|
+
step internally uses an ascending-order polynomial convention:
|
|
10
|
+
|
|
11
|
+
e_i = X_i * Omega(X_i^-1) / Lambda'(X_i^-1)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .galois import (
|
|
17
|
+
GF_EXP,
|
|
18
|
+
gf_div,
|
|
19
|
+
gf_inv,
|
|
20
|
+
gf_mul,
|
|
21
|
+
gf_poly_add,
|
|
22
|
+
gf_poly_eval,
|
|
23
|
+
gf_poly_mul,
|
|
24
|
+
gf_poly_scale,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def rs_generator_poly(nsym: int):
|
|
29
|
+
"""Generator polynomial prod_{i=0..nsym-1} (x - alpha^i)."""
|
|
30
|
+
g = [1]
|
|
31
|
+
for i in range(nsym):
|
|
32
|
+
g = gf_poly_mul(g, [1, GF_EXP[i]])
|
|
33
|
+
return g
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def rs_encode_msg(msg_in, nsym: int):
|
|
37
|
+
"""Return ``msg_in`` followed by ``nsym`` ECC symbols."""
|
|
38
|
+
if len(msg_in) + nsym > 255:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
"block too long for RS over GF(256): %d + %d > 255"
|
|
41
|
+
% (len(msg_in), nsym))
|
|
42
|
+
gen = rs_generator_poly(nsym)
|
|
43
|
+
msg_out = list(msg_in) + [0] * nsym
|
|
44
|
+
for i in range(len(msg_in)):
|
|
45
|
+
coef = msg_out[i]
|
|
46
|
+
if coef:
|
|
47
|
+
for j in range(1, len(gen)):
|
|
48
|
+
msg_out[i + j] ^= gf_mul(gen[j], coef)
|
|
49
|
+
return list(msg_in) + msg_out[len(msg_in):]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rs_calc_syndromes(msg, nsym):
|
|
53
|
+
"""Evaluate the received word at alpha^0 .. alpha^(nsym-1)."""
|
|
54
|
+
return [gf_poly_eval(msg, GF_EXP[i]) for i in range(nsym)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def rs_find_error_locator(synd, nsym):
|
|
58
|
+
"""Berlekamp-Massey: derive the error locator polynomial L(x)."""
|
|
59
|
+
err_loc = [1]
|
|
60
|
+
old_loc = [1]
|
|
61
|
+
for i in range(nsym):
|
|
62
|
+
delta = synd[i]
|
|
63
|
+
for j in range(1, len(err_loc)):
|
|
64
|
+
delta ^= gf_mul(err_loc[-(j + 1)], synd[i - j])
|
|
65
|
+
old_loc = old_loc + [0]
|
|
66
|
+
if delta != 0:
|
|
67
|
+
if len(old_loc) > len(err_loc):
|
|
68
|
+
new_loc = gf_poly_scale(old_loc, delta)
|
|
69
|
+
old_loc = gf_poly_scale(err_loc, gf_inv(delta))
|
|
70
|
+
err_loc = new_loc
|
|
71
|
+
err_loc = gf_poly_add(err_loc, gf_poly_scale(old_loc, delta))
|
|
72
|
+
while err_loc and err_loc[0] == 0:
|
|
73
|
+
del err_loc[0]
|
|
74
|
+
return err_loc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def rs_find_errors(err_loc_rev, nmess):
|
|
78
|
+
"""Chien search: positions of errors, counted from the start."""
|
|
79
|
+
errs = len(err_loc_rev) - 1
|
|
80
|
+
pos = []
|
|
81
|
+
for i in range(nmess):
|
|
82
|
+
if gf_poly_eval(err_loc_rev, GF_EXP[i % 255]) == 0:
|
|
83
|
+
pos.append(nmess - 1 - i)
|
|
84
|
+
if len(pos) != errs:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"Chien: found %d roots, expected %d" % (len(pos), errs))
|
|
87
|
+
return pos
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def rs_find_errata_locator(coef_pos):
|
|
91
|
+
"""Errata locator in ascending order: prod (1 + X_i * x)."""
|
|
92
|
+
e_loc = [1]
|
|
93
|
+
for i in coef_pos:
|
|
94
|
+
e_loc = gf_poly_mul(e_loc, gf_poly_add([1], [GF_EXP[i], 0]))
|
|
95
|
+
return e_loc
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def rs_find_error_evaluator(synd_rev, err_loc, nsym):
|
|
99
|
+
"""Error evaluator polynomial Omega = (S * Lambda) truncated to nsym."""
|
|
100
|
+
prod = gf_poly_mul(synd_rev, err_loc)
|
|
101
|
+
return prod[len(prod) - (nsym + 1):]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rs_correct_errata(msg_in, synd, err_pos):
|
|
105
|
+
"""Forney algorithm (pure ascending convention).
|
|
106
|
+
|
|
107
|
+
e_i = X_i * Omega(X_i^-1) / Lambda'(X_i^-1). All polynomials are
|
|
108
|
+
kept internally in ASCENDING order of degree.
|
|
109
|
+
"""
|
|
110
|
+
nmess = len(msg_in)
|
|
111
|
+
coef_pos = [nmess - 1 - p for p in err_pos]
|
|
112
|
+
Xs = [GF_EXP[c % 255] for c in coef_pos]
|
|
113
|
+
nsym = len(synd)
|
|
114
|
+
|
|
115
|
+
# Lambda in ascending order: Pi(1 + X_i * x)
|
|
116
|
+
lam = rs_find_errata_locator(coef_pos)[::-1]
|
|
117
|
+
|
|
118
|
+
# Omega = (S * Lambda) mod x^nsym (ascending)
|
|
119
|
+
omega = gf_poly_mul(list(synd), lam)[:nsym]
|
|
120
|
+
|
|
121
|
+
# formal derivative of Lambda: odd indices
|
|
122
|
+
lam_d = [0] * max(1, len(lam) - 1)
|
|
123
|
+
for i in range(1, len(lam)):
|
|
124
|
+
if i % 2 == 1:
|
|
125
|
+
lam_d[i - 1] = lam[i]
|
|
126
|
+
|
|
127
|
+
E = [0] * nmess
|
|
128
|
+
for i, Xi in enumerate(Xs):
|
|
129
|
+
Xi_inv = gf_inv(Xi)
|
|
130
|
+
num = gf_poly_eval(omega[::-1], Xi_inv)
|
|
131
|
+
den = gf_poly_eval(lam_d[::-1], Xi_inv)
|
|
132
|
+
E[err_pos[i]] = gf_div(gf_mul(Xi, num), den)
|
|
133
|
+
return [m ^ e for m, e in zip(msg_in, E)]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def rs_correct_msg(msg_in, nsym):
|
|
137
|
+
"""Correct up to floor(nsym/2) symbol errors in one block.
|
|
138
|
+
|
|
139
|
+
Returns the data symbols only. Raises ValueError when the number of
|
|
140
|
+
errors exceeds the correction capacity (or decoding otherwise fails
|
|
141
|
+
to verify).
|
|
142
|
+
"""
|
|
143
|
+
if len(msg_in) > 255:
|
|
144
|
+
raise ValueError("block too long for RS")
|
|
145
|
+
msg = list(msg_in)
|
|
146
|
+
synd = rs_calc_syndromes(msg, nsym)
|
|
147
|
+
if max(synd) == 0:
|
|
148
|
+
return msg[:-nsym]
|
|
149
|
+
err_loc = rs_find_error_locator(synd, nsym)
|
|
150
|
+
errs = len(err_loc) - 1
|
|
151
|
+
if errs == 0:
|
|
152
|
+
raise ValueError("RS: unexpected locator state")
|
|
153
|
+
if 2 * errs > nsym:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
"RS: too many errors (%d errors, capacity %d)"
|
|
156
|
+
% (errs, nsym // 2))
|
|
157
|
+
err_pos = rs_find_errors(err_loc[::-1], len(msg))
|
|
158
|
+
msg = rs_correct_errata(msg, synd, err_pos)
|
|
159
|
+
if max(rs_calc_syndromes(msg, nsym)) != 0:
|
|
160
|
+
raise ValueError("RS: correction failed verification")
|
|
161
|
+
return msg[:-nsym]
|
hexatess/render.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""PNG rendering and ideal re-sampling of Hexatess Code symbols."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
from .geometry import hex_corner, hex_distance, hex_ring, hex_to_pixel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def render(grid, path, size_px=18, quiet_module=1.5,
|
|
11
|
+
dark=(24, 22, 18), light=(255, 255, 255), ss=3):
|
|
12
|
+
"""Render a module grid to a PNG file.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
grid : dict
|
|
17
|
+
Mapping ``{(q, r): 0|1}`` (1 = dark module).
|
|
18
|
+
path : str
|
|
19
|
+
Output PNG path.
|
|
20
|
+
size_px : int
|
|
21
|
+
Hexagon radius in pixels.
|
|
22
|
+
quiet_module : float
|
|
23
|
+
Quiet zone width in modules.
|
|
24
|
+
dark, light : tuple
|
|
25
|
+
RGB colours of dark / light modules.
|
|
26
|
+
ss : int
|
|
27
|
+
Supersampling factor for smooth edges.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
str
|
|
32
|
+
The output path.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
from PIL import Image, ImageDraw
|
|
36
|
+
except ImportError:
|
|
37
|
+
raise SystemExit("rendering requires Pillow: pip install pillow")
|
|
38
|
+
rmax = max(hex_distance(*c) for c in grid)
|
|
39
|
+
width = int(math.sqrt(3.0) * size_px * (2 * rmax + 1 + 2 * quiet_module))
|
|
40
|
+
height = int(1.5 * size_px * (2 * rmax + 2 * quiet_module + 2))
|
|
41
|
+
img = Image.new("RGB", (width * ss, height * ss), light)
|
|
42
|
+
dr = ImageDraw.Draw(img)
|
|
43
|
+
cx0, cy0 = width * ss / 2.0, height * ss / 2.0
|
|
44
|
+
s = size_px * ss
|
|
45
|
+
for (q, r), val in grid.items():
|
|
46
|
+
if not val:
|
|
47
|
+
continue
|
|
48
|
+
x, y = hex_to_pixel(q, r, s)
|
|
49
|
+
pts = [hex_corner(cx0 + x, cy0 + y, s * 0.995, i) for i in range(6)]
|
|
50
|
+
dr.polygon(pts, fill=dark)
|
|
51
|
+
img = img.resize((width, height), Image.LANCZOS)
|
|
52
|
+
img.save(path)
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sample_grid_from_image(path, rmax, size_px=18, quiet_module=1.5):
|
|
57
|
+
"""Re-sample a rendered image back into a module grid (ideal sampling).
|
|
58
|
+
|
|
59
|
+
This is a self-test / conformance helper: it assumes the image was
|
|
60
|
+
produced by :func:`render` with the same geometry parameters and is
|
|
61
|
+
perfectly upright. Real-world scanning requires finder detection
|
|
62
|
+
and perspective correction (planned for a later release).
|
|
63
|
+
"""
|
|
64
|
+
from PIL import Image
|
|
65
|
+
img = Image.open(path).convert("L")
|
|
66
|
+
width, height = img.size
|
|
67
|
+
cx0, cy0 = width / 2.0, height / 2.0
|
|
68
|
+
s = size_px
|
|
69
|
+
grid = {}
|
|
70
|
+
for k in range(rmax + 1):
|
|
71
|
+
for (q, r) in hex_ring(k):
|
|
72
|
+
x, y = hex_to_pixel(q, r, s)
|
|
73
|
+
v = img.getpixel((int(cx0 + x), int(cy0 + y)))
|
|
74
|
+
grid[(q, r)] = 1 if v < 128 else 0
|
|
75
|
+
return grid
|
hexatess/resilience.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Robustness self-tests: random noise and blob (smudge) damage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from .decoder import decode
|
|
8
|
+
from .encoder import encode
|
|
9
|
+
from .geometry import hex_distance, hex_ring
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def data_cell_list(rmax):
|
|
13
|
+
"""All payload cells from ring DATA_RING0 to ``rmax`` in spiral order."""
|
|
14
|
+
from .header import DATA_RING0
|
|
15
|
+
return [c for k in range(DATA_RING0, rmax + 1) for c in hex_ring(k)]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_random_noise(grid, params, p, rng):
|
|
19
|
+
"""Flip each payload cell independently with probability ``p``."""
|
|
20
|
+
rmax = params["rmax"]
|
|
21
|
+
g = dict(grid)
|
|
22
|
+
for c in data_cell_list(rmax):
|
|
23
|
+
if rng.random() < p:
|
|
24
|
+
g[c] ^= 1
|
|
25
|
+
return g
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_blob_damage(grid, params, target_frac, rng):
|
|
29
|
+
"""Smudge damage: set all cells within radius ``d`` of a random
|
|
30
|
+
payload cell to 0, growing ``d`` until ~``target_frac`` of payload
|
|
31
|
+
cells are covered. Returns ``(grid, blob_size)``."""
|
|
32
|
+
rmax = params["rmax"]
|
|
33
|
+
data_cells = data_cell_list(rmax)
|
|
34
|
+
total = len(data_cells)
|
|
35
|
+
d = 1
|
|
36
|
+
while True:
|
|
37
|
+
seed = rng.choice(data_cells)
|
|
38
|
+
blob = [c for c in data_cells
|
|
39
|
+
if hex_distance(c[0] - seed[0], c[1] - seed[1]) <= d]
|
|
40
|
+
if len(blob) >= total * target_frac or d > rmax:
|
|
41
|
+
break
|
|
42
|
+
d += 1
|
|
43
|
+
g = dict(grid)
|
|
44
|
+
for c in blob:
|
|
45
|
+
g[c] = 0
|
|
46
|
+
return g, len(blob)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_tests(text="Hexatess Code - hexagonal 2D code with EC. ",
|
|
50
|
+
trials=30, seed=42, verbose=True):
|
|
51
|
+
"""Round-trip plus random-noise and blob-damage survival statistics.
|
|
52
|
+
|
|
53
|
+
Physical note: one flipped module is one RS *symbol* error, so the
|
|
54
|
+
theoretical tolerance to uniformly random noise is roughly
|
|
55
|
+
``ec_pct / 16`` percent of modules; blob damage survives much higher
|
|
56
|
+
area fractions because flips cluster inside whole bytes.
|
|
57
|
+
"""
|
|
58
|
+
rng = random.Random(seed)
|
|
59
|
+
results = []
|
|
60
|
+
for ec in (30, 50, 90):
|
|
61
|
+
grid, params = encode(text, ec_pct=ec, mask_id="auto")
|
|
62
|
+
t, _ = decode(grid)
|
|
63
|
+
assert t == text, "round-trip failed at EC=%d" % ec
|
|
64
|
+
for p in (0.01, 0.02, 0.03, 0.05):
|
|
65
|
+
ok = 0
|
|
66
|
+
for _ in range(trials):
|
|
67
|
+
g2 = add_random_noise(grid, params, p, rng)
|
|
68
|
+
try:
|
|
69
|
+
t2, _ = decode(g2)
|
|
70
|
+
ok += (t2 == text)
|
|
71
|
+
except (ValueError, UnicodeDecodeError):
|
|
72
|
+
pass
|
|
73
|
+
results.append(("noise %.0f%%" % (p * 100), ec, ok, trials))
|
|
74
|
+
for frac in (0.05, 0.10, 0.15, 0.20):
|
|
75
|
+
ok = 0
|
|
76
|
+
for _ in range(trials):
|
|
77
|
+
g2, _blob = add_blob_damage(grid, params, frac, rng)
|
|
78
|
+
try:
|
|
79
|
+
t2, _ = decode(g2)
|
|
80
|
+
ok += (t2 == text)
|
|
81
|
+
except (ValueError, UnicodeDecodeError):
|
|
82
|
+
pass
|
|
83
|
+
results.append(("blob %.0f%%" % (frac * 100), ec, ok, trials))
|
|
84
|
+
if verbose:
|
|
85
|
+
print("=" * 56)
|
|
86
|
+
print("ROBUSTNESS TESTS (%d trials per setting, seed=%d)"
|
|
87
|
+
% (trials, seed))
|
|
88
|
+
print("payload: %d bytes; random-noise tolerance ~ EC/16%%"
|
|
89
|
+
% len(text.encode("utf-8")))
|
|
90
|
+
print("=" * 56)
|
|
91
|
+
print("%-14s %-6s %s" % ("damage", "EC%", "survival"))
|
|
92
|
+
for name, ec, ok, n in results:
|
|
93
|
+
print("%-14s %-6d %s (%d/%d)"
|
|
94
|
+
% (name, ec, "%.0f%%" % (100.0 * ok / n), ok, n))
|
|
95
|
+
return results
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexatess-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hexatess Code - an experimental 2D barcode on a hexagonal grid with Reed-Solomon error correction
|
|
5
|
+
Author: The Hexatess Code Authors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 The Hexatess Code Authors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
----------------------------------------------------------------------
|
|
29
|
+
Note on the specification:
|
|
30
|
+
|
|
31
|
+
The format specification (SPECIFICATION.md) is additionally released under
|
|
32
|
+
the Creative Commons Attribution 4.0 International license (CC-BY-4.0), so
|
|
33
|
+
that anyone may implement compatible encoders/decoders in any language,
|
|
34
|
+
under any license, without restriction. Provide attribution as:
|
|
35
|
+
"Hexatess Code Specification v0.1, https://github.com/<you>/hexatess-code".
|
|
36
|
+
|
|
37
|
+
Project-URL: Homepage, https://github.com/lovro-abram/hexatess-code
|
|
38
|
+
Project-URL: Specification, https://github.com/lovro-abram/hexatess-code/blob/main/SPECIFICATION.md
|
|
39
|
+
Project-URL: Issues, https://github.com/lovro-abram/hexatess-code/issues
|
|
40
|
+
Keywords: barcode,2d-barcode,qr-code,hexagonal,hexatess,reed-solomon,error-correction,aztec
|
|
41
|
+
Classifier: Development Status :: 3 - Alpha
|
|
42
|
+
Classifier: Intended Audience :: Developers
|
|
43
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
44
|
+
Classifier: Operating System :: OS Independent
|
|
45
|
+
Classifier: Programming Language :: Python :: 3
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
47
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
49
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
50
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
51
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
52
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
53
|
+
Requires-Python: >=3.8
|
|
54
|
+
Description-Content-Type: text/markdown
|
|
55
|
+
License-File: LICENSE
|
|
56
|
+
Requires-Dist: pillow>=9.0
|
|
57
|
+
Provides-Extra: dev
|
|
58
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
59
|
+
Dynamic: license-file
|
|
60
|
+
|
|
61
|
+
# Hexatess Code 🐝
|
|
62
|
+
|
|
63
|
+
**An experimental 2D barcode on a hexagonal grid** — with a hexagonal
|
|
64
|
+
bullseye finder, spiral serialization and a continuously selectable
|
|
65
|
+
Reed-Solomon error-correction budget of 5–90 %.
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from hexatess import encode, decode, render
|
|
71
|
+
|
|
72
|
+
grid, params = encode("Hello, Hexatess!", ec_pct=30)
|
|
73
|
+
render(grid, "hello.png")
|
|
74
|
+
text, stats = decode(grid) # ('Hello, Hexatess!', {...})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Symbol anatomy
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
* **A** — a real encoded symbol: hexagonal bullseye finder (rings 0–4),
|
|
82
|
+
orientation key (ring 5: two dark cells), data region (rings 6…)
|
|
83
|
+
filled in spiral order, and a quiet zone of at least 1 module;
|
|
84
|
+
* **B** — finder close-up: light centre (v0.1 rule `bit = ring mod 2`),
|
|
85
|
+
alternating dark/light rings, and the key — the first two canonical
|
|
86
|
+
ring-5 cells set dark, breaking the 60-fold symmetry and marking the
|
|
87
|
+
spiral start direction;
|
|
88
|
+
* **C** — spiral bit order across rings 6–7 (bit 0 at cell `(−6, +6)`),
|
|
89
|
+
rendered from the actual reference encoder output.
|
|
90
|
+
|
|
91
|
+
## Why hexagons?
|
|
92
|
+
|
|
93
|
+
* **+15.5 % packing density** over the square grid — hexagons tile the
|
|
94
|
+
plane with ~15.5 % more modules per area at equal module size, which
|
|
95
|
+
directly translates into more data per printed area.
|
|
96
|
+
* **Rotational isotropy** — three axes of symmetry instead of two;
|
|
97
|
+
damage from any direction is statistically equivalent.
|
|
98
|
+
* **Proven heritage** — MaxiCode (UPS, ISO/IEC 16023) already proved a
|
|
99
|
+
hexagonal 2D code works in the field; Hexatess Code generalizes the
|
|
100
|
+
idea to variable-size, high-capacity, Aztec-style symbols.
|
|
101
|
+
* **Modern error control** — continuous EC budget from 5 % to 90 %
|
|
102
|
+
(not 7 discrete levels), independent RS blocks of ≤ 50 data bytes,
|
|
103
|
+
and a double-protected header.
|
|
104
|
+
|
|
105
|
+
> **Status: experimental.** This is a young format: the symbol
|
|
106
|
+
> specification and reference implementation are solid and heavily
|
|
107
|
+
> tested (2,500+ tests, conformance vectors), but there is **no camera
|
|
108
|
+
> decoder yet** — reading images assumes ideal upright sampling. See
|
|
109
|
+
> the roadmap below. Adopting a young format is a deliberate bet; the
|
|
110
|
+
> [full format specification](SPECIFICATION.md) is the insurance.
|
|
111
|
+
|
|
112
|
+
## Installation
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install hexatess-code # from PyPI (once published)
|
|
116
|
+
# or from a source checkout:
|
|
117
|
+
pip install -e .
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Requires Python ≥ 3.8 and Pillow (for rendering only).
|
|
121
|
+
|
|
122
|
+
## Command line
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
hexatess "Hello world" -o koda.png --ec 30
|
|
126
|
+
hexatess "Important URL https://example.org" -o url.png --ec 55
|
|
127
|
+
hexatess-code --demo # demo symbol + robustness statistics
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## API
|
|
131
|
+
|
|
132
|
+
| Function | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `encode(text, ec_pct=30, mask_id="auto", min_rings=None)` | UTF-8 text → `(grid, params)`; `grid` maps axial `(q, r)` to `0/1` |
|
|
135
|
+
| `decode(grid)` | grid → `(text, stats)`; RS-corrects transparently |
|
|
136
|
+
| `render(grid, path, size_px=18, ...)` | grid → PNG (pointy-top hexagons, quiet zone, supersampling) |
|
|
137
|
+
| `sample_grid_from_image(path, rmax, ...)` | ideal re-sampling of a rendered PNG (self-test helper) |
|
|
138
|
+
| `run_tests(...)` | noise/blob robustness statistics |
|
|
139
|
+
|
|
140
|
+
`params` / `stats` contain `rmax` (radius in rings), `mask`, `ec`,
|
|
141
|
+
`blocks` (list of `(data_bytes, ecc_bytes)`) and `data_len`.
|
|
142
|
+
|
|
143
|
+
## Error-correction budget
|
|
144
|
+
|
|
145
|
+
Choose any multiple of 5 between 5 and 90:
|
|
146
|
+
|
|
147
|
+
| EC | Character |
|
|
148
|
+
|---|---|
|
|
149
|
+
| 5–15 | maximum capacity, clean environments |
|
|
150
|
+
| 25–40 | general use (default 30) |
|
|
151
|
+
| 50–70 | industrial / outdoor |
|
|
152
|
+
| 80–90 | extreme damage tolerance |
|
|
153
|
+
|
|
154
|
+
Physical behaviour (measured on the reference implementation): one
|
|
155
|
+
flipped module is one RS *symbol* error, so uniform-noise tolerance is
|
|
156
|
+
roughly `EC / 16` percent of modules, while clustered (smudge/blob)
|
|
157
|
+
damage survives several times higher area fractions because flips
|
|
158
|
+
concentrate inside whole bytes.
|
|
159
|
+
|
|
160
|
+
## Implement it in your own language
|
|
161
|
+
|
|
162
|
+
The format is deliberately **specification-first**: everything needed
|
|
163
|
+
for an independent implementation is in
|
|
164
|
+
[`SPECIFICATION.md`](SPECIFICATION.md), and
|
|
165
|
+
[`test_vectors/vectors_v0.1.json`](test_vectors/vectors_v0.1.json)
|
|
166
|
+
contains fixed inputs/outputs (grids, headers, damaged symbols, expected
|
|
167
|
+
results) to verify conformance. If your Rust/Go/JS decoder passes the
|
|
168
|
+
vectors, it speaks Hexatess Code.
|
|
169
|
+
|
|
170
|
+
## Roadmap
|
|
171
|
+
|
|
172
|
+
1. **v0.2 — camera decoding:** bullseye detection + perspective
|
|
173
|
+
correction (the critical ecosystem step).
|
|
174
|
+
2. **v0.2 — erasure decoding:** declare blob-occluded modules as
|
|
175
|
+
erasures → doubles correctable symbol counts.
|
|
176
|
+
3. **JavaScript/TypeScript SDK** + online playground (generate a code
|
|
177
|
+
in the browser in 10 seconds).
|
|
178
|
+
4. Larger radii / capacity beyond 329 bytes (breaking header change).
|
|
179
|
+
|
|
180
|
+
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
* Code: [MIT](LICENSE)
|
|
185
|
+
* Specification: CC-BY-4.0 — implement it anywhere, commercially, under
|
|
186
|
+
any license, no royalties, forever.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
*Hexatess Code stands on the shoulders of giants: Aztec Code (bullseye +
|
|
191
|
+
spiral), MaxiCode (hexagonal lattice), QR Code and Data Matrix
|
|
192
|
+
(Reed-Solomon practice).*
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
hexatess/__init__.py,sha256=Ipr99x_VZl4jkL8ri47afisdIFYzwV8xNSrQWQK1L5Q,2351
|
|
2
|
+
hexatess/cli.py,sha256=e_Ou7vb4FNp_sT_viXhqWU4dAoONjiEQwMby0HRNcBY,2333
|
|
3
|
+
hexatess/decoder.py,sha256=jvpWhnI3ZRWGjh_oHSRtFI2EgIYeAXooivV7kaRSqCU,2404
|
|
4
|
+
hexatess/encoder.py,sha256=KbNUXCq51HeNf0oTdNHgi-6Gge_08h4zOAz_08QaqKQ,4362
|
|
5
|
+
hexatess/galois.py,sha256=x-Sr8Kvi_lKY2lHPa-od2AdEU7Wty5qdowFXyKjYksU,2177
|
|
6
|
+
hexatess/geometry.py,sha256=iCJ7NlqCKggm3yWacjHFJfA-b2SWTPcS68ZQAlIw_Rg,2130
|
|
7
|
+
hexatess/header.py,sha256=ZhCDsEM_dKYBuaBleBddde5zAnxhRLKtQ0wQNPYD-cs,4495
|
|
8
|
+
hexatess/masks.py,sha256=0xE7pAUCXIKkiZGEFZtDm76NaKkeLn1Ysw5Gf9g9mLk,1590
|
|
9
|
+
hexatess/reedsolomon.py,sha256=0FYrjDC19oHmjo3VxFvVL_S6_QFVnDiytelZPKIgGZI,5174
|
|
10
|
+
hexatess/render.py,sha256=Y8Lms4pXdKiqjlTaG-59WaOaPyBr25eXUwln5LLSuiQ,2440
|
|
11
|
+
hexatess/resilience.py,sha256=1K6KihCizObGNwpF3r5qqr3eFrXVEkVnN_YnIUDl0iI,3418
|
|
12
|
+
hexatess_code-0.1.0.dist-info/licenses/LICENSE,sha256=flrfY-EcdSw9OVuzA3XFA_zxkR5pwK8tB_gRIalGf34,1544
|
|
13
|
+
hexatess_code-0.1.0.dist-info/METADATA,sha256=wJ8aNdPa-Ij8XI_m08m90Us4LY-iSKAJcAztBYyjUwM,8239
|
|
14
|
+
hexatess_code-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
hexatess_code-0.1.0.dist-info/entry_points.txt,sha256=iSchC2Ecsc8ksfVhHt24ulfqTL3ghIAeF6TcWbrLO_A,47
|
|
16
|
+
hexatess_code-0.1.0.dist-info/top_level.txt,sha256=ZN2IgJdzQcKwK8XNCymg8LelJWQtP20L91WEFvO56KM,9
|
|
17
|
+
hexatess_code-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Hexatess Code Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
----------------------------------------------------------------------
|
|
24
|
+
Note on the specification:
|
|
25
|
+
|
|
26
|
+
The format specification (SPECIFICATION.md) is additionally released under
|
|
27
|
+
the Creative Commons Attribution 4.0 International license (CC-BY-4.0), so
|
|
28
|
+
that anyone may implement compatible encoders/decoders in any language,
|
|
29
|
+
under any license, without restriction. Provide attribution as:
|
|
30
|
+
"Hexatess Code Specification v0.1, https://github.com/<you>/hexatess-code".
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hexatess
|