hexatess-code 0.2.0__tar.gz → 0.3.1__tar.gz

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.

Potentially problematic release.


This version of hexatess-code might be problematic. Click here for more details.

Files changed (36) hide show
  1. {hexatess_code-0.2.0/src/hexatess_code.egg-info → hexatess_code-0.3.1}/PKG-INFO +59 -16
  2. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/README.md +54 -15
  3. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/pyproject.toml +6 -1
  4. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/__init__.py +12 -8
  5. hexatess_code-0.3.1/src/hexatess/camera.py +1117 -0
  6. hexatess_code-0.3.1/src/hexatess/cli.py +112 -0
  7. hexatess_code-0.3.1/src/hexatess/decoder.py +110 -0
  8. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/encoder.py +39 -6
  9. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/header.py +27 -3
  10. {hexatess_code-0.2.0 → hexatess_code-0.3.1/src/hexatess_code.egg-info}/PKG-INFO +59 -16
  11. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess_code.egg-info/SOURCES.txt +2 -0
  12. hexatess_code-0.3.1/src/hexatess_code.egg-info/requires.txt +9 -0
  13. hexatess_code-0.3.1/tests/test_camera.py +87 -0
  14. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_codec.py +72 -1
  15. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_conformance.py +6 -3
  16. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_header.py +32 -0
  17. hexatess_code-0.2.0/src/hexatess/cli.py +0 -65
  18. hexatess_code-0.2.0/src/hexatess/decoder.py +0 -75
  19. hexatess_code-0.2.0/src/hexatess_code.egg-info/requires.txt +0 -4
  20. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/LICENSE +0 -0
  21. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/setup.cfg +0 -0
  22. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/galois.py +0 -0
  23. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/geometry.py +0 -0
  24. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/masks.py +0 -0
  25. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/reedsolomon.py +0 -0
  26. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/render.py +0 -0
  27. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess/resilience.py +0 -0
  28. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess_code.egg-info/dependency_links.txt +0 -0
  29. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess_code.egg-info/entry_points.txt +0 -0
  30. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/src/hexatess_code.egg-info/top_level.txt +0 -0
  31. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_galois.py +0 -0
  32. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_geometry.py +0 -0
  33. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_masks.py +0 -0
  34. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_reedsolomon.py +0 -0
  35. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_render.py +0 -0
  36. {hexatess_code-0.2.0 → hexatess_code-0.3.1}/tests/test_resilience.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexatess-code
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Hexatess Code - an experimental 2D barcode on a hexagonal grid with Reed-Solomon error correction
5
5
  Author: The Hexatess Code Authors
6
6
  License: MIT License
@@ -56,6 +56,10 @@ License-File: LICENSE
56
56
  Requires-Dist: pillow>=9.0
57
57
  Provides-Extra: dev
58
58
  Requires-Dist: pytest>=7.0; extra == "dev"
59
+ Provides-Extra: camera
60
+ Requires-Dist: numpy>=1.24; extra == "camera"
61
+ Requires-Dist: opencv-python>=4.8; extra == "camera"
62
+ Requires-Dist: scipy>=1.10; extra == "camera"
59
63
  Dynamic: license-file
60
64
 
61
65
  # Hexatess Code 🐝
@@ -109,41 +113,73 @@ text, stats = decode(grid) # ('Hello, Hexatess!', {...})
109
113
 
110
114
  > **Status: experimental.** This is a young format: the symbol
111
115
  > specification and reference implementation are solid and heavily
112
- > tested (2,500+ tests, conformance vectors), but there is **no camera
113
- > decoder yet** — reading images assumes ideal upright sampling. See
114
- > the roadmap below. Adopting a young format is a deliberate bet; the
115
- > [full format specification](SPECIFICATION.md) is the insurance.
116
+ > tested (2,500+ tests, conformance vectors). A **camera decoder**
117
+ > (`hexatess.camera`, optional `[camera]` extra) already reads symbols
118
+ > from real photographs in about a second — printed labels, foil
119
+ > transparencies, tilted and rotated shots. Since spec v0.3 payload
120
+ > text is zlib-compressed automatically, so long texts fit into
121
+ > considerably smaller symbols. See the roadmap below. Adopting a
122
+ > young format is a deliberate bet; the [full format
123
+ > specification](SPECIFICATION.md) is the insurance.
116
124
 
