@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,313 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ * Copyright (c) 2026 Sythos
6
+ * SPDX-License-Identifier: MIT
7
+ *
8
+ * Original work. No code from any other barcode implementation.
9
+ */
10
+
11
+ /** Code 16K decoder for sampled, axis-aligned module matrices. @module code16k/decoder */
12
+
13
+ import { FormatError } from '../core/errors.js';
14
+ import {
15
+ CODE16K_CODEWORD_WIDTH,
16
+ CODE16K_MAX_ROWS,
17
+ CODE16K_MIN_ROWS,
18
+ CODE16K_PAD,
19
+ CODE16K_SYMBOLS_PER_ROW,
20
+ code16kModeInfo,
21
+ code16kStartModules,
22
+ code16kStopModules,
23
+ type Code16KDecodeOptions,
24
+ } from './tables.js';
25
+ import {
26
+ code128ValueFromModules,
27
+ } from '../stacked128/common.js';
28
+ import type { Code16KMatrix } from './encoder.js';
29
+
30
+ export type Code16KRow = {
31
+ row: number;
32
+ values: number[];
33
+ };
34
+
35
+ export type Code16KDecodeResult = {
36
+ format: 'code16k';
37
+ text: string;
38
+ rows: number;
39
+ columns: number;
40
+ rowHeight: number;
41
+ separatorHeight: number;
42
+ mode: number;
43
+ codewords: number[];
44
+ checksum: true;
45
+ symbologyIdentifier: string;
46
+ gs1?: boolean;
47
+ fnc1AtStart?: boolean;
48
+ fnc1Positions?: number[];
49
+ };
50
+
51
+ function equalAt(source: string, offset: number, expected: string): boolean {
52
+ return source.slice(offset, offset + expected.length) === expected;
53
+ }
54
+
55
+ /** Decode one complete 70-module Code 16K row. */
56
+ export function decodeCode16KRow(modules: string, expectedRow: number | undefined = undefined): Code16KRow | null {
57
+ if (typeof modules !== 'string' || modules.length !== 70) return null;
58
+ const candidates = expectedRow === undefined
59
+ ? Array.from({ length: CODE16K_MAX_ROWS }, (_, index) => index)
60
+ : [expectedRow];
61
+ for (const row of candidates) {
62
+ if (!Number.isInteger(row) || row < 0 || row >= CODE16K_MAX_ROWS) continue;
63
+ if (!equalAt(modules, 0, code16kStartModules(row))) continue;
64
+ if (modules[7] !== '1') continue;
65
+ if (!equalAt(modules, 63, code16kStopModules(row))) continue;
66
+ const values: number[] = [];
67
+ let valid = true;
68
+ for (let column = 0; column < CODE16K_SYMBOLS_PER_ROW; column++) {
69
+ const value = code128ValueFromModules(modules, 8 + column * CODE16K_CODEWORD_WIDTH);
70
+ if (value === null) {
71
+ valid = false;
72
+ break;
73
+ }
74
+ values.push(value);
75
+ }
76
+ if (valid) return { row, values };
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function separatorIsDark(matrix: Code16KMatrix, y: number): boolean {
82
+ if (y < 0 || y >= matrix.height) return false;
83
+ for (let x = 0; x < matrix.width; x++) if (!matrix.get(x, y)) return false;
84
+ return true;
85
+ }
86
+
87
+ function readRow(matrix: Code16KMatrix, y: number): Code16KRow | null {
88
+ if (y < 0 || y >= matrix.height) return null;
89
+ let modules = '';
90
+ for (let x = 0; x < matrix.width; x++) modules += matrix.get(x, y) ? '1' : '0';
91
+ return decodeCode16KRow(modules);
92
+ }
93
+
94
+ function rowIsConsistent(
95
+ matrix: Code16KMatrix,
96
+ y: number,
97
+ rowHeight: number,
98
+ expected: Code16KRow,
99
+ ): boolean {
100
+ for (let offset = 0; offset < rowHeight; offset++) {
101
+ const parsed = readRow(matrix, y + offset);
102
+ if (!parsed || parsed.row !== expected.row ||
103
+ parsed.values.some((value, index) => value !== expected.values[index])) return false;
104
+ }
105
+ return true;
106
+ }
107
+
108
+ function code128StartForMode(mode: number): number {
109
+ if (mode === 0) return 103;
110
+ if (mode === 1 || mode === 3) return 104;
111
+ if (mode === 2 || mode === 4) return 105;
112
+ return -1;
113
+ }
114
+
115
+ function checksumOne(values: readonly number[]): number {
116
+ let sum = 0;
117
+ for (let index = 0; index < values.length; index++) sum += (index + 2) * values[index];
118
+ return sum % 107;
119
+ }
120
+
121
+ function checksumTwo(valuesThroughC1: readonly number[]): number {
122
+ let sum = 0;
123
+ for (let index = 0; index < valuesThroughC1.length; index++) {
124
+ sum += (index + 1) * valuesThroughC1[index];
125
+ }
126
+ return sum % 107;
127
+ }
128
+
129
+ function decodeRows(rows: readonly Code16KRow[], rowHeight: number, separatorHeight: number): Code16KDecodeResult {
130
+ if (rows.length < CODE16K_MIN_ROWS || rows.length > CODE16K_MAX_ROWS) {
131
+ throw new FormatError('Code 16K: row count must be in 2..16');
132
+ }
133
+ const first = rows[0];
134
+ if (first.row !== 0 || first.values.length !== CODE16K_SYMBOLS_PER_ROW) {
135
+ throw new FormatError('Code 16K: first row is missing or malformed');
136
+ }
137
+ const symbol = first.values[0];
138
+ if (!Number.isInteger(symbol) || symbol < 0 || symbol > 104) {
139
+ throw new FormatError('Code 16K: invalid S symbol');
140
+ }
141
+ const rowCount = Math.floor(symbol / 7) + 2;
142
+ const mode = symbol % 7;
143
+ if (rowCount !== rows.length || rowCount < CODE16K_MIN_ROWS || rowCount > CODE16K_MAX_ROWS) {
144
+ throw new FormatError('Code 16K: S symbol does not agree with the row count');
145
+ }
146
+ if (mode === 5 || mode === 6) {
147
+ throw new FormatError('Code 16K: shift modes 5 and 6 are not supported by this decoder');
148
+ }
149
+ const expectedRows = Array.from({ length: rowCount }, (_, index) => index);
150
+ for (let index = 0; index < rowCount; index++) {
151
+ if (rows[index].row !== expectedRows[index] || rows[index].values.length !== CODE16K_SYMBOLS_PER_ROW) {
152
+ throw new FormatError('Code 16K: rows are missing or out of order');
153
+ }
154
+ }
155
+
156
+ const codewords = rows.flatMap((entry) => entry.values);
157
+ if (codewords.length !== rowCount * CODE16K_SYMBOLS_PER_ROW) {
158
+ throw new FormatError('Code 16K: invalid codeword count');
159
+ }
160
+ const c1 = codewords[codewords.length - 2];
161
+ const c2 = codewords[codewords.length - 1];
162
+ const beforeChecks = codewords.slice(0, -2);
163
+ if (checksumOne(beforeChecks) !== c1 || checksumTwo([...beforeChecks, c1]) !== c2) {
164
+ throw new FormatError('Code 16K: check character mismatch');
165
+ }
166
+
167
+ const data = codewords.slice(1, -2);
168
+ while (data.length && data[data.length - 1] === CODE16K_PAD) data.pop();
169
+ if (!data.length) throw new FormatError('Code 16K: data region is empty');
170
+ const expectedStart = code128StartForMode(mode);
171
+ if (expectedStart < 0) throw new FormatError('Code 16K: unsupported mode');
172
+ const info = code16kModeInfo(mode as 0 | 1 | 2 | 3 | 4, mode === 3 || mode === 4);
173
+ const firstIsFnc1 = data[0] === 102;
174
+ if (info.gs1 !== firstIsFnc1) {
175
+ throw new FormatError('Code 16K: mode and leading FNC1 do not agree');
176
+ }
177
+
178
+ // Keep Code 16K aligned with ordinary Code 128 A/B/C decoding while the row
179
+ // framing and checks remain format-specific.
180
+ const decoded = decodeCode128ValuesLocal(expectedStart, data);
181
+ if (!decoded) throw new FormatError('Code 16K: invalid Code 128 data stream');
182
+ return {
183
+ format: 'code16k',
184
+ text: decoded.text,
185
+ rows: rowCount,
186
+ columns: CODE16K_SYMBOLS_PER_ROW,
187
+ rowHeight,
188
+ separatorHeight,
189
+ mode,
190
+ codewords,
191
+ checksum: true,
192
+ symbologyIdentifier: info.gs1 ? ']K1' : ']K0',
193
+ ...(info.gs1 ? { gs1: true } : {}),
194
+ ...(decoded.fnc1AtStart ? { fnc1AtStart: true } : {}),
195
+ ...(decoded.fnc1Positions?.length ? { fnc1Positions: decoded.fnc1Positions } : {}),
196
+ };
197
+ }
198
+
199
+ /* Keep the decoder self-contained if this module is consumed without the full common declaration graph. */
200
+ function decodeCode128ValuesLocal(
201
+ startCode: number,
202
+ dataValues: readonly number[],
203
+ ): { text: string; fnc1AtStart?: boolean; fnc1Positions?: number[] } | null {
204
+ if (![103, 104, 105].includes(startCode) || !dataValues.length) return null;
205
+ let mode: 'A' | 'B' | 'C' = startCode === 103 ? 'A' : startCode === 104 ? 'B' : 'C';
206
+ let shifted: 'A' | 'B' | null = null;
207
+ let text = '';
208
+ const fnc1AtStart = dataValues[0] === 102;
209
+ const fnc1Positions: number[] = [];
210
+ for (let index = 0; index < dataValues.length; index++) {
211
+ const value = dataValues[index];
212
+ const active = shifted ?? mode;
213
+ shifted = null;
214
+ if (value === 101 && mode !== 'A') { mode = 'A'; continue; }
215
+ if (value === 100 && mode !== 'B') { mode = 'B'; continue; }
216
+ if (value === 99) { mode = 'C'; continue; }
217
+ if (value === 98) { shifted = mode === 'A' ? 'B' : 'A'; continue; }
218
+ if (value === 102) {
219
+ if (index > 0) {
220
+ fnc1Positions.push(text.length);
221
+ text += '\x1d';
222
+ }
223
+ continue;
224
+ }
225
+ if (value >= 96 && value <= 102) continue;
226
+ if (active === 'C') {
227
+ if (value > 99) return null;
228
+ text += String(value).padStart(2, '0');
229
+ } else if (active === 'A') {
230
+ if (value > 95) return null;
231
+ text += value < 64 ? String.fromCharCode(value + 32) : String.fromCharCode(value - 64);
232
+ } else {
233
+ if (value > 95) return null;
234
+ text += String.fromCharCode(value + 32);
235
+ }
236
+ if (text.length > 100000) return null;
237
+ }
238
+ return text.length ? { text, ...(fnc1AtStart ? { fnc1AtStart: true } : {}), fnc1Positions } : null;
239
+ }
240
+
241
+ function candidateGeometry(matrix: Code16KMatrix, options: Code16KDecodeOptions): Array<{
242
+ rows: number;
243
+ rowHeight: number;
244
+ separatorHeight: number;
245
+ outer: boolean;
246
+ }> {
247
+ const metadata = matrix.code16k;
248
+ const requestedRows = options.rows ?? metadata?.rows;
249
+ const requestedRowHeight = options.rowHeight ?? metadata?.rowHeight;
250
+ const requestedSeparatorHeight = options.separatorHeight ?? metadata?.separatorHeight;
251
+ const candidates: Array<{ rows: number; rowHeight: number; separatorHeight: number; outer: boolean }> = [];
252
+ for (const outer of [true, false]) {
253
+ const innerHeight = matrix.height - (outer ? 2 : 0);
254
+ for (let rows = CODE16K_MIN_ROWS; rows <= CODE16K_MAX_ROWS; rows++) {
255
+ if (requestedRows !== undefined && rows !== requestedRows) continue;
256
+ for (let separatorHeight = requestedSeparatorHeight ?? 1; separatorHeight <= (requestedSeparatorHeight ?? 16); separatorHeight++) {
257
+ if (!Number.isInteger(separatorHeight) || separatorHeight < 1 || separatorHeight > 16) continue;
258
+ const numerator = innerHeight - (rows - 1) * separatorHeight;
259
+ if (numerator < rows || numerator % rows !== 0) continue;
260
+ const rowHeight = numerator / rows;
261
+ if (rowHeight < 1 || rowHeight > 128) continue;
262
+ if (requestedRowHeight !== undefined && rowHeight !== requestedRowHeight) continue;
263
+ candidates.push({ rows, rowHeight, separatorHeight, outer });
264
+ }
265
+ }
266
+ }
267
+ return candidates;
268
+ }
269
+
270
+ /**
271
+ * Decode a complete, axis-aligned Code 16K module matrix.
272
+ *
273
+ * The decoder re-checks row geometry, row identifiers, both modulo-107 check
274
+ * characters and the Code 128 data stream. Metadata attached by the encoder
275
+ * is used only as a geometry hint and never substitutes for validation.
276
+ */
277
+ export function decodeCode16K(
278
+ matrix: Code16KMatrix,
279
+ options: Code16KDecodeOptions = {},
280
+ ): Code16KDecodeResult {
281
+ if (!matrix?.width || !matrix?.height || typeof matrix.get !== 'function') {
282
+ throw new FormatError('Code 16K: matrix with width, height and get() is required');
283
+ }
284
+ if (matrix.width !== 70) throw new FormatError('Code 16K: matrix width must be 70 modules');
285
+ for (const geometry of candidateGeometry(matrix, options)) {
286
+ const first = readRow(matrix, geometry.outer ? 1 : 0);
287
+ if (!first || first.row !== 0) continue;
288
+ const encodedRows = Math.floor(first.values[0] / 7) + 2;
289
+ if (encodedRows !== geometry.rows) continue;
290
+ const rows: Code16KRow[] = [];
291
+ let valid = true;
292
+ for (let row = 0; row < geometry.rows; row++) {
293
+ const y = (geometry.outer ? 1 : 0) + row * (geometry.rowHeight + geometry.separatorHeight);
294
+ const parsed = readRow(matrix, y);
295
+ if (!parsed || parsed.row !== row) { valid = false; break; }
296
+ if (!rowIsConsistent(matrix, y, geometry.rowHeight, parsed)) { valid = false; break; }
297
+ rows.push(parsed);
298
+ if (row < geometry.rows - 1 && !separatorIsDark(matrix, y + geometry.rowHeight)) {
299
+ valid = false;
300
+ break;
301
+ }
302
+ }
303
+ if (!valid) continue;
304
+ if (geometry.outer && (!separatorIsDark(matrix, 0) ||
305
+ !separatorIsDark(matrix, matrix.height - 1))) continue;
306
+ try {
307
+ return decodeRows(rows, geometry.rowHeight, geometry.separatorHeight);
308
+ } catch {
309
+ // Try another legal geometry before rejecting the matrix.
310
+ }
311
+ }
312
+ throw new FormatError('Code 16K: no valid row geometry or checksum found');
313
+ }
@@ -0,0 +1,21 @@
1
+ /*! SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net) */
2
+ /*! SPDX-License-Identifier: MIT */
3
+ import type { BitMatrix } from '../core/bit-matrix.js';
4
+ import type { Code16KDecodeResult } from './decoder.js';
5
+ export type Code16KDetectOptions = {
6
+ rowHeight?: number;
7
+ separatorHeight?: number;
8
+ moduleSize?: number;
9
+ };
10
+ export type Code16KPoint = {
11
+ x: number;
12
+ y: number;
13
+ };
14
+ export type Code16KDetectedResult = Code16KDecodeResult & {
15
+ matrix: BitMatrix;
16
+ corners: Code16KPoint[];
17
+ rotation: 0 | 90 | 180 | 270;
18
+ moduleSize: number;
19
+ };
20
+ export declare function detectCode16K(binaryImage: BitMatrix, options?: Code16KDetectOptions): Code16KDetectedResult | null;
21
+ export declare function detectAndDecodeCode16K(binaryImage: BitMatrix, options?: Code16KDetectOptions): Code16KDetectedResult | null;
@@ -0,0 +1,203 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ * Copyright (c) 2026 Sythos
6
+ * SPDX-License-Identifier: MIT
7
+ *
8
+ * Original work. No code from any other barcode implementation.
9
+ */
10
+
11
+ /** Conservative integer-module Code 16K detector. @module code16k/detector */
12
+
13
+ import { BitMatrix } from '../core/bit-matrix.js';
14
+ import {
15
+ code128PatternModules,
16
+ renderStackedRows,
17
+ sampleModules,
18
+ scanStackedRows,
19
+ } from '../stacked128/common.js';
20
+ import { code16kStartModules, code16kStopModules } from './tables.js';
21
+ import { decodeCode16K, decodeCode16KRow } from './decoder.js';
22
+ import type { Code16KDecodeResult } from './decoder.js';
23
+
24
+ export type Code16KDetectOptions = {
25
+ rowHeight?: number;
26
+ separatorHeight?: number;
27
+ moduleSize?: number;
28
+ };
29
+
30
+ export type Code16KPoint = { x: number; y: number };
31
+
32
+ export type Code16KDetectedResult = Code16KDecodeResult & {
33
+ matrix: BitMatrix;
34
+ corners: Code16KPoint[];
35
+ rotation: 0 | 90 | 180 | 270;
36
+ moduleSize: number;
37
+ };
38
+
39
+ function rowModules(row: number, values: readonly number[]): string {
40
+ return code16kStartModules(row) + '1' + values.map((value) => code128PatternModules(value)).join('') +
41
+ code16kStopModules(row);
42
+ }
43
+
44
+ function rotateClockwise(source: BitMatrix): BitMatrix {
45
+ const rotated = new BitMatrix(source.height, source.width);
46
+ for (let y = 0; y < source.height; y++) {
47
+ for (let x = 0; x < source.width; x++) {
48
+ if (source.get(x, y)) rotated.set(source.height - 1 - y, x);
49
+ }
50
+ }
51
+ return rotated;
52
+ }
53
+
54
+ function cornersFor(x: number, y: number, width: number, height: number) {
55
+ return [
56
+ { x, y }, { x: x + width, y },
57
+ { x: x + width, y: y + height }, { x, y: y + height },
58
+ ];
59
+ }
60
+
61
+ function rowHasDarkPixels(image: BitMatrix, y: number, x: number, width: number): boolean {
62
+ if (y < 0 || y >= image.height) return false;
63
+ for (let offset = 0; offset < width; offset++) if (!image.get(x + offset, y)) return false;
64
+ return true;
65
+ }
66
+
67
+ function contiguousSpan(values: readonly number[]): number {
68
+ if (!values.length) return 0;
69
+ const sorted = [...new Set(values)].sort((a, b) => a - b);
70
+ let best = 1;
71
+ let current = 1;
72
+ for (let index = 1; index < sorted.length; index++) {
73
+ if (sorted[index] === sorted[index - 1] + 1) current++;
74
+ else { best = Math.max(best, current); current = 1; }
75
+ }
76
+ return Math.max(best, current);
77
+ }
78
+
79
+ function parseSample(row: Uint8Array, start: number, moduleSize: number) {
80
+ const modules = sampleModules(row, start, moduleSize, 70);
81
+ if (!modules) return null;
82
+ const parsed = decodeCode16KRow(modules);
83
+ return parsed ? { ...parsed, modules } : null;
84
+ }
85
+
86
+ function detectOriented(image: BitMatrix, options: Code16KDetectOptions) {
87
+ const requestedScale = options.moduleSize;
88
+ if (requestedScale !== undefined &&
89
+ (!Number.isInteger(requestedScale) || requestedScale < 1 || requestedScale > 32)) return null;
90
+ const maxModules = 70 * (requestedScale ?? 32);
91
+ const found = scanStackedRows(image, 70, parseSample, maxModules)
92
+ .filter((candidate) => requestedScale === undefined || candidate.moduleSize === requestedScale);
93
+ const groups = new Map<string, {
94
+ x: number;
95
+ moduleSize: number;
96
+ rows: Map<number, Array<{ y: number; parsed: { row: number; values: number[]; modules: string } }>>;
97
+ }>();
98
+ for (const candidate of found) {
99
+ const key = `${candidate.x}:${candidate.moduleSize}`;
100
+ let group = groups.get(key);
101
+ if (!group) {
102
+ group = { x: candidate.x, moduleSize: candidate.moduleSize, rows: new Map() };
103
+ groups.set(key, group);
104
+ }
105
+ let values = group.rows.get(candidate.parsed.row);
106
+ if (!values) { values = []; group.rows.set(candidate.parsed.row, values); }
107
+ values.push({ y: candidate.y, parsed: candidate.parsed });
108
+ }
109
+
110
+ for (const group of groups.values()) {
111
+ const firstCandidates = group.rows.get(0);
112
+ if (!firstCandidates?.length) continue;
113
+ for (const first of firstCandidates) {
114
+ const encodedRows = Math.floor(first.parsed.values[0] / 7) + 2;
115
+ if (encodedRows < 2 || encodedRows > 16 || group.rows.size !== encodedRows) continue;
116
+ const chosen: Array<{ y: number; parsed: { row: number; values: number[]; modules: string } }> = [];
117
+ let complete = true;
118
+ for (let row = 0; row < encodedRows; row++) {
119
+ const entries = group.rows.get(row);
120
+ if (!entries?.length) { complete = false; break; }
121
+ const sorted = entries.slice().sort((a, b) => a.y - b.y);
122
+ chosen.push({ y: sorted[0].y, parsed: sorted[0].parsed });
123
+ }
124
+ if (!complete) continue;
125
+ const scale = group.moduleSize;
126
+ const spans = chosen.map((entry) => {
127
+ const entries = group.rows.get(entry.parsed.row) ?? [];
128
+ return contiguousSpan(entries.map((candidate) => candidate.y));
129
+ });
130
+ if (!spans.every((span) => span === spans[0]) || spans[0] < scale || spans[0] % scale !== 0) continue;
131
+ const rowHeight = spans[0] / scale;
132
+ const firstY = chosen[0].y;
133
+ const steps = chosen.slice(1).map((entry, index) => entry.y - chosen[index].y);
134
+ if (!steps.length || !steps.every((step) => step === steps[0]) || steps[0] % scale !== 0) continue;
135
+ const separatorHeight = steps[0] / scale - rowHeight;
136
+ if (separatorHeight < 1 || separatorHeight > 16) continue;
137
+ if (options.rowHeight !== undefined && rowHeight !== options.rowHeight) continue;
138
+ if (options.separatorHeight !== undefined && separatorHeight !== options.separatorHeight) continue;
139
+ const modules = chosen.map((entry) => rowModules(entry.parsed.row, entry.parsed.values));
140
+ let matrix;
141
+ try {
142
+ matrix = renderStackedRows(modules, rowHeight, separatorHeight);
143
+ const result = decodeCode16K(matrix, { rowHeight, separatorHeight, rows: encodedRows });
144
+ const topSeparator = rowHasDarkPixels(image, firstY - scale, group.x, 70 * scale) ? scale : 0;
145
+ const last = chosen[chosen.length - 1];
146
+ const bottomY = last.y + spans[spans.length - 1];
147
+ const bottomSeparator = rowHasDarkPixels(image, bottomY, group.x, 70 * scale) ? scale : 0;
148
+ const boundsY = firstY - topSeparator;
149
+ const boundsHeight = (bottomY + bottomSeparator) - boundsY;
150
+ return {
151
+ ...result,
152
+ matrix,
153
+ corners: cornersFor(group.x, boundsY, 70 * scale, boundsHeight),
154
+ rotation: 0,
155
+ moduleSize: scale,
156
+ };
157
+ } catch {
158
+ // A candidate is accepted only after all rows and checks decode.
159
+ }
160
+ }
161
+ }
162
+ return null;
163
+ }
164
+
165
+ /**
166
+ * Detect and decode one Code 16K symbol from a binarized image.
167
+ *
168
+ * The detector is intentionally conservative and enumerates integer module
169
+ * scales and the four orthogonal orientations. Non-integer perspective and
170
+ * photographic threshold recovery remain the responsibility of the generic
171
+ * image pipeline.
172
+ */
173
+ export function detectCode16K(
174
+ binaryImage: BitMatrix,
175
+ options: Code16KDetectOptions = {},
176
+ ): Code16KDetectedResult | null {
177
+ if (!binaryImage?.width || !binaryImage?.height || typeof binaryImage.get !== 'function') return null;
178
+ let oriented = binaryImage;
179
+ let toOriginal = (point: { x: number; y: number }) => ({ x: point.x, y: point.y });
180
+ for (let rotation = 0; rotation < 4; rotation++) {
181
+ const found = detectOriented(oriented, options);
182
+ if (found) {
183
+ return {
184
+ ...found,
185
+ corners: found.corners.map(toOriginal),
186
+ rotation: rotation * 90 as 0 | 90 | 180 | 270,
187
+ };
188
+ }
189
+ const previous = oriented;
190
+ const previousToOriginal = toOriginal;
191
+ oriented = rotateClockwise(previous);
192
+ toOriginal = (point) => previousToOriginal({ x: point.y, y: previous.height - point.x });
193
+ }
194
+ return null;
195
+ }
196
+
197
+ /** Alias matching the detector naming used by the other format modules. */
198
+ export function detectAndDecodeCode16K(
199
+ binaryImage: BitMatrix,
200
+ options: Code16KDetectOptions = {},
201
+ ): Code16KDetectedResult | null {
202
+ return detectCode16K(binaryImage, options);
203
+ }
@@ -0,0 +1,26 @@
1
+ /*! SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net) */
2
+ /*! SPDX-License-Identifier: MIT */
3
+ import { BitMatrix } from '../core/bit-matrix.js';
4
+ import type { Code16KEncodeOptions } from './tables.js';
5
+ export type Code16KMetadata = {
6
+ format: 'code16k';
7
+ rows: number;
8
+ columns: number;
9
+ rowHeight: number;
10
+ separatorHeight: number;
11
+ mode: number;
12
+ codewords: number[];
13
+ checksum: true;
14
+ };
15
+ export type Code16KMatrix = BitMatrix & {
16
+ code16k?: Code16KMetadata;
17
+ };
18
+ export declare function encodeCode16K(value: string | Uint8Array | readonly number[], options?: Code16KEncodeOptions): Code16KMatrix;
19
+ export declare function code16kDimensions(value: string | Uint8Array | readonly number[], options?: Code16KEncodeOptions): {
20
+ rows: number;
21
+ columns: number;
22
+ width: number;
23
+ height: number;
24
+ rowHeight: number;
25
+ separatorHeight: number;
26
+ };