@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,208 @@
1
+ /*
2
+ * Sythos Barcode Suite — shared stacked Code 128 helpers
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
+ import { BitMatrix } from '../core/bit-matrix.js';
11
+ import { EncodeError } from '../core/errors.js';
12
+ import { CODE128, CODE128_START_A, CODE128_START_B, CODE128_START_C, CODE128_FNC1, CODE128_CODE_A, CODE128_CODE_B, CODE128_CODE_C, CODE128_SHIFT, } from '../oned/patterns.js';
13
+ /** Expand a Code 128 width string into dark/light modules. */
14
+ export function code128PatternModules(value) {
15
+ if (!Number.isInteger(value) || value < 0 || value >= CODE128.length) {
16
+ throw new EncodeError(`Code 128: invalid symbol value ${value}`);
17
+ }
18
+ const pattern = CODE128[value];
19
+ let modules = '';
20
+ let dark = true;
21
+ for (const digit of pattern) {
22
+ const width = Number(digit);
23
+ modules += (dark ? '1' : '0').repeat(width);
24
+ dark = !dark;
25
+ }
26
+ return modules;
27
+ }
28
+ /** Render complete Code 128 codewords, including their 13-module stop. */
29
+ export function code128CodewordsModules(values) {
30
+ return values.map((value) => code128PatternModules(value)).join('');
31
+ }
32
+ /** Code 128 checksum for a sequence that includes the start symbol. */
33
+ export function code128Checksum(values) {
34
+ if (values.length === 0)
35
+ throw new EncodeError('Code 128: checksum needs a start symbol');
36
+ let sum = values[0];
37
+ for (let i = 1; i < values.length; i++)
38
+ sum += values[i] * i;
39
+ return sum % 103;
40
+ }
41
+ const CODE128_BITS = new Map();
42
+ for (let value = 0; value < CODE128.length; value++) {
43
+ CODE128_BITS.set(code128PatternModules(value), value);
44
+ }
45
+ /** Decode a single 11-module Code 128 symbol from a sampled module string. */
46
+ export function code128ValueFromModules(modules, offset) {
47
+ const slice = modules.slice(offset, offset + 11);
48
+ if (slice.length !== 11)
49
+ return null;
50
+ return CODE128_BITS.get(slice) ?? null;
51
+ }
52
+ /**
53
+ * Interpret Code 128 data values after the start symbol and checksum.
54
+ *
55
+ * This deliberately mirrors the public Code 128 reader but keeps stacked
56
+ * decoders independent from scanline state. The result is null for an empty
57
+ * or structurally impossible stream.
58
+ */
59
+ export function decodeCode128Values(startCode, dataValues) {
60
+ if (![CODE128_START_A, CODE128_START_B, CODE128_START_C].includes(startCode))
61
+ return null;
62
+ if (dataValues.length === 0)
63
+ return null;
64
+ let mode = startCode === CODE128_START_A
65
+ ? 'A' : startCode === CODE128_START_B ? 'B' : 'C';
66
+ let shifted = null;
67
+ let text = '';
68
+ const fnc1AtStart = dataValues[0] === CODE128_FNC1;
69
+ const fnc1Positions = [];
70
+ for (let index = 0; index < dataValues.length; index++) {
71
+ const value = dataValues[index];
72
+ const active = shifted ?? mode;
73
+ shifted = null;
74
+ if (value === CODE128_CODE_A && mode !== 'A') {
75
+ mode = 'A';
76
+ continue;
77
+ }
78
+ if (value === CODE128_CODE_B && mode !== 'B') {
79
+ mode = 'B';
80
+ continue;
81
+ }
82
+ if (value === CODE128_CODE_C) {
83
+ mode = 'C';
84
+ continue;
85
+ }
86
+ if (value === CODE128_SHIFT) {
87
+ shifted = mode === 'A' ? 'B' : 'A';
88
+ continue;
89
+ }
90
+ if (value === CODE128_FNC1) {
91
+ if (index > 0) {
92
+ fnc1Positions.push(text.length);
93
+ text += '\x1d';
94
+ }
95
+ continue;
96
+ }
97
+ // FNC2, FNC3 and FNC4 are control values. They are not part of the
98
+ // textual payload exposed by this SDK, but remain valid Code 128 symbols.
99
+ if (value >= 96 && value <= 102)
100
+ continue;
101
+ if (active === 'C') {
102
+ if (value > 99)
103
+ return null;
104
+ text += String(value).padStart(2, '0');
105
+ }
106
+ else if (active === 'A') {
107
+ if (value > 95)
108
+ return null;
109
+ text += value < 64 ? String.fromCharCode(value + 32) : String.fromCharCode(value - 64);
110
+ }
111
+ else {
112
+ if (value > 95)
113
+ return null;
114
+ text += String.fromCharCode(value + 32);
115
+ }
116
+ if (text.length > 100000)
117
+ return null;
118
+ }
119
+ if (text.length === 0)
120
+ return null;
121
+ return fnc1AtStart
122
+ ? { text, fnc1AtStart: true, fnc1Positions }
123
+ : { text };
124
+ }
125
+ /** Sample one module at the centre of every integer-sized module. */
126
+ export function sampleModules(row, start, moduleSize, count) {
127
+ if (!Number.isInteger(moduleSize) || moduleSize < 1 || start < 0)
128
+ return null;
129
+ const end = start + moduleSize * count;
130
+ if (end > row.length)
131
+ return null;
132
+ let modules = '';
133
+ const centre = Math.floor((moduleSize - 1) / 2);
134
+ for (let index = 0; index < count; index++) {
135
+ modules += row[start + index * moduleSize + centre] ? '1' : '0';
136
+ }
137
+ return modules;
138
+ }
139
+ /**
140
+ * Scan a binarized image for stacked symbols with integer module scaling.
141
+ * The parser receives every plausible dark-run origin and can try one or more
142
+ * geometry variants. A bounded scale keeps camera frames predictable.
143
+ */
144
+ export function scanStackedRows(image, minimumModules, parser, maximumModules = 900) {
145
+ const candidates = [];
146
+ const rowBuffer = new Uint8Array(image.width);
147
+ const maximumScale = Math.min(32, Math.max(1, Math.floor(maximumModules / Math.max(1, minimumModules))));
148
+ for (let y = 0; y < image.height; y++) {
149
+ const row = image.getRow(y, rowBuffer);
150
+ for (let start = 0; start < row.length; start++) {
151
+ if (!row[start] || (start > 0 && row[start - 1]))
152
+ continue;
153
+ for (let moduleSize = 1; moduleSize <= maximumScale; moduleSize++) {
154
+ if (start + minimumModules * moduleSize > row.length)
155
+ break;
156
+ const parsed = parser(row, start, moduleSize);
157
+ if (parsed !== null)
158
+ candidates.push({ x: start, y, moduleSize, parsed });
159
+ }
160
+ }
161
+ }
162
+ return candidates;
163
+ }
164
+ /** Paint a set of row module strings with separator bars and quiet rows. */
165
+ export function renderStackedRows(rowModules, rowHeight, separatorHeight) {
166
+ if (!Number.isInteger(rowHeight) || rowHeight < 1 || rowHeight > 128) {
167
+ throw new EncodeError('Stacked Code 128: rowHeight must be an integer in 1..128');
168
+ }
169
+ if (!Number.isInteger(separatorHeight) || separatorHeight < 1 || separatorHeight > 16) {
170
+ throw new EncodeError('Stacked Code 128: separatorHeight must be an integer in 1..16');
171
+ }
172
+ if (rowModules.length < 1)
173
+ throw new EncodeError('Stacked Code 128: at least one row is required');
174
+ const width = rowModules[0].length;
175
+ if (width < 1 || rowModules.some((modules) => modules.length !== width)) {
176
+ throw new EncodeError('Stacked Code 128: rows must have equal module widths');
177
+ }
178
+ const height = 2 + rowModules.length * rowHeight + (rowModules.length - 1) * separatorHeight;
179
+ const matrix = new BitMatrix(width, height);
180
+ matrix.setRegion(0, 0, width, 1);
181
+ matrix.setRegion(0, height - 1, width, 1);
182
+ let y = 1;
183
+ for (let row = 0; row < rowModules.length; row++) {
184
+ const modules = rowModules[row];
185
+ for (let x = 0; x < width; x++) {
186
+ if (modules[x] === '1')
187
+ matrix.setRegion(x, y, 1, rowHeight);
188
+ }
189
+ y += rowHeight;
190
+ if (row < rowModules.length - 1) {
191
+ matrix.setRegion(0, y, width, separatorHeight);
192
+ y += separatorHeight;
193
+ }
194
+ }
195
+ return matrix;
196
+ }
197
+ /** Build an alternating-width pattern, starting with dark or light. */
198
+ export function alternatingWidths(widths, startsDark) {
199
+ let modules = '';
200
+ let dark = startsDark;
201
+ for (const width of widths) {
202
+ if (!Number.isInteger(width) || width < 1)
203
+ throw new EncodeError('Stacked barcode: invalid guard width');
204
+ modules += (dark ? '1' : '0').repeat(width);
205
+ dark = !dark;
206
+ }
207
+ return modules;
208
+ }
@@ -0,0 +1,212 @@
1
+ /*
2
+ * Sythos Barcode Suite — Codablock-F reader
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
+ import { BitMatrix } from '../core/bit-matrix.js';
12
+ import { FormatError } from '../core/errors.js';
13
+ import {
14
+ code128Checksum,
15
+ code128CodewordsModules,
16
+ code128ValueFromModules,
17
+ decodeCode128Values,
18
+ sampleModules,
19
+ scanStackedRows,
20
+ } from '../stacked128/common.js';
21
+ import { codablockFChecks } from './encoder.js';
22
+
23
+ const START_A = 103;
24
+ const CODE_B = 100;
25
+ const STOP = 106;
26
+ const PAD = 103;
27
+ const MIN_COLUMNS = 4;
28
+ const MAX_COLUMNS = 62;
29
+
30
+ export interface CodablockFDecodeResult {
31
+ readonly format: 'codablockf';
32
+ readonly text: string;
33
+ readonly rows: number;
34
+ readonly columns: number;
35
+ readonly moduleSize: number;
36
+ readonly checksum: true;
37
+ }
38
+
39
+ interface ParsedRow {
40
+ readonly indicator: number;
41
+ readonly data: number[];
42
+ }
43
+
44
+ interface CodablockFMetadataHint {
45
+ readonly rows: number;
46
+ readonly columns: number;
47
+ readonly rowHeight: number;
48
+ readonly separatorHeight: number;
49
+ }
50
+
51
+ function parseRow(row: Uint8Array, start: number, moduleSize: number, columns: number): ParsedRow | null {
52
+ const count = columns + 4;
53
+ const moduleCount = 11 * count + 13;
54
+ const modules = sampleModules(row, start, moduleSize, moduleCount);
55
+ if (!modules) return null;
56
+ let offset = 0;
57
+ const startValue = code128ValueFromModules(modules, offset);
58
+ if (startValue !== START_A) return null;
59
+ offset += 11;
60
+ const selector = code128ValueFromModules(modules, offset);
61
+ if (selector !== CODE_B) return null;
62
+ offset += 11;
63
+ const indicator = code128ValueFromModules(modules, offset);
64
+ if (indicator === null || indicator < 1 || indicator > 44) return null;
65
+ offset += 11;
66
+ const data: number[] = [];
67
+ for (let i = 0; i < columns; i++) {
68
+ const value = code128ValueFromModules(modules, offset);
69
+ if (value === null || value === STOP) return null;
70
+ data.push(value);
71
+ offset += 11;
72
+ }
73
+ const rowCheck = code128ValueFromModules(modules, offset);
74
+ if (rowCheck === null || rowCheck >= 103) return null;
75
+ const withoutRowCheck = [START_A, CODE_B, indicator, ...data];
76
+ if (code128Checksum(withoutRowCheck) !== rowCheck) return null;
77
+ offset += 11;
78
+ const stop = modules.slice(offset, offset + 13);
79
+ if (stop !== code128CodewordsModules([STOP])) return null;
80
+ return { indicator, data };
81
+ }
82
+
83
+ function rowBits(matrix: BitMatrix, y: number): string {
84
+ let bits = '';
85
+ for (let x = 0; x < matrix.width; x++) bits += matrix.get(x, y) ? '1' : '0';
86
+ return bits;
87
+ }
88
+
89
+ function allDark(matrix: BitMatrix, y: number): boolean {
90
+ if (y < 0 || y >= matrix.height) return false;
91
+ for (let x = 0; x < matrix.width; x++) if (!matrix.get(x, y)) return false;
92
+ return true;
93
+ }
94
+
95
+ function decodeRows(
96
+ rowsByNumber: readonly ParsedRow[],
97
+ rows: number,
98
+ columns: number,
99
+ moduleSize: number,
100
+ ): CodablockFDecodeResult | null {
101
+ if (rowsByNumber.length !== rows || rows < 2 || rows > 44) return null;
102
+ const stream = rowsByNumber.flatMap((entry) => entry.data);
103
+ if (stream.length < 2) return null;
104
+ const checks = codablockFChecks(stream.slice(0, -2));
105
+ if (stream[stream.length - 2] !== checks[0] || stream[stream.length - 1] !== checks[1]) return null;
106
+ const body = stream.slice(0, -2);
107
+ while (body.length > 0 && body[body.length - 1] === PAD) body.pop();
108
+ const decoded = decodeCode128Values(104, body);
109
+ if (!decoded) return null;
110
+ return {
111
+ format: 'codablockf',
112
+ text: decoded.text,
113
+ rows,
114
+ columns,
115
+ moduleSize,
116
+ checksum: true,
117
+ };
118
+ }
119
+
120
+ /** Validate an encoder-shaped matrix row by row, including every row-height pixel. */
121
+ function decodeCanonical(matrix: BitMatrix, metadata: CodablockFMetadataHint): CodablockFDecodeResult | null {
122
+ const { rows, columns, rowHeight, separatorHeight } = metadata;
123
+ if (!Number.isInteger(rows) || rows < 2 || rows > 44 ||
124
+ !Number.isInteger(columns) || columns < MIN_COLUMNS || columns > MAX_COLUMNS) return null;
125
+ const expectedWidth = 11 * (columns + 4) + 13;
126
+ const expectedHeight = 2 + rows * rowHeight + (rows - 1) * separatorHeight;
127
+ if (matrix.width !== expectedWidth || matrix.height !== expectedHeight) return null;
128
+ if (!allDark(matrix, 0) || !allDark(matrix, matrix.height - 1)) return null;
129
+ const parsedRows: ParsedRow[] = [];
130
+ for (let row = 0; row < rows; row++) {
131
+ const y = 1 + row * (rowHeight + separatorHeight);
132
+ const source = rowBits(matrix, y);
133
+ const parsed = parseRow(Uint8Array.from(source, (bit) => bit === '1' ? 1 : 0), 0, 1, columns);
134
+ if (!parsed || parsed.indicator !== (row === 0 ? rows : row)) return null;
135
+ for (let offset = 1; offset < rowHeight; offset++) {
136
+ if (rowBits(matrix, y + offset) !== source) return null;
137
+ }
138
+ parsedRows.push(parsed);
139
+ if (row < rows - 1) {
140
+ for (let offset = 0; offset < separatorHeight; offset++) {
141
+ if (!allDark(matrix, y + rowHeight + offset)) return null;
142
+ }
143
+ }
144
+ }
145
+ return decodeRows(parsedRows, rows, columns, 1);
146
+ }
147
+
148
+ function assemble(image: BitMatrix): CodablockFDecodeResult | null {
149
+ const minimumModules = 11 * (MIN_COLUMNS + 4) + 13;
150
+ const candidates = scanStackedRows(image, minimumModules, (row, start, moduleSize) => {
151
+ for (let columns = MIN_COLUMNS; columns <= MAX_COLUMNS; columns++) {
152
+ const parsed = parseRow(row, start, moduleSize, columns);
153
+ if (parsed) return { columns, ...parsed };
154
+ }
155
+ return null;
156
+ }, 2200);
157
+
158
+ const groups = new Map<string, Map<number, { columns: number; parsed: ParsedRow; moduleSize: number }>>();
159
+ for (const candidate of candidates) {
160
+ const signature = `${candidate.x}:${candidate.moduleSize}`;
161
+ const group = groups.get(signature) ?? new Map();
162
+ const current = group.get(candidate.parsed.indicator);
163
+ // Rows are repeated vertically; keeping one exact candidate per indicator
164
+ // prevents rowHeight from multiplying the work without losing a symbol.
165
+ if (!current) group.set(candidate.parsed.indicator, {
166
+ columns: candidate.parsed.columns,
167
+ parsed: candidate.parsed,
168
+ moduleSize: candidate.moduleSize,
169
+ });
170
+ groups.set(signature, group);
171
+ }
172
+
173
+ for (const group of groups.values()) {
174
+ for (const [rows, first] of group) {
175
+ if (rows < 2 || rows > 44) continue;
176
+ const columns = first.parsed.columns;
177
+ const rowsByNumber: ParsedRow[] = [];
178
+ let complete = true;
179
+ for (let row = 1; row < rows; row++) {
180
+ const found = group.get(row);
181
+ if (!found || found.columns !== columns) { complete = false; break; }
182
+ rowsByNumber.push(found.parsed);
183
+ }
184
+ if (!complete) continue;
185
+ rowsByNumber.unshift(first.parsed);
186
+ const decoded = decodeRows(rowsByNumber, rows, columns, first.moduleSize);
187
+ if (decoded) return decoded;
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+
193
+ /** Decode a Codablock-F matrix or throw when its structure is invalid. */
194
+ export function decodeCodablockF(matrix: BitMatrix): CodablockFDecodeResult {
195
+ const metadata = (matrix as BitMatrix & { codablockf?: CodablockFMetadataHint }).codablockf;
196
+ if (metadata) {
197
+ const canonical = decodeCanonical(matrix, metadata);
198
+ if (canonical) return canonical;
199
+ throw new FormatError('Codablock-F: invalid rows, checks or Code 128 framing');
200
+ }
201
+ const result = assemble(matrix);
202
+ if (!result) throw new FormatError('Codablock-F: invalid rows, checks or Code 128 framing');
203
+ return result;
204
+ }
205
+
206
+ /** Locate and decode one Codablock-F symbol in a binarized raster. */
207
+ export function detectCodablockF(image: BitMatrix): CodablockFDecodeResult | null {
208
+ return assemble(image);
209
+ }
210
+
211
+ /** Alias used by the generic image decoder. */
212
+ export const detectAndDecodeCodablockF = detectCodablockF;
@@ -0,0 +1,128 @@
1
+ /*
2
+ * Sythos Barcode Suite — Codablock-F writer
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
+ import { BitMatrix } from '../core/bit-matrix.js';
12
+ import { EncodeError } from '../core/errors.js';
13
+ import { code128DataCodewords } from '../oned/writers.js';
14
+ import {
15
+ code128Checksum,
16
+ code128CodewordsModules,
17
+ renderStackedRows,
18
+ } from '../stacked128/common.js';
19
+
20
+ export interface CodablockFEncodeOptions {
21
+ /** Number of stacked rows, from 2 through 44. */
22
+ rows?: number;
23
+ /** Data symbols per row, from 4 through 62. */
24
+ columns?: number;
25
+ /** Height of each row in modules. */
26
+ rowHeight?: number;
27
+ /** Height of the separator bars in modules. */
28
+ separatorHeight?: number;
29
+ }
30
+
31
+ export interface CodablockFMatrixMetadata {
32
+ readonly rows: number;
33
+ readonly columns: number;
34
+ readonly rowHeight: number;
35
+ readonly separatorHeight: number;
36
+ readonly checks: readonly [number, number];
37
+ }
38
+
39
+ const START_A = 103;
40
+ const CODE_B = 100;
41
+ const PAD = 103;
42
+ const MIN_ROWS = 2;
43
+ const MAX_ROWS = 44;
44
+ const MIN_COLUMNS = 4;
45
+ const MAX_COLUMNS = 62;
46
+
47
+ /** Weighted modulo-86 K1/K2 checks used by the complete Codablock stream. */
48
+ export function codablockFChecks(values: readonly number[]): [number, number] {
49
+ let k1 = 0;
50
+ let k2 = 0;
51
+ for (let i = 0; i < values.length; i++) {
52
+ k1 = (k1 + (i + 1) * values[i]) % 86;
53
+ k2 = (k2 + (i + 2) * values[i]) % 86;
54
+ }
55
+ k2 = (k2 + k1) % 86;
56
+ return [k1, k2];
57
+ }
58
+
59
+ function validateDimensions(rows: number | undefined, columns: number | undefined) {
60
+ if (rows !== undefined && (!Number.isInteger(rows) || rows < MIN_ROWS || rows > MAX_ROWS)) {
61
+ throw new EncodeError(`Codablock-F: rows must be an integer in ${MIN_ROWS}..${MAX_ROWS}`);
62
+ }
63
+ if (columns !== undefined && (!Number.isInteger(columns) || columns < MIN_COLUMNS || columns > MAX_COLUMNS)) {
64
+ throw new EncodeError(`Codablock-F: columns must be an integer in ${MIN_COLUMNS}..${MAX_COLUMNS}`);
65
+ }
66
+ }
67
+
68
+ function chooseDimensions(length: number, rowsOption?: number, columnsOption?: number) {
69
+ validateDimensions(rowsOption, columnsOption);
70
+ const candidates: { rows: number; columns: number; score: number }[] = [];
71
+ for (let rows = rowsOption ?? MIN_ROWS; rows <= (rowsOption ?? MAX_ROWS); rows++) {
72
+ for (let columns = columnsOption ?? MIN_COLUMNS; columns <= (columnsOption ?? MAX_COLUMNS); columns++) {
73
+ if (rows * columns - 2 < length) continue;
74
+ // Keep the default reasonably compact while favouring a readable width.
75
+ const ratio = (columns + 4) / rows;
76
+ const score = (rows * columns - 2 - length) * 10 + Math.abs(ratio - 2.5);
77
+ candidates.push({ rows, columns, score });
78
+ }
79
+ }
80
+ if (candidates.length === 0) {
81
+ throw new EncodeError('Codablock-F: payload does not fit the requested rows and columns');
82
+ }
83
+ candidates.sort((a, b) => a.score - b.score || a.rows - b.rows || a.columns - b.columns);
84
+ return candidates[0];
85
+ }
86
+
87
+ /** Encode an ASCII payload as a conservative, standards-shaped Codablock-F symbol. */
88
+ export function encodeCodablockF(value: string, options: CodablockFEncodeOptions = {}): BitMatrix {
89
+ if (typeof value !== 'string' || value.length === 0) {
90
+ throw new EncodeError('Codablock-F: payload must be a non-empty ASCII string');
91
+ }
92
+ const dataStream = code128DataCodewords(value, { startSet: 'B' });
93
+ const { rows, columns } = chooseDimensions(dataStream.values.length, options.rows, options.columns);
94
+ const rowHeight = options.rowHeight ?? 3;
95
+ const separatorHeight = options.separatorHeight ?? 1;
96
+ if (!Number.isInteger(rowHeight) || rowHeight < 1 || rowHeight > 128) {
97
+ throw new EncodeError('Codablock-F: rowHeight must be an integer in 1..128');
98
+ }
99
+ if (!Number.isInteger(separatorHeight) || separatorHeight < 1 || separatorHeight > 16) {
100
+ throw new EncodeError('Codablock-F: separatorHeight must be an integer in 1..16');
101
+ }
102
+
103
+ const slotCount = rows * columns;
104
+ const body = dataStream.values.slice();
105
+ while (body.length < slotCount - 2) body.push(PAD);
106
+ const checks = codablockFChecks(body);
107
+ body.push(checks[0], checks[1]);
108
+
109
+ const rowModules: string[] = [];
110
+ for (let row = 0; row < rows; row++) {
111
+ const indicator = row === 0 ? rows : row;
112
+ const data = body.slice(row * columns, (row + 1) * columns);
113
+ const withoutRowCheck = [START_A, CODE_B, indicator, ...data];
114
+ const rowCheck = code128Checksum(withoutRowCheck);
115
+ const codewords = [...withoutRowCheck, rowCheck, 106];
116
+ rowModules.push(code128CodewordsModules(codewords));
117
+ }
118
+
119
+ const matrix = renderStackedRows(rowModules, rowHeight, separatorHeight);
120
+ (matrix as BitMatrix & { codablockf?: CodablockFMatrixMetadata }).codablockf = {
121
+ rows,
122
+ columns,
123
+ rowHeight,
124
+ separatorHeight,
125
+ checks,
126
+ };
127
+ return matrix;
128
+ }
@@ -0,0 +1,31 @@
1
+ import type { BitMatrix } from '../core/bit-matrix.js';
2
+
3
+ export interface CodablockFEncodeOptions {
4
+ rows?: number;
5
+ columns?: number;
6
+ rowHeight?: number;
7
+ separatorHeight?: number;
8
+ }
9
+
10
+ export interface CodablockFMatrixMetadata {
11
+ readonly rows: number;
12
+ readonly columns: number;
13
+ readonly rowHeight: number;
14
+ readonly separatorHeight: number;
15
+ readonly checks: readonly [number, number];
16
+ }
17
+
18
+ export interface CodablockFDecodeResult {
19
+ readonly format: 'codablockf';
20
+ readonly text: string;
21
+ readonly rows: number;
22
+ readonly columns: number;
23
+ readonly moduleSize: number;
24
+ readonly checksum: true;
25
+ }
26
+
27
+ export declare function encodeCodablockF(value: string, options?: CodablockFEncodeOptions): BitMatrix;
28
+ export declare function codablockFChecks(values: readonly number[]): [number, number];
29
+ export declare function decodeCodablockF(matrix: BitMatrix): CodablockFDecodeResult;
30
+ export declare function detectCodablockF(image: BitMatrix): CodablockFDecodeResult | null;
31
+ export declare const detectAndDecodeCodablockF: typeof detectCodablockF;
@@ -0,0 +1,6 @@
1
+ /* Codablock-F public entry points. */
2
+
3
+ export { encodeCodablockF, codablockFChecks } from './encoder.js';
4
+ export type { CodablockFEncodeOptions, CodablockFMatrixMetadata } from './encoder.js';
5
+ export { decodeCodablockF, detectCodablockF, detectAndDecodeCodablockF } from './decoder.js';
6
+ export type { CodablockFDecodeResult } from './decoder.js';
@@ -0,0 +1,25 @@
1
+ /*! SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net) */
2
+ /*! SPDX-License-Identifier: MIT */
3
+ import type { Code16KMatrix } from './encoder.js';
4
+ import type { Code16KDecodeOptions } from './tables.js';
5
+ export type Code16KRow = {
6
+ row: number;
7
+ values: number[];
8
+ };
9
+ export type Code16KDecodeResult = {
10
+ format: 'code16k';
11
+ text: string;
12
+ rows: number;
13
+ columns: number;
14
+ rowHeight: number;
15
+ separatorHeight: number;
16
+ mode: number;
17
+ codewords: number[];
18
+ checksum: true;
19
+ symbologyIdentifier: string;
20
+ gs1?: boolean;
21
+ fnc1AtStart?: boolean;
22
+ fnc1Positions?: number[];
23
+ };
24
+ export declare function decodeCode16KRow(modules: string, expectedRow?: number): Code16KRow | null;
25
+ export declare function decodeCode16K(matrix: Code16KMatrix, options?: Code16KDecodeOptions): Code16KDecodeResult;