117
125
  ## Installation
118
126
 
119
127
  ```bash
120
128
  pip install hexatess-code # from PyPI (once published)
129
+ pip install "hexatess-code[camera]" # + photo decoding (numpy, opencv, scipy)
121
130
  # or from a source checkout:
122
131
  pip install -e .
123
132
  ```
124
133
 
125
- Requires Python ≥ 3.8 and Pillow (for rendering only).
134
+ Requires Python ≥ 3.8; Pillow for rendering, numpy + OpenCV + SciPy
135
+ for the optional camera decoder.
126
136
 
127
137
  ## Command line
128
138
 
129
139
  ```bash
130
140
  hexatess "Hello world" -o koda.png --ec 30
131
141
  hexatess "Important URL https://example.org" -o url.png --ec 55
132
- hexatess-code --demo # demo symbol + robustness statistics
142
+ hexatess --demo # demo symbol + robustness statistics
143
+ hexatess decode-photo photo1.jpg photo2.jpg # read symbols from photos
133
144
  ```
134
145
 
146
+ Payload text is zlib-compressed automatically when that saves space
147
+ (`--no-compress` disables it; the header flag keeps decoders fully
148
+ backward compatible).
149
+
150
+ ## Payload compression (spec v0.3)
151
+
152
+ One header bit marks the payload as a zlib stream. The encoder applies
153
+ it only when it strictly helps, and decoders inflate transparently —
154
+ symbols without the flag are byte-identical to v0.2. What that means
155
+ in practice (EC 30 unless noted):
156
+
157
+ | payload | raw | stored | symbol |
158
+ |---|---|---|---|
159
+ | 80 digits | 80 B | 21 B | rmax 17 → 11 |
160
+ | `"X" × 250` | 250 B | 12 B | rmax 30 → 10 |
161
+ | 849-byte Slovene paragraph | 849 B | 203 B | would not fit → rmax 28 |
162
+ | short strings (≤ ~30 B) | — | unchanged | overhead wins |
163
+
164
+ The maximum *stored* capacity is unchanged (329 bytes at EC 5), so
165
+ incompressible data behaves exactly as before.
166
+
135
167
  ## API
136
168
 
137
169
  | Function | Description |
138
170
  |---|---|
139
- | `encode(text, ec_pct=30, mask_id="auto", min_rings=None)` | UTF-8 text → `(grid, params)`; `grid` maps axial `(q, r)` to `0/1` |
140
- | `decode(grid)` | grid → `(text, stats)`; RS-corrects transparently |
171
+ | `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` |
172
+ | `decode(grid)` | grid → `(text, stats)`; RS-corrects and inflates transparently |
141
173
  | `render(grid, path, size_px=18, ...)` | grid → PNG (pointy-top hexagons, quiet zone, supersampling) |
142
174
  | `sample_grid_from_image(path, rmax, ...)` | ideal re-sampling of a rendered PNG (self-test helper) |
143
175
  | `run_tests(...)` | noise/blob robustness statistics |
176
+ | `hexatess.camera.decode_photo(path)` | photograph → `(text, stats)`; finder detection, perspective handling, adaptive sampling (optional `[camera]` extra) |
144
177
 
145
178
  `params` / `stats` contain `rmax` (radius in rings), `mask`, `ec`,
146
- `blocks` (list of `(data_bytes, ecc_bytes)`) and `data_len`.
179
+ `blocks` (list of `(data_bytes, ecc_bytes)`), `data_len` (stored
180
+ length) and `compressed`; `stats` also reports `repair_bits` (the RS
181
+ correction ledger) and, for camera decodes, `sector` and
182
+ `finder_hits`.
147
183
 
148
184
  ## Error-correction budget
149
185
 
@@ -167,20 +203,27 @@ concentrate inside whole bytes.
167
203
  The format is deliberately **specification-first**: everything needed
168
204
  for an independent implementation is in
169
205
  [`SPECIFICATION.md`](SPECIFICATION.md), and
170
- [`test_vectors/vectors_v0.2.json`](test_vectors/vectors_v0.2.json)
206
+ [`test_vectors/vectors_v0.3.json`](test_vectors/vectors_v0.3.json)
171
207
  contains fixed inputs/outputs (grids, headers, damaged symbols, expected
172
208
  results) to verify conformance. If your Rust/Go/JS decoder passes the
