@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,299 @@
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
+ * Strict Han Xin Code decoder for the alignment-free versions 1-3.
14
+ *
15
+ * The reader treats the matrix as untrusted input. It first validates the
16
+ * fixed corner patterns and both structural-information copies, then checks
17
+ * Reed--Solomon parity and finally parses a complete payload. A partially
18
+ * readable stream is never returned as a successful result.
19
+ *
20
+ * @module hanxin/decoder
21
+ */
22
+ import { BitMatrix } from '../core/bit-matrix.js';
23
+ import { BitReader } from '../core/bit-buffer.js';
24
+ import { FormatError } from '../core/errors.js';
25
+ import { rsDecode } from '../core/reed-solomon.js';
26
+ import { GF256_HANXIN } from './tables.js';
27
+ import { HANXIN_ECC_LEVELS, HANXIN_DATA_MODULES, HANXIN_TOTAL_CODEWORDS, createHanXinFunctionGrid, decodeHanXinFunctionInfo, hanXinDataCoordinates, hanXinEcLayout, hanXinMaskFlip, hanXinSize, } from './tables.js';
28
+ function rotateMatrix(source, rotation) {
29
+ if (rotation === 0)
30
+ return source;
31
+ const output = new BitMatrix(source.height, source.width);
32
+ for (let y = 0; y < source.height; y++)
33
+ for (let x = 0; x < source.width; x++) {
34
+ if (!source.get(x, y))
35
+ continue;
36
+ if (rotation === 90)
37
+ output.set(source.height - 1 - y, x);
38
+ else if (rotation === 180)
39
+ output.set(source.width - 1 - x, source.height - 1 - y);
40
+ else
41
+ output.set(y, source.width - 1 - x);
42
+ }
43
+ return output;
44
+ }
45
+ function invertMatrix(source) {
46
+ const output = source.clone();
47
+ for (let y = 0; y < output.height; y++)
48
+ for (let x = 0; x < output.width; x++)
49
+ output.flip(x, y);
50
+ return output;
51
+ }
52
+ function functionInfoCells(version) {
53
+ const size = hanXinSize(version);
54
+ const cells = new Set();
55
+ for (let i = 0; i < 9; i++) {
56
+ cells.add(`${i},8`);
57
+ cells.add(`${size - 1 - i},${size - 9}`);
58
+ cells.add(`8,${8 - i}`);
59
+ cells.add(`${size - 9},${size - 9 + i}`);
60
+ cells.add(`${size - 9},${i}`);
61
+ cells.add(`8,${size - 1 - i}`);
62
+ cells.add(`${size - 9 + i},8`);
63
+ cells.add(`${8 - i},${size - 9}`);
64
+ }
65
+ return cells;
66
+ }
67
+ /** Verify finder patterns, separators and all non-information function cells. */
68
+ export function hanXinStructureMatches(matrix, version) {
69
+ const size = hanXinSize(version);
70
+ if (matrix.width !== size || matrix.height !== size)
71
+ return false;
72
+ const template = createHanXinFunctionGrid(version);
73
+ const info = functionInfoCells(version);
74
+ for (let y = 0; y < size; y++)
75
+ for (let x = 0; x < size; x++) {
76
+ if (template.reserved[y * size + x] === 0 || info.has(`${x},${y}`))
77
+ continue;
78
+ if (matrix.get(x, y) !== template.matrix.get(x, y))
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ function inversePicketFence(wire) {
84
+ const codewords = new Array(wire.length).fill(0);
85
+ let cursor = 0;
86
+ for (let column = 0; column < 13; column++) {
87
+ for (let i = column; i < codewords.length; i += 13)
88
+ codewords[i] = wire[cursor++];
89
+ }
90
+ if (cursor !== wire.length)
91
+ throw new FormatError('Han Xin: codeword reordering is inconsistent');
92
+ return codewords;
93
+ }
94
+ function readCodewords(matrix, version, mask) {
95
+ const coordinates = hanXinDataCoordinates(version);
96
+ const total = HANXIN_DATA_MODULES[version - 1];
97
+ const codewordBits = HANXIN_TOTAL_CODEWORDS[version - 1] * 8;
98
+ const wire = new Array(HANXIN_TOTAL_CODEWORDS[version - 1]).fill(0);
99
+ for (let i = 0; i < total; i++) {
100
+ const [x, y] = coordinates[i];
101
+ let dark = matrix.get(x, y);
102
+ if (hanXinMaskFlip(mask, x, y))
103
+ dark = !dark;
104
+ if (i < codewordBits) {
105
+ if (dark)
106
+ wire[i >>> 3] |= 1 << (7 - (i & 7));
107
+ }
108
+ else if (dark) {
109
+ throw new FormatError('Han Xin: non-zero remainder modules');
110
+ }
111
+ }
112
+ return inversePicketFence(wire);
113
+ }
114
+ function ensureZeroPadding(reader) {
115
+ while (reader.available() > 0) {
116
+ if (reader.readBit())
117
+ throw new FormatError('Han Xin: non-zero data follows the payload terminator');
118
+ }
119
+ }
120
+ function utf8Bytes(text) {
121
+ return new TextEncoder().encode(text);
122
+ }
123
+ function decodeByteText(bytes) {
124
+ try {
125
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
126
+ }
127
+ catch {
128
+ // A byte-mode symbol is allowed to carry arbitrary octets. Preserve
129
+ // every octet losslessly when it is not valid UTF-8.
130
+ return Array.from(bytes, (value) => String.fromCharCode(value)).join('');
131
+ }
132
+ }
133
+ function decodeNumeric(reader) {
134
+ const groups = [];
135
+ while (reader.available() >= 10) {
136
+ const value = reader.read(10);
137
+ if (value >= 1021 && value <= 1023) {
138
+ const count = value - 1020;
139
+ if (groups.length === 0)
140
+ throw new FormatError('Han Xin: numeric payload has no data group');
141
+ const last = groups.pop();
142
+ if (last >= 10 ** count)
143
+ throw new FormatError('Han Xin: numeric final group has an invalid width');
144
+ return groups.map((group) => String(group).padStart(3, '0')).join('') +
145
+ String(last).padStart(count, '0');
146
+ }
147
+ if (value > 999)
148
+ throw new FormatError('Han Xin: numeric group is outside the 000-999 range');
149
+ groups.push(value);
150
+ if (groups.length > 2730)
151
+ throw new FormatError('Han Xin: numeric payload exceeds the supported stream limit');
152
+ }
153
+ throw new FormatError('Han Xin: numeric payload has no complete terminator');
154
+ }
155
+ function text1Character(value) {
156
+ if (value >= 0 && value <= 9)
157
+ return String.fromCharCode(0x30 + value);
158
+ if (value >= 10 && value <= 35)
159
+ return String.fromCharCode(0x41 + value - 10);
160
+ if (value >= 36 && value <= 61)
161
+ return String.fromCharCode(0x61 + value - 36);
162
+ return null;
163
+ }
164
+ function text2Character(value) {
165
+ if (value >= 0 && value <= 27)
166
+ return String.fromCharCode(value);
167
+ if (value >= 28 && value <= 43)
168
+ return String.fromCharCode(0x20 + value - 28);
169
+ if (value >= 44 && value <= 50)
170
+ return String.fromCharCode(0x3a + value - 44);
171
+ if (value >= 51 && value <= 56)
172
+ return String.fromCharCode(0x5b + value - 51);
173
+ if (value >= 57 && value <= 61)
174
+ return String.fromCharCode(0x7b + value - 57);
175
+ return null;
176
+ }
177
+ function decodeText(reader) {
178
+ let submode = 1;
179
+ const characters = [];
180
+ while (reader.available() >= 6) {
181
+ const value = reader.read(6);
182
+ if (value === 63) {
183
+ if (characters.length === 0)
184
+ throw new FormatError('Han Xin: text payload is empty');
185
+ return characters.join('');
186
+ }
187
+ if (value === 62) {
188
+ submode = submode === 1 ? 2 : 1;
189
+ continue;
190
+ }
191
+ const character = submode === 1 ? text1Character(value) : text2Character(value);
192
+ if (character === null)
193
+ throw new FormatError(`Han Xin: text value ${value} is not assigned in submode ${submode}`);
194
+ characters.push(character);
195
+ if (characters.length > 8191)
196
+ throw new FormatError('Han Xin: text payload exceeds the supported stream limit');
197
+ }
198
+ throw new FormatError('Han Xin: text payload has no complete terminator');
199
+ }
200
+ function decodeByte(reader) {
201
+ if (reader.available() < 13)
202
+ throw new FormatError('Han Xin: byte payload has no complete length field');
203
+ const count = reader.read(13);
204
+ if (count < 1 || count > 8191)
205
+ throw new FormatError(`Han Xin: byte count ${count} is outside the supported range`);
206
+ if (count * 8 > reader.available())
207
+ throw new FormatError('Han Xin: byte payload is truncated');
208
+ const bytes = new Uint8Array(count);
209
+ for (let i = 0; i < count; i++)
210
+ bytes[i] = reader.read(8);
211
+ return bytes;
212
+ }
213
+ function parsePayload(data) {
214
+ const reader = new BitReader(data);
215
+ if (reader.available() < 4)
216
+ throw new FormatError('Han Xin: payload has no mode indicator');
217
+ const mode = reader.read(4);
218
+ if (mode === 1) {
219
+ const text = decodeNumeric(reader);
220
+ ensureZeroPadding(reader);
221
+ return { text, bytes: utf8Bytes(text), mode: 'numeric' };
222
+ }
223
+ if (mode === 2) {
224
+ const text = decodeText(reader);
225
+ ensureZeroPadding(reader);
226
+ return { text, bytes: utf8Bytes(text), mode: 'text' };
227
+ }
228
+ if (mode === 3) {
229
+ const bytes = decodeByte(reader);
230
+ ensureZeroPadding(reader);
231
+ return { text: decodeByteText(bytes), bytes, mode: 'byte' };
232
+ }
233
+ throw new FormatError(`Han Xin: unsupported mode indicator ${mode}`);
234
+ }
235
+ function candidateRotations(option) {
236
+ if (option === 'auto' || option == null)
237
+ return [0, 90, 180, 270];
238
+ if (option === 0 || option === 90 || option === 180 || option === 270)
239
+ return [option];
240
+ throw new FormatError('Han Xin: rotation must be 0, 90, 180, 270 or auto');
241
+ }
242
+ function candidatePolarities(option) {
243
+ if (option === 'auto' || option == null)
244
+ return [false, true];
245
+ if (option === false || option === true)
246
+ return [option];
247
+ throw new FormatError('Han Xin: inverted must be true, false or auto');
248
+ }
249
+ /** Decode a verified Han Xin module matrix. */
250
+ export function decodeHanXin(matrix, options = {}) {
251
+ if (!matrix || !Number.isInteger(matrix.width) || !Number.isInteger(matrix.height)) {
252
+ throw new FormatError('Han Xin: a BitMatrix is required');
253
+ }
254
+ const rotations = candidateRotations(options.rotation);
255
+ const polarities = candidatePolarities(options.inverted);
256
+ let lastError = null;
257
+ for (const rotation of rotations) {
258
+ const rotated = rotateMatrix(matrix, rotation);
259
+ for (const inverted of polarities) {
260
+ const candidate = inverted ? invertMatrix(rotated) : rotated;
261
+ for (const version of [1, 2, 3]) {
262
+ if (candidate.width !== hanXinSize(version) || candidate.height !== hanXinSize(version))
263
+ continue;
264
+ if (!hanXinStructureMatches(candidate, version))
265
+ continue;
266
+ try {
267
+ const info = decodeHanXinFunctionInfo(candidate, version);
268
+ if (!HANXIN_ECC_LEVELS.includes(info.level))
269
+ throw new FormatError('Han Xin: invalid error-correction level');
270
+ const codewords = readCodewords(candidate, version, info.mask);
271
+ const layout = hanXinEcLayout(version, info.level);
272
+ const corrections = rsDecode(codewords, layout.eccCodewords, GF256_HANXIN, 1);
273
+ const data = Uint8Array.from(codewords.slice(0, layout.dataCodewords));
274
+ const payload = parsePayload(data);
275
+ return {
276
+ format: 'hanxin',
277
+ text: payload.text,
278
+ bytes: payload.bytes,
279
+ version,
280
+ ecc: info.level,
281
+ mask: info.mask,
282
+ mode: payload.mode,
283
+ corrections: info.corrections + corrections,
284
+ rows: candidate.height,
285
+ columns: candidate.width,
286
+ inverted,
287
+ rotation,
288
+ };
289
+ }
290
+ catch (error) {
291
+ lastError = error;
292
+ }
293
+ }
294
+ }
295
+ }
296
+ if (lastError instanceof FormatError)
297
+ throw lastError;
298
+ throw new FormatError('Han Xin: no valid symbol found in the supplied matrix');
299
+ }
@@ -0,0 +1,122 @@
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
+ * Integer-scale Han Xin detector.
14
+ *
15
+ * Detection deliberately accepts one prominent, axis-aligned symbol from a
16
+ * binarized image. It does not guess a perspective quadrilateral or return a
17
+ * low-confidence payload: the strict module decoder remains the final gate.
18
+ *
19
+ * @module hanxin/detector
20
+ */
21
+ import { BitMatrix } from '../core/bit-matrix.js';
22
+ import { NotFoundError } from '../core/errors.js';
23
+ import { decodeHanXin } from './decoder.js';
24
+ import { hanXinSize } from './tables.js';
25
+ function boundsForPolarity(image, inverted) {
26
+ let left = image.width;
27
+ let top = image.height;
28
+ let right = -1;
29
+ let bottom = -1;
30
+ for (let y = 0; y < image.height; y++)
31
+ for (let x = 0; x < image.width; x++) {
32
+ const dark = image.get(x, y) !== inverted;
33
+ if (!dark)
34
+ continue;
35
+ if (x < left)
36
+ left = x;
37
+ if (x > right)
38
+ right = x;
39
+ if (y < top)
40
+ top = y;
41
+ if (y > bottom)
42
+ bottom = y;
43
+ }
44
+ return right < left || bottom < top ? null : {
45
+ x: left,
46
+ y: top,
47
+ width: right - left + 1,
48
+ height: bottom - top + 1,
49
+ };
50
+ }
51
+ function sampleBounds(source, bounds, version, inverted) {
52
+ const size = hanXinSize(version);
53
+ const matrix = new BitMatrix(size, size);
54
+ for (let y = 0; y < size; y++)
55
+ for (let x = 0; x < size; x++) {
56
+ const px = Math.min(source.width - 1, Math.max(0, Math.floor(bounds.x + ((x + 0.5) * bounds.width) / size)));
57
+ const py = Math.min(source.height - 1, Math.max(0, Math.floor(bounds.y + ((y + 0.5) * bounds.height) / size)));
58
+ if (source.get(px, py) !== inverted)
59
+ matrix.set(x, y);
60
+ }
61
+ return matrix;
62
+ }
63
+ /** Detect one strict, integer-scale Han Xin symbol in a binarized image. */
64
+ export function detectHanXin(binaryImage) {
65
+ if (!binaryImage || !Number.isInteger(binaryImage.width) || !Number.isInteger(binaryImage.height) ||
66
+ binaryImage.width < 1 || binaryImage.height < 1) {
67
+ throw new NotFoundError('detectHanXin: no image supplied');
68
+ }
69
+ const candidates = [];
70
+ for (const inverted of [false, true]) {
71
+ const bounds = boundsForPolarity(binaryImage, inverted);
72
+ if (!bounds)
73
+ continue;
74
+ for (const version of [1, 2, 3]) {
75
+ const size = hanXinSize(version);
76
+ if (bounds.width !== bounds.height || bounds.width % size !== 0)
77
+ continue;
78
+ const moduleSize = bounds.width / size;
79
+ if (!Number.isInteger(moduleSize) || moduleSize < 1)
80
+ continue;
81
+ const matrix = sampleBounds(binaryImage, bounds, version, inverted);
82
+ let result;
83
+ try {
84
+ result = decodeHanXin(matrix, { rotation: 'auto', inverted: false });
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ candidates.push({
90
+ corners: [
91
+ { x: bounds.x, y: bounds.y },
92
+ { x: bounds.x + bounds.width, y: bounds.y },
93
+ { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
94
+ { x: bounds.x, y: bounds.y + bounds.height },
95
+ ],
96
+ dimension: { width: size, height: size },
97
+ moduleSize,
98
+ matrix,
99
+ result: { ...result, inverted: result.inverted || inverted },
100
+ });
101
+ }
102
+ }
103
+ candidates.sort((left, right) => right.moduleSize - left.moduleSize);
104
+ return candidates[0] ?? null;
105
+ }
106
+ /** Detect and decode one verified Han Xin symbol, or return null. */
107
+ export function detectAndDecodeHanXin(binaryImage) {
108
+ let detection;
109
+ try {
110
+ detection = detectHanXin(binaryImage);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ if (!detection)
116
+ return null;
117
+ return {
118
+ ...detection.result,
119
+ corners: detection.corners,
120
+ moduleSize: detection.moduleSize,
121
+ };
122
+ }
@@ -0,0 +1,259 @@
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
+ import { BitWriter } from '../core/bit-buffer.js';
13
+ import { EncodeError } from '../core/errors.js';
14
+ import { rsEncode } from '../core/reed-solomon.js';
15
+ import { GF256_HANXIN, HANXIN_VERSIONS, createHanXinFunctionGrid, hanXinDataCoordinates, hanXinDataCodewords, hanXinEcLayout, hanXinMaskFlip, hanXinSize, normalizeHanXinEcc, normalizeHanXinVersion, placeHanXinFunctionInfo, } from './tables.js';
16
+ function inputBytes(value) {
17
+ if (value instanceof Uint8Array) {
18
+ if (value.length === 0)
19
+ throw new EncodeError('Han Xin: payload must not be empty');
20
+ return { bytes: new Uint8Array(value), text: null };
21
+ }
22
+ if (Array.isArray(value)) {
23
+ if (value.length === 0 || value.length > 8191 || value.some((item) => !Number.isInteger(item) || item < 0 || item > 255)) {
24
+ throw new EncodeError('Han Xin: byte arrays must contain at most 8191 values from 0 to 255');
25
+ }
26
+ return { bytes: Uint8Array.from(value), text: null };
27
+ }
28
+ if (typeof value !== 'string')
29
+ throw new EncodeError('Han Xin: value must be text or bytes');
30
+ if (!value.length)
31
+ throw new EncodeError('Han Xin: payload must not be empty');
32
+ return { bytes: new TextEncoder().encode(value), text: value };
33
+ }
34
+ function text1Value(codePoint) {
35
+ if (codePoint >= 0x30 && codePoint <= 0x39)
36
+ return codePoint - 0x30;
37
+ if (codePoint >= 0x41 && codePoint <= 0x5a)
38
+ return codePoint - 0x41 + 10;
39
+ if (codePoint >= 0x61 && codePoint <= 0x7a)
40
+ return codePoint - 0x61 + 36;
41
+ return null;
42
+ }
43
+ function text2Value(codePoint) {
44
+ if (codePoint >= 0 && codePoint <= 0x1b)
45
+ return codePoint;
46
+ if (codePoint >= 0x20 && codePoint <= 0x2f)
47
+ return codePoint - 0x20 + 28;
48
+ if (codePoint >= 0x3a && codePoint <= 0x40)
49
+ return codePoint - 0x3a + 44;
50
+ if (codePoint >= 0x5b && codePoint <= 0x60)
51
+ return codePoint - 0x5b + 51;
52
+ if (codePoint >= 0x7b && codePoint <= 0x7f)
53
+ return codePoint - 0x7b + 57;
54
+ return null;
55
+ }
56
+ function textValue(codePoint, mode) {
57
+ return mode === 1 ? text1Value(codePoint) : text2Value(codePoint);
58
+ }
59
+ function selectMode(text, requested) {
60
+ const mode = requested == null ? 'auto' : String(requested).toLowerCase();
61
+ if (!['auto', 'numeric', 'text', 'byte'].includes(mode)) {
62
+ throw new EncodeError(`Han Xin: unsupported payload mode ${String(requested)}`);
63
+ }
64
+ if (text == null) {
65
+ if (mode === 'numeric' || mode === 'text')
66
+ throw new EncodeError('Han Xin: numeric and text modes require a string input');
67
+ return 'byte';
68
+ }
69
+ const points = Array.from(text);
70
+ const numeric = /^\d+$/.test(text);
71
+ const textCompatible = points.every((point) => {
72
+ const cp = point.codePointAt(0);
73
+ return cp <= 0x7f && (text1Value(cp) !== null || text2Value(cp) !== null);
74
+ });
75
+ if (mode === 'numeric' && !numeric)
76
+ throw new EncodeError('Han Xin: numeric mode accepts digits only');
77
+ if (mode === 'text' && !textCompatible)
78
+ throw new EncodeError('Han Xin: text mode accepts supported ASCII characters only');
79
+ if (mode === 'auto')
80
+ return numeric ? 'numeric' : textCompatible ? 'text' : 'byte';
81
+ return mode;
82
+ }
83
+ function encodeNumeric(text, writer) {
84
+ let finalCount = 0;
85
+ for (let offset = 0; offset < text.length; offset += 3) {
86
+ finalCount = Math.min(3, text.length - offset);
87
+ writer.put(Number(text.slice(offset, offset + finalCount)), 10);
88
+ }
89
+ writer.put(1020 + finalCount, 10);
90
+ }
91
+ function encodeText(text, writer) {
92
+ let submode = 1;
93
+ for (const point of Array.from(text)) {
94
+ const cp = point.codePointAt(0);
95
+ let value = textValue(cp, submode);
96
+ if (value === null) {
97
+ submode = submode === 1 ? 2 : 1;
98
+ writer.put(62, 6);
99
+ value = textValue(cp, submode);
100
+ }
101
+ if (value === null)
102
+ throw new EncodeError(`Han Xin: character U+${cp.toString(16).padStart(4, '0')} is not encodable in text mode`);
103
+ writer.put(value, 6);
104
+ }
105
+ writer.put(63, 6);
106
+ }
107
+ function encodeByte(bytes, writer) {
108
+ if (bytes.length > 8191)
109
+ throw new EncodeError('Han Xin: byte mode supports at most 8191 bytes');
110
+ writer.put(3, 4);
111
+ writer.put(bytes.length, 13);
112
+ writer.putBytes(bytes);
113
+ }
114
+ function payloadBits(value, mode) {
115
+ const source = inputBytes(value);
116
+ const writer = new BitWriter();
117
+ if (mode === 'numeric') {
118
+ writer.put(1, 4);
119
+ encodeNumeric(source.text, writer);
120
+ }
121
+ else if (mode === 'text') {
122
+ writer.put(2, 4);
123
+ encodeText(source.text, writer);
124
+ }
125
+ else {
126
+ encodeByte(source.bytes, writer);
127
+ }
128
+ return writer;
129
+ }
130
+ function picketFence(codewords) {
131
+ const output = [];
132
+ for (let column = 0; column < 13; column++) {
133
+ for (let i = column; i < codewords.length; i += 13)
134
+ output.push(codewords[i]);
135
+ }
136
+ return output;
137
+ }
138
+ function placePayload(matrix, version, codewords, mask) {
139
+ const coordinates = hanXinDataCoordinates(version);
140
+ const bits = picketFence(codewords);
141
+ const totalBits = bits.length * 8;
142
+ for (let i = 0; i < coordinates.length; i++) {
143
+ const [x, y] = coordinates[i];
144
+ // Compact versions have five remainder modules after the final complete
145
+ // codeword. They carry zero before masking and are checked by the reader.
146
+ let dark = i < totalBits && ((bits[i >>> 3] >>> (7 - (i & 7))) & 1) !== 0;
147
+ if (hanXinMaskFlip(mask, x, y))
148
+ dark = !dark;
149
+ matrix.setValue(x, y, dark);
150
+ }
151
+ }
152
+ function maskPenalty(matrix, version, level, mask) {
153
+ const candidate = matrix.clone();
154
+ for (const [x, y] of hanXinDataCoordinates(version)) {
155
+ if (hanXinMaskFlip(mask, x, y))
156
+ candidate.flip(x, y);
157
+ }
158
+ placeHanXinFunctionInfo(candidate, version, level, mask);
159
+ let penalty = 0;
160
+ const runPenalty = (values) => {
161
+ let current = values[0];
162
+ let run = 1;
163
+ for (let i = 1; i <= values.length; i++) {
164
+ if (i < values.length && values[i] === current)
165
+ run++;
166
+ else {
167
+ if (run >= 3)
168
+ penalty += run * 4;
169
+ current = values[i];
170
+ run = 1;
171
+ }
172
+ }
173
+ };
174
+ for (let y = 0; y < candidate.height; y++) {
175
+ const row = [];
176
+ for (let x = 0; x < candidate.width; x++)
177
+ row.push(candidate.get(x, y));
178
+ runPenalty(row);
179
+ }
180
+ for (let x = 0; x < candidate.width; x++) {
181
+ const column = [];
182
+ for (let y = 0; y < candidate.height; y++)
183
+ column.push(candidate.get(x, y));
184
+ runPenalty(column);
185
+ }
186
+ return penalty;
187
+ }
188
+ function chooseMask(base, version, level, requested) {
189
+ if (requested !== undefined) {
190
+ if (!Number.isInteger(requested) || requested < 0 || requested > 3)
191
+ throw new EncodeError('Han Xin: mask must be an integer from 0 to 3');
192
+ return requested;
193
+ }
194
+ let best = 0;
195
+ let score = Number.POSITIVE_INFINITY;
196
+ for (let mask = 0; mask < 4; mask++) {
197
+ const candidate = base.clone();
198
+ // The score is intentionally simple and stable. Every candidate is still
199
+ // validated by the strict reader, so no mask choice can affect integrity.
200
+ const value = maskPenalty(candidate, version, level, mask);
201
+ if (value < score) {
202
+ score = value;
203
+ best = mask;
204
+ }
205
+ }
206
+ return best;
207
+ }
208
+ function encodeVersion(writer, version, level, requestedMask) {
209
+ const dataCodewords = hanXinDataCodewords(version, level);
210
+ const layout = hanXinEcLayout(version, level);
211
+ const capacity = dataCodewords * 8;
212
+ if (writer.length > capacity)
213
+ throw new EncodeError(`Han Xin: payload needs ${writer.length} bits but ${capacity} are available`);
214
+ const data = new Uint8Array(dataCodewords);
215
+ const encoded = writer.toBytes();
216
+ data.set(encoded.subarray(0, data.length));
217
+ const parity = rsEncode(Array.from(data), layout.eccCodewords, GF256_HANXIN, 1);
218
+ const full = Array.from(data).concat(parity);
219
+ const { matrix } = createHanXinFunctionGrid(version);
220
+ placePayload(matrix, version, full, 0);
221
+ const mask = chooseMask(matrix, version, level, requestedMask);
222
+ placePayload(matrix, version, full, mask);
223
+ placeHanXinFunctionInfo(matrix, version, level, mask);
224
+ return matrix;
225
+ }
226
+ /** Encode a string or byte array as a Han Xin Code module matrix. */
227
+ export function encodeHanXin(value, options = {}) {
228
+ const source = inputBytes(value);
229
+ const mode = selectMode(source.text, options.mode);
230
+ const writer = payloadBits(value, mode);
231
+ const level = normalizeHanXinEcc(options.ecc);
232
+ const wantedVersion = options.version == null ? null : normalizeHanXinVersion(options.version);
233
+ const requestedMask = options.mask == null ? undefined : Number(options.mask);
234
+ if (requestedMask !== undefined &&
235
+ (!Number.isInteger(requestedMask) || requestedMask < 0 || requestedMask > 3)) {
236
+ throw new EncodeError('Han Xin: mask must be an integer from 0 to 3');
237
+ }
238
+ for (const version of HANXIN_VERSIONS) {
239
+ if (wantedVersion !== null && version !== wantedVersion)
240
+ continue;
241
+ try {
242
+ return encodeVersion(writer, version, level, requestedMask);
243
+ }
244
+ catch (error) {
245
+ if (!(error instanceof EncodeError))
246
+ throw error;
247
+ }
248
+ }
249
+ const target = wantedVersion == null ? 'versions 1-3' : `version ${wantedVersion}`;
250
+ throw new EncodeError(`Han Xin: payload does not fit ${target} at error correction ${level}`);
251
+ }
252
+ /** Encode explicitly in byte mode; useful when the input contains arbitrary data. */
253
+ export function encodeHanXinBytes(bytes, options = {}) {
254
+ return encodeHanXin(bytes, { ...options, mode: 'byte' });
255
+ }
256
+ /** Expose the implemented geometric side length for callers building rasters. */
257
+ export function hanXinDimension(version) {
258
+ return hanXinSize(Number(version));
259
+ }
@@ -0,0 +1,16 @@
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
+ /** Public Han Xin Code API. @module hanxin */
13
+ export * from './tables.js';
14
+ export * from './encoder.js';
15
+ export * from './decoder.js';
16
+ export * from './detector.js';