@sythos/js_barcode_universal 0.1.0

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 (53) hide show
  1. package/LICENSE +215 -0
  2. package/NOTICE.md +106 -0
  3. package/README.md +433 -0
  4. package/bundle/sythos-barcode.esm.js +7998 -0
  5. package/bundle/sythos-barcode.js +7948 -0
  6. package/examples/create.html +731 -0
  7. package/examples/read.html +341 -0
  8. package/licenses/README.md +42 -0
  9. package/licenses/codabar.license +74 -0
  10. package/licenses/code-11.license +69 -0
  11. package/licenses/code-128.license +69 -0
  12. package/licenses/code-39.license +70 -0
  13. package/licenses/code-93.license +71 -0
  14. package/licenses/ean-13.license +70 -0
  15. package/licenses/ean-8.license +70 -0
  16. package/licenses/gs1-128.license +71 -0
  17. package/licenses/isbn.license +76 -0
  18. package/licenses/itf-14.license +69 -0
  19. package/licenses/itf.license +70 -0
  20. package/licenses/msi-plessey.license +72 -0
  21. package/licenses/pharmacode.license +71 -0
  22. package/licenses/qr-code.license +75 -0
  23. package/licenses/upc-a.license +72 -0
  24. package/licenses/upc-e.license +69 -0
  25. package/package.json +89 -0
  26. package/src/core/bit-buffer.js +174 -0
  27. package/src/core/bit-matrix.js +241 -0
  28. package/src/core/errors.js +61 -0
  29. package/src/core/galois-field.js +204 -0
  30. package/src/core/index.js +56 -0
  31. package/src/core/reed-solomon.js +313 -0
  32. package/src/image/binarizer.js +270 -0
  33. package/src/image/grid-sampler.js +164 -0
  34. package/src/image/index.js +40 -0
  35. package/src/image/luminance.js +196 -0
  36. package/src/image/perspective.js +195 -0
  37. package/src/index.js +240 -0
  38. package/src/oned/index.js +89 -0
  39. package/src/oned/patterns.js +384 -0
  40. package/src/oned/reader.js +918 -0
  41. package/src/oned/writers.js +741 -0
  42. package/src/qr/decoder.js +575 -0
  43. package/src/qr/detector.js +630 -0
  44. package/src/qr/encoder.js +958 -0
  45. package/src/qr/index.js +44 -0
  46. package/src/qr/tables.js +737 -0
  47. package/src/render/image-data.js +125 -0
  48. package/src/render/index.js +130 -0
  49. package/src/render/options.js +160 -0
  50. package/src/render/png.js +295 -0
  51. package/src/render/svg.js +120 -0
  52. package/src/render/webgl.js +206 -0
  53. package/src/render/webgpu.js +369 -0