173
209
  vectors, it speaks Hexatess Code.
174
210
 
175
211
  ## Roadmap
176
212
 
177
- 1. **v0.2 — camera decoding:** bullseye detection + perspective
178
- correction (the critical ecosystem step).
179
- 2. **v0.2 — erasure decoding:** declare blob-occluded modules as
213
+ 1. ~~v0.2/0.3 — camera decoding~~ **done (v0.3.0):** `hexatess.camera`
214
+ reads symbols from photographs — bullseye detection, homography +
215
+ correction-field warp handling, adaptive sampling; validated on
216
+ printed foil with curl and glare. **v0.3.1:** ≈10× faster
217
+ (a typical 12 MP photo now takes about a second) plus stable
218
+ outer-ring sampling and mis-decode-proof pose selection.
219
+ 2. ~~v0.3 — payload compression~~ **done (v0.3.1):** zlib flag bit in
220
+ the header, applied automatically when it helps.
221
+ 3. **Erasure decoding:** declare blob-occluded modules as
180
222
  erasures → doubles correctable symbol counts.
181
- 3. **JavaScript/TypeScript SDK** + online playground (generate a code
223
+ 4. **JavaScript/TypeScript SDK** + online playground (generate a code
182
224
  in the browser in 10 seconds).
183
- 4. Larger radii / capacity beyond 329 bytes (breaking header change).
225
+ 5. Larger radii / capacity beyond 329 stored bytes (breaking header
226
+ change).
184
227
 
185
228
  Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
186
229
 
@@ -49,41 +49,73 @@ text, stats = decode(grid) # ('Hello, Hexatess!', {...})
49
49
 
50
50
  > **Status: experimental.** This is a young format: the symbol
51
51
  > specification and reference implementation are solid and heavily
52
- > tested (2,500+ tests, conformance vectors), but there is **no camera
53
- > decoder yet** — reading images assumes ideal upright sampling. See
54
- > the roadmap below. Adopting a young format is a deliberate bet; the
55
- > [full format specification](SPECIFICATION.md) is the insurance.
52
+ > tested (2,500+ tests, conformance vectors). A **camera decoder**
53
+ > (`hexatess.camera`, optional `[camera]` extra) already reads symbols
54
+ > from real photographs in about a second — printed labels, foil
55
+ > transparencies, tilted and rotated shots. Since spec v0.3 payload
56
+ > text is zlib-compressed automatically, so long texts fit into
57
+ > considerably smaller symbols. See the roadmap below. Adopting a
58
+ > young format is a deliberate bet; the [full format
59
+ > specification](SPECIFICATION.md) is the insurance.
56
60
 
57
61
  ## Installation
58
62
 
59
63
  ```bash
60
64
  pip install hexatess-code # from PyPI (once published)
65
+ pip install "hexatess-code[camera]" # + photo decoding (numpy, opencv, scipy)
61
66
  # or from a source checkout:
62
67
  pip install -e .
63
68
  ```
64
69
 
65
- Requires Python ≥ 3.8 and Pillow (for rendering only).
70
+ Requires Python ≥ 3.8; Pillow for rendering, numpy + OpenCV + SciPy
71
+ for the optional camera decoder.
66
72
 
67
73
  ## Command line
68
74
 
69
75
  ```bash
70
76
  hexatess "Hello world" -o koda.png --ec 30
71
77
  hexatess "Important URL https://example.org" -o url.png --ec 55
72
- hexatess-code --demo # demo symbol + robustness statistics
78
+ hexatess --demo # demo symbol + robustness statistics
79
+ hexatess decode-photo photo1.jpg photo2.jpg # read symbols from photos
73
80
  ```
74
81
 
82
+ Payload text is zlib-compressed automatically when that saves space
83
+ (`--no-compress` disables it; the header flag keeps decoders fully
84
+ backward compatible).
85
+
86
+ ## Payload compression (spec v0.3)
87
+
88
+ One header bit marks the payload as a zlib stream. The encoder applies
89
+ it only when it strictly helps, and decoders inflate transparently —
90
+ symbols without the flag are byte-identical to v0.2. What that means
91
+ in practice (EC 30 unless noted):
92
+
93
+ | payload | raw | stored | symbol |
94
+ |---|---|---|---|
95
+ | 80 digits | 80 B | 21 B | rmax 17 → 11 |
96
+ | `"X" × 250` | 250 B | 12 B | rmax 30 → 10 |
97
+ | 849-byte Slovene paragraph | 849 B | 203 B | would not fit → rmax 28 |
98
+ | short strings (≤ ~30 B) | — | unchanged | overhead wins |
99
+
100
+ The maximum *stored* capacity is unchanged (329 bytes at EC 5), so
101
+ incompressible data behaves exactly as before.
102
+
75
103
  ## API
76
104
 
77
105
  | Function | Description |
78
106
  |---|---|
79
- | `encode(text, ec_pct=30, mask_id="auto", min_rings=None)` | UTF-8 text → `(grid, params)`; `grid` maps axial `(q, r)` to `0/1` |
80
- | `decode(grid)` | grid → `(text, stats)`; RS-corrects transparently |
107
+ | `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` |
108
+ | `decode(grid)` | grid → `(text, stats)`; RS-corrects and inflates transparently |
81
109
  | `render(grid, path, size_px=18, ...)` | grid → PNG (pointy-top hexagons, quiet zone, supersampling) |
82
110
  | `sample_grid_from_image(path, rmax, ...)` | ideal re-sampling of a rendered PNG (self-test helper) |
83
111
  | `run_tests(...)` | noise/blob robustness statistics |
112
+ | `hexatess.camera.decode_photo(path)` | photograph → `(text, stats)`; finder detection, perspective handling, adaptive sampling (optional `[camera]` extra) |
84
113
 
85
114
  `params` / `stats` contain `rmax` (radius in rings), `mask`, `ec`,
86
- `blocks` (list of `(data_bytes, ecc_bytes)`) and `data_len`.
115
+ `blocks` (list of `(data_bytes, ecc_bytes)`), `data_len` (stored
116
+ length) and `compressed`; `stats` also reports `repair_bits` (the RS
117
+ correction ledger) and, for camera decodes, `sector` and
118
+ `finder_hits`.
87
119
 
88
120
  ## Error-correction budget
89
121
 
@@ -107,20 +139,27 @@ concentrate inside whole bytes.
107
139
  The format is deliberately **specification-first**: everything needed
108
140
  for an independent implementation is in
109
141
  [`SPECIFICATION.md`](SPECIFICATION.md), and
110
- [`test_vectors/vectors_v0.2.json`](test_vectors/vectors_v0.2.json)
142
+ [`test_vectors/vectors_v0.3.json`](test_vectors/vectors_v0.3.json)
111
143
  contains fixed inputs/outputs (grids, headers, damaged symbols, expected
112
144
  results) to verify conformance. If your Rust/Go/JS decoder passes the
113
145
  vectors, it speaks Hexatess Code.
114
146
 
115
147
  ## Roadmap
116
148
 
117
- 1. **v0.2 — camera decoding:** bullseye detection + perspective
118
- correction (the critical ecosystem step).
119
- 2. **v0.2 — erasure decoding:** declare blob-occluded modules as
149
+ 1. ~~v0.2/0.3 — camera decoding~~ **done (v0.3.0):** `hexatess.camera`
150
+ reads symbols from photographs — bullseye detection, homography +
151
+ correction-field warp handling, adaptive sampling; validated on
152
+ printed foil with curl and glare. **v0.3.1:** ≈10× faster
153
+ (a typical 12 MP photo now takes about a second) plus stable
154
+ outer-ring sampling and mis-decode-proof pose selection.
155
+ 2. ~~v0.3 — payload compression~~ **done (v0.3.1):** zlib flag bit in
156
+ the header, applied automatically when it helps.
157
+ 3. **Erasure decoding:** declare blob-occluded modules as
120
158
  erasures → doubles correctable symbol counts.
121
- 3. **JavaScript/TypeScript SDK** + online playground (generate a code
159
+ 4. **JavaScript/TypeScript SDK** + online playground (generate a code
122
160
  in the browser in 10 seconds).
123
- 4. Larger radii / capacity beyond 329 bytes (breaking header change).
161
+ 5. Larger radii / capacity beyond 329 stored bytes (breaking header
162
+ change).
124
163
 
125
164
  Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
126
165
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hexatess-code"
7
- version = "0.2.0"
7
+ version = "0.3.1"
8
8
  description = "Hexatess Code - an experimental 2D barcode on a hexagonal grid with Reed-Solomon error correction"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -36,6 +36,11 @@ dependencies = [
36
36
  dev = [
37
37
  "pytest>=7.0",
38
38
  ]
39
+ camera = [
40
+ "numpy>=1.24",
41
+ "opencv-python>=4.8",
42
+ "scipy>=1.10",
43
+ ]
39
44
 
40
45
  [project.urls]
41
46
  Homepage = "https://github.com/lovro-abram/hexatess-code"
@@ -1,10 +1,12 @@
1
1
  """Hexatess Code - an experimental 2D barcode on a hexagonal grid.
2
2
 
3
- Reference implementation of specification v0.2. The symbol is a
3
+ Reference implementation of specification v0.3. The symbol is a
4
4
  hexagonal lattice with a hexagonal bullseye finder, an
5
5
  orientation key ring, spiral serialization from the centre outwards,
6
6
  a Reed-Solomon protected header and a continuously selectable
7
- error-correction budget of 5-90 percent (Aztec-style).
7
+ error-correction budget of 5-90 percent (Aztec-style). Since spec
8
+ v0.3 the payload can optionally be zlib-compressed (one header flag
9
+ bit; applied automatically whenever it saves space).
8
10
 
9
11
  Quick start
10
12
  -----------
@@ -18,13 +20,13 @@ Quick start
18
20
  'Hello, Hexatess!'
19
21
 
20
22
  See SPECIFICATION.md in the repository for the full format
21
- specification, and test_vectors/vectors_v0.2.json for conformance
23
+ specification, and test_vectors/vectors_v0.3.json for conformance
22
24
  data usable by independent implementations.
23
25
  """
24
26
 
25
27
  from __future__ import annotations
26
28
 
27
- from .decoder import decode
29
+ from .decoder import decode, payload_to_text
28
30
  from .encoder import encode
29
31
  from .geometry import (
30
32
  DIRS,
@@ -37,6 +39,7 @@ from .geometry import (
37
39
  from .header import (
38
40
  BULLSEYE_RINGS,
39
41
  BLOCK_DATA_MAX,
42
+ COMPRESSED_FLAG,
40
43
  DATA_RING0,
41
44
  KEY_RING,
42
45
  MAX_DATA_BYTES,
@@ -51,14 +54,15 @@ from .header import (
51
54
  pack_mode,
52
55
  plan_blocks,
53
56
  unpack_mode,
57
+ unpack_mode_ex,
54
58
  )
55
59
  from .masks import evaluate_mask, mask_bit, mask_payload, select_mask
56
60
  from .reedsolomon import rs_correct_msg, rs_encode_msg
57
61
  from .render import render, sample_grid_from_image
58
62
  from .resilience import add_blob_damage, add_random_noise, run_tests
59
63
 
60
- __version__ = "0.2.0"
61
- SPEC_VERSION = "0.2"
64
+ __version__ = "0.3.1"
65
+ SPEC_VERSION = "0.3"
62
66
 
63
67
  __all__ = [
64
68
  # high-level API
@@ -69,7 +73,7 @@ __all__ = [
69
73
  "ring_capacity",
70
74
  # framing
71
75
  "bytes_to_bits", "bits_to_bytes", "pack_mode", "unpack_mode",
72
- "plan_blocks",
76
+ "unpack_mode_ex", "plan_blocks", "payload_to_text",
73
77
  # masks
74
78
  "mask_bit", "mask_payload", "evaluate_mask", "select_mask",
75
79
  # error correction
@@ -77,7 +81,7 @@ __all__ = [
77
81
  # constants
78
82
  "BULLSEYE_RINGS", "KEY_RING", "DATA_RING0", "MAX_RINGS",
79
83
  "BLOCK_DATA_MAX", "MODE_BYTES", "MODE_ECC", "MODE_BITS",
80
- "MIN_EC_PCT", "MAX_EC_PCT", "MAX_DATA_BYTES",
84
+ "MIN_EC_PCT", "MAX_EC_PCT", "MAX_DATA_BYTES", "COMPRESSED_FLAG",
81
85
  # meta
82
86
  "__version__", "SPEC_VERSION",
83
87
  ]