@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.
- package/LICENSE +215 -0
- package/NOTICE.md +106 -0
- package/README.md +433 -0
- package/bundle/sythos-barcode.esm.js +7998 -0
- package/bundle/sythos-barcode.js +7948 -0
- package/examples/create.html +731 -0
- package/examples/read.html +341 -0
- package/licenses/README.md +42 -0
- package/licenses/codabar.license +74 -0
- package/licenses/code-11.license +69 -0
- package/licenses/code-128.license +69 -0
- package/licenses/code-39.license +70 -0
- package/licenses/code-93.license +71 -0
- package/licenses/ean-13.license +70 -0
- package/licenses/ean-8.license +70 -0
- package/licenses/gs1-128.license +71 -0
- package/licenses/isbn.license +76 -0
- package/licenses/itf-14.license +69 -0
- package/licenses/itf.license +70 -0
- package/licenses/msi-plessey.license +72 -0
- package/licenses/pharmacode.license +71 -0
- package/licenses/qr-code.license +75 -0
- package/licenses/upc-a.license +72 -0
- package/licenses/upc-e.license +69 -0
- package/package.json +89 -0
- package/src/core/bit-buffer.js +174 -0
- package/src/core/bit-matrix.js +241 -0
- package/src/core/errors.js +61 -0
- package/src/core/galois-field.js +204 -0
- package/src/core/index.js +56 -0
- package/src/core/reed-solomon.js +313 -0
- package/src/image/binarizer.js +270 -0
- package/src/image/grid-sampler.js +164 -0
- package/src/image/index.js +40 -0
- package/src/image/luminance.js +196 -0
- package/src/image/perspective.js +195 -0
- package/src/index.js +240 -0
- package/src/oned/index.js +89 -0
- package/src/oned/patterns.js +384 -0
- package/src/oned/reader.js +918 -0
- package/src/oned/writers.js +741 -0
- package/src/qr/decoder.js +575 -0
- package/src/qr/detector.js +630 -0
- package/src/qr/encoder.js +958 -0
- package/src/qr/index.js +44 -0
- package/src/qr/tables.js +737 -0
- package/src/render/image-data.js +125 -0
- package/src/render/index.js +130 -0
- package/src/render/options.js +160 -0
- package/src/render/png.js +295 -0
- package/src/render/svg.js +120 -0
- package/src/render/webgl.js +206 -0
- 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
|
+
}
|