@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,174 @@
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
+ * DotCode structural constants and the public 5-of-9 symbol assignment.
33
+ *
34
+ * The pattern assignment is a normative symbology table. It is recorded as
35
+ * compact hexadecimal values so the runtime does not need a third-party table
36
+ * or a generated dependency. Bit 8 is the first dot in the nine-dot pattern.
37
+ * Every entry contains exactly five dark dots.
38
+ *
39
+ * @module dotcode/tables
40
+ */
41
+
42
+ import { GaloisField } from '../core/galois-field.js';
43
+
44
+ export const DOTCODE_MIN_DIMENSION = 5;
45
+ export const DOTCODE_MAX_DIMENSION = 200;
46
+ export const DOTCODE_FIELD_SIZE = 113;
47
+ export const DOTCODE_CODEWORD_COUNT = 113;
48
+ export const DOTCODE_MIN_ECC = 3;
49
+ export const DOTCODE_MASK_STEPS = Object.freeze([0, 3, 7, 17]);
50
+
51
+ /** GF(113), with the primitive root required by the DotCode RS procedure. */
52
+ export const GF113_DOTCODE = new GaloisField({
53
+ size: DOTCODE_FIELD_SIZE,
54
+ prime: true,
55
+ generator: 3,
56
+ name: 'GF(113)/DotCode',
57
+ });
58
+
59
+ /**
60
+ * DotCode Annex C's 113 legal 5-of-9 patterns.
61
+ *
62
+ * This is format data, not executable code copied from an implementation.
63
+ * Keeping it in the authoritative TypeScript source makes the JS runtime and
64
+ * its declarations reproducible without a package-time generator.
65
+ */
66
+ export const DOTCODE_PATTERNS: readonly number[] = Object.freeze([
67
+ 0x155, 0x0ab, 0x0ad, 0x0b5, 0x0d5, 0x156, 0x15a, 0x16a, 0x1aa, 0x0ae,
68
+ 0x0b6, 0x0ba, 0x0d6, 0x0da, 0x0ea, 0x12b, 0x12d, 0x135, 0x14b, 0x14d,
69
+ 0x153, 0x159, 0x165, 0x169, 0x195, 0x1a5, 0x1a9, 0x057, 0x05b, 0x05d,
70
+ 0x06b, 0x06d, 0x075, 0x097, 0x09b, 0x09d, 0x0a7, 0x0b3, 0x0b9, 0x0cb,
71
+ 0x0cd, 0x0d3, 0x0d9, 0x0e5, 0x0e9, 0x12e, 0x136, 0x13a, 0x14e, 0x15c,
72
+ 0x166, 0x16c, 0x172, 0x174, 0x196, 0x19a, 0x1a6, 0x1ac, 0x1b2, 0x1b4,
73
+ 0x1ca, 0x1d2, 0x1d4, 0x05e, 0x06e, 0x076, 0x07a, 0x09e, 0x0bc, 0x0ce,
74
+ 0x0dc, 0x0e6, 0x0ec, 0x0f2, 0x0f4, 0x117, 0x11b, 0x11d, 0x127, 0x133,
75
+ 0x139, 0x147, 0x163, 0x171, 0x18b, 0x18d, 0x193, 0x199, 0x1a3, 0x1b1,
76
+ 0x1c5, 0x1c9, 0x1d1, 0x02f, 0x037, 0x03b, 0x03d, 0x04f, 0x067, 0x073,
77
+ 0x079, 0x08f, 0x0c7, 0x0e3, 0x0f1, 0x11e, 0x13c, 0x178, 0x18e, 0x19c,
78
+ 0x1b8, 0x1c6, 0x1cc,
79
+ ]);
80
+
81
+ const PATTERN_TO_CODEWORD = new Map<number, number>(DOTCODE_PATTERNS.map((pattern, value) => [pattern, value]));
82
+
83
+ /** Return the nine-dot pattern assigned to a codeword. */
84
+ export function dotCodePattern(codeword: number): number {
85
+ if (!Number.isInteger(codeword) || codeword < 0 || codeword >= DOTCODE_CODEWORD_COUNT) {
86
+ throw new RangeError(`DotCode: codeword must be an integer in 0..${DOTCODE_CODEWORD_COUNT - 1}`);
87
+ }
88
+ return DOTCODE_PATTERNS[codeword];
89
+ }
90
+
91
+ /** Return a codeword for a nine-dot pattern, or -1 when it is not assigned. */
92
+ export function dotCodeCodeword(pattern: number): number {
93
+ return PATTERN_TO_CODEWORD.get(pattern) ?? -1;
94
+ }
95
+
96
+ /** Number of active alternating positions in a W by H symbol. */
97
+ export function dotCodeActivePositions(width: number, height: number): number {
98
+ return Math.floor((width * height) / 2);
99
+ }
100
+
101
+ /** Number of complete nine-bit codewords available after the mask bits. */
102
+ export function dotCodeCodewordCapacity(width: number, height: number): number {
103
+ return Math.floor((dotCodeActivePositions(width, height) - 2) / 9);
104
+ }
105
+
106
+ /** DotCode uses alternating positions; the six corner positions carry tail bits. */
107
+ export function dotCodeIsDataPosition(column: number, row: number): boolean {
108
+ return ((column + row) & 1) === 0;
109
+ }
110
+
111
+ /** Return true for one of the six corner positions used by the folded stream. */
112
+ export function dotCodeIsCorner(column: number, row: number, width: number, height: number): boolean {
113
+ if (column === 0 && row === 0) return true;
114
+ if (height & 1) {
115
+ if ((column === width - 2 && row === 0) || (column === width - 1 && row === 1)) return true;
116
+ if (column === 0 && row === height - 1) return true;
117
+ } else {
118
+ if (column === width - 1 && row === 0) return true;
119
+ if ((column === 0 && row === height - 2) || (column === 1 && row === height - 1)) return true;
120
+ }
121
+ return (column === width - 2 && row === height - 1) ||
122
+ (column === width - 1 && row === height - 2);
123
+ }
124
+
125
+ /** Return the six corner coordinates in wire order for a canonical matrix. */
126
+ export function dotCodeCornerOrder(width: number, height: number): readonly [number, number][] {
127
+ if (height & 1) {
128
+ return [
129
+ [width - 2, 0],
130
+ [width - 2, height - 1],
131
+ [width - 1, 1],
132
+ [width - 1, height - 2],
133
+ [0, 0],
134
+ [0, height - 1],
135
+ ];
136
+ }
137
+ return [
138
+ [width - 1, height - 2],
139
+ [0, height - 2],
140
+ [width - 2, height - 1],
141
+ [1, height - 1],
142
+ [width - 1, 0],
143
+ [0, 0],
144
+ ];
145
+ }
146
+
147
+ /** Largest data-codeword count that fits the supplied nine-bit capacity. */
148
+ export function dotCodeDataCapacity(codewordCapacity: number): number {
149
+ if (!Number.isInteger(codewordCapacity) || codewordCapacity < DOTCODE_MIN_ECC + 1) return 0;
150
+ let best = 0;
151
+ for (let data = 1; data <= codewordCapacity; data++) {
152
+ const ecc = DOTCODE_MIN_ECC + Math.floor(data / 2);
153
+ if (data + ecc <= codewordCapacity) best = data;
154
+ }
155
+ return best;
156
+ }
157
+
158
+ /** Check public structural constants and pattern invariants. */
159
+ export function validateDotCodeTables(): string[] {
160
+ const errors: string[] = [];
161
+ if (DOTCODE_PATTERNS.length !== DOTCODE_CODEWORD_COUNT) {
162
+ errors.push(`pattern count ${DOTCODE_PATTERNS.length} is not ${DOTCODE_CODEWORD_COUNT}`);
163
+ }
164
+ const seen = new Set<number>();
165
+ for (let value = 0; value < DOTCODE_PATTERNS.length; value++) {
166
+ const pattern = DOTCODE_PATTERNS[value];
167
+ if (seen.has(pattern)) errors.push(`pattern 0x${pattern.toString(16)} is duplicated`);
168
+ seen.add(pattern);
169
+ if (pattern < 0 || pattern > 0x1ff || pattern.toString(2).split('1').length - 1 !== 5) {
170
+ errors.push(`pattern ${value} is not a 5-of-9 value`);
171
+ }
172
+ }
173
+ return errors;
174
+ }
@@ -0,0 +1,48 @@
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 { HanXinEccLevel, HanXinVersion } from './tables.js';
24
+ export type HanXinRotation = 0 | 90 | 180 | 270;
25
+ export interface HanXinDecodeOptions {
26
+ /** Try the listed clockwise orientation, or all right-angle turns. */
27
+ rotation?: HanXinRotation | 'auto';
28
+ /** Select polarity, or try normal then inverted modules. */
29
+ inverted?: boolean | 'auto';
30
+ }
31
+ export interface HanXinDecodeResult {
32
+ format: 'hanxin';
33
+ text: string;
34
+ bytes: Uint8Array;
35
+ version: HanXinVersion;
36
+ ecc: HanXinEccLevel;
37
+ mask: 0 | 1 | 2 | 3;
38
+ mode: 'numeric' | 'text' | 'byte';
39
+ corrections: number;
40
+ rows: number;
41
+ columns: number;
42
+ inverted: boolean;
43
+ rotation: HanXinRotation;
44
+ }
45
+ /** Verify finder patterns, separators and all non-information function cells. */
46
+ export declare function hanXinStructureMatches(matrix: BitMatrix, version: HanXinVersion): boolean;
47
+ /** Decode a verified Han Xin module matrix. */
48
+ export declare function decodeHanXin(matrix: BitMatrix, options?: HanXinDecodeOptions): HanXinDecodeResult;
@@ -0,0 +1,314 @@
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
+ * Strict Han Xin Code decoder for the alignment-free versions 1-3.
15
+ *
16
+ * The reader treats the matrix as untrusted input. It first validates the
17
+ * fixed corner patterns and both structural-information copies, then checks
18
+ * Reed--Solomon parity and finally parses a complete payload. A partially
19
+ * readable stream is never returned as a successful result.
20
+ *
21
+ * @module hanxin/decoder
22
+ */
23
+
24
+ import { BitMatrix } from '../core/bit-matrix.js';
25
+ import { BitReader } from '../core/bit-buffer.js';
26
+ import { FormatError } from '../core/errors.js';
27
+ import { rsDecode } from '../core/reed-solomon.js';
28
+ import { GF256_HANXIN } from './tables.js';
29
+ import {
30
+ HANXIN_ECC_LEVELS,
31
+ HanXinEccLevel,
32
+ HanXinVersion,
33
+ HANXIN_DATA_MODULES,
34
+ HANXIN_TOTAL_CODEWORDS,
35
+ createHanXinFunctionGrid,
36
+ decodeHanXinFunctionInfo,
37
+ hanXinDataCoordinates,
38
+ hanXinEcLayout,
39
+ hanXinMaskFlip,
40
+ hanXinSize,
41
+ } from './tables.js';
42
+
43
+ export type HanXinRotation = 0 | 90 | 180 | 270;
44
+
45
+ export interface HanXinDecodeOptions {
46
+ /** Try the listed clockwise orientation, or all right-angle turns. */
47
+ rotation?: HanXinRotation | 'auto';
48
+ /** Select polarity, or try normal then inverted modules. */
49
+ inverted?: boolean | 'auto';
50
+ }
51
+
52
+ export interface HanXinDecodeResult {
53
+ format: 'hanxin';
54
+ text: string;
55
+ bytes: Uint8Array;
56
+ version: HanXinVersion;
57
+ ecc: HanXinEccLevel;
58
+ mask: 0 | 1 | 2 | 3;
59
+ mode: 'numeric' | 'text' | 'byte';
60
+ corrections: number;
61
+ rows: number;
62
+ columns: number;
63
+ inverted: boolean;
64
+ rotation: HanXinRotation;
65
+ }
66
+
67
+ interface PayloadResult {
68
+ text: string;
69
+ bytes: Uint8Array;
70
+ mode: HanXinDecodeResult['mode'];
71
+ }
72
+
73
+ function rotateMatrix(source: BitMatrix, rotation: HanXinRotation): BitMatrix {
74
+ if (rotation === 0) return source;
75
+ const output = new BitMatrix(source.height, source.width);
76
+ for (let y = 0; y < source.height; y++) for (let x = 0; x < source.width; x++) {
77
+ if (!source.get(x, y)) continue;
78
+ if (rotation === 90) output.set(source.height - 1 - y, x);
79
+ else if (rotation === 180) output.set(source.width - 1 - x, source.height - 1 - y);
80
+ else output.set(y, source.width - 1 - x);
81
+ }
82
+ return output;
83
+ }
84
+
85
+ function invertMatrix(source: BitMatrix): BitMatrix {
86
+ const output = source.clone();
87
+ for (let y = 0; y < output.height; y++) for (let x = 0; x < output.width; x++) output.flip(x, y);
88
+ return output;
89
+ }
90
+
91
+ function functionInfoCells(version: HanXinVersion): Set<string> {
92
+ const size = hanXinSize(version);
93
+ const cells = new Set<string>();
94
+ for (let i = 0; i < 9; i++) {
95
+ cells.add(`${i},8`);
96
+ cells.add(`${size - 1 - i},${size - 9}`);
97
+ cells.add(`8,${8 - i}`);
98
+ cells.add(`${size - 9},${size - 9 + i}`);
99
+ cells.add(`${size - 9},${i}`);
100
+ cells.add(`8,${size - 1 - i}`);
101
+ cells.add(`${size - 9 + i},8`);
102
+ cells.add(`${8 - i},${size - 9}`);
103
+ }
104
+ return cells;
105
+ }
106
+
107
+ /** Verify finder patterns, separators and all non-information function cells. */
108
+ export function hanXinStructureMatches(matrix: BitMatrix, version: HanXinVersion): boolean {
109
+ const size = hanXinSize(version);
110
+ if (matrix.width !== size || matrix.height !== size) return false;
111
+ const template = createHanXinFunctionGrid(version);
112
+ const info = functionInfoCells(version);
113
+ for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
114
+ if (template.reserved[y * size + x] === 0 || info.has(`${x},${y}`)) continue;
115
+ if (matrix.get(x, y) !== template.matrix.get(x, y)) return false;
116
+ }
117
+ return true;
118
+ }
119
+
120
+ function inversePicketFence(wire: number[]): number[] {
121
+ const codewords = new Array<number>(wire.length).fill(0);
122
+ let cursor = 0;
123
+ for (let column = 0; column < 13; column++) {
124
+ for (let i = column; i < codewords.length; i += 13) codewords[i] = wire[cursor++];
125
+ }
126
+ if (cursor !== wire.length) throw new FormatError('Han Xin: codeword reordering is inconsistent');
127
+ return codewords;
128
+ }
129
+
130
+ function readCodewords(matrix: BitMatrix, version: HanXinVersion, mask: number): number[] {
131
+ const coordinates = hanXinDataCoordinates(version);
132
+ const total = HANXIN_DATA_MODULES[version - 1];
133
+ const codewordBits = HANXIN_TOTAL_CODEWORDS[version - 1] * 8;
134
+ const wire = new Array<number>(HANXIN_TOTAL_CODEWORDS[version - 1]).fill(0);
135
+ for (let i = 0; i < total; i++) {
136
+ const [x, y] = coordinates[i];
137
+ let dark = matrix.get(x, y);
138
+ if (hanXinMaskFlip(mask, x, y)) dark = !dark;
139
+ if (i < codewordBits) {
140
+ if (dark) wire[i >>> 3] |= 1 << (7 - (i & 7));
141
+ } else if (dark) {
142
+ throw new FormatError('Han Xin: non-zero remainder modules');
143
+ }
144
+ }
145
+ return inversePicketFence(wire);
146
+ }
147
+
148
+ function ensureZeroPadding(reader: BitReader): void {
149
+ while (reader.available() > 0) {
150
+ if (reader.readBit()) throw new FormatError('Han Xin: non-zero data follows the payload terminator');
151
+ }
152
+ }
153
+
154
+ function utf8Bytes(text: string): Uint8Array {
155
+ return new TextEncoder().encode(text);
156
+ }
157
+
158
+ function decodeByteText(bytes: Uint8Array): string {
159
+ try {
160
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
161
+ } catch {
162
+ // A byte-mode symbol is allowed to carry arbitrary octets. Preserve
163
+ // every octet losslessly when it is not valid UTF-8.
164
+ return Array.from(bytes, (value) => String.fromCharCode(value)).join('');
165
+ }
166
+ }
167
+
168
+ function decodeNumeric(reader: BitReader): string {
169
+ const groups: number[] = [];
170
+ while (reader.available() >= 10) {
171
+ const value = reader.read(10);
172
+ if (value >= 1021 && value <= 1023) {
173
+ const count = value - 1020;
174
+ if (groups.length === 0) throw new FormatError('Han Xin: numeric payload has no data group');
175
+ const last = groups.pop() as number;
176
+ if (last >= 10 ** count) throw new FormatError('Han Xin: numeric final group has an invalid width');
177
+ return groups.map((group) => String(group).padStart(3, '0')).join('') +
178
+ String(last).padStart(count, '0');
179
+ }
180
+ if (value > 999) throw new FormatError('Han Xin: numeric group is outside the 000-999 range');
181
+ groups.push(value);
182
+ if (groups.length > 2730) throw new FormatError('Han Xin: numeric payload exceeds the supported stream limit');
183
+ }
184
+ throw new FormatError('Han Xin: numeric payload has no complete terminator');
185
+ }
186
+
187
+ function text1Character(value: number): string | null {
188
+ if (value >= 0 && value <= 9) return String.fromCharCode(0x30 + value);
189
+ if (value >= 10 && value <= 35) return String.fromCharCode(0x41 + value - 10);
190
+ if (value >= 36 && value <= 61) return String.fromCharCode(0x61 + value - 36);
191
+ return null;
192
+ }
193
+
194
+ function text2Character(value: number): string | null {
195
+ if (value >= 0 && value <= 27) return String.fromCharCode(value);
196
+ if (value >= 28 && value <= 43) return String.fromCharCode(0x20 + value - 28);
197
+ if (value >= 44 && value <= 50) return String.fromCharCode(0x3a + value - 44);
198
+ if (value >= 51 && value <= 56) return String.fromCharCode(0x5b + value - 51);
199
+ if (value >= 57 && value <= 61) return String.fromCharCode(0x7b + value - 57);
200
+ return null;
201
+ }
202
+
203
+ function decodeText(reader: BitReader): string {
204
+ let submode: 1 | 2 = 1;
205
+ const characters: string[] = [];
206
+ while (reader.available() >= 6) {
207
+ const value = reader.read(6);
208
+ if (value === 63) {
209
+ if (characters.length === 0) throw new FormatError('Han Xin: text payload is empty');
210
+ return characters.join('');
211
+ }
212
+ if (value === 62) {
213
+ submode = submode === 1 ? 2 : 1;
214
+ continue;
215
+ }
216
+ const character = submode === 1 ? text1Character(value) : text2Character(value);
217
+ if (character === null) throw new FormatError(`Han Xin: text value ${value} is not assigned in submode ${submode}`);
218
+ characters.push(character);
219
+ if (characters.length > 8191) throw new FormatError('Han Xin: text payload exceeds the supported stream limit');
220
+ }
221
+ throw new FormatError('Han Xin: text payload has no complete terminator');
222
+ }
223
+
224
+ function decodeByte(reader: BitReader): Uint8Array {
225
+ if (reader.available() < 13) throw new FormatError('Han Xin: byte payload has no complete length field');
226
+ const count = reader.read(13);
227
+ if (count < 1 || count > 8191) throw new FormatError(`Han Xin: byte count ${count} is outside the supported range`);
228
+ if (count * 8 > reader.available()) throw new FormatError('Han Xin: byte payload is truncated');
229
+ const bytes = new Uint8Array(count);
230
+ for (let i = 0; i < count; i++) bytes[i] = reader.read(8);
231
+ return bytes;
232
+ }
233
+
234
+ function parsePayload(data: Uint8Array): PayloadResult {
235
+ const reader = new BitReader(data);
236
+ if (reader.available() < 4) throw new FormatError('Han Xin: payload has no mode indicator');
237
+ const mode = reader.read(4);
238
+ if (mode === 1) {
239
+ const text = decodeNumeric(reader);
240
+ ensureZeroPadding(reader);
241
+ return { text, bytes: utf8Bytes(text), mode: 'numeric' };
242
+ }
243
+ if (mode === 2) {
244
+ const text = decodeText(reader);
245
+ ensureZeroPadding(reader);
246
+ return { text, bytes: utf8Bytes(text), mode: 'text' };
247
+ }
248
+ if (mode === 3) {
249
+ const bytes = decodeByte(reader);
250
+ ensureZeroPadding(reader);
251
+ return { text: decodeByteText(bytes), bytes, mode: 'byte' };
252
+ }
253
+ throw new FormatError(`Han Xin: unsupported mode indicator ${mode}`);
254
+ }
255
+
256
+ function candidateRotations(option: HanXinDecodeOptions['rotation']): HanXinRotation[] {
257
+ if (option === 'auto' || option == null) return [0, 90, 180, 270];
258
+ if (option === 0 || option === 90 || option === 180 || option === 270) return [option];
259
+ throw new FormatError('Han Xin: rotation must be 0, 90, 180, 270 or auto');
260
+ }
261
+
262
+ function candidatePolarities(option: HanXinDecodeOptions['inverted']): boolean[] {
263
+ if (option === 'auto' || option == null) return [false, true];
264
+ if (option === false || option === true) return [option];
265
+ throw new FormatError('Han Xin: inverted must be true, false or auto');
266
+ }
267
+
268
+ /** Decode a verified Han Xin module matrix. */
269
+ export function decodeHanXin(matrix: BitMatrix, options: HanXinDecodeOptions = {}): HanXinDecodeResult {
270
+ if (!matrix || !Number.isInteger(matrix.width) || !Number.isInteger(matrix.height)) {
271
+ throw new FormatError('Han Xin: a BitMatrix is required');
272
+ }
273
+ const rotations = candidateRotations(options.rotation);
274
+ const polarities = candidatePolarities(options.inverted);
275
+ let lastError: unknown = null;
276
+
277
+ for (const rotation of rotations) {
278
+ const rotated = rotateMatrix(matrix, rotation);
279
+ for (const inverted of polarities) {
280
+ const candidate = inverted ? invertMatrix(rotated) : rotated;
281
+ for (const version of [1, 2, 3] as HanXinVersion[]) {
282
+ if (candidate.width !== hanXinSize(version) || candidate.height !== hanXinSize(version)) continue;
283
+ if (!hanXinStructureMatches(candidate, version)) continue;
284
+ try {
285
+ const info = decodeHanXinFunctionInfo(candidate, version);
286
+ if (!HANXIN_ECC_LEVELS.includes(info.level)) throw new FormatError('Han Xin: invalid error-correction level');
287
+ const codewords = readCodewords(candidate, version, info.mask);
288
+ const layout = hanXinEcLayout(version, info.level);
289
+ const corrections = rsDecode(codewords, layout.eccCodewords, GF256_HANXIN, 1);
290
+ const data = Uint8Array.from(codewords.slice(0, layout.dataCodewords));
291
+ const payload = parsePayload(data);
292
+ return {
293
+ format: 'hanxin',
294
+ text: payload.text,
295
+ bytes: payload.bytes,
296
+ version,
297
+ ecc: info.level,
298
+ mask: info.mask as 0 | 1 | 2 | 3,
299
+ mode: payload.mode,
300
+ corrections: info.corrections + corrections,
301
+ rows: candidate.height,
302
+ columns: candidate.width,
303
+ inverted,
304
+ rotation,
305
+ };
306
+ } catch (error) {
307
+ lastError = error;
308
+ }
309
+ }
310
+ }
311
+ }
312
+ if (lastError instanceof FormatError) throw lastError;
313
+ throw new FormatError('Han Xin: no valid symbol found in the supplied matrix');
314
+ }
@@ -0,0 +1,45 @@
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 { HanXinDecodeResult } from './decoder.js';
23
+ export interface HanXinDetection {
24
+ corners: Array<{
25
+ x: number;
26
+ y: number;
27
+ }>;
28
+ dimension: {
29
+ width: number;
30
+ height: number;
31
+ };
32
+ moduleSize: number;
33
+ matrix: BitMatrix;
34
+ result: HanXinDecodeResult;
35
+ }
36
+ /** Detect one strict, integer-scale Han Xin symbol in a binarized image. */
37
+ export declare function detectHanXin(binaryImage: BitMatrix): HanXinDetection | null;
38
+ /** Detect and decode one verified Han Xin symbol, or return null. */
39
+ export declare function detectAndDecodeHanXin(binaryImage: BitMatrix): (HanXinDecodeResult & {
40
+ corners: Array<{
41
+ x: number;
42
+ y: number;
43
+ }>;
44
+ moduleSize: number;
45
+ }) | null;