@sythos/js_barcode_universal 1.5.13 → 1.5.15

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.
Files changed (137) hide show
  1. package/LICENSE +22 -4
  2. package/NOTICE.md +10 -1
  3. package/README.md +416 -22
  4. package/bundle/sythos-barcode.esm.js +22176 -11991
  5. package/bundle/sythos-barcode.js +22059 -11991
  6. package/licenses/README.md +12 -1
  7. package/licenses/codablockf.license +51 -0
  8. package/licenses/code16k.license +52 -0
  9. package/licenses/code25.license +58 -0
  10. package/licenses/code32.license +51 -0
  11. package/licenses/dotcode.license +66 -0
  12. package/licenses/gs1-composite.license +106 -0
  13. package/licenses/gs1-databar.license +21 -14
  14. package/licenses/hanxin.license +95 -0
  15. package/licenses/maxicode.license +62 -0
  16. package/licenses/postal.license +61 -0
  17. package/licenses/pzn.license +53 -0
  18. package/licenses/telepen.license +73 -0
  19. package/llms.txt +62 -6
  20. package/package.json +47 -3
  21. package/src/index.d.ts +111 -6
  22. package/src/index.js +362 -13
  23. package/src/js/codablockf/decoder.js +198 -0
  24. package/src/js/codablockf/encoder.js +97 -0
  25. package/src/js/codablockf/index.js +3 -0
  26. package/src/js/code16k/decoder.js +303 -0
  27. package/src/js/code16k/detector.js +187 -0
  28. package/src/js/code16k/encoder.js +153 -0
  29. package/src/js/code16k/index.js +14 -0
  30. package/src/js/code16k/tables.js +152 -0
  31. package/src/js/composite/index.js +570 -0
  32. package/src/js/core/detection-contract.js +185 -0
  33. package/src/js/core/symbol-layout.js +155 -0
  34. package/src/js/databar/expanded.js +968 -0
  35. package/src/js/databar/index.js +4 -0
  36. package/src/js/databar/layout.js +192 -0
  37. package/src/js/databar/limited.js +533 -0
  38. package/src/js/databar/stacked-omnidirectional.js +548 -0
  39. package/src/js/databar/stacked.js +528 -0
  40. package/src/js/dotcode/decoder.js +548 -0
  41. package/src/js/dotcode/detector.js +251 -0
  42. package/src/js/dotcode/encoder.js +453 -0
  43. package/src/js/dotcode/index.js +34 -0
  44. package/src/js/dotcode/tables.js +168 -0
  45. package/src/js/hanxin/decoder.js +299 -0
  46. package/src/js/hanxin/detector.js +122 -0
  47. package/src/js/hanxin/encoder.js +259 -0
  48. package/src/js/hanxin/index.js +16 -0
  49. package/src/js/hanxin/tables.js +316 -0
  50. package/src/js/image/height-coded.js +164 -0
  51. package/src/js/maxicode/decoder.js +275 -0
  52. package/src/js/maxicode/detector.js +118 -0
  53. package/src/js/maxicode/encoder.js +438 -0
  54. package/src/js/maxicode/index.js +34 -0
  55. package/src/js/maxicode/tables.js +132 -0
  56. package/src/js/oned/code25.js +148 -0
  57. package/src/js/oned/index.js +21 -3
  58. package/src/js/oned/postal.js +889 -0
  59. package/src/js/oned/reader.js +272 -7
  60. package/src/js/oned/telepen.js +361 -0
  61. package/src/js/oned/writers.js +186 -12
  62. package/src/js/stacked128/common.js +208 -0
  63. package/src/ts/codablockf/decoder.ts +212 -0
  64. package/src/ts/codablockf/encoder.ts +128 -0
  65. package/src/ts/codablockf/index.d.ts +31 -0
  66. package/src/ts/codablockf/index.ts +6 -0
  67. package/src/ts/code16k/decoder.d.ts +25 -0
  68. package/src/ts/code16k/decoder.ts +313 -0
  69. package/src/ts/code16k/detector.d.ts +21 -0
  70. package/src/ts/code16k/detector.ts +203 -0
  71. package/src/ts/code16k/encoder.d.ts +26 -0
  72. package/src/ts/code16k/encoder.ts +198 -0
  73. package/src/ts/code16k/index.d.ts +10 -0
  74. package/src/ts/code16k/index.ts +47 -0
  75. package/src/ts/code16k/tables.d.ts +54 -0
  76. package/src/ts/code16k/tables.ts +195 -0
  77. package/src/ts/composite/index.d.ts +74 -0
  78. package/src/ts/composite/index.ts +547 -0
  79. package/src/ts/core/detection-contract.d.ts +119 -0
  80. package/src/ts/core/detection-contract.ts +267 -0
  81. package/src/ts/core/symbol-layout.d.ts +108 -0
  82. package/src/ts/core/symbol-layout.ts +236 -0
  83. package/src/ts/databar/expanded.d.ts +40 -0
  84. package/src/ts/databar/expanded.ts +948 -0
  85. package/src/ts/databar/index.d.ts +48 -0
  86. package/src/ts/databar/index.ts +48 -0
  87. package/src/ts/databar/layout.d.ts +122 -0
  88. package/src/ts/databar/layout.ts +275 -0
  89. package/src/ts/databar/limited.d.ts +85 -0
  90. package/src/ts/databar/limited.ts +553 -0
  91. package/src/ts/databar/stacked-omnidirectional.d.ts +128 -0
  92. package/src/ts/databar/stacked-omnidirectional.ts +559 -0
  93. package/src/ts/databar/stacked.d.ts +96 -0
  94. package/src/ts/databar/stacked.ts +549 -0
  95. package/src/ts/dotcode/decoder.d.ts +58 -0
  96. package/src/ts/dotcode/decoder.ts +467 -0
  97. package/src/ts/dotcode/detector.d.ts +63 -0
  98. package/src/ts/dotcode/detector.ts +263 -0
  99. package/src/ts/dotcode/encoder.d.ts +69 -0
  100. package/src/ts/dotcode/encoder.ts +427 -0
  101. package/src/ts/dotcode/index.d.ts +37 -0
  102. package/src/ts/dotcode/index.ts +72 -0
  103. package/src/ts/dotcode/tables.d.ts +74 -0
  104. package/src/ts/dotcode/tables.ts +174 -0
  105. package/src/ts/hanxin/decoder.d.ts +48 -0
  106. package/src/ts/hanxin/decoder.ts +314 -0
  107. package/src/ts/hanxin/detector.d.ts +45 -0
  108. package/src/ts/hanxin/detector.ts +124 -0
  109. package/src/ts/hanxin/encoder.d.ts +41 -0
  110. package/src/ts/hanxin/encoder.ts +290 -0
  111. package/src/ts/hanxin/index.d.ts +16 -0
  112. package/src/ts/hanxin/index.ts +18 -0
  113. package/src/ts/hanxin/tables.d.ts +80 -0
  114. package/src/ts/hanxin/tables.ts +348 -0
  115. package/src/ts/image/height-coded.d.ts +101 -0
  116. package/src/ts/image/height-coded.ts +227 -0
  117. package/src/ts/index.d.ts +46 -2
  118. package/src/ts/index.ts +380 -13
  119. package/src/ts/maxicode/decoder.ts +265 -0
  120. package/src/ts/maxicode/detector.ts +109 -0
  121. package/src/ts/maxicode/encoder.ts +414 -0
  122. package/src/ts/maxicode/index.d.ts +96 -0
  123. package/src/ts/maxicode/index.ts +44 -0
  124. package/src/ts/maxicode/tables.ts +136 -0
  125. package/src/ts/oned/code25.d.ts +42 -0
  126. package/src/ts/oned/code25.ts +170 -0
  127. package/src/ts/oned/index.d.ts +7 -2
  128. package/src/ts/oned/index.ts +45 -3
  129. package/src/ts/oned/postal.d.ts +44 -0
  130. package/src/ts/oned/postal.ts +819 -0
  131. package/src/ts/oned/reader.d.ts +35 -0
  132. package/src/ts/oned/reader.ts +261 -7
  133. package/src/ts/oned/telepen.d.ts +65 -0
  134. package/src/ts/oned/telepen.ts +368 -0
  135. package/src/ts/oned/writers.d.ts +32 -0
  136. package/src/ts/oned/writers.ts +184 -13
  137. package/src/ts/stacked128/common.ts +225 -0
