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