@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,547 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
8
+ * SPDX-License-Identifier: MIT
9
+ *
10
+ * Original work. No code from any other barcode implementation.
11
+ */
12
+
13
+ /**
14
+ * Bounded GS1 DataBar Composite profile.
15
+ *
16
+ * The complete ISO/IEC 24723 composite symbology has several component
17
+ * layouts and a dedicated compaction system. This SDK profile keeps the
18
+ * pairing contract deliberately narrow: one validated GS1 DataBar host is
19
+ * linked to one strict MicroPDF417-derived CC-A or CC-B component. The marker
20
+ * and geometry checks make the profile safe for round trips without claiming
21
+ * interchange certification for every normative composite variant.
22
+ *
23
+ * @module composite
24
+ */
25
+
26
+ import { BitMatrix } from '../core/bit-matrix.js';
27
+ import { EncodeError, FormatError } from '../core/errors.js';
28
+ import * as databar from '../databar/index.js';
29
+ import {
30
+ decodeGS1ElementString,
31
+ encodeGS1ElementString,
32
+ formatGS1Elements,
33
+ } from '../databar/gs1.js';
34
+ import * as micropdf417 from '../micropdf417/index.js';
35
+
36
+ export const GS1_COMPOSITE_PROFILE = 'sythos-gs1-composite-bounded';
37
+ export const GS1_COMPOSITE_HOSTS = Object.freeze([
38
+ 'databar14',
39
+ 'databar-truncated',
40
+ 'databar-stacked',
41
+ 'databar-stacked-omnidirectional',
42
+ 'databar-limited',
43
+ 'databar-expanded',
44
+ ]);
45
+
46
+ const COMPONENT_WIDTH = 55;
47
+ const COMPONENT_VARIANTS = Object.freeze({
48
+ 'cc-a': Object.freeze([7]),
49
+ 'cc-b': Object.freeze([8, 9, 10, 11, 12, 13]),
50
+ });
51
+ const ALL_COMPONENT_VARIANTS = Object.freeze([7, 8, 9, 10, 11, 12, 13]);
52
+ const MAX_SCALE = 8;
53
+ const MAX_DIMENSION = 16_777_216;
54
+ const MAX_MODULES = 67_108_864;
55
+ // A short, private marker is part of this bounded profile. It is intentionally
56
+ // not presented as the ISO/IEC composite linkage flag or an interoperability
57
+ // claim; it only prevents a standalone component from being accepted here.
58
+ const COMPONENT_PREFIX = Object.freeze({ 'cc-a': 'A|', 'cc-b': 'B|' });
59
+
60
+ function isRecord(value) {
61
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
62
+ }
63
+
64
+ function positiveInteger(value, fallback, label, maximum = MAX_SCALE) {
65
+ const result = value ?? fallback;
66
+ if (!Number.isSafeInteger(result) || result < 1 || result > maximum) {
67
+ throw new EncodeError(`GS1 Composite ${label} must be an integer in 1..${maximum}`);
68
+ }
69
+ return result;
70
+ }
71
+
72
+ function normalizeHost(value) {
73
+ if (typeof value !== 'string') throw new EncodeError('GS1 Composite linear.format must be a string');
74
+ const id = value.toLowerCase().replace(/_/g, '-');
75
+ if (id === 'databar' || id === 'databar14' || id === 'gs1databar14' || id === 'gs1-databar14'
76
+ || id === 'databar-omnidirectional' || id === 'omnidirectional') return 'databar14';
77
+ if (id === 'databar-truncated' || id === 'gs1-databar-truncated' || id === 'truncated') return 'databar-truncated';
78
+ if (id === 'databar-stacked' || id === 'gs1databar-stacked' || id === 'gs1-databar-stacked') return 'databar-stacked';
79
+ if (id === 'databar-stacked-omnidirectional' || id === 'gs1databar-stacked-omnidirectional'
80
+ || id === 'gs1-databar-stacked-omni' || id === 'databar-stacked-omni') return 'databar-stacked-omnidirectional';
81
+ if (id === 'databar-limited' || id === 'gs1databar-limited' || id === 'gs1-databar-limited') return 'databar-limited';
82
+ if (id === 'databar-expanded' || id === 'gs1databar-expanded' || id === 'gs1-databar-expanded') return 'databar-expanded';
83
+ throw new EncodeError(`GS1 Composite linear format is not supported: ${value}`);
84
+ }
85
+
86
+ function hostWidth(host) {
87
+ if (host === 'databar14' || host === 'databar-truncated') return 96;
88
+ if (host === 'databar-stacked' || host === 'databar-stacked-omnidirectional') return 50;
89
+ if (host === 'databar-limited') return 79;
90
+ return null;
91
+ }
92
+
93
+ function encodeHost(host, value, options) {
94
+ if (host === 'databar14') {
95
+ return databar.encodeDataBar14(value, { ...options, linkage: true, variant: 'omnidirectional' });
96
+ }
97
+ if (host === 'databar-truncated') {
98
+ return databar.encodeDataBar14(value, { ...options, linkage: true, variant: 'truncated' });
99
+ }
100
+ if (host === 'databar-stacked') {
101
+ return databar.encodeDataBar14Stacked(value, { ...options, linkage: true });
102
+ }
103
+ if (host === 'databar-stacked-omnidirectional') {
104
+ return databar.encodeDataBarStackedOmnidirectional(value, { ...options, linkage: true });
105
+ }
106
+ if (host === 'databar-limited') {
107
+ return databar.encodeDataBarLimited(value, { ...options, linkage: true });
108
+ }
109
+ return databar.encodeDataBarExpanded(value, { ...options, linkage: true });
110
+ }
111
+
112
+ function decodeHost(host, matrix) {
113
+ if (host === 'databar14' || host === 'databar-truncated') return databar.decodeDataBar14(matrix);
114
+ if (host === 'databar-stacked') return databar.decodeDataBar14Stacked(matrix);
115
+ if (host === 'databar-stacked-omnidirectional') return databar.decodeDataBarStackedOmnidirectional(matrix);
116
+ if (host === 'databar-limited') return databar.decodeDataBarLimited(matrix);
117
+ return databar.decodeDataBarExpanded(matrix);
118
+ }
119
+
120
+ function normalizedGS1Data(input) {
121
+ let raw;
122
+ if (Array.isArray(input)) {
123
+ raw = encodeGS1ElementString(input);
124
+ } else if (typeof input === 'string') {
125
+ if (input.startsWith('(')) raw = encodeGS1ElementString(input);
126
+ else {
127
+ decodeGS1ElementString(input);
128
+ raw = input;
129
+ }
130
+ } else {
131
+ throw new EncodeError('GS1 Composite data must be a GS1 element string or element array');
132
+ }
133
+ const elements = decodeGS1ElementString(raw);
134
+ return { raw, elements };
135
+ }
136
+
137
+ function expandedInput(value) {
138
+ if (typeof value !== 'string' || value.startsWith('(')) return value;
139
+ try {
140
+ return formatGS1Elements(decodeGS1ElementString(value));
141
+ } catch {
142
+ return value;
143
+ }
144
+ }
145
+
146
+ function normalizedLinearOptions(options) {
147
+ if (!isRecord(options)) throw new EncodeError('GS1 Composite linear.options must be an object');
148
+ if (options.linkage === false) throw new EncodeError('GS1 Composite linear host must use linkage=true');
149
+ for (const key of ['moduleScale', 'scale']) {
150
+ if (options[key] !== undefined && options[key] !== 1) {
151
+ throw new EncodeError('GS1 Composite controls the common module scale; linear scaling must be 1');
152
+ }
153
+ }
154
+ return { ...options, linkage: true };
155
+ }
156
+
157
+ function selectComponent(raw, requested, rowHeight) {
158
+ if (requested !== 'auto' && requested !== 'cc-a' && requested !== 'cc-b') {
159
+ throw new EncodeError('GS1 Composite component must be auto, cc-a or cc-b');
160
+ }
161
+ const order = requested === 'auto'
162
+ ? ALL_COMPONENT_VARIANTS
163
+ : COMPONENT_VARIANTS[requested];
164
+ const kindFor = (variant) => variant === 7 ? 'cc-a' : 'cc-b';
165
+ let lastError;
166
+ for (const variant of order) {
167
+ const kind = kindFor(variant);
168
+ const payload = `${COMPONENT_PREFIX[kind]}${raw}`;
169
+ try {
170
+ const matrix = micropdf417.encodeMicroPDF417(payload, {
171
+ variant,
172
+ rowHeight,
173
+ compaction: 'byte',
174
+ eci: 3,
175
+ });
176
+ return Object.freeze({ matrix, kind, variant, payload });
177
+ } catch (error) {
178
+ lastError = error;
179
+ if (!(error instanceof EncodeError)) throw error;
180
+ }
181
+ }
182
+ throw new EncodeError(`GS1 Composite data does not fit the selected ${requested} component${lastError ? `: ${lastError.message}` : ''}`);
183
+ }
184
+
185
+ function copyMatrix(source, target, offsetX, offsetY) {
186
+ for (let y = 0; y < source.height; y++) {
187
+ for (let x = 0; x < source.width; x++) {
188
+ if (source.get(x, y)) target.set(offsetX + x, offsetY + y);
189
+ }
190
+ }
191
+ }
192
+
193
+ function metadataFor({ component, linear, width, height, scale, gap, host }) {
194
+ return Object.freeze({
195
+ profile: GS1_COMPOSITE_PROFILE,
196
+ component: component.kind,
197
+ componentVariant: component.variant,
198
+ componentRows: component.matrix.micropdf417?.rows ?? component.variant,
199
+ componentColumns: component.matrix.micropdf417?.columns ?? 2,
200
+ componentRowHeight: component.matrix.micropdf417?.rowHeight ?? 2,
201
+ componentX: component.x * scale,
202
+ componentY: component.y * scale,
203
+ componentWidth: component.matrix.width * scale,
204
+ componentHeight: component.matrix.height * scale,
205
+ linearFormat: host,
206
+ linearX: linear.x * scale,
207
+ linearY: linear.y * scale,
208
+ linearWidth: linear.matrix.width * scale,
209
+ linearHeight: linear.matrix.height * scale,
210
+ separatorGap: gap * scale,
211
+ moduleScale: scale,
212
+ width: width * scale,
213
+ height: height * scale,
214
+ });
215
+ }
216
+
217
+ /** Encode a bounded, linked GS1 DataBar Composite profile. */
218
+ export function encodeGS1Composite(input, options = {}) {
219
+ if (!isRecord(input)) throw new EncodeError('GS1 Composite input must be an object');
220
+ if (!isRecord(options)) throw new EncodeError('GS1 Composite options must be an object');
221
+ if (!isRecord(input.linear)) throw new EncodeError('GS1 Composite linear host is required');
222
+ const host = normalizeHost(input.linear.format);
223
+ if (input.linear.value === undefined || input.linear.value === null) {
224
+ throw new EncodeError('GS1 Composite linear.value is required');
225
+ }
226
+ const data = normalizedGS1Data(input.data);
227
+ const rowHeight = positiveInteger(input.rowHeight ?? options.rowHeight, 2, 'rowHeight', 64);
228
+ if (rowHeight < 2) throw new EncodeError('GS1 Composite rowHeight must be at least 2');
229
+ const separatorGap = positiveInteger(input.separatorGap ?? options.separatorGap, 1, 'separatorGap', 3);
230
+ const moduleScale = positiveInteger(input.moduleScale ?? options.moduleScale, 1, 'moduleScale', MAX_SCALE);
231
+ const requestedComponent = input.component ?? options.component ?? 'auto';
232
+ const linearOptions = normalizedLinearOptions(input.linear.options ?? {});
233
+ const linearValue = host === 'databar-expanded' ? expandedInput(input.linear.value) : input.linear.value;
234
+ const linearMatrix = encodeHost(host, linearValue, linearOptions);
235
+ if (linearMatrix.databar?.linkage !== true) {
236
+ throw new EncodeError('GS1 Composite host encoder did not preserve linkage=true');
237
+ }
238
+ const component = selectComponent(data.raw, requestedComponent, rowHeight);
239
+ const width = Math.max(linearMatrix.width, component.matrix.width);
240
+ const componentX = Math.floor((width - component.matrix.width) / 2);
241
+ const linearX = Math.floor((width - linearMatrix.width) / 2);
242
+ const linearY = component.matrix.height + separatorGap;
243
+ const height = linearY + linearMatrix.height;
244
+ if (width > MAX_DIMENSION || height > MAX_DIMENSION || width * height > MAX_MODULES) {
245
+ throw new EncodeError('GS1 Composite matrix exceeds the safe allocation budget');
246
+ }
247
+ const base = new BitMatrix(width, height);
248
+ copyMatrix(component.matrix, base, componentX, 0);
249
+ copyMatrix(linearMatrix, base, linearX, linearY);
250
+ const matrix = moduleScale === 1 ? base : base.scale(moduleScale);
251
+ const componentMeta = { ...component, x: componentX, y: 0 };
252
+ const linearMeta = { matrix: linearMatrix, x: linearX, y: linearY };
253
+ matrix.gs1composite = metadataFor({
254
+ component: componentMeta,
255
+ linear: linearMeta,
256
+ width,
257
+ height,
258
+ scale: moduleScale,
259
+ gap: separatorGap,
260
+ host,
261
+ });
262
+ return matrix;
263
+ }
264
+
265
+ function crop(source, x, y, width, height) {
266
+ if (!Number.isSafeInteger(x) || !Number.isSafeInteger(y) || !Number.isSafeInteger(width)
267
+ || !Number.isSafeInteger(height) || width < 1 || height < 1
268
+ || x < 0 || y < 0 || x + width > source.width || y + height > source.height) return null;
269
+ const output = new BitMatrix(width, height);
270
+ for (let row = 0; row < height; row++) {
271
+ for (let column = 0; column < width; column++) {
272
+ if (source.get(x + column, y + row)) output.set(column, row);
273
+ }
274
+ }
275
+ return output;
276
+ }
277
+
278
+ function collapseScale(source, scale) {
279
+ if (!Number.isSafeInteger(scale) || scale < 1
280
+ || source.width % scale !== 0 || source.height % scale !== 0) return null;
281
+ if (scale === 1) return source;
282
+ const output = new BitMatrix(source.width / scale, source.height / scale);
283
+ for (let y = 0; y < output.height; y++) {
284
+ for (let x = 0; x < output.width; x++) {
285
+ const expected = Boolean(source.get(x * scale, y * scale));
286
+ for (let dy = 0; dy < scale; dy++) {
287
+ for (let dx = 0; dx < scale; dx++) {
288
+ if (Boolean(source.get(x * scale + dx, y * scale + dy)) !== expected) return null;
289
+ }
290
+ }
291
+ if (expected) output.set(x, y);
292
+ }
293
+ }
294
+ return output;
295
+ }
296
+
297
+ function hasDark(source, x, y, width, height) {
298
+ for (let row = y; row < y + height; row++) {
299
+ for (let column = x; column < x + width; column++) {
300
+ if (source.get(column, row)) return true;
301
+ }
302
+ }
303
+ return false;
304
+ }
305
+
306
+ function payloadFromComponent(decoded, kind) {
307
+ const prefix = COMPONENT_PREFIX[kind];
308
+ if (!decoded || typeof decoded.text !== 'string' || !decoded.text.startsWith(prefix)) {
309
+ throw new FormatError('GS1 Composite component marker is invalid');
310
+ }
311
+ const raw = decoded.text.slice(prefix.length);
312
+ const elements = decodeGS1ElementString(raw);
313
+ return { raw, elements };
314
+ }
315
+
316
+ function compositeResult(host, linear, component, kind, raw, elements, geometry = {}) {
317
+ if (linear.linkage !== true) throw new FormatError('GS1 Composite linear host is not linked');
318
+ return Object.freeze({
319
+ format: 'gs1composite',
320
+ profile: GS1_COMPOSITE_PROFILE,
321
+ certified: false,
322
+ text: raw,
323
+ raw,
324
+ gs1: true,
325
+ linkage: true,
326
+ elements: Object.freeze(elements),
327
+ linearFormat: host,
328
+ linear: Object.freeze({ ...linear, format: host }),
329
+ component: kind,
330
+ componentVariant: component.variant,
331
+ componentRows: component.rows,
332
+ componentColumns: component.columns,
333
+ componentRowHeight: component.rowHeight,
334
+ corrections: component.corrections,
335
+ moduleScale: geometry.moduleScale ?? 1,
336
+ separatorGap: geometry.separatorGap,
337
+ symbologyIdentifier: kind === 'cc-a' ? ']e1' : ']e2',
338
+ ...geometry,
339
+ });
340
+ }
341
+
342
+ function decodeFromMetadata(matrix) {
343
+ const meta = matrix.gs1composite;
344
+ if (!isRecord(meta) || meta.profile !== GS1_COMPOSITE_PROFILE) return null;
345
+ const componentMatrix = crop(matrix, meta.componentX, meta.componentY, meta.componentWidth, meta.componentHeight);
346
+ const linearMatrix = crop(matrix, meta.linearX, meta.linearY, meta.linearWidth, meta.linearHeight);
347
+ if (!componentMatrix || !linearMatrix) throw new FormatError('GS1 Composite metadata points outside the matrix');
348
+ const logicalComponent = collapseScale(componentMatrix, meta.moduleScale ?? 1);
349
+ if (!logicalComponent) throw new FormatError('GS1 Composite component is not integer-scaled');
350
+ const component = micropdf417.decodeMicroPDF417(logicalComponent, { variant: meta.componentVariant });
351
+ const payload = payloadFromComponent(component, meta.component);
352
+ const host = normalizeHost(meta.linearFormat);
353
+ const linear = decodeHost(host, linearMatrix);
354
+ return compositeResult(host, linear, component, meta.component, payload.raw, payload.elements, {
355
+ moduleScale: meta.moduleScale,
356
+ separatorGap: meta.separatorGap,
357
+ bounds: { x: 0, y: 0, width: matrix.width, height: matrix.height },
358
+ });
359
+ }
360
+
361
+ function attemptCandidate(image, host, variant, rowHeight, scale, canvasX, topY, gap, canvasWidth, bounds) {
362
+ const entry = micropdf417.microPdf417VariantByNumber(variant);
363
+ const componentHeight = entry.rows * rowHeight * scale;
364
+ const componentWidth = COMPONENT_WIDTH * scale;
365
+ // Centre in logical modules first, then apply the common raster scale. This
366
+ // preserves the encoder's floor-centering for odd width differences.
367
+ const logicalCanvasWidth = Math.floor(canvasWidth / scale);
368
+ const componentX = canvasX + Math.floor((logicalCanvasWidth - COMPONENT_WIDTH) / 2) * scale;
369
+ const hostWidthModules = hostWidth(host);
370
+ const linearWidth = hostWidthModules === null ? canvasWidth : hostWidthModules * scale;
371
+ const linearModules = hostWidthModules === null ? logicalCanvasWidth : hostWidthModules;
372
+ const linearX = canvasX + Math.floor((logicalCanvasWidth - linearModules) / 2) * scale;
373
+ const linearY = topY + componentHeight + gap * scale;
374
+ const linearHeight = bounds.maxY - linearY + 1;
375
+ if (linearHeight < 1 || linearX < 0 || linearX + linearWidth > image.width
376
+ || componentX < 0 || componentX + componentWidth > image.width) return null;
377
+ // The two DataBar-14 host presentations share the same 96-module geometry;
378
+ // their normative heights provide the only reliable distinction after
379
+ // image metadata has been discarded.
380
+ if (host === 'databar14' && linearHeight < 33 * scale) return null;
381
+ if (host === 'databar-truncated' && linearHeight >= 33 * scale) return null;
382
+ if (hasDark(image, canvasX, topY + componentHeight, canvasWidth, gap * scale)) return null;
383
+ const componentMatrix = crop(image, componentX, topY, componentWidth, componentHeight);
384
+ const linearMatrix = crop(image, linearX, linearY, linearWidth, linearHeight);
385
+ if (!componentMatrix || !linearMatrix) return null;
386
+ try {
387
+ const logicalComponent = collapseScale(componentMatrix, scale);
388
+ if (!logicalComponent) return null;
389
+ const component = micropdf417.decodeMicroPDF417(logicalComponent, { variant });
390
+ const kind = variant === 7 ? 'cc-a' : 'cc-b';
391
+ const payload = payloadFromComponent(component, kind);
392
+ const linear = decodeHost(host, linearMatrix);
393
+ return {
394
+ result: compositeResult(host, linear, component, kind, payload.raw, payload.elements, {
395
+ moduleScale: scale,
396
+ separatorGap: gap * scale,
397
+ bounds: { x: canvasX, y: topY, width: canvasWidth, height: linearY + linearHeight - topY },
398
+ }),
399
+ matrix: componentMatrix,
400
+ geometry: { x: canvasX, y: topY, width: canvasWidth, height: linearY + linearHeight - topY },
401
+ };
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+
407
+ function fallbackDecode(image) {
408
+ const bounds = image.getBounds?.();
409
+ if (!bounds) return null;
410
+ const topY = bounds.y;
411
+ for (let scale = 1; scale <= MAX_SCALE; scale++) {
412
+ const xStart = Math.max(0, bounds.x - 2 * scale);
413
+ const xEnd = Math.min(image.width - 1, bounds.x);
414
+ for (const host of ['databar-stacked', 'databar-stacked-omnidirectional', 'databar-limited', 'databar-truncated', 'databar14']) {
415
+ const modules = hostWidth(host);
416
+ const canvasModules = Math.max(modules, COMPONENT_WIDTH);
417
+ const canvasWidth = canvasModules * scale;
418
+ if (canvasWidth > image.width) continue;
419
+ for (let canvasX = xStart; canvasX <= xEnd; canvasX++) {
420
+ for (let rowHeight = 2; rowHeight <= 6; rowHeight++) {
421
+ for (const variant of ALL_COMPONENT_VARIANTS) {
422
+ const found = attemptCandidate(image, host, variant, rowHeight, scale, canvasX, topY, 1, canvasWidth, {
423
+ maxY: bounds.y + bounds.height - 1,
424
+ });
425
+ if (found) return found;
426
+ for (const gap of [2, 3]) {
427
+ const withGap = attemptCandidate(image, host, variant, rowHeight, scale, canvasX, topY, gap, canvasWidth, {
428
+ maxY: bounds.y + bounds.height - 1,
429
+ });
430
+ if (withGap) return withGap;
431
+ }
432
+ }
433
+ }
434
+ }
435
+ }
436
+ // Expanded symbols have a payload-dependent width. Their dark bounds are
437
+ // already the logical width, so try a few light-edge corrections around it.
438
+ for (const delta of [-3, -2, -1, 0, 1, 2, 3]) {
439
+ const canvasWidth = bounds.width + delta * scale;
440
+ if (canvasWidth < COMPONENT_WIDTH * scale || canvasWidth > image.width) continue;
441
+ for (const variant of ALL_COMPONENT_VARIANTS) {
442
+ for (let rowHeight = 2; rowHeight <= 6; rowHeight++) {
443
+ const found = attemptCandidate(image, 'databar-expanded', variant, rowHeight, scale, bounds.x, topY, 1, canvasWidth, {
444
+ maxY: bounds.y + bounds.height - 1,
445
+ });
446
+ if (found) return found;
447
+ }
448
+ }
449
+ }
450
+ }
451
+ return null;
452
+ }
453
+
454
+ function rotateClockwise(source) {
455
+ const output = new BitMatrix(source.height, source.width);
456
+ for (let y = 0; y < source.height; y++) {
457
+ for (let x = 0; x < source.width; x++) if (source.get(x, y)) output.set(source.height - 1 - y, x);
458
+ }
459
+ return output;
460
+ }
461
+
462
+ function mapPoint(point, previous) {
463
+ return { x: point.y, y: previous.height - point.x };
464
+ }
465
+
466
+ function rectangle(bounds) {
467
+ return [
468
+ { x: bounds.x, y: bounds.y },
469
+ { x: bounds.x + bounds.width, y: bounds.y },
470
+ { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
471
+ { x: bounds.x, y: bounds.y + bounds.height },
472
+ ];
473
+ }
474
+
475
+ function validImage(image) {
476
+ return isRecord(image) && typeof image.get === 'function'
477
+ && Number.isSafeInteger(image.width) && Number.isSafeInteger(image.height)
478
+ && image.width > 0 && image.height > 0
479
+ && image.width <= MAX_DIMENSION && image.height <= MAX_DIMENSION
480
+ && image.width * image.height <= MAX_MODULES;
481
+ }
482
+
483
+ /** Decode one complete, axis-aligned bounded GS1 Composite symbol. */
484
+ export function decodeGS1Composite(matrix) {
485
+ if (!validImage(matrix)) throw new FormatError('GS1 Composite decoder expects a bounded BitMatrix-like value');
486
+ const fromMetadata = decodeFromMetadata(matrix);
487
+ if (fromMetadata) return fromMetadata;
488
+ const found = fallbackDecode(matrix);
489
+ if (!found) throw new FormatError('GS1 Composite geometry, linkage or component marker is invalid');
490
+ return found.result;
491
+ }
492
+
493
+ /** Detect one complete composite symbol in a clean binary raster. */
494
+ export function detectGS1Composite(binaryImage) {
495
+ if (!validImage(binaryImage)) return null;
496
+ let oriented = binaryImage;
497
+ let toOriginal = (point) => ({ x: point.x, y: point.y });
498
+ for (let turns = 0; turns < 4; turns++) {
499
+ try {
500
+ const metadataResult = decodeFromMetadata(oriented);
501
+ if (metadataResult) {
502
+ const bounds = { x: 0, y: 0, width: oriented.width, height: oriented.height };
503
+ return Object.freeze({
504
+ ...metadataResult,
505
+ bounds,
506
+ corners: rectangle(bounds).map(toOriginal),
507
+ matrix: oriented,
508
+ moduleSize: metadataResult.moduleScale ?? 1,
509
+ rotation: (360 - turns * 90) % 360,
510
+ confidence: 1,
511
+ quality: { quietZone: true, checksum: true, rows: metadataResult.componentRows, consistency: 1 },
512
+ });
513
+ }
514
+ } catch {
515
+ // Metadata may be stale after a caller edited the matrix. Fall back to geometry.
516
+ }
517
+ const found = fallbackDecode(oriented);
518
+ if (found) {
519
+ const bounds = found.geometry;
520
+ return Object.freeze({
521
+ ...found.result,
522
+ bounds,
523
+ corners: rectangle(bounds).map(toOriginal),
524
+ matrix: found.matrix,
525
+ moduleSize: found.result.moduleScale ?? 1,
526
+ rotation: (360 - turns * 90) % 360,
527
+ confidence: 1,
528
+ quality: {
529
+ quietZone: bounds.x > 0 && bounds.y > 0
530
+ && bounds.x + bounds.width < oriented.width
531
+ && bounds.y + bounds.height < oriented.height,
532
+ checksum: true,
533
+ rows: found.result.componentRows,
534
+ consistency: 1,
535
+ },
536
+ });
537
+ }
538
+ const previous = oriented;
539
+ const previousToOriginal = toOriginal;
540
+ oriented = rotateClockwise(previous);
541
+ toOriginal = (point) => previousToOriginal(mapPoint(point, previous));
542
+ }
543
+ return null;
544
+ }
545
+
546
+ /** Alias kept consistent with the other two-dimensional detectors. */
547
+ export const detectAndDecodeGS1Composite = detectGS1Composite;
@@ -0,0 +1,119 @@
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-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ /**
33
+ * Shared, format-neutral detector contracts.
34
+ *
35
+ * The module deliberately has no dependency on a particular matrix
36
+ * implementation. A detector can supply its own matrix type through the
37
+ * `TMatrix` generic while keeping the common geometry and candidate metadata.
38
+ *
39
+ * @module core/detection-contract
40
+ */
41
+
42
+ /** A point in the source image, expressed in image coordinates. */
43
+ export type Point = {
44
+ x: number;
45
+ y: number;
46
+ };
47
+ /** The canonical in-plane orientations supported by shared detectors. */
48
+ export type Rotation = 0 | 45 | 90 | 135 | 180 | 225 | 270 | 315;
49
+ /**
50
+ * Geometry recovered by a detector.
51
+ *
52
+ * `corners` are ordered top-left, top-right, bottom-right, bottom-left in the
53
+ * source image. `matrix` is the detector's rectified or otherwise decoder-ready
54
+ * representation, and is intentionally generic to keep this contract
55
+ * dependency-free.
56
+ */
57
+ export interface DetectionGeometry<TMatrix = unknown> {
58
+ corners: Point[];
59
+ moduleSize: number;
60
+ rotation: Rotation;
61
+ matrix: TMatrix;
62
+ confidence?: number;
63
+ }
64
+ /**
65
+ * Evidence collected while validating a candidate.
66
+ *
67
+ * Fields are optional because a 1D or 2D detector may not be able to provide
68
+ * every signal. When present, `rows` is a non-negative row count and
69
+ * `consistency` is a normalized value from zero to one.
70
+ */
71
+ export interface ValidationQuality {
72
+ quietZone?: boolean;
73
+ checksum?: boolean | null;
74
+ rows?: number | null;
75
+ consistency?: number | null;
76
+ [key: string]: unknown;
77
+ }
78
+ /** Optional decoded result and ranking metadata for a detection. */
79
+ export interface DetectionCandidateOptions<TResult = unknown> {
80
+ result?: TResult;
81
+ quality?: ValidationQuality;
82
+ score?: number;
83
+ }
84
+ /** A validated detector geometry with optional decode and quality metadata. */
85
+ export type DetectionCandidate<TResult = unknown, TMatrix = unknown> = DetectionGeometry<TMatrix> & DetectionCandidateOptions<TResult>;
86
+ /**
87
+ * Normalize a detector rotation to one of the supported 45-degree turns.
88
+ *
89
+ * This function is intentionally strict: arbitrary angles are not silently
90
+ * snapped to a nearby orientation. The accepted domain is the integer range
91
+ * `0..359`, and only exact multiples of 45 degrees are canonical.
92
+ *
93
+ * @throws {TypeError} If `rotation` is not a number.
94
+ * @throws {RangeError} If `rotation` is outside the canonical domain.
95
+ */
96
+ export declare function normalizeRotation(rotation: number): Rotation;
97
+ /**
98
+ * Return whether four finite points form an ordered, non-degenerate quad.
99
+ *
100
+ * The required order is top-left, top-right, bottom-right, bottom-left. Both
101
+ * clockwise and counter-clockwise winding are accepted; repeated points,
102
+ * zero-length edges, collinear turns, concave quads, and self-intersections
103
+ * are rejected.
104
+ */
105
+ export declare function isValidCorners(corners: unknown): corners is [Point, Point, Point, Point];
106
+ /**
107
+ * Create a validated detector candidate.
108
+ *
109
+ * All optional decoded-result and ranking metadata must be supplied in the
110
+ * options object: `createDetectionCandidate(geometry, { result, quality, score })`.
111
+ * Keeping the decoded result under the explicit `result` key avoids confusing
112
+ * a result object with candidate metadata. The returned value owns a fresh
113
+ * corner array and canonical rotation, and the input objects are never
114
+ * mutated.
115
+ *
116
+ * @throws {TypeError} If the geometry, matrix, or options have an invalid type.
117
+ * @throws {RangeError} If a numeric geometry or metadata value is invalid.
118
+ */
119
+ export declare function createDetectionCandidate<TResult = unknown, TMatrix = unknown>(geometry: DetectionGeometry<TMatrix>, options?: DetectionCandidateOptions<TResult>): DetectionCandidate<TResult, TMatrix>;