@@ -0,0 +1,348 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
8
+ * SPDX-License-Identifier: MIT
9
+ *
10
+ * Original work. No code from any other barcode implementation.
11
+ */
12
+
13
+ /**
14
+ * Han Xin Code structural tables and geometry.
15
+ *
16
+ * This module deliberately starts with the compact, alignment-free part of
17
+ * ISO/IEC 20830: versions 1 through 3. Keeping the function pattern mask in
18
+ * one place makes the encoder, decoder and detector agree about every payload
19
+ * cell and prevents accidental data placement over structural information.
20
+ *
21
+ * @module hanxin/tables
22
+ */
23
+
24
+ import { BitMatrix } from '../core/bit-matrix.js';
25
+ import { GF16, GaloisField } from '../core/galois-field.js';
26
+ import { rsDecode, rsEncode } from '../core/reed-solomon.js';
27
+ import { FormatError } from '../core/errors.js';
28
+
29
+ export const HANXIN_MIN_VERSION = 1;
30
+ export const HANXIN_MAX_VERSION = 3;
31
+ export const HANXIN_VERSIONS = [1, 2, 3] as const;
32
+ export type HanXinVersion = typeof HANXIN_VERSIONS[number];
33
+
34
+ export const HANXIN_ECC_LEVELS = ['L1', 'L2', 'L3', 'L4'] as const;
35
+ export type HanXinEccLevel = typeof HANXIN_ECC_LEVELS[number];
36
+
37
+ /** Han Xin's data field is GF(2^8) with x^8+x^6+x^5+x+1. */
38
+ export const GF256_HANXIN = new GaloisField({
39
+ size: 256,
40
+ primitive: 0x163,
41
+ name: 'GF(256)/HanXin',
42
+ });
43
+
44
+ /** Total codewords, including error correction, for versions 1 through 3. */
45
+ export const HANXIN_TOTAL_CODEWORDS = [25, 37, 50] as const;
46
+
47
+ /** Data modules, including the five zero remainder modules in each compact version. */
48
+ export const HANXIN_DATA_MODULES = [205, 301, 405] as const;
49
+
50
+ /** Unused tail modules after the complete codeword stream. */
51
+ export const HANXIN_REMAINDER_BITS = [5, 5, 5] as const;
52
+
53
+ /**
54
+ * One Reed--Solomon batch is `(blockCount, dataCodewords, eccCodewords)`.
55
+ * The compact versions use one block at every error-correction level.
56
+ */
57
+ const EC_BATCHES: ReadonlyArray<ReadonlyArray<readonly [number, number, number]>> = [
58
+ [[1, 21, 4], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
59
+ [[1, 17, 8], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
60
+ [[1, 13, 12], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
61
+ [[1, 9, 16], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
62
+ [[1, 31, 6], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
63
+ [[1, 25, 12], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
64
+ [[1, 19, 18], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
65
+ [[1, 15, 22], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
66
+ [[1, 42, 8], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
67
+ [[1, 34, 16], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
68
+ [[1, 26, 24], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
69
+ [[1, 20, 30], [0, 0, 0], [0, 0, 0], [0, 0, 0]],
70
+ ];
71
+
72
+ /** @returns {number} Side length in modules. */
73
+ export function hanXinSize(version: number): number {
74
+ if (!Number.isInteger(version) || version < HANXIN_MIN_VERSION || version > HANXIN_MAX_VERSION) {
75
+ throw new FormatError(`Han Xin: supported versions are 1-3, got ${version}`);
76
+ }
77
+ return 21 + version * 2;
78
+ }
79
+
80
+ /** @returns {HanXinVersion} */
81
+ export function normalizeHanXinVersion(value: unknown): HanXinVersion {
82
+ const version = typeof value === 'string' ? Number(value.replace(/^V/i, '')) : Number(value);
83
+ if (!Number.isInteger(version) || version < HANXIN_MIN_VERSION || version > HANXIN_MAX_VERSION) {
84
+ throw new FormatError(`Han Xin: supported versions are 1-3, got ${String(value)}`);
85
+ }
86
+ return version as HanXinVersion;
87
+ }
88
+
89
+ /** @returns {HanXinEccLevel} */
90
+ export function normalizeHanXinEcc(value: unknown): HanXinEccLevel {
91
+ if (value == null) return 'L1';
92
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 4) {
93
+ return HANXIN_ECC_LEVELS[value - 1];
94
+ }
95
+ const text = String(value).toUpperCase();
96
+ if ((HANXIN_ECC_LEVELS as readonly string[]).includes(text)) return text as HanXinEccLevel;
97
+ throw new FormatError(`Han Xin: error correction must be L1, L2, L3 or L4, got ${String(value)}`);
98
+ }
99
+
100
+ /** @returns {number} Index of an error-correction level. */
101
+ export function hanXinEccIndex(level: HanXinEccLevel): number {
102
+ return HANXIN_ECC_LEVELS.indexOf(level);
103
+ }
104
+
105
+ /** @returns {{blockCount:number,dataCodewords:number,eccCodewords:number}} */
106
+ export function hanXinEcLayout(version: HanXinVersion, level: HanXinEccLevel) {
107
+ const entry = EC_BATCHES[(version - 1) * 4 + hanXinEccIndex(level)][0];
108
+ return { blockCount: entry[0], dataCodewords: entry[1], eccCodewords: entry[2] };
109
+ }
110
+
111
+ /** @returns {number} Data codewords for a version and EC level. */
112
+ export function hanXinDataCodewords(version: HanXinVersion, level: HanXinEccLevel): number {
113
+ return hanXinEcLayout(version, level).dataCodewords;
114
+ }
115
+
116
+ /** Four orientation-specific 7x7 finder patterns, packed MSB first. */
117
+ export const HANXIN_FINDER_TOP_LEFT = [0x7f, 0x40, 0x5f, 0x50, 0x57, 0x57, 0x57] as const;
118
+ export const HANXIN_FINDER_SIDE = [0x7f, 0x01, 0x7d, 0x05, 0x75, 0x75, 0x75] as const;
119
+ export const HANXIN_FINDER_BOTTOM_RIGHT = [0x75, 0x75, 0x75, 0x05, 0x7d, 0x01, 0x7f] as const;
120
+
121
+ export type HanXinCoordinate = readonly [number, number];
122
+
123
+ function index(size: number, x: number, y: number): number {
124
+ return y * size + x;
125
+ }
126
+
127
+ function reserve(mask: Uint8Array, matrix: BitMatrix, x: number, y: number, dark: boolean): void {
128
+ if (x < 0 || y < 0 || x >= matrix.width || y >= matrix.height) return;
129
+ mask[index(matrix.width, x, y)] = 1;
130
+ matrix.setValue(x, y, dark);
131
+ }
132
+
133
+ function placeFinder(
134
+ mask: Uint8Array,
135
+ matrix: BitMatrix,
136
+ offsetX: number,
137
+ offsetY: number,
138
+ rows: readonly number[],
139
+ ): void {
140
+ for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
141
+ reserve(mask, matrix, offsetX + x, offsetY + y, (rows[y] & (0x40 >> x)) !== 0);
142
+ }
143
+ }
144
+
145
+ /** Return the function modules and their fixed darkness for a version. */
146
+ export function createHanXinFunctionGrid(version: HanXinVersion): {
147
+ matrix: BitMatrix;
148
+ reserved: Uint8Array;
149
+ } {
150
+ const size = hanXinSize(version);
151
+ const matrix = new BitMatrix(size, size);
152
+ const reserved = new Uint8Array(size * size);
153
+
154
+ placeFinder(reserved, matrix, 0, 0, HANXIN_FINDER_TOP_LEFT);
155
+ placeFinder(reserved, matrix, size - 7, 0, HANXIN_FINDER_SIDE);
156
+ placeFinder(reserved, matrix, 0, size - 7, HANXIN_FINDER_SIDE);
157
+ placeFinder(reserved, matrix, size - 7, size - 7, HANXIN_FINDER_BOTTOM_RIGHT);
158
+
159
+ // The one-module light separators belong to the function region.
160
+ for (let i = 0; i < 8; i++) {
161
+ reserve(reserved, matrix, i, 7, false);
162
+ reserve(reserved, matrix, 7, i, false);
163
+ reserve(reserved, matrix, size - i - 1, 7, false);
164
+ reserve(reserved, matrix, 7, size - i - 1, false);
165
+ reserve(reserved, matrix, size - 8, i, false);
166
+ reserve(reserved, matrix, i, size - 8, false);
167
+ reserve(reserved, matrix, size - 8, size - i - 1, false);
168
+ reserve(reserved, matrix, size - i - 1, size - 8, false);
169
+ }
170
+
171
+ // Two redundant copies of the 34-bit structural information are carried
172
+ // around the finder patterns. The four 9-module strips below reserve all
173
+ // positions; shared corners make the wire order 9+8+9+8 modules per copy.
174
+ for (let i = 0; i < 9; i++) {
175
+ reserve(reserved, matrix, i, 8, false);
176
+ reserve(reserved, matrix, 8, i, false);
177
+ reserve(reserved, matrix, size - i - 1, 8, false);
178
+ reserve(reserved, matrix, 8, size - i - 1, false);
179
+ reserve(reserved, matrix, size - 9, i, false);
180
+ reserve(reserved, matrix, i, size - 9, false);
181
+ reserve(reserved, matrix, size - 9, size - i - 1, false);
182
+ reserve(reserved, matrix, size - i - 1, size - 9, false);
183
+ }
184
+
185
+ return { matrix, reserved };
186
+ }
187
+
188
+ /** Return payload positions in the normative row-major order. */
189
+ export function hanXinDataCoordinates(version: HanXinVersion): HanXinCoordinate[] {
190
+ const { reserved } = createHanXinFunctionGrid(version);
191
+ const size = hanXinSize(version);
192
+ const result: HanXinCoordinate[] = [];
193
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
194
+ if (reserved[index(size, x, y)] === 0) result.push([x, y]);
195
+ }
196
+ return result;
197
+ }
198
+
199
+ /** Whether a data module is inverted by one of Han Xin's four masks. */
200
+ export function hanXinMaskFlip(mask: number, x: number, y: number): boolean {
201
+ if (!Number.isInteger(mask) || mask < 0 || mask > 3) return false;
202
+ const i = y + 1;
203
+ const j = x + 1;
204
+ // The first public mask is the constant-zero (no inversion) mask. The
205
+ // remaining three masks are the parity expressions from the format
206
+ // definition, represented here with zero-based API values 1-3.
207
+ if (mask === 0) return false;
208
+ if (mask === 1) return ((i + j) & 1) === 0;
209
+ if (mask === 2) return ((((i + j) % 3) + (j % 3)) & 1) === 0;
210
+ if (j === 0 || i === 0) return false;
211
+ return (((i % j) + (j % i) + (i % 3) + (j % 3)) & 1) === 0;
212
+ }
213
+
214
+ /** Build the 34 structural bits protected by the GF(16) short RS block. */
215
+ export function hanXinFunctionInfoBits(
216
+ version: HanXinVersion,
217
+ level: HanXinEccLevel,
218
+ mask: number,
219
+ ): boolean[] {
220
+ if (!Number.isInteger(mask) || mask < 0 || mask > 3) throw new FormatError('Han Xin: mask must be an integer from 0 to 3');
221
+ const value = ((version + 20) << 4) | (hanXinEccIndex(level) << 2) | mask;
222
+ const data = [(value >>> 8) & 0x0f, (value >>> 4) & 0x0f, value & 0x0f];
223
+ const ecc = rsEncode(data, 4, GF16, 1);
224
+ const bits: boolean[] = [];
225
+ for (const symbol of data.concat(ecc)) for (let bit = 3; bit >= 0; bit--) bits.push(((symbol >>> bit) & 1) !== 0);
226
+ // The six non-codeword bits are part of the fixed Han Xin function
227
+ // information pattern, not arbitrary padding.
228
+ for (const bit of [false, true, false, true, false, true]) bits.push(bit);
229
+ if (bits.length !== 34) throw new FormatError('Han Xin: invalid function information length');
230
+ return bits;
231
+ }
232
+
233
+ /** Place both redundant structural-information copies in the fixed strips. */
234
+ export function placeHanXinFunctionInfo(
235
+ matrix: BitMatrix,
236
+ version: HanXinVersion,
237
+ level: HanXinEccLevel,
238
+ mask: number,
239
+ ): void {
240
+ const size = hanXinSize(version);
241
+ const bits = hanXinFunctionInfoBits(version, level, mask);
242
+ // The four strips contain 9 + 8 + 9 + 8 modules. The corner cells where
243
+ // two strips meet are shared; they must not consume the preceding bit a
244
+ // second time.
245
+ for (let i = 0; i < 9; i++) {
246
+ matrix.setValue(i, 8, bits[i]);
247
+ matrix.setValue(size - 1 - i, size - 9, bits[i]);
248
+ }
249
+ for (let i = 0; i < 8; i++) {
250
+ matrix.setValue(8, 7 - i, bits[9 + i]);
251
+ matrix.setValue(size - 9, size - 8 + i, bits[9 + i]);
252
+ }
253
+ for (let i = 0; i < 9; i++) {
254
+ matrix.setValue(size - 9, i, bits[i + 17]);
255
+ matrix.setValue(8, size - 1 - i, bits[i + 17]);
256
+ }
257
+ for (let i = 0; i < 8; i++) {
258
+ matrix.setValue(size - 8 + i, 8, bits[26 + i]);
259
+ matrix.setValue(7 - i, size - 9, bits[26 + i]);
260
+ }
261
+ }
262
+
263
+ function readInfoCopy(matrix: BitMatrix, version: HanXinVersion): boolean[] {
264
+ const size = hanXinSize(version);
265
+ const bits: boolean[] = [];
266
+ for (let i = 0; i < 9; i++) bits.push(matrix.get(i, 8));
267
+ for (let i = 0; i < 8; i++) bits.push(matrix.get(8, 7 - i));
268
+ for (let i = 0; i < 9; i++) bits.push(matrix.get(size - 9, i));
269
+ for (let i = 0; i < 8; i++) bits.push(matrix.get(size - 8 + i, 8));
270
+ return bits;
271
+ }
272
+
273
+ /** Decode a structural information copy, correcting up to two nibble errors. */
274
+ export function decodeHanXinFunctionInfo(matrix: BitMatrix, version: HanXinVersion): {
275
+ version: HanXinVersion;
276
+ level: HanXinEccLevel;
277
+ mask: number;
278
+ corrections: number;
279
+ } {
280
+ const copies = [readInfoCopy(matrix, version)];
281
+ const size = hanXinSize(version);
282
+ const second = new BitMatrix(size, size);
283
+ // The second copy is read directly in its wire order. Keeping this helper
284
+ // local avoids exposing an orientation-specific representation publicly.
285
+ for (let i = 0; i < 9; i++) {
286
+ second.setValue(i, 8, matrix.get(size - 1 - i, size - 9));
287
+ }
288
+ for (let i = 0; i < 8; i++) {
289
+ second.setValue(8, 7 - i, matrix.get(size - 9, size - 8 + i));
290
+ }
291
+ for (let i = 0; i < 9; i++) {
292
+ second.setValue(size - 9, i, matrix.get(8, size - 1 - i));
293
+ }
294
+ for (let i = 0; i < 8; i++) {
295
+ second.setValue(size - 8 + i, 8, matrix.get(7 - i, size - 9));
296
+ }
297
+ copies.push(readInfoCopy(second, version));
298
+
299
+ let best: { value: number[]; corrections: number } | null = null;
300
+ for (const bits of copies) {
301
+ const fixedTail = [false, true, false, true, false, true];
302
+ if (bits.length !== 34 || fixedTail.some((bit, index) => bits[28 + index] !== bit)) continue;
303
+ const symbols = [] as number[];
304
+ for (let i = 0; i < 7; i++) {
305
+ let symbol = 0;
306
+ for (let bit = 0; bit < 4; bit++) symbol = (symbol << 1) | (bits[i * 4 + bit] ? 1 : 0);
307
+ symbols.push(symbol);
308
+ }
309
+ try {
310
+ const corrections = rsDecode(symbols, 4, GF16, 1);
311
+ if (!best || corrections < best.corrections) best = { value: symbols, corrections };
312
+ } catch {
313
+ // Try the redundant copy before rejecting the symbol.
314
+ }
315
+ }
316
+ if (!best) throw new FormatError('Han Xin: structural information is unreadable');
317
+ const value = (best.value[0] << 8) | (best.value[1] << 4) | best.value[2];
318
+ const encodedVersion = (value >>> 4) - 20;
319
+ const levelIndex = (value >>> 2) & 3;
320
+ const mask = value & 3;
321
+ if (encodedVersion !== version) throw new FormatError('Han Xin: structural version disagrees with matrix dimensions');
322
+ return {
323
+ version,
324
+ level: HANXIN_ECC_LEVELS[levelIndex],
325
+ mask,
326
+ corrections: best.corrections,
327
+ };
328
+ }
329
+
330
+ /** Verify all fixed modules and count the payload cells. */
331
+ export function validateHanXinTables(): string[] {
332
+ const errors: string[] = [];
333
+ for (const version of HANXIN_VERSIONS) {
334
+ const size = hanXinSize(version);
335
+ const cells = hanXinDataCoordinates(version).length;
336
+ if (cells !== HANXIN_DATA_MODULES[version - 1]) {
337
+ errors.push(`Version ${version}: ${cells} data modules, expected ${HANXIN_DATA_MODULES[version - 1]}`);
338
+ }
339
+ for (const level of HANXIN_ECC_LEVELS) {
340
+ const layout = hanXinEcLayout(version, level);
341
+ if (layout.dataCodewords + layout.eccCodewords !== HANXIN_TOTAL_CODEWORDS[version - 1]) {
342
+ errors.push(`Version ${version} ${level}: invalid RS block total`);
343
+ }
344
+ }
345
+ if (size !== 21 + version * 2) errors.push(`Version ${version}: invalid dimension`);
346
+ }
347
+ return errors;
348
+ }
@@ -0,0 +1,101 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+ /**
32
+ * Generic normalized bar-height profiles for future height-coded symbols.
33
+ *
34
+ * A profile uses a top and bottom edge in a unit-height coordinate system: zero
35
+ * is the top of the symbol and one is the bottom. The four-state profiles use
36
+ * the two independent edge extensions that are common to this class of
37
+ * symbols, while the two-state profile is the tracker/full subset. A concrete
38
+ * symbology remains responsible for its own data, checksum and orientation
39
+ * rules.
40
+ *
41
+ * @module image/height-coded
42
+ */
43
+ /** A state value accepted by a height-coded bar profile. */
44
+ export type HeightState = 0 | 1 | 2 | 3;
45
+ /** Supported numbers of states. */
46
+ export type HeightStateCount = 2 | 4;
47
+ /** Normalized top and bottom edges of one height-coded bar. */
48
+ export interface HeightCodedBar {
49
+ /** Top edge, normalized to the inclusive range 0..1. */
50
+ top: number;
51
+ /** Bottom edge, normalized to the inclusive range 0..1. */
52
+ bottom: number;
53
+ }
54
+ /**
55
+ * Validate one state and return its narrowed value.
56
+ *
57
+ * When `stateCount` is supplied, states outside that alphabet are rejected;
58
+ * leaving it out validates the complete four-state range.
59
+ *
60
+ * @param {number} state
61
+ * @param {2|4} [stateCount]
62
+ * @returns {HeightState}
63
+ */
64
+ export declare function validateHeightState(state: number, stateCount?: HeightStateCount): HeightState;
65
+ /**
66
+ * Return the normalized top and bottom edges for one state.
67
+ *
68
+ * State numbering is intentionally generic: 0 is tracker, 1 is ascender,
69
+ * 2 is descender and 3 is full for the four-state alphabet. The two-state
70
+ * alphabet exposes tracker (0) and full (1), so it is a strict subset of the
71
+ * four-state geometry and does not prescribe a postal format's semantics.
72
+ *
73
+ * @param {number} state
74
+ * @param {2|4} stateCount
75
+ * @returns {HeightCodedBar}
76
+ */
77
+ export declare function barHeightProfile(state: number, stateCount: HeightStateCount): HeightCodedBar;
78
+ /**
79
+ * Convert a state sequence to normalized top/bottom bar profiles.
80
+ *
81
+ * The returned array and every profile in it are newly allocated, so callers
82
+ * can safely adjust rendering coordinates without changing shared constants.
83
+ *
84
+ * @param {ArrayLike<number>} states
85
+ * @param {2|4} stateCount
86
+ * @returns {HeightCodedBar[]}
87
+ */
88
+ export declare function encodeHeightProfile(states: ArrayLike<number>, stateCount: HeightStateCount): HeightCodedBar[];
89
+ /**
90
+ * Convert normalized top/bottom bar profiles back to their states.
91
+ *
92
+ * Decoding is deliberately exact: a profile must match one of the canonical
93
+ * normalized pairs for the selected alphabet. Raster or measurement code can
94
+ * quantize its observations before calling this helper; accepting arbitrary
95
+ * in-range pairs here would turn malformed bars into valid states.
96
+ *
97
+ * @param {ArrayLike<HeightCodedBar>} profile
98
+ * @param {2|4} stateCount
99
+ * @returns {HeightState[]}
100
+ */
101
+ export declare function decodeHeightProfile(profile: ArrayLike<HeightCodedBar>, stateCount: HeightStateCount): HeightState[];
@@ -0,0 +1,227 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * SPDX-License-Identifier: MIT
27
+ *
28
+ * Original work. No code from any other barcode implementation.
29
+ */
30
+
31
+ /**
32
+ * Generic normalized bar-height profiles for future height-coded symbols.
33
+ *
34
+ * A profile uses a top and bottom edge in a unit-height coordinate system: zero
35
+ * is the top of the symbol and one is the bottom. The four-state profiles use
36
+ * the two independent edge extensions that are common to this class of
37
+ * symbols, while the two-state profile is the tracker/full subset. A concrete
38
+ * symbology remains responsible for its own data, checksum and orientation
39
+ * rules.
40
+ *
41
+ * @module image/height-coded
42
+ */
43
+
44
+ /** A state value accepted by a height-coded bar profile. */
45
+ export type HeightState = 0 | 1 | 2 | 3;
46
+
47
+ /** Supported numbers of states. */
48
+ export type HeightStateCount = 2 | 4;
49
+
50
+ /** Normalized top and bottom edges of one height-coded bar. */
51
+ export interface HeightCodedBar {
52
+ /** Top edge, normalized to the inclusive range 0..1. */
53
+ top: number;
54
+ /** Bottom edge, normalized to the inclusive range 0..1. */
55
+ bottom: number;
56
+ }
57
+
58
+ const TRACKER_PROFILE: HeightCodedBar = { top: 0.25, bottom: 0.75 };
59
+ const ASCENDER_PROFILE: HeightCodedBar = { top: 0, bottom: 0.75 };
60
+ const DESCENDER_PROFILE: HeightCodedBar = { top: 0.25, bottom: 1 };
61
+ const FULL_PROFILE: HeightCodedBar = { top: 0, bottom: 1 };
62
+
63
+ // Keep validation and output allocations bounded when a caller passes data
64
+ // originating in a file or camera pipeline.
65
+ const MAX_PROFILE_LENGTH = 1_000_000;
66
+
67
+ function validateStateCount(stateCount: unknown): HeightStateCount {
68
+ if (stateCount !== 2 && stateCount !== 4) {
69
+ throw new RangeError(`Height state count must be 2 or 4, got ${String(stateCount)}`);
70
+ }
71
+ return stateCount;
72
+ }
73
+
74
+ function validateProfileLength(value: unknown, label: string): number {
75
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
76
+ throw new TypeError(`${label} must be an array-like object`);
77
+ }
78
+
79
+ const length = (value as { length?: unknown }).length;
80
+ if (typeof length !== 'number'
81
+ || !Number.isSafeInteger(length) || length < 0 || length > MAX_PROFILE_LENGTH) {
82
+ throw new RangeError(
83
+ `${label} length must be a safe integer between 0 and ${MAX_PROFILE_LENGTH}`
84
+ );
85
+ }
86
+ return length;
87
+ }
88
+
89
+ /**
90
+ * Validate one state and return its narrowed value.
91
+ *
92
+ * When `stateCount` is supplied, states outside that alphabet are rejected;
93
+ * leaving it out validates the complete four-state range.
94
+ *
95
+ * @param {number} state
96
+ * @param {2|4} [stateCount]
97
+ * @returns {HeightState}
98
+ */
99
+ export function validateHeightState(
100
+ state: number,
101
+ stateCount?: HeightStateCount,
102
+ ): HeightState {
103
+ if (stateCount !== undefined) validateStateCount(stateCount);
104
+ if (!Number.isSafeInteger(state)) {
105
+ throw new RangeError(`Height state must be a finite safe integer, got ${String(state)}`);
106
+ }
107
+
108
+ const maximum = stateCount === 2 ? 1 : 3;
109
+ if (state < 0 || state > maximum) {
110
+ throw new RangeError(
111
+ `Height state must be between 0 and ${maximum} for ${stateCount ?? 4} states, got ${state}`
112
+ );
113
+ }
114
+ return state as HeightState;
115
+ }
116
+
117
+ function copyProfile(profile: HeightCodedBar): HeightCodedBar {
118
+ return { top: profile.top, bottom: profile.bottom };
119
+ }
120
+
121
+ /**
122
+ * Return the normalized top and bottom edges for one state.
123
+ *
124
+ * State numbering is intentionally generic: 0 is tracker, 1 is ascender,
125
+ * 2 is descender and 3 is full for the four-state alphabet. The two-state
126
+ * alphabet exposes tracker (0) and full (1), so it is a strict subset of the
127
+ * four-state geometry and does not prescribe a postal format's semantics.
128
+ *
129
+ * @param {number} state
130
+ * @param {2|4} stateCount
131
+ * @returns {HeightCodedBar}
132
+ */
133
+ export function barHeightProfile(
134
+ state: number,
135
+ stateCount: HeightStateCount,
136
+ ): HeightCodedBar {
137
+ const count = validateStateCount(stateCount);
138
+ const checked = validateHeightState(state, count);
139
+
140
+ if (count === 2) {
141
+ return copyProfile(checked === 0 ? TRACKER_PROFILE : FULL_PROFILE);
142
+ }
143
+
144
+ switch (checked) {
145
+ case 0: return copyProfile(TRACKER_PROFILE);
146
+ case 1: return copyProfile(ASCENDER_PROFILE);
147
+ case 2: return copyProfile(DESCENDER_PROFILE);
148
+ default: return copyProfile(FULL_PROFILE);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Convert a state sequence to normalized top/bottom bar profiles.
154
+ *
155
+ * The returned array and every profile in it are newly allocated, so callers
156
+ * can safely adjust rendering coordinates without changing shared constants.
157
+ *
158
+ * @param {ArrayLike<number>} states
159
+ * @param {2|4} stateCount
160
+ * @returns {HeightCodedBar[]}
161
+ */
162
+ export function encodeHeightProfile(
163
+ states: ArrayLike<number>,
164
+ stateCount: HeightStateCount,
165
+ ): HeightCodedBar[] {
166
+ const count = validateStateCount(stateCount);
167
+ const length = validateProfileLength(states, 'Height states');
168
+ const profile = new Array<HeightCodedBar>(length);
169
+ for (let index = 0; index < length; index++) {
170
+ profile[index] = barHeightProfile(states[index], count);
171
+ }
172
+ return profile;
173
+ }
174
+
175
+ function decodeBar(profile: unknown, stateCount: HeightStateCount, index: number): HeightState {
176
+ if (profile === null || typeof profile !== 'object' || Array.isArray(profile)) {
177
+ throw new TypeError(`Height profile at index ${index} must be an object`);
178
+ }
179
+
180
+ const { top, bottom } = profile as Partial<HeightCodedBar>;
181
+ if (typeof top !== 'number' || typeof bottom !== 'number'
182
+ || !Number.isFinite(top) || !Number.isFinite(bottom)) {
183
+ throw new RangeError(`Height profile at index ${index} must contain finite edges`);
184
+ }
185
+ if (top < 0 || top > 1 || bottom < 0 || bottom > 1 || top >= bottom) {
186
+ throw new RangeError(
187
+ `Height profile at index ${index} must satisfy 0 <= top < bottom <= 1`
188
+ );
189
+ }
190
+
191
+ const maximum = stateCount === 2 ? 1 : 3;
192
+ for (let state = 0; state <= maximum; state++) {
193
+ const expected = barHeightProfile(state, stateCount);
194
+ if (top === expected.top && bottom === expected.bottom) {
195
+ return state as HeightState;
196
+ }
197
+ }
198
+
199
+ throw new RangeError(
200
+ `Height profile at index ${index} does not match a canonical ${stateCount}-state profile`
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Convert normalized top/bottom bar profiles back to their states.
206
+ *
207
+ * Decoding is deliberately exact: a profile must match one of the canonical
208
+ * normalized pairs for the selected alphabet. Raster or measurement code can
209
+ * quantize its observations before calling this helper; accepting arbitrary
210
+ * in-range pairs here would turn malformed bars into valid states.
211
+ *
212
+ * @param {ArrayLike<HeightCodedBar>} profile
213
+ * @param {2|4} stateCount
214
+ * @returns {HeightState[]}
215
+ */
216
+ export function decodeHeightProfile(
217
+ profile: ArrayLike<HeightCodedBar>,
218
+ stateCount: HeightStateCount,
219
+ ): HeightState[] {
220
+ const count = validateStateCount(stateCount);
221
+ const length = validateProfileLength(profile, 'Height profile');
222
+ const states = new Array<HeightState>(length);
223
+ for (let index = 0; index < length; index++) {
224
+ states[index] = decodeBar(profile[index], count, index);
225
+ }
226
+ return states;
227
+ }