@@ -0,0 +1,369 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * SPDX-License-Identifier: MIT
27
+ *
28
+ * Original work. No code from any other barcode implementation.
29
+ */
30
+
31
+ /**
32
+ * WebGPU drawing.
33
+ *
34
+ * The same idea as the WebGL2 backend, expressed in WGSL: the matrix is
35
+ * uploaded as a one-byte-per-module `r8unorm` texture and sampled with NEAREST
36
+ * filtering, so module edges stay perfectly sharp at any size. A barcode
37
+ * resampled with interpolation stops being a barcode, which is why the filter
38
+ * choice is not a detail.
39
+ *
40
+ * Every entry point is failure-tolerant: no `navigator.gpu`, no adapter, a
41
+ * device that refuses the shader, a lost device — all return false (or a
42
+ * resolved false) so the caller falls back rather than showing nothing. The
43
+ * functions here are async only because WebGPU's own setup is; nothing about
44
+ * the drawing needs to be.
45
+ *
46
+ * @module render/webgpu
47
+ */
48
+
49
+ import { normalizeOptions, parseColor } from './options.js';
50
+
51
+ /**
52
+ * Vertex and fragment stages in one module, mirroring the WebGL2 pair.
53
+ *
54
+ * The vertex stage builds one oversized triangle from `vertex_index` alone:
55
+ * three positions, no vertex buffer to allocate, bind or release.
56
+ */
57
+ const SHADER = `
58
+ struct VertexOutput {
59
+ @builtin(position) position : vec4<f32>,
60
+ @location(0) uv : vec2<f32>,
61
+ };
62
+
63
+ @vertex
64
+ fn vertexMain(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {
65
+ let x = f32((vertexIndex << 1u) & 2u);
66
+ let y = f32(vertexIndex & 2u);
67
+ var result : VertexOutput;
68
+ result.uv = vec2<f32>(x, y);
69
+ result.position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
70
+ return result;
71
+ }
72
+
73
+ struct Style {
74
+ dark : vec4<f32>,
75
+ light : vec4<f32>,
76
+ size : vec2<f32>,
77
+ };
78
+
79
+ @group(0) @binding(0) var moduleTexture : texture_2d<f32>;
80
+ @group(0) @binding(1) var moduleSampler : sampler;
81
+ @group(0) @binding(2) var<uniform> style : Style;
82
+
83
+ @fragment
84
+ fn fragmentMain(@location(0) quadUV : vec2<f32>) -> @location(0) vec4<f32> {
85
+ // Flip Y: texture row 0 is the top of the symbol, but clip space puts +Y at
86
+ // the top, so the interpolated quad coordinate runs the other way.
87
+ let uv = vec2<f32>(quadUV.x, 1.0 - quadUV.y);
88
+ // Sample at the centre of the module, never on a boundary, so rounding
89
+ // cannot pull a neighbouring module's value in at fractional scales.
90
+ let texel = (floor(uv * style.size) + vec2<f32>(0.5, 0.5)) / style.size;
91
+ let v = textureSample(moduleTexture, moduleSampler, texel).r;
92
+ return select(style.light, style.dark, v > 0.5);
93
+ }
94
+ `;
95
+
96
+ /**
97
+ * The adapter and device are per-page, not per-barcode.
98
+ *
99
+ * Unlike a WebGL context — which the canvas owns, and which dies with it —
100
+ * a `GPUDevice` is independent of any canvas, and requesting one is slow. A
101
+ * page drawing a table of barcodes would otherwise pay for a full adapter
102
+ * negotiation per symbol, and leak a device per symbol on top, because the
103
+ * device cannot be destroyed while the canvas it configured is still on screen.
104
+ *
105
+ * @type {Promise<any> | null}
106
+ */
107
+ let sharedDevice = null;
108
+
109
+ /**
110
+ * Get the shared device, requesting one on first use.
111
+ *
112
+ * Never rejects: an unusable platform resolves to null.
113
+ *
114
+ * @returns {Promise<any>} The device, or null.
115
+ */
116
+ function acquireDevice() {
117
+ if (sharedDevice) return sharedDevice;
118
+
119
+ // Hoisted so the `lost` handler below can compare against this exact
120
+ // promise, and never clear a newer one that has replaced it.
121
+ function forget() {
122
+ if (sharedDevice === pending) sharedDevice = null;
123
+ }
124
+
125
+ const pending = (async () => {
126
+ try {
127
+ if (typeof navigator === 'undefined' || !navigator.gpu) return null;
128
+ const adapter = await navigator.gpu.requestAdapter();
129
+ if (!adapter) {
130
+ forget();
131
+ return null;
132
+ }
133
+ const device = await adapter.requestDevice();
134
+ if (!device) {
135
+ forget();
136
+ return null;
137
+ }
138
+ // A lost device can never be revived, so drop it and let the next
139
+ // render ask for a fresh one instead of failing forever.
140
+ if (device.lost && typeof device.lost.then === 'function') {
141
+ device.lost.then(forget, forget);
142
+ }
143
+ return device;
144
+ } catch {
145
+ forget();
146
+ return null;
147
+ }
148
+ })();
149
+
150
+ sharedDevice = pending;
151
+ return pending;
152
+ }
153
+
154
+ /**
155
+ * Is WebGPU usable here?
156
+ *
157
+ * Resolves false rather than throwing on every unsupported path, including
158
+ * Node, where there is no `navigator.gpu` at all.
159
+ *
160
+ * @returns {Promise<boolean>}
161
+ */
162
+ export async function isWebGPUAvailable() {
163
+ try {
164
+ if (typeof navigator === 'undefined') return false;
165
+ if (!navigator.gpu || typeof navigator.gpu.requestAdapter !== 'function') return false;
166
+ // An adapter is the real test: `navigator.gpu` exists on machines whose
167
+ // GPU is blocklisted, where every request still comes back null.
168
+ const adapter = await navigator.gpu.requestAdapter();
169
+ return Boolean(adapter);
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Did the shader compile?
177
+ *
178
+ * The WGSL analogue of checking `COMPILE_STATUS` in WebGL. Diagnostics are
179
+ * optional in practice, so an implementation that cannot report them is given
180
+ * the benefit of the doubt and the pipeline decides instead.
181
+ *
182
+ * @param {any} module
183
+ * @returns {Promise<boolean>}
184
+ */
185
+ async function compiles(module) {
186
+ try {
187
+ const query = module.getCompilationInfo ?? module.compilationInfo;
188
+ if (typeof query !== 'function') return true;
189
+ const info = await query.call(module);
190
+ if (!info || !info.messages) return true;
191
+ for (let i = 0; i < info.messages.length; i++) {
192
+ if (info.messages[i].type === 'error') return false;
193
+ }
194
+ return true;
195
+ } catch {
196
+ return true;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Draw a matrix into a canvas with WebGPU.
202
+ *
203
+ * @param {import('../core/bit-matrix.js').BitMatrix} matrix
204
+ * @param {HTMLCanvasElement | OffscreenCanvas} canvas
205
+ * @param {import('./options.js').RenderOptions} [options]
206
+ * @returns {Promise<boolean>} True if it drew; false means the caller should
207
+ * fall back. Note that a canvas whose context has already been taken for
208
+ * WebGPU cannot then be handed to WebGL2 or 2D, so callers should probe
209
+ * availability before committing a canvas to this path.
210
+ */
211
+ export async function renderToCanvasWebGPU(matrix, canvas, options = {}) {
212
+ let device = null;
213
+ let texture = null;
214
+ let uniforms = null;
215
+
216
+ try {
217
+ if (typeof navigator === 'undefined' || !navigator.gpu) return false;
218
+
219
+ const opts = normalizeOptions(matrix, options);
220
+ const { source, pixelWidth, pixelHeight } = opts;
221
+
222
+ device = await acquireDevice();
223
+ if (!device) return false;
224
+
225
+ // Oversized symbols are a legitimate failure, not a crash: say so and let
226
+ // the caller fall back to a path with no texture ceiling.
227
+ // 8192 is the floor the specification guarantees, so it is the right
228
+ // assumption when an implementation does not report its limits.
229
+ const maxDimension = (device.limits && device.limits.maxTextureDimension2D) || 8192;
230
+ if (source.width > maxDimension || source.height > maxDimension) return false;
231
+
232
+ const context = canvas.getContext('webgpu');
233
+ if (!context) return false;
234
+
235
+ canvas.width = pixelWidth;
236
+ canvas.height = pixelHeight;
237
+
238
+ const format = navigator.gpu.getPreferredCanvasFormat();
239
+ context.configure({ device, format, alphaMode: 'premultiplied' });
240
+
241
+ // One byte per module. r8unorm is the narrowest format every WebGPU
242
+ // implementation is required to support as a sampled texture.
243
+ const pixels = new Uint8Array(source.width * source.height);
244
+ for (let y = 0; y < source.height; y++) {
245
+ for (let x = 0; x < source.width; x++) {
246
+ pixels[y * source.width + x] = source.get(x, y) ? 255 : 0;
247
+ }
248
+ }
249
+
250
+ texture = device.createTexture({
251
+ size: { width: source.width, height: source.height, depthOrArrayLayers: 1 },
252
+ format: 'r8unorm',
253
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
254
+ });
255
+
256
+ // `bytesPerRow` needs no 256-byte alignment here — that rule belongs to
257
+ // buffer-to-texture copies, not to writeTexture's linear source data.
258
+ device.queue.writeTexture(
259
+ { texture },
260
+ pixels,
261
+ { offset: 0, bytesPerRow: source.width, rowsPerImage: source.height },
262
+ { width: source.width, height: source.height, depthOrArrayLayers: 1 }
263
+ );
264
+
265
+ const sampler = device.createSampler({
266
+ magFilter: 'nearest',
267
+ minFilter: 'nearest',
268
+ addressModeU: 'clamp-to-edge',
269
+ addressModeV: 'clamp-to-edge',
270
+ });
271
+
272
+ // The canvas is configured as premultiplied, so the colours have to be
273
+ // too — otherwise a translucent `light` would come out too bright and a
274
+ // fully transparent one would tint the page behind it.
275
+ const dark = parseColor(opts.dark);
276
+ const light = parseColor(opts.light);
277
+ const premultiplied = (c) => {
278
+ const a = c[3] / 255;
279
+ return [(c[0] / 255) * a, (c[1] / 255) * a, (c[2] / 255) * a, a];
280
+ };
281
+ const darkF = premultiplied(dark);
282
+ const lightF = premultiplied(light);
283
+
284
+ // std140-style layout: two vec4 then a vec2, rounded up to the struct's
285
+ // 16-byte alignment. 48 bytes, of which the last 8 are padding.
286
+ const style = new Float32Array(12);
287
+ style.set(darkF, 0);
288
+ style.set(lightF, 4);
289
+ style[8] = source.width;
290
+ style[9] = source.height;
291
+
292
+ uniforms = device.createBuffer({
293
+ size: style.byteLength,
294
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
295
+ });
296
+ device.queue.writeBuffer(uniforms, 0, style);
297
+
298
+ const shader = device.createShaderModule({ code: SHADER });
299
+ if (!(await compiles(shader))) return false;
300
+
301
+ // An error scope is the WGSL analogue of checking LINK_STATUS: a rejected
302
+ // pipeline is reported here instead of surfacing later as a lost device.
303
+ let pipeline = null;
304
+ if (typeof device.pushErrorScope === 'function') {
305
+ device.pushErrorScope('validation');
306
+ pipeline = device.createRenderPipeline({
307
+ layout: 'auto',
308
+ vertex: { module: shader, entryPoint: 'vertexMain' },
309
+ fragment: { module: shader, entryPoint: 'fragmentMain', targets: [{ format }] },
310
+ primitive: { topology: 'triangle-list' },
311
+ });
312
+ const failure = await device.popErrorScope();
313
+ if (failure) return false;
314
+ } else {
315
+ pipeline = device.createRenderPipeline({
316
+ layout: 'auto',
317
+ vertex: { module: shader, entryPoint: 'vertexMain' },
318
+ fragment: { module: shader, entryPoint: 'fragmentMain', targets: [{ format }] },
319
+ primitive: { topology: 'triangle-list' },
320
+ });
321
+ }
322
+ if (!pipeline) return false;
323
+
324
+ const bindGroup = device.createBindGroup({
325
+ layout: pipeline.getBindGroupLayout(0),
326
+ entries: [
327
+ { binding: 0, resource: texture.createView() },
328
+ { binding: 1, resource: sampler },
329
+ { binding: 2, resource: { buffer: uniforms } },
330
+ ],
331
+ });
332
+
333
+ const encoder = device.createCommandEncoder();
334
+ const pass = encoder.beginRenderPass({
335
+ colorAttachments: [{
336
+ view: context.getCurrentTexture().createView(),
337
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
338
+ loadOp: 'clear',
339
+ storeOp: 'store',
340
+ }],
341
+ });
342
+ pass.setPipeline(pipeline);
343
+ pass.setBindGroup(0, bindGroup);
344
+ pass.draw(3);
345
+ pass.end();
346
+ device.queue.submit([encoder.finish()]);
347
+
348
+ // Wait for the draw before the `finally` below frees its inputs, so
349
+ // returning true genuinely means the pixels are there.
350
+ if (device.queue && typeof device.queue.onSubmittedWorkDone === 'function') {
351
+ await device.queue.onSubmittedWorkDone();
352
+ }
353
+
354
+ return true;
355
+ } catch {
356
+ return false;
357
+ } finally {
358
+ // Release eagerly: a page generating many barcodes would otherwise hold
359
+ // every texture until GC caught up, and GPU memory is not GC's priority.
360
+ // The device itself is deliberately kept — it is shared, and destroying it
361
+ // would blank every canvas already configured with it.
362
+ try {
363
+ if (texture && typeof texture.destroy === 'function') texture.destroy();
364
+ if (uniforms && typeof uniforms.destroy === 'function') uniforms.destroy();
365
+ } catch {
366
+ /* device already gone */
367
+ }
368
+ }
369
+ }