@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,889 @@
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
+ * Four-state postal barcode family.
32
+ *
33
+ * The writers below use the public bar-state descriptions of each format and
34
+ * render an eight-module vertical profile. Keeping the state geometry in a
35
+ * shared module makes the reader deliberately strict: a scan must contain the
36
+ * expected start/stop markers, state sequence and checksum before it is
37
+ * promoted to a result.
38
+ *
39
+ * @module oned/postal
40
+ */
41
+ import { BitMatrix } from '../core/bit-matrix.js';
42
+ import { EncodeError } from '../core/errors.js';
43
+ /**
44
+ * State geometry used by the postal family in matrix coordinates (Y grows
45
+ * downwards). The one/ two state ordering follows the public postal glyph
46
+ * tables and the orientation emitted by the black-box validation oracle.
47
+ */
48
+ const STATE_PROFILES = [
49
+ [3, 5], // tracker
50
+ [3, 8], // lower extension
51
+ [0, 5], // upper extension
52
+ [0, 8], // full
53
+ ];
54
+ /** POSTNET and PLANET use half-height bars aligned to the lower edge. */
55
+ const POSTAL_HALF_TOP = [0, 4];
56
+ const POSTAL_HALF_BOTTOM = [4, 8];
57
+ /** Australia Post's physical state numbering is full/descender/ascender/tracker. */
58
+ const AUS_STATE_TO_SEMANTIC = [3, 2, 1, 0];
59
+ const AUS_SEMANTIC_TO_STATE = [3, 2, 1, 0];
60
+ const POSTNET_PATTERNS = ['55222', '22255', '22525', '22552', '25225', '25252', '25522', '52225', '52252', '52522'];
61
+ const PLANET_PATTERNS = ['22555', '55522', '55252', '55225', '52552', '52525', '52255', '25552', '25525', '25255'];
62
+ const RM_ALPHABET = 'ZUVWXY501234B6789AHCDEFGNIJKLMTOPQRS';
63
+ const RM_PATTERNS = [
64
+ '3300', '2211', '2301', '2310', '3201', '3210', '1122', '0033', '0123', '0132',
65
+ '1023', '1032', '1302', '0213', '0303', '0312', '1203', '1212', '1320', '0231',
66
+ '0321', '0330', '1221', '1230', '3102', '2013', '2103', '2112', '3003', '3012',
67
+ '3120', '2031', '2121', '2130', '3021', '3030',
68
+ ];
69
+ const KIX_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
70
+ const KIX_PATTERNS = [
71
+ '0033', '0123', '0132', '1023', '1032', '1122', '0213', '0303', '0312', '1203',
72
+ '1212', '1302', '0231', '0321', '0330', '1221', '1230', '1320', '2013', '2103',
73
+ '2112', '3003', '3012', '3102', '2031', '2121', '2130', '3021', '3030', '3120',
74
+ '2211', '2301', '2310', '3201', '3210', '3300',
75
+ ];
76
+ const JAPAN_PATTERNS = [
77
+ '300', '330', '312', '132', '321', '303', '123', '231', '213', '033', '030',
78
+ '120', '102', '210', '012', '201', '021', '003', '333', '31', '13',
79
+ ];
80
+ const JAPAN_ALPHABET = '0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ';
81
+ const AUS_PATTERNS = [
82
+ '000', '001', '002', '010', '011', '012', '020', '021', '022', '100', '101', '102',
83
+ '110', '111', '112', '120', '121', '122', '200', '201', '202', '210', '211', '212',
84
+ '220', '221', '222', '300', '301', '302', '310', '311', '312', '320', '321', '322',
85
+ '023', '030', '031', '032', '033', '103', '113', '123', '130', '131', '132', '133',
86
+ '203', '213', '223', '230', '231', '232', '233', '303', '313', '323', '330', '331',
87
+ '332', '333', '003', '013', '00', '01', '02', '10', '11', '12', '20', '21', '22',
88
+ '30', '13', '3',
89
+ ];
90
+ const AUS_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz #';
91
+ const AUS_FCC_LENGTHS = {
92
+ '11': 37, '45': 37, '59': 52, '62': 67, '87': 37, '92': 37,
93
+ };
94
+ const AUS_LENGTH_FCC = Object.entries(AUS_FCC_LENGTHS)
95
+ .reduce((map, [fcc, length]) => {
96
+ (map[length] ?? (map[length] = [])).push(fcc);
97
+ return map;
98
+ }, {});
99
+ const POSTAL_FORMATS = ['postnet', 'planet', 'rm4scc', 'kix', 'auspost', 'japanpost', 'imb'];
100
+ const POSTAL_ALIASES = {
101
+ postnet: 'postnet', 'usps-postnet': 'postnet',
102
+ planet: 'planet', 'usps-planet': 'planet',
103
+ rm4scc: 'rm4scc', royalmail: 'rm4scc', 'royal-mail': 'rm4scc',
104
+ kix: 'kix',
105
+ auspost: 'auspost', 'australia-post': 'auspost', 'australiapost': 'auspost',
106
+ japanpost: 'japanpost', 'japan-post': 'japanpost',
107
+ imb: 'imb', onecode: 'imb', 'usps-onecode': 'imb',
108
+ };
109
+ function requireText(value, label) {
110
+ const text = String(value);
111
+ if (text.length === 0)
112
+ throw new EncodeError(`${label}: payload must not be empty`);
113
+ return text;
114
+ }
115
+ function requireDigits(value, label) {
116
+ const text = requireText(value, label);
117
+ if (!/^[0-9]+$/u.test(text))
118
+ throw new EncodeError(`${label}: payload must contain only digits`);
119
+ return text;
120
+ }
121
+ function checkMod10(value) {
122
+ let sum = 0;
123
+ for (const ch of value)
124
+ sum += Number(ch);
125
+ return (10 - (sum % 10)) % 10;
126
+ }
127
+ function appendOrValidateCheck(value, lengths, label, options) {
128
+ const text = requireDigits(value, label);
129
+ if (lengths.includes(text.length)) {
130
+ const check = checkMod10(text);
131
+ return { body: text, check };
132
+ }
133
+ if (options.checkDigit === true && lengths.some((length) => text.length === length + 1)) {
134
+ const body = text.slice(0, -1);
135
+ const expected = checkMod10(body);
136
+ if (Number(text[text.length - 1]) !== expected) {
137
+ throw new EncodeError(`${label}: invalid check digit, expected ${expected}`);
138
+ }
139
+ return { body, check: expected };
140
+ }
141
+ throw new EncodeError(`${label}: payload must be ${lengths.join(', ')} digits, excluding the check digit`);
142
+ }
143
+ /** Convert semantic states into an 8-module-tall matrix with one light module between bars. */
144
+ function statesToMatrix(states, australia = false, halfHeight = false) {
145
+ if (states.length < 1 || states.length > 5000)
146
+ throw new EncodeError('Postal symbol is too large');
147
+ const matrix = new BitMatrix(states.length * 2 - 1, 8);
148
+ for (let i = 0; i < states.length; i++) {
149
+ if (halfHeight && states[i] !== 0 && states[i] !== 3) {
150
+ throw new EncodeError('Postal five-state symbol contains an invalid bar state');
151
+ }
152
+ if (halfHeight) {
153
+ const profile = states[i] === 3 ? STATE_PROFILES[3] : POSTAL_HALF_BOTTOM;
154
+ const x = i * 2;
155
+ for (let y = profile[0]; y < profile[1]; y++)
156
+ matrix.set(x, y);
157
+ continue;
158
+ }
159
+ const state = australia ? AUS_STATE_TO_SEMANTIC[states[i]] : states[i];
160
+ const profile = STATE_PROFILES[state];
161
+ if (!profile)
162
+ throw new EncodeError('Postal state is outside the four-state range');
163
+ const x = i * 2;
164
+ for (let y = profile[0]; y < profile[1]; y++)
165
+ matrix.set(x, y);
166
+ }
167
+ return matrix;
168
+ }
169
+ function fiveStatePattern(pattern) {
170
+ return [...pattern].map((ch) => ch === '5' ? 3 : 0);
171
+ }
172
+ function fourStatePattern(pattern) {
173
+ return [...pattern].map(Number);
174
+ }
175
+ /** Encode USPS POSTNET. Accepts 5, 9 or 11 body digits and appends Mod-10. */
176
+ export function encodePostnet(value, options = {}) {
177
+ const { body, check } = appendOrValidateCheck(value, [5, 9, 11], 'POSTNET', options);
178
+ const states = [3, ...[...body].flatMap((ch) => fiveStatePattern(POSTNET_PATTERNS[Number(ch)])), ...fiveStatePattern(POSTNET_PATTERNS[check]), 3];
179
+ return statesToMatrix(states, false, true);
180
+ }
181
+ /** Encode USPS PLANET. Accepts 11 or 13 body digits and appends Mod-10. */
182
+ export function encodePlanet(value, options = {}) {
183
+ const { body, check } = appendOrValidateCheck(value, [11, 13], 'PLANET', options);
184
+ const states = [3, ...[...body].flatMap((ch) => fiveStatePattern(PLANET_PATTERNS[Number(ch)])), ...fiveStatePattern(PLANET_PATTERNS[check]), 3];
185
+ return statesToMatrix(states, false, true);
186
+ }
187
+ function rmCheckDigit(body) {
188
+ let row = 0;
189
+ let column = 0;
190
+ for (const ch of body) {
191
+ const index = RM_ALPHABET.indexOf(ch);
192
+ if (index < 0)
193
+ throw new EncodeError('RM4SCC: payload must contain capital letters and digits');
194
+ row += Math.floor(index / 6);
195
+ column += index % 6;
196
+ }
197
+ return RM_ALPHABET[(row % 6) * 6 + (column % 6)];
198
+ }
199
+ /** Encode Royal Mail 4-State Customer Code (RM4SCC). */
200
+ export function encodeRM4SCC(value, options = {}) {
201
+ let text = requireText(value, 'RM4SCC');
202
+ if (text.length > 500)
203
+ throw new EncodeError('RM4SCC: payload is limited to 500 characters');
204
+ if (options.checkDigit === true) {
205
+ if (text.length < 2)
206
+ throw new EncodeError('RM4SCC: a checked payload needs a body and check character');
207
+ const supplied = text.slice(-1);
208
+ text = text.slice(0, -1);
209
+ if (rmCheckDigit(text) !== supplied)
210
+ throw new EncodeError(`RM4SCC: invalid check character, expected ${rmCheckDigit(text)}`);
211
+ }
212
+ const check = rmCheckDigit(text);
213
+ const states = [2];
214
+ for (const ch of text) {
215
+ const index = RM_ALPHABET.indexOf(ch);
216
+ if (index < 0)
217
+ throw new EncodeError('RM4SCC: payload must contain capital letters and digits');
218
+ states.push(...fourStatePattern(RM_PATTERNS[index]));
219
+ }
220
+ states.push(...fourStatePattern(RM_PATTERNS[RM_ALPHABET.indexOf(check)]), 3);
221
+ return statesToMatrix(states);
222
+ }
223
+ /** Encode KIX (the Dutch postal four-state alphabet). */
224
+ export function encodeKIX(value) {
225
+ const text = requireText(value, 'KIX').toUpperCase();
226
+ if (text.length > 500)
227
+ throw new EncodeError('KIX: payload is limited to 500 characters');
228
+ const states = [];
229
+ for (const ch of text) {
230
+ const index = KIX_ALPHABET.indexOf(ch);
231
+ if (index < 0)
232
+ throw new EncodeError('KIX: payload must contain capital letters and digits');
233
+ states.push(...fourStatePattern(KIX_PATTERNS[index]));
234
+ }
235
+ return statesToMatrix(states);
236
+ }
237
+ function japanDataValues(value) {
238
+ const text = requireText(value, 'Japan Post').toUpperCase();
239
+ const values = [];
240
+ for (const ch of text) {
241
+ const index = JAPAN_ALPHABET.indexOf(ch);
242
+ if (index < 0)
243
+ throw new EncodeError('Japan Post: payload must contain digits, capital letters and dash');
244
+ if (index <= 10)
245
+ values.push(index);
246
+ else {
247
+ values.push(Math.floor((index - 1) / 10) + 10, (index - 1) % 10);
248
+ }
249
+ }
250
+ if (values.length > 20)
251
+ throw new EncodeError('Japan Post: payload expands to more than 20 data groups');
252
+ return values;
253
+ }
254
+ /** Encode Japan Post. The check and padding groups are generated automatically. */
255
+ export function encodeJapanPost(value) {
256
+ const values = japanDataValues(value);
257
+ let checksum = 0;
258
+ const data = [...values];
259
+ while (data.length < 20)
260
+ data.push(14);
261
+ for (const item of data)
262
+ checksum += item;
263
+ const check = 19 - (checksum % 19);
264
+ const states = [...fourStatePattern('31')];
265
+ for (const item of data)
266
+ states.push(...fourStatePattern(JAPAN_PATTERNS[item]));
267
+ states.push(...fourStatePattern(JAPAN_PATTERNS[check]), ...fourStatePattern('13'));
268
+ return statesToMatrix(states);
269
+ }
270
+ function gf64Multiply(a, b) {
271
+ let result = 0;
272
+ let left = a & 63;
273
+ let right = b & 63;
274
+ while (right !== 0) {
275
+ if (right & 1)
276
+ result ^= left;
277
+ left <<= 1;
278
+ if (left & 64)
279
+ left ^= 0x43;
280
+ right >>>= 1;
281
+ }
282
+ return result & 63;
283
+ }
284
+ function gf64Alpha(power) {
285
+ let value = 1;
286
+ for (let i = 0; i < power; i++)
287
+ value = gf64Multiply(value, 2);
288
+ return value;
289
+ }
290
+ function gf64Generator() {
291
+ let polynomial = [1];
292
+ for (let rootIndex = 1; rootIndex <= 4; rootIndex++) {
293
+ const root = gf64Alpha(rootIndex);
294
+ const next = new Array(polynomial.length + 1).fill(0);
295
+ for (let i = 0; i < polynomial.length; i++) {
296
+ next[i] ^= gf64Multiply(polynomial[i], root);
297
+ next[i + 1] ^= polynomial[i];
298
+ }
299
+ polynomial = next;
300
+ }
301
+ return polynomial;
302
+ }
303
+ const AUS_RS_GENERATOR = gf64Generator();
304
+ function australiaCodeword(states, offset) {
305
+ return (states[offset] * 16) + (states[offset + 1] * 4) + states[offset + 2];
306
+ }
307
+ function australiaParity(data) {
308
+ const dataCount = data.length;
309
+ const codewords = new Array(dataCount + 4).fill(0);
310
+ for (let i = 0; i < dataCount; i++)
311
+ codewords[dataCount + 3 - i] = data[i];
312
+ for (let offset = dataCount - 1; offset >= 0; offset--) {
313
+ const factor = codewords[offset + 4];
314
+ for (let coefficient = 0; coefficient < 5; coefficient++) {
315
+ codewords[offset + coefficient] ^= gf64Multiply(factor, AUS_RS_GENERATOR[coefficient]);
316
+ }
317
+ }
318
+ return codewords;
319
+ }
320
+ function australiaSymbolLength(fcc) {
321
+ const length = AUS_FCC_LENGTHS[fcc];
322
+ if (!length)
323
+ throw new EncodeError('Australia Post: FCC must be 11, 45, 59, 62, 87 or 92');
324
+ return length;
325
+ }
326
+ /** Encode Australia Post 4-state symbols, including FCC/DPID and optional customer data. */
327
+ export function encodeAustraliaPost(value, options = {}) {
328
+ const text = requireText(value, 'Australia Post');
329
+ if (text.length < 10)
330
+ throw new EncodeError('Australia Post: payload must contain an FCC and eight-digit DPID');
331
+ const fcc = text.slice(0, 2);
332
+ const length = australiaSymbolLength(fcc);
333
+ if (!/^\d{8}$/u.test(text.slice(2, 10)))
334
+ throw new EncodeError('Australia Post: DPID must be eight digits');
335
+ const encoding = options.customerEncoding ?? options.custinfoenc ?? 'character';
336
+ if (encoding !== 'character' && encoding !== 'numeric')
337
+ throw new EncodeError('Australia Post: customerEncoding must be character or numeric');
338
+ const customer = text.slice(10);
339
+ const customerStates = [];
340
+ if (encoding === 'numeric') {
341
+ if (!/^\d*$/u.test(customer))
342
+ throw new EncodeError('Australia Post: numeric customer data must contain digits');
343
+ for (const ch of customer)
344
+ customerStates.push(...[...AUS_PATTERNS[64 + Number(ch)]].map(Number));
345
+ }
346
+ else {
347
+ for (const ch of customer) {
348
+ const index = AUS_ALPHABET.indexOf(ch);
349
+ if (index < 0)
350
+ throw new EncodeError('Australia Post: customer data contains an unsupported character');
351
+ customerStates.push(...[...AUS_PATTERNS[index]].map(Number));
352
+ }
353
+ }
354
+ const dataStateLength = length - 16;
355
+ if (customerStates.length > dataStateLength - 20)
356
+ throw new EncodeError('Australia Post: customer data does not fit the FCC symbol');
357
+ const states = [1, 3];
358
+ // Australia Post encodes the two FCC digits and the eight DPID digits as
359
+ // ten two-state digit pairs. The FCC therefore participates in the symbol
360
+ // geometry as well as selecting the symbol length.
361
+ for (const ch of text.slice(0, 10))
362
+ states.push(...[...AUS_PATTERNS[64 + Number(ch)]].map(Number));
363
+ states.push(...customerStates);
364
+ while (states.length < dataStateLength + 2)
365
+ states.push(3);
366
+ const dataWords = [];
367
+ for (let offset = 2; offset <= length - 16; offset += 3)
368
+ dataWords.push(australiaCodeword(states, offset));
369
+ const codewords = australiaParity(dataWords);
370
+ const checkStates = [];
371
+ for (let i = 0; i < 4; i++)
372
+ checkStates.push(...codewords[3 - i].toString(4).padStart(3, '0').split('').map(Number));
373
+ states.push(...checkStates, 1, 3);
374
+ if (states.length !== length)
375
+ throw new EncodeError('Australia Post: internal symbol geometry mismatch');
376
+ return statesToMatrix(states, true);
377
+ }
378
+ function reverse13(mask) {
379
+ let value = 0;
380
+ for (let i = 0; i < 13; i++)
381
+ value = (value << 1) | ((mask >>> i) & 1);
382
+ return value;
383
+ }
384
+ function popcount(mask) {
385
+ let count = 0;
386
+ for (let value = mask; value !== 0; value >>>= 1)
387
+ count += value & 1;
388
+ return count;
389
+ }
390
+ /** Generate the USPS OneCode combinatorial table in its normative order. */
391
+ function combinationTable(weight) {
392
+ const values = [];
393
+ const palindromes = [];
394
+ for (let mask = 0; mask < 8192; mask++) {
395
+ if (popcount(mask) !== weight)
396
+ continue;
397
+ const reversed = reverse13(mask);
398
+ if (mask < reversed)
399
+ values.push(mask, reversed);
400
+ else if (mask === reversed)
401
+ palindromes.push(mask);
402
+ }
403
+ palindromes.sort((a, b) => b - a);
404
+ values.push(...palindromes);
405
+ return values;
406
+ }
407
+ const IMB_TAB513 = combinationTable(5);
408
+ const IMB_TAB213 = combinationTable(2);
409
+ const IMB_TAB513_INDEX = new Map(IMB_TAB513.map((mask, index) => [mask, index]));
410
+ const IMB_TAB213_INDEX = new Map(IMB_TAB213.map((mask, index) => [mask, index + 1287]));
411
+ // USPS OneCode's 65-bar placement order. It is a public normative placement
412
+ // mapping represented as an independent Sythos array; no implementation code
413
+ // is copied from an external encoder.
414
+ const IMB_BAR_MAP = [
415
+ 7, 2, 4, 3, 1, 10, 0, 0, 9, 12, 2, 8, 5, 5, 6, 11, 8, 9, 3, 1,
416
+ 0, 1, 5, 12, 2, 5, 1, 8, 4, 4, 9, 11, 6, 3, 8, 10, 3, 9, 7, 6,
417
+ 5, 11, 1, 4, 8, 5, 2, 12, 9, 10, 0, 2, 7, 1, 6, 7, 3, 6, 4, 9,
418
+ 0, 3, 8, 6, 6, 4, 2, 7, 1, 1, 9, 9, 7, 10, 5, 2, 4, 0, 3, 8,
419
+ 6, 2, 0, 4, 8, 11, 1, 0, 9, 8, 3, 12, 2, 6, 7, 7, 5, 1, 4, 10,
420
+ 1, 12, 6, 9, 7, 3, 8, 0, 5, 8, 9, 7, 4, 6, 2, 10, 3, 4, 0, 5,
421
+ 8, 4, 5, 7, 7, 11, 1, 9, 6, 0, 9, 6, 0, 6, 4, 8, 2, 1, 3, 2,
422
+ 5, 9, 8, 12, 4, 11, 6, 1, 9, 5, 7, 4, 3, 3, 1, 2, 0, 7, 2, 0,
423
+ 1, 3, 4, 1, 6, 10, 3, 5, 8, 7, 9, 4, 2, 11, 5, 6, 0, 8, 7, 12,
424
+ 4, 2, 8, 1, 5, 10, 3, 0, 9, 3, 0, 9, 6, 5, 2, 4, 7, 8, 1, 7,
425
+ 5, 0, 4, 5, 2, 3, 0, 10, 6, 12, 9, 2, 3, 11, 1, 6, 8, 8, 7, 9,
426
+ 5, 4, 0, 11, 1, 5, 2, 2, 9, 1, 4, 12, 8, 3, 6, 6, 7, 0, 3, 7,
427
+ 4, 7, 7, 5, 0, 12, 1, 11, 2, 9, 9, 0, 6, 8, 5, 3, 3, 10, 8, 2,
428
+ ];
429
+ function bigIntBytes(value) {
430
+ const bytes = new Array(13).fill(0);
431
+ let remaining = value;
432
+ for (let i = 12; i >= 0; i--) {
433
+ bytes[i] = Number(remaining & 255n);
434
+ remaining >>= 8n;
435
+ }
436
+ if (remaining !== 0n)
437
+ throw new EncodeError('USPS IMb: payload exceeds the 13-byte binary envelope');
438
+ return bytes;
439
+ }
440
+ function imbBinaryValue(text) {
441
+ const length = text.length;
442
+ const start = length === 20 ? 0n : length === 25 ? 1n : length === 29 ? 100001n : 1000010001n;
443
+ const suffix = BigInt(text.slice(20) || '0');
444
+ let value = (start + suffix) * 10n + BigInt(text.charCodeAt(0) - 48);
445
+ value = value * 5n + BigInt(text.charCodeAt(1) - 48);
446
+ for (let i = 2; i < 20; i++)
447
+ value = value * 10n + BigInt(text.charCodeAt(i) - 48);
448
+ return value;
449
+ }
450
+ function imbFcs(bytes) {
451
+ let fcs = 2047;
452
+ let data = bytes[0] << 5;
453
+ for (let i = 0; i < 6; i++) {
454
+ fcs = ((fcs << 1) ^ (((fcs ^ data) & 1024) !== 0 ? 3893 : 0)) & 2047;
455
+ data <<= 1;
456
+ }
457
+ for (let index = 1; index <= 12; index++) {
458
+ data = bytes[index] << 3;
459
+ for (let i = 0; i < 8; i++) {
460
+ fcs = ((fcs << 1) ^ (((fcs ^ data) & 1024) !== 0 ? 3893 : 0)) & 2047;
461
+ data <<= 1;
462
+ }
463
+ }
464
+ return fcs;
465
+ }
466
+ /** Encode USPS Intelligent Mail Barcode (OneCode), for all four legal lengths. */
467
+ export function encodeIMB(value) {
468
+ const text = requireDigits(value, 'USPS IMb');
469
+ if (![20, 25, 29, 31].includes(text.length))
470
+ throw new EncodeError('USPS IMb: payload must be 20, 25, 29 or 31 digits');
471
+ let binary = imbBinaryValue(text);
472
+ const bytes = bigIntBytes(binary);
473
+ const fcs = imbFcs(bytes);
474
+ const codewords = new Array(10).fill(0);
475
+ for (let index = 9; index >= 0; index--) {
476
+ const base = index === 9 ? 636n : 1365n;
477
+ codewords[index] = Number(binary % base);
478
+ binary /= base;
479
+ }
480
+ codewords[9] *= 2;
481
+ if ((fcs & 1024) !== 0)
482
+ codewords[0] += 659;
483
+ const chars = codewords.map((word) => {
484
+ const character = word <= 1286 ? IMB_TAB513[word] : IMB_TAB213[word - 1287];
485
+ if (character === undefined)
486
+ throw new EncodeError('USPS IMb: codeword is outside the OneCode character tables');
487
+ return character;
488
+ });
489
+ for (let index = 9; index >= 0; index--)
490
+ if ((fcs & (1 << index)) !== 0)
491
+ chars[index] ^= 8191;
492
+ const states = new Array(65);
493
+ // The bar map maps pairs of bits to each physical bar. Each pair is decoded
494
+ // independently so the emitted 65-state sequence is explicit and auditable.
495
+ for (let index = 0; index < 65; index++) {
496
+ const dec = (chars[IMB_BAR_MAP[index * 4]] & (1 << IMB_BAR_MAP[index * 4 + 1])) !== 0;
497
+ const asc = (chars[IMB_BAR_MAP[index * 4 + 2]] & (1 << IMB_BAR_MAP[index * 4 + 3])) !== 0;
498
+ // Physical state 1 is the top extension (the `dec` bit), while state 2
499
+ // is the bottom extension (the `asc` bit) in matrix coordinates.
500
+ states[index] = (dec ? 1 : 0) | (asc ? 2 : 0);
501
+ }
502
+ return statesToMatrix(states);
503
+ }
504
+ function patternKey(states, offset, length) {
505
+ return states.slice(offset, offset + length).join('');
506
+ }
507
+ function decodePostnetStates(states) {
508
+ const bodyLength = (states.length - 7) / 5;
509
+ if (![5, 9, 11].includes(bodyLength) || states[0] !== 3 || states[states.length - 1] !== 3)
510
+ return null;
511
+ const body = [];
512
+ for (let i = 0; i < bodyLength; i++) {
513
+ const key = states.slice(1 + i * 5, 6 + i * 5).map((state) => state === 3 ? '5' : state === 0 || state === 4 || state === 5 ? '2' : '?').join('');
514
+ const digit = POSTNET_PATTERNS.indexOf(key);
515
+ if (digit < 0)
516
+ return null;
517
+ body.push(String(digit));
518
+ }
519
+ const checkKey = states.slice(1 + bodyLength * 5, 6 + bodyLength * 5).map((state) => state === 3 ? '5' : state === 0 || state === 4 || state === 5 ? '2' : '?').join('');
520
+ const check = POSTNET_PATTERNS.indexOf(checkKey);
521
+ if (check < 0 || check !== checkMod10(body.join('')))
522
+ return null;
523
+ return { format: 'postnet', text: body.join(''), checkDigit: true };
524
+ }
525
+ function decodePlanetStates(states) {
526
+ const bodyLength = (states.length - 7) / 5;
527
+ if (![11, 13].includes(bodyLength) || states[0] !== 3 || states[states.length - 1] !== 3)
528
+ return null;
529
+ const body = [];
530
+ const toKey = (offset) => states.slice(offset, offset + 5).map((state) => state === 3 ? '5' : state === 0 || state === 4 || state === 5 ? '2' : '?').join('');
531
+ for (let i = 0; i < bodyLength; i++) {
532
+ const digit = PLANET_PATTERNS.indexOf(toKey(1 + i * 5));
533
+ if (digit < 0)
534
+ return null;
535
+ body.push(String(digit));
536
+ }
537
+ const check = PLANET_PATTERNS.indexOf(toKey(1 + bodyLength * 5));
538
+ if (check < 0 || check !== checkMod10(body.join('')))
539
+ return null;
540
+ return { format: 'planet', text: body.join(''), checkDigit: true };
541
+ }
542
+ function decodeRMStates(states) {
543
+ if (states.length < 10 || states[0] !== 2 || states[states.length - 1] !== 3 || (states.length - 6) % 4 !== 0)
544
+ return null;
545
+ const bodyLength = (states.length - 6) / 4;
546
+ if (bodyLength < 1 || bodyLength > 500)
547
+ return null;
548
+ const body = [];
549
+ for (let i = 0; i < bodyLength; i++) {
550
+ const index = RM_PATTERNS.indexOf(patternKey(states, 1 + i * 4, 4));
551
+ if (index < 0)
552
+ return null;
553
+ body.push(RM_ALPHABET[index]);
554
+ }
555
+ const checkIndex = RM_PATTERNS.indexOf(patternKey(states, 1 + bodyLength * 4, 4));
556
+ if (checkIndex < 0 || RM_ALPHABET[checkIndex] !== rmCheckDigit(body.join('')))
557
+ return null;
558
+ return { format: 'rm4scc', text: body.join(''), checkDigit: true };
559
+ }
560
+ function decodeKIXStates(states) {
561
+ if (states.length < 4 || states.length % 4 !== 0 || states.length > 2000)
562
+ return null;
563
+ const text = [];
564
+ for (let i = 0; i < states.length; i += 4) {
565
+ const index = KIX_PATTERNS.indexOf(patternKey(states, i, 4));
566
+ if (index < 0)
567
+ return null;
568
+ text.push(KIX_ALPHABET[index]);
569
+ }
570
+ return { format: 'kix', text: text.join(''), checkDigit: false };
571
+ }
572
+ function decodeJapanStates(states) {
573
+ if (states.length !== 67 || patternKey(states, 0, 2) !== '31' || patternKey(states, 65, 2) !== '13')
574
+ return null;
575
+ const values = [];
576
+ for (let i = 0; i < 20; i++) {
577
+ const value = JAPAN_PATTERNS.indexOf(patternKey(states, 2 + i * 3, 3));
578
+ if (value < 0 || value > 20)
579
+ return null;
580
+ values.push(value);
581
+ }
582
+ const check = JAPAN_PATTERNS.indexOf(patternKey(states, 62, 3));
583
+ if (check < 1 || check > 19)
584
+ return null;
585
+ let sum = 0;
586
+ for (const value of values)
587
+ sum += value;
588
+ if (19 - (sum % 19) !== check)
589
+ return null;
590
+ const text = [];
591
+ for (let i = 0; i < values.length; i++) {
592
+ const value = values[i];
593
+ if (value === 14) {
594
+ if (values.slice(i).some((item) => item !== 14))
595
+ return null;
596
+ break;
597
+ }
598
+ if (value >= 11 && value <= 13) {
599
+ const digit = values[++i];
600
+ if (digit > 9)
601
+ return null;
602
+ const index = (value - 10) * 10 + digit + 1;
603
+ if (!JAPAN_ALPHABET[index])
604
+ return null;
605
+ text.push(JAPAN_ALPHABET[index]);
606
+ }
607
+ else if (value >= 0 && value <= 10) {
608
+ text.push(JAPAN_ALPHABET[value]);
609
+ }
610
+ else
611
+ return null;
612
+ }
613
+ return { format: 'japanpost', text: text.join(''), checkDigit: true };
614
+ }
615
+ function decodeAustraliaStates(states, options) {
616
+ if (states.length < 37 || !AUS_LENGTH_FCC[states.length] || patternKey(states, 0, 2) !== '20' || patternKey(states, states.length - 2, 2) !== '20')
617
+ return null;
618
+ const codes = states.map((state) => AUS_SEMANTIC_TO_STATE[state]);
619
+ const headerDigits = [];
620
+ for (let i = 0; i < 10; i++) {
621
+ const index = AUS_PATTERNS.indexOf(patternKey(codes, 2 + i * 2, 2));
622
+ if (index < 64 || index > 73)
623
+ return null;
624
+ headerDigits.push(String(index - 64));
625
+ }
626
+ const fccCandidates = AUS_LENGTH_FCC[states.length];
627
+ const matchingFCC = fccCandidates.filter((fcc) => fcc === headerDigits.slice(0, 2).join(''));
628
+ if (matchingFCC.length === 0)
629
+ return null;
630
+ const dataWords = [];
631
+ for (let offset = 2; offset <= states.length - 16; offset += 3)
632
+ dataWords.push(australiaCodeword(codes, offset));
633
+ const expected = australiaParity(dataWords);
634
+ const checkWords = [];
635
+ for (let i = 0; i < 4; i++) {
636
+ const value = Number.parseInt(patternKey(codes, states.length - 14 + i * 3, 3), 4);
637
+ if (!Number.isInteger(value) || value > 63)
638
+ return null;
639
+ checkWords.push(value);
640
+ }
641
+ if (checkWords[0] !== expected[3] || checkWords[1] !== expected[2] || checkWords[2] !== expected[1] || checkWords[3] !== expected[0])
642
+ return null;
643
+ const customerEnd = states.length - 14;
644
+ const customerStates = codes.slice(22, customerEnd);
645
+ const requestedEncoding = options.customerEncoding ?? options.custinfoenc;
646
+ const encodings = requestedEncoding
647
+ ? [requestedEncoding]
648
+ : ['character', 'numeric'];
649
+ for (const encoding of encodings) {
650
+ const customer = [];
651
+ let offset = 0;
652
+ let valid = true;
653
+ while (offset < customerStates.length) {
654
+ if (customerStates.slice(offset).every((state) => state === 3))
655
+ break;
656
+ const width = encoding === 'numeric' ? 2 : 3;
657
+ if (offset + width > customerStates.length) {
658
+ valid = false;
659
+ break;
660
+ }
661
+ const pattern = customerStates.slice(offset, offset + width).join('');
662
+ const index = AUS_PATTERNS.indexOf(pattern);
663
+ if (encoding === 'numeric') {
664
+ if (index < 64 || index > 73) {
665
+ valid = false;
666
+ break;
667
+ }
668
+ customer.push(String(index - 64));
669
+ }
670
+ else {
671
+ if (index < 0 || index > 63) {
672
+ valid = false;
673
+ break;
674
+ }
675
+ customer.push(AUS_ALPHABET[index]);
676
+ }
677
+ offset += width;
678
+ }
679
+ if (valid)
680
+ return { format: 'auspost', text: `${matchingFCC[0]}${headerDigits.slice(2).join('')}${customer.join('')}`, checkDigit: true };
681
+ }
682
+ return null;
683
+ }
684
+ function decodeIMBStates(states) {
685
+ if (states.length !== 65)
686
+ return null;
687
+ const chars = new Array(10).fill(0);
688
+ for (let index = 0; index < 65; index++) {
689
+ const dec = (states[index] & 1) !== 0;
690
+ const asc = (states[index] & 2) !== 0;
691
+ const bit0 = 1 << IMB_BAR_MAP[index * 4 + 1];
692
+ const bit1 = 1 << IMB_BAR_MAP[index * 4 + 3];
693
+ const char0 = IMB_BAR_MAP[index * 4];
694
+ const char1 = IMB_BAR_MAP[index * 4 + 2];
695
+ if (dec)
696
+ chars[char0] |= bit0;
697
+ if (asc)
698
+ chars[char1] |= bit1;
699
+ }
700
+ for (let fcs = 0; fcs < 2048; fcs++) {
701
+ const decodedChars = chars.map((character, index) => (fcs & (1 << index)) !== 0 ? character ^ 8191 : character);
702
+ const codewords = decodedChars.map((character) => IMB_TAB513_INDEX.get(character) ?? IMB_TAB213_INDEX.get(character) ?? -1);
703
+ if (codewords.some((word) => word < 0))
704
+ continue;
705
+ if ((fcs & 1024) !== 0) {
706
+ codewords[0] -= 659;
707
+ if (codewords[0] < 0)
708
+ continue;
709
+ }
710
+ if ((codewords[9] & 1) !== 0)
711
+ continue;
712
+ codewords[9] /= 2;
713
+ let binary = 0n;
714
+ for (let i = 0; i < 9; i++)
715
+ binary = binary * 1365n + BigInt(codewords[i]);
716
+ binary = binary * 636n + BigInt(codewords[9]);
717
+ let bytes;
718
+ try {
719
+ bytes = bigIntBytes(binary);
720
+ }
721
+ catch {
722
+ continue;
723
+ }
724
+ if (imbFcs(bytes) !== fcs)
725
+ continue;
726
+ const tail = (binary % 1000000000000000000n).toString().padStart(18, '0');
727
+ const prefix = binary / 1000000000000000000n;
728
+ const second = Number(prefix % 5n);
729
+ const quotient = prefix / 5n;
730
+ const first = Number(quotient % 10n);
731
+ const intermediate = quotient / 10n;
732
+ for (const length of [20, 25, 29, 31]) {
733
+ const start = length === 20 ? 0n : length === 25 ? 1n : length === 29 ? 100001n : 1000010001n;
734
+ const suffixValue = intermediate - start;
735
+ if (suffixValue < 0n)
736
+ continue;
737
+ const suffixLength = length - 20;
738
+ const suffix = suffixLength === 0 ? '' : suffixValue.toString().padStart(suffixLength, '0');
739
+ if (suffix.length !== length - 20)
740
+ continue;
741
+ const text = `${first}${second}${tail}${suffix}`;
742
+ if (text.length !== length)
743
+ continue;
744
+ try {
745
+ if (imbFcs(bigIntBytes(imbBinaryValue(text))) !== fcs)
746
+ continue;
747
+ }
748
+ catch {
749
+ continue;
750
+ }
751
+ return { format: 'imb', text, checkDigit: true };
752
+ }
753
+ }
754
+ return null;
755
+ }
756
+ /** Extract bars and four-state profiles from a binarized raster. */
757
+ function classifyBars(image) {
758
+ if (image.width < 3 || image.height < 4 || image.width > 20000 || image.height > 20000)
759
+ return null;
760
+ const projection = [];
761
+ for (let x = 0; x < image.width; x++) {
762
+ let dark = false;
763
+ for (let y = 0; y < image.height; y++)
764
+ if (image.get(x, y)) {
765
+ dark = true;
766
+ break;
767
+ }
768
+ if (dark)
769
+ projection.push(x);
770
+ }
771
+ if (projection.length === 0)
772
+ return null;
773
+ const runs = [];
774
+ let start = projection[0];
775
+ let previous = start;
776
+ for (let i = 1; i < projection.length; i++) {
777
+ const x = projection[i];
778
+ if (x !== previous + 1) {
779
+ runs.push({ start, end: previous + 1 });
780
+ start = x;
781
+ }
782
+ previous = x;
783
+ }
784
+ runs.push({ start, end: previous + 1 });
785
+ if (runs.length < 5)
786
+ return null;
787
+ const widths = runs.map((run) => run.end - run.start).sort((a, b) => a - b);
788
+ const unit = widths[widths.length >> 1];
789
+ if (!Number.isFinite(unit) || unit <= 0)
790
+ return null;
791
+ const barBounds = [];
792
+ let minY = image.height;
793
+ let maxY = 0;
794
+ for (const run of runs) {
795
+ let top = image.height;
796
+ let bottom = 0;
797
+ for (let x = run.start; x < run.end; x++) {
798
+ for (let y = 0; y < image.height; y++) {
799
+ if (!image.get(x, y))
800
+ continue;
801
+ if (y < top)
802
+ top = y;
803
+ if (y + 1 > bottom)
804
+ bottom = y + 1;
805
+ }
806
+ }
807
+ if (top === image.height)
808
+ return null;
809
+ minY = Math.min(minY, top);
810
+ maxY = Math.max(maxY, bottom);
811
+ barBounds.push({ top, bottom });
812
+ }
813
+ if (maxY <= minY)
814
+ return null;
815
+ // Renderers are free to choose a horizontal module width and a different
816
+ // vertical bar height (BWIPP, for example, uses a 1.75:1 ratio). Infer the
817
+ // vertical module from the observed symbol extent instead of reusing the
818
+ // horizontal run width, otherwise a full-height bar can look like a
819
+ // tracker in a compact postal raster.
820
+ const centre = (minY + maxY) / 2;
821
+ const verticalUnit = (maxY - minY) / 8;
822
+ const states = [];
823
+ const profiles = [...STATE_PROFILES, POSTAL_HALF_TOP, POSTAL_HALF_BOTTOM];
824
+ for (const { top, bottom } of barBounds) {
825
+ let best = -1;
826
+ let bestScore = Infinity;
827
+ for (let state = 0; state < profiles.length; state++) {
828
+ const profile = profiles[state];
829
+ const expectedTop = centre + (profile[0] - 4) * verticalUnit;
830
+ const expectedBottom = centre + (profile[1] - 4) * verticalUnit;
831
+ const score = Math.abs(top - expectedTop) + Math.abs(bottom - expectedBottom);
832
+ if (score < bestScore) {
833
+ bestScore = score;
834
+ best = state;
835
+ }
836
+ }
837
+ if (best < 0 || bestScore > Math.max(verticalUnit * 2.4, 1.5))
838
+ return null;
839
+ states.push(best);
840
+ }
841
+ return { states, unit, bounds: { x: runs[0].start, y: minY, width: runs[runs.length - 1].end - runs[0].start, height: maxY - minY } };
842
+ }
843
+ function canonicalFormat(value) {
844
+ return POSTAL_ALIASES[String(value).toLowerCase()] ?? null;
845
+ }
846
+ /** Decode a postal raster, returning only structurally complete symbols. */
847
+ export function decodePostal(image, options = {}) {
848
+ const bars = classifyBars(image);
849
+ if (!bars)
850
+ return [];
851
+ const requested = options.formats?.map(canonicalFormat).filter((format) => format !== null);
852
+ const formats = options.formats ? [...new Set(requested)] : POSTAL_FORMATS;
853
+ if (options.profile === 'camera') {
854
+ const leftQuiet = bars.bounds.x >= bars.unit * 2;
855
+ const rightQuiet = image.width - (bars.bounds.x + bars.bounds.width) >= bars.unit * 2;
856
+ if (!leftQuiet || !rightQuiet)
857
+ return [];
858
+ }
859
+ const candidates = [];
860
+ const orientations = [bars.states, [...bars.states].reverse()];
861
+ for (const states of orientations) {
862
+ for (const format of formats) {
863
+ let result = null;
864
+ if (format === 'postnet')
865
+ result = decodePostnetStates(states);
866
+ else if (format === 'planet')
867
+ result = decodePlanetStates(states);
868
+ else if (format === 'rm4scc')
869
+ result = decodeRMStates(states);
870
+ else if (format === 'kix')
871
+ result = decodeKIXStates(states);
872
+ else if (format === 'auspost')
873
+ result = decodeAustraliaStates(states, options);
874
+ else if (format === 'japanpost')
875
+ result = decodeJapanStates(states);
876
+ else if (format === 'imb')
877
+ result = decodeIMBStates(states);
878
+ if (result && !candidates.some((item) => item.format === result?.format && item.text === result?.text))
879
+ candidates.push(result);
880
+ }
881
+ // KIX and IMb have no directional guard. Once the native orientation has
882
+ // produced a validated result, the reversed pass would only add a second
883
+ // spelling of the same physical symbol.
884
+ if (candidates.length > 0)
885
+ break;
886
+ }
887
+ return candidates;
888
+ }
889
+ export { POSTAL_FORMATS, POSTAL_ALIASES, STATE_PROFILES };