@sythos/js_barcode_universal 1.0.0 → 1.2.5
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 +16 -17
- package/NOTICE.md +24 -22
- package/README.md +137 -60
- package/bundle/sythos-barcode.esm.js +3380 -216
- package/bundle/sythos-barcode.js +3368 -216
- package/examples/create.html +2 -0
- package/examples/read.html +341 -341
- package/licenses/README.md +58 -29
- package/licenses/aztec-code.license +74 -0
- package/licenses/codabar.license +9 -9
- package/licenses/code-11.license +9 -9
- package/licenses/code-128.license +6 -6
- package/licenses/code-39.license +6 -6
- package/licenses/code-93.license +7 -7
- package/licenses/data-matrix.license +11 -11
- package/licenses/ean-13.license +6 -6
- package/licenses/ean-8.license +6 -6
- package/licenses/gs1-128.license +6 -6
- package/licenses/isbn.license +7 -7
- package/licenses/itf-14.license +6 -6
- package/licenses/itf.license +6 -6
- package/licenses/micropdf417.license +96 -0
- package/licenses/msi-plessey.license +7 -7
- package/licenses/pdf417.license +37 -0
- package/licenses/pharmacode.license +7 -7
- package/licenses/qr-code.license +6 -6
- package/licenses/upc-a.license +7 -7
- package/licenses/upc-e.license +6 -6
- package/package.json +13 -3
- package/src/aztec/decoder.js +317 -0
- package/src/aztec/detector.js +224 -0
- package/src/aztec/encoder.js +257 -0
- package/src/aztec/high-level.js +211 -0
- package/src/aztec/index.js +45 -0
- package/src/aztec/tables.js +210 -0
- package/src/core/galois-field.js +3 -0
- package/src/core/reed-solomon.js +64 -50
- package/src/datamatrix/decoder.js +262 -262
- package/src/datamatrix/detector.js +225 -225
- package/src/datamatrix/encoder.js +191 -191
- package/src/datamatrix/index.js +42 -42
- package/src/datamatrix/tables.js +123 -123
- package/src/index.js +113 -3
- package/src/micropdf417/compaction.js +116 -0
- package/src/micropdf417/decoder.js +183 -0
- package/src/micropdf417/detector.js +149 -0
- package/src/micropdf417/encoder.js +209 -0
- package/src/micropdf417/error-correction.js +55 -0
- package/src/micropdf417/index.js +49 -0
- package/src/micropdf417/tables.js +184 -0
- package/src/pdf417/compaction.js +298 -0
- package/src/pdf417/decoder.js +75 -0
- package/src/pdf417/detector.js +468 -0
- package/src/pdf417/encoder.js +91 -0
- package/src/pdf417/error-correction.js +47 -0
- package/src/pdf417/index.js +6 -0
- package/src/pdf417/tables.js +317 -0
package/examples/read.html
CHANGED
|
@@ -1,341 +1,341 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Sythos Barcode Suite — Read</title>
|
|
7
|
-
<!--
|
|
8
|
-
Sythos Barcode Suite — example
|
|
9
|
-
Copyright (c) 2026 Sythos
|
|
10
|
-
SPDX-License-Identifier: MIT
|
|
11
|
-
|
|
12
|
-
File input first, camera second — deliberately.
|
|
13
|
-
|
|
14
|
-
getUserMedia requires a secure context, so the camera cannot work when this
|
|
15
|
-
page is opened from file://. Reading a dropped or chosen image has no such
|
|
16
|
-
restriction and works everywhere, including iOS Safari opened from disk. The
|
|
17
|
-
camera is offered as a progressive enhancement, and the page says plainly why
|
|
18
|
-
it is unavailable rather than appearing broken.
|
|
19
|
-
-->
|
|
20
|
-
<style>
|
|
21
|
-
:root {
|
|
22
|
-
--bg: #10131a; --panel: #181c26; --line: #262c3a;
|
|
23
|
-
--ink: #e6e9f0; --muted: #8b93a7; --accent: #5ac8fa;
|
|
24
|
-
--ok: #4ade80; --warn: #fbbf24;
|
|
25
|
-
color-scheme: dark;
|
|
26
|
-
}
|
|
27
|
-
@media (prefers-color-scheme: light) {
|
|
28
|
-
:root {
|
|
29
|
-
--bg: #f5f6f8; --panel: #ffffff; --line: #dfe3ea;
|
|
30
|
-
--ink: #171a21; --muted: #666e80; --accent: #0a84c4;
|
|
31
|
-
--ok: #16a34a; --warn: #b45309;
|
|
32
|
-
color-scheme: light;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
* { box-sizing: border-box; }
|
|
36
|
-
body {
|
|
37
|
-
margin: 0; padding: 24px;
|
|
38
|
-
font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
39
|
-
background: var(--bg); color: var(--ink);
|
|
40
|
-
}
|
|
41
|
-
header { max-width: 1000px; margin: 0 auto 20px; }
|
|
42
|
-
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
43
|
-
.sub { color: var(--muted); font-size: 13px; }
|
|
44
|
-
.sub a { color: var(--accent); }
|
|
45
|
-
.layout { max-width: 1000px; margin: 0 auto; display: grid; gap: 20px;
|
|
46
|
-
grid-template-columns: 1fr 1fr; align-items: start; }
|
|
47
|
-
@media (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
|
|
48
|
-
.panel { background: var(--panel); border: 1px solid var(--line);
|
|
49
|
-
border-radius: 12px; padding: 18px; }
|
|
50
|
-
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em;
|
|
51
|
-
color: var(--muted); margin: 0 0 12px; }
|
|
52
|
-
#drop {
|
|
53
|
-
border: 2px dashed var(--line); border-radius: 10px; padding: 32px 16px;
|
|
54
|
-
text-align: center; color: var(--muted); cursor: pointer; font-size: 14px;
|
|
55
|
-
transition: border-color .15s, background .15s;
|
|
56
|
-
}
|
|
57
|
-
#drop.over { border-color: var(--accent); background: rgba(90,200,250,0.08); }
|
|
58
|
-
#drop b { color: var(--ink); }
|
|
59
|
-
button {
|
|
60
|
-
padding: 10px 16px; font: inherit; font-weight: 600; font-size: 13px;
|
|
61
|
-
cursor: pointer; border-radius: 8px; border: 1px solid var(--line);
|
|
62
|
-
background: var(--bg); color: var(--ink);
|
|
63
|
-
}
|
|
64
|
-
button.primary { background: var(--accent); border-color: var(--accent); color: #051019; }
|
|
65
|
-
button:disabled { opacity: .45; cursor: not-allowed; }
|
|
66
|
-
video, #preview {
|
|
67
|
-
width: 100%; border-radius: 8px; background: #000; display: block; margin-top: 12px;
|
|
68
|
-
}
|
|
69
|
-
#preview { background: #fff; }
|
|
70
|
-
.note {
|
|
71
|
-
margin-top: 12px; padding: 10px 12px; border-radius: 8px; font-size: 12.5px;
|
|
72
|
-
background: rgba(251,191,36,0.12); border: 1px solid var(--warn); color: var(--warn);
|
|
73
|
-
}
|
|
74
|
-
.note code { font-family: ui-monospace, monospace; }
|
|
75
|
-
#results:empty::after {
|
|
76
|
-
content: "Nothing decoded yet."; color: var(--muted); font-size: 13px;
|
|
77
|
-
}
|
|
78
|
-
.hit {
|
|
79
|
-
border: 1px solid var(--line); border-left: 3px solid var(--ok);
|
|
80
|
-
border-radius: 8px; padding: 10px 12px; margin-bottom: 8px;
|
|
81
|
-
background: var(--bg);
|
|
82
|
-
}
|
|
83
|
-
.hit .fmt { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
|
|
84
|
-
color: var(--ok); font-weight: 700; }
|
|
85
|
-
.hit .val { font-family: ui-monospace, monospace; font-size: 13px;
|
|
86
|
-
word-break: break-all; margin-top: 3px; }
|
|
87
|
-
.hit .val a { color: var(--accent); }
|
|
88
|
-
.status { font-size: 12px; color: var(--muted); margin-top: 10px;
|
|
89
|
-
font-family: ui-monospace, monospace; }
|
|
90
|
-
</style>
|
|
91
|
-
</head>
|
|
92
|
-
<body>
|
|
93
|
-
|
|
94
|
-
<header>
|
|
95
|
-
<h1>Read a barcode</h1>
|
|
96
|
-
<p class="sub">
|
|
97
|
-
Sythos Barcode Suite — 100% original JavaScript, zero dependencies, MIT.
|
|
98
|
-
See also <a href="create.html">create.html</a>.
|
|
99
|
-
</p>
|
|
100
|
-
</header>
|
|
101
|
-
|
|
102
|
-
<div class="layout">
|
|
103
|
-
<div>
|
|
104
|
-
<div class="panel">
|
|
105
|
-
<h2>From an image</h2>
|
|
106
|
-
<div id="drop">
|
|
107
|
-
<b>Drop an image here</b><br>or click to choose a file
|
|
108
|
-
</div>
|
|
109
|
-
<input type="file" id="file" accept="image/*" hidden>
|
|
110
|
-
<canvas id="preview"></canvas>
|
|
111
|
-
<div class="status" id="fileStatus"></div>
|
|
112
|
-
</div>
|
|
113
|
-
|
|
114
|
-
<div class="panel" style="margin-top:20px">
|
|
115
|
-
<h2>From the camera</h2>
|
|
116
|
-
<button class="primary" id="camStart">Start camera</button>
|
|
117
|
-
<button id="camStop" disabled>Stop</button>
|
|
118
|
-
<!-- playsinline is required on iOS: without it the video takes over the
|
|
119
|
-
whole screen and the canvas never sees a frame. -->
|
|
120
|
-
<video id="video" playsinline muted autoplay hidden></video>
|
|
121
|
-
<div id="camNote"></div>
|
|
122
|
-
<div class="status" id="camStatus"></div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div class="panel">
|
|
127
|
-
<h2>Results</h2>
|
|
128
|
-
<div id="results"></div>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<canvas id="work" hidden></canvas>
|
|
133
|
-
|
|
134
|
-
<script src="../bundle/sythos-barcode.js"></script>
|
|
135
|
-
<script>
|
|
136
|
-
(function () {
|
|
137
|
-
'use strict';
|
|
138
|
-
var B = window.SythosBarcode;
|
|
139
|
-
|
|
140
|
-
var el = function (id) { return document.getElementById(id); };
|
|
141
|
-
var work = el('work');
|
|
142
|
-
var results = el('results');
|
|
143
|
-
|
|
144
|
-
function show(hits, source) {
|
|
145
|
-
if (!hits.length) return false;
|
|
146
|
-
results.innerHTML = '';
|
|
147
|
-
hits.forEach(function (h) {
|
|
148
|
-
var div = document.createElement('div');
|
|
149
|
-
div.className = 'hit';
|
|
150
|
-
|
|
151
|
-
var fmt = document.createElement('div');
|
|
152
|
-
fmt.className = 'fmt';
|
|
153
|
-
fmt.textContent = h.format + (source ? ' · ' + source : '');
|
|
154
|
-
|
|
155
|
-
var val = document.createElement('div');
|
|
156
|
-
val.className = 'val';
|
|
157
|
-
// Linkify URLs, but build the anchor with the DOM so decoded content is
|
|
158
|
-
// never interpreted as markup. Scanned data is untrusted input.
|
|
159
|
-
if (/^https?:\/\//i.test(h.text)) {
|
|
160
|
-
var a = document.createElement('a');
|
|
161
|
-
a.href = h.text;
|
|
162
|
-
a.textContent = h.text;
|
|
163
|
-
a.rel = 'noopener noreferrer';
|
|
164
|
-
a.target = '_blank';
|
|
165
|
-
val.appendChild(a);
|
|
166
|
-
} else {
|
|
167
|
-
val.textContent = h.text;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
div.appendChild(fmt);
|
|
171
|
-
div.appendChild(val);
|
|
172
|
-
results.appendChild(div);
|
|
173
|
-
});
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function scanCanvas(canvas) {
|
|
178
|
-
var ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
179
|
-
if (!ctx || !canvas.width || !canvas.height) return [];
|
|
180
|
-
var image = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
181
|
-
try {
|
|
182
|
-
return B.decode(image, { tryHarder: true });
|
|
183
|
-
} catch (e) {
|
|
184
|
-
return [];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ---------------- file input ---------------- */
|
|
189
|
-
|
|
190
|
-
var drop = el('drop');
|
|
191
|
-
var fileInput = el('file');
|
|
192
|
-
|
|
193
|
-
drop.addEventListener('click', function () { fileInput.click(); });
|
|
194
|
-
['dragenter', 'dragover'].forEach(function (ev) {
|
|
195
|
-
drop.addEventListener(ev, function (e) {
|
|
196
|
-
e.preventDefault(); drop.classList.add('over');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
['dragleave', 'drop'].forEach(function (ev) {
|
|
200
|
-
drop.addEventListener(ev, function (e) {
|
|
201
|
-
e.preventDefault(); drop.classList.remove('over');
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
drop.addEventListener('drop', function (e) {
|
|
205
|
-
if (e.dataTransfer && e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
|
|
206
|
-
});
|
|
207
|
-
fileInput.addEventListener('change', function () {
|
|
208
|
-
if (fileInput.files[0]) handleFile(fileInput.files[0]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
function handleFile(file) {
|
|
212
|
-
var url = URL.createObjectURL(file);
|
|
213
|
-
var img = new Image();
|
|
214
|
-
img.onload = function () {
|
|
215
|
-
URL.revokeObjectURL(url);
|
|
216
|
-
|
|
217
|
-
// Very large photos cost time without adding information; cap the long
|
|
218
|
-
// edge so a 48-megapixel phone shot still scans promptly.
|
|
219
|
-
var max = 1600;
|
|
220
|
-
var s = Math.min(1, max / Math.max(img.width, img.height));
|
|
221
|
-
var w = Math.max(1, Math.round(img.width * s));
|
|
222
|
-
var h = Math.max(1, Math.round(img.height * s));
|
|
223
|
-
|
|
224
|
-
var preview = el('preview');
|
|
225
|
-
preview.width = w; preview.height = h;
|
|
226
|
-
preview.getContext('2d').drawImage(img, 0, 0, w, h);
|
|
227
|
-
|
|
228
|
-
work.width = w; work.height = h;
|
|
229
|
-
work.getContext('2d', { willReadFrequently: true }).drawImage(img, 0, 0, w, h);
|
|
230
|
-
|
|
231
|
-
var t0 = (performance && performance.now) ? performance.now() : 0;
|
|
232
|
-
var hits = scanCanvas(work);
|
|
233
|
-
var ms = ((performance && performance.now) ? performance.now() : 0) - t0;
|
|
234
|
-
|
|
235
|
-
el('fileStatus').textContent =
|
|
236
|
-
w + '×' + h + ' · ' + ms.toFixed(0) + ' ms · ' + hits.length + ' found';
|
|
237
|
-
if (!show(hits, 'image')) {
|
|
238
|
-
results.innerHTML = '<div class="status">No barcode found in that image.</div>';
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
img.onerror = function () {
|
|
242
|
-
URL.revokeObjectURL(url);
|
|
243
|
-
el('fileStatus').textContent = 'Could not load that file as an image.';
|
|
244
|
-
};
|
|
245
|
-
img.src = url;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/* ---------------- camera ---------------- */
|
|
249
|
-
|
|
250
|
-
var video = el('video');
|
|
251
|
-
var stream = null;
|
|
252
|
-
var raf = null;
|
|
253
|
-
|
|
254
|
-
var secure = window.isSecureContext ||
|
|
255
|
-
location.protocol === 'https:' ||
|
|
256
|
-
location.hostname === 'localhost';
|
|
257
|
-
|
|
258
|
-
if (!secure) {
|
|
259
|
-
el('camNote').innerHTML =
|
|
260
|
-
'<div class="note">The camera needs a secure context, so it is unavailable ' +
|
|
261
|
-
'when this page is opened from <code>file://</code>. Serve the folder over ' +
|
|
262
|
-
'http and reload — for example <code>npx serve</code> — or use ' +
|
|
263
|
-
'the image input above, which works anywhere.</div>';
|
|
264
|
-
el('camStart').disabled = true;
|
|
265
|
-
} else if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
266
|
-
el('camNote').innerHTML =
|
|
267
|
-
'<div class="note">This browser does not expose a camera API.</div>';
|
|
268
|
-
el('camStart').disabled = true;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
el('camStart').addEventListener('click', function () {
|
|
272
|
-
// Called from a click, which is what iOS requires for both getUserMedia
|
|
273
|
-
// and the subsequent play().
|
|
274
|
-
navigator.mediaDevices.getUserMedia({
|
|
275
|
-
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } },
|
|
276
|
-
audio: false
|
|
277
|
-
}).then(function (s) {
|
|
278
|
-
stream = s;
|
|
279
|
-
video.srcObject = s;
|
|
280
|
-
video.hidden = false;
|
|
281
|
-
el('camStart').disabled = true;
|
|
282
|
-
el('camStop').disabled = false;
|
|
283
|
-
return video.play();
|
|
284
|
-
}).then(function () {
|
|
285
|
-
loop();
|
|
286
|
-
}).catch(function (e) {
|
|
287
|
-
el('camNote').innerHTML =
|
|
288
|
-
'<div class="note">Camera unavailable: ' + String(e.message || e) + '</div>';
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
el('camStop').addEventListener('click', stopCamera);
|
|
293
|
-
|
|
294
|
-
function stopCamera() {
|
|
295
|
-
if (raf) { cancelAnimationFrame(raf); raf = null; }
|
|
296
|
-
if (stream) {
|
|
297
|
-
stream.getTracks().forEach(function (t) { t.stop(); });
|
|
298
|
-
stream = null;
|
|
299
|
-
}
|
|
300
|
-
video.hidden = true;
|
|
301
|
-
el('camStart').disabled = false;
|
|
302
|
-
el('camStop').disabled = true;
|
|
303
|
-
el('camStatus').textContent = '';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
var frames = 0;
|
|
307
|
-
var lastReport = 0;
|
|
308
|
-
|
|
309
|
-
function loop() {
|
|
310
|
-
raf = requestAnimationFrame(loop);
|
|
311
|
-
if (!video.videoWidth) return;
|
|
312
|
-
|
|
313
|
-
// Scan at a reduced size: a 720p frame carries far more detail than the
|
|
314
|
-
// decoder needs, and the binarizer cost is what sets the frame rate.
|
|
315
|
-
var scale = Math.min(1, 800 / video.videoWidth);
|
|
316
|
-
work.width = Math.round(video.videoWidth * scale);
|
|
317
|
-
work.height = Math.round(video.videoHeight * scale);
|
|
318
|
-
work.getContext('2d', { willReadFrequently: true })
|
|
319
|
-
.drawImage(video, 0, 0, work.width, work.height);
|
|
320
|
-
|
|
321
|
-
frames++;
|
|
322
|
-
var hits = scanCanvas(work);
|
|
323
|
-
if (hits.length) {
|
|
324
|
-
show(hits, 'camera');
|
|
325
|
-
stopCamera();
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
var now = (performance && performance.now) ? performance.now() : 0;
|
|
330
|
-
if (now - lastReport > 500) {
|
|
331
|
-
lastReport = now;
|
|
332
|
-
el('camStatus').textContent =
|
|
333
|
-
'scanning ' + work.width + '×' + work.height + ' · ' + frames + ' frames';
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
window.addEventListener('pagehide', stopCamera);
|
|
338
|
-
})();
|
|
339
|
-
</script>
|
|
340
|
-
</body>
|
|
341
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Sythos Barcode Suite — Read</title>
|
|
7
|
+
<!--
|
|
8
|
+
Sythos Barcode Suite — example
|
|
9
|
+
Copyright (c) 2026 Sythos
|
|
10
|
+
SPDX-License-Identifier: MIT
|
|
11
|
+
|
|
12
|
+
File input first, camera second — deliberately.
|
|
13
|
+
|
|
14
|
+
getUserMedia requires a secure context, so the camera cannot work when this
|
|
15
|
+
page is opened from file://. Reading a dropped or chosen image has no such
|
|
16
|
+
restriction and works everywhere, including iOS Safari opened from disk. The
|
|
17
|
+
camera is offered as a progressive enhancement, and the page says plainly why
|
|
18
|
+
it is unavailable rather than appearing broken.
|
|
19
|
+
-->
|
|
20
|
+
<style>
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #10131a; --panel: #181c26; --line: #262c3a;
|
|
23
|
+
--ink: #e6e9f0; --muted: #8b93a7; --accent: #5ac8fa;
|
|
24
|
+
--ok: #4ade80; --warn: #fbbf24;
|
|
25
|
+
color-scheme: dark;
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: light) {
|
|
28
|
+
:root {
|
|
29
|
+
--bg: #f5f6f8; --panel: #ffffff; --line: #dfe3ea;
|
|
30
|
+
--ink: #171a21; --muted: #666e80; --accent: #0a84c4;
|
|
31
|
+
--ok: #16a34a; --warn: #b45309;
|
|
32
|
+
color-scheme: light;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
* { box-sizing: border-box; }
|
|
36
|
+
body {
|
|
37
|
+
margin: 0; padding: 24px;
|
|
38
|
+
font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
39
|
+
background: var(--bg); color: var(--ink);
|
|
40
|
+
}
|
|
41
|
+
header { max-width: 1000px; margin: 0 auto 20px; }
|
|
42
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
43
|
+
.sub { color: var(--muted); font-size: 13px; }
|
|
44
|
+
.sub a { color: var(--accent); }
|
|
45
|
+
.layout { max-width: 1000px; margin: 0 auto; display: grid; gap: 20px;
|
|
46
|
+
grid-template-columns: 1fr 1fr; align-items: start; }
|
|
47
|
+
@media (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
|
|
48
|
+
.panel { background: var(--panel); border: 1px solid var(--line);
|
|
49
|
+
border-radius: 12px; padding: 18px; }
|
|
50
|
+
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em;
|
|
51
|
+
color: var(--muted); margin: 0 0 12px; }
|
|
52
|
+
#drop {
|
|
53
|
+
border: 2px dashed var(--line); border-radius: 10px; padding: 32px 16px;
|
|
54
|
+
text-align: center; color: var(--muted); cursor: pointer; font-size: 14px;
|
|
55
|
+
transition: border-color .15s, background .15s;
|
|
56
|
+
}
|
|
57
|
+
#drop.over { border-color: var(--accent); background: rgba(90,200,250,0.08); }
|
|
58
|
+
#drop b { color: var(--ink); }
|
|
59
|
+
button {
|
|
60
|
+
padding: 10px 16px; font: inherit; font-weight: 600; font-size: 13px;
|
|
61
|
+
cursor: pointer; border-radius: 8px; border: 1px solid var(--line);
|
|
62
|
+
background: var(--bg); color: var(--ink);
|
|
63
|
+
}
|
|
64
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: #051019; }
|
|
65
|
+
button:disabled { opacity: .45; cursor: not-allowed; }
|
|
66
|
+
video, #preview {
|
|
67
|
+
width: 100%; border-radius: 8px; background: #000; display: block; margin-top: 12px;
|
|
68
|
+
}
|
|
69
|
+
#preview { background: #fff; }
|
|
70
|
+
.note {
|
|
71
|
+
margin-top: 12px; padding: 10px 12px; border-radius: 8px; font-size: 12.5px;
|
|
72
|
+
background: rgba(251,191,36,0.12); border: 1px solid var(--warn); color: var(--warn);
|
|
73
|
+
}
|
|
74
|
+
.note code { font-family: ui-monospace, monospace; }
|
|
75
|
+
#results:empty::after {
|
|
76
|
+
content: "Nothing decoded yet."; color: var(--muted); font-size: 13px;
|
|
77
|
+
}
|
|
78
|
+
.hit {
|
|
79
|
+
border: 1px solid var(--line); border-left: 3px solid var(--ok);
|
|
80
|
+
border-radius: 8px; padding: 10px 12px; margin-bottom: 8px;
|
|
81
|
+
background: var(--bg);
|
|
82
|
+
}
|
|
83
|
+
.hit .fmt { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
|
|
84
|
+
color: var(--ok); font-weight: 700; }
|
|
85
|
+
.hit .val { font-family: ui-monospace, monospace; font-size: 13px;
|
|
86
|
+
word-break: break-all; margin-top: 3px; }
|
|
87
|
+
.hit .val a { color: var(--accent); }
|
|
88
|
+
.status { font-size: 12px; color: var(--muted); margin-top: 10px;
|
|
89
|
+
font-family: ui-monospace, monospace; }
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
|
|
94
|
+
<header>
|
|
95
|
+
<h1>Read a barcode</h1>
|
|
96
|
+
<p class="sub">
|
|
97
|
+
Sythos Barcode Suite — 100% original JavaScript, zero dependencies, MIT.
|
|
98
|
+
See also <a href="create.html">create.html</a>.
|
|
99
|
+
</p>
|
|
100
|
+
</header>
|
|
101
|
+
|
|
102
|
+
<div class="layout">
|
|
103
|
+
<div>
|
|
104
|
+
<div class="panel">
|
|
105
|
+
<h2>From an image</h2>
|
|
106
|
+
<div id="drop">
|
|
107
|
+
<b>Drop an image here</b><br>or click to choose a file
|
|
108
|
+
</div>
|
|
109
|
+
<input type="file" id="file" accept="image/*" hidden>
|
|
110
|
+
<canvas id="preview"></canvas>
|
|
111
|
+
<div class="status" id="fileStatus"></div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="panel" style="margin-top:20px">
|
|
115
|
+
<h2>From the camera</h2>
|
|
116
|
+
<button class="primary" id="camStart">Start camera</button>
|
|
117
|
+
<button id="camStop" disabled>Stop</button>
|
|
118
|
+
<!-- playsinline is required on iOS: without it the video takes over the
|
|
119
|
+
whole screen and the canvas never sees a frame. -->
|
|
120
|
+
<video id="video" playsinline muted autoplay hidden></video>
|
|
121
|
+
<div id="camNote"></div>
|
|
122
|
+
<div class="status" id="camStatus"></div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div class="panel">
|
|
127
|
+
<h2>Results</h2>
|
|
128
|
+
<div id="results"></div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<canvas id="work" hidden></canvas>
|
|
133
|
+
|
|
134
|
+
<script src="../bundle/sythos-barcode.js"></script>
|
|
135
|
+
<script>
|
|
136
|
+
(function () {
|
|
137
|
+
'use strict';
|
|
138
|
+
var B = window.SythosBarcode;
|
|
139
|
+
|
|
140
|
+
var el = function (id) { return document.getElementById(id); };
|
|
141
|
+
var work = el('work');
|
|
142
|
+
var results = el('results');
|
|
143
|
+
|
|
144
|
+
function show(hits, source) {
|
|
145
|
+
if (!hits.length) return false;
|
|
146
|
+
results.innerHTML = '';
|
|
147
|
+
hits.forEach(function (h) {
|
|
148
|
+
var div = document.createElement('div');
|
|
149
|
+
div.className = 'hit';
|
|
150
|
+
|
|
151
|
+
var fmt = document.createElement('div');
|
|
152
|
+
fmt.className = 'fmt';
|
|
153
|
+
fmt.textContent = h.format + (source ? ' · ' + source : '');
|
|
154
|
+
|
|
155
|
+
var val = document.createElement('div');
|
|
156
|
+
val.className = 'val';
|
|
157
|
+
// Linkify URLs, but build the anchor with the DOM so decoded content is
|
|
158
|
+
// never interpreted as markup. Scanned data is untrusted input.
|
|
159
|
+
if (/^https?:\/\//i.test(h.text)) {
|
|
160
|
+
var a = document.createElement('a');
|
|
161
|
+
a.href = h.text;
|
|
162
|
+
a.textContent = h.text;
|
|
163
|
+
a.rel = 'noopener noreferrer';
|
|
164
|
+
a.target = '_blank';
|
|
165
|
+
val.appendChild(a);
|
|
166
|
+
} else {
|
|
167
|
+
val.textContent = h.text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
div.appendChild(fmt);
|
|
171
|
+
div.appendChild(val);
|
|
172
|
+
results.appendChild(div);
|
|
173
|
+
});
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scanCanvas(canvas) {
|
|
178
|
+
var ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
179
|
+
if (!ctx || !canvas.width || !canvas.height) return [];
|
|
180
|
+
var image = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
181
|
+
try {
|
|
182
|
+
return B.decode(image, { tryHarder: true });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ---------------- file input ---------------- */
|
|
189
|
+
|
|
190
|
+
var drop = el('drop');
|
|
191
|
+
var fileInput = el('file');
|
|
192
|
+
|
|
193
|
+
drop.addEventListener('click', function () { fileInput.click(); });
|
|
194
|
+
['dragenter', 'dragover'].forEach(function (ev) {
|
|
195
|
+
drop.addEventListener(ev, function (e) {
|
|
196
|
+
e.preventDefault(); drop.classList.add('over');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
['dragleave', 'drop'].forEach(function (ev) {
|
|
200
|
+
drop.addEventListener(ev, function (e) {
|
|
201
|
+
e.preventDefault(); drop.classList.remove('over');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
drop.addEventListener('drop', function (e) {
|
|
205
|
+
if (e.dataTransfer && e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
|
|
206
|
+
});
|
|
207
|
+
fileInput.addEventListener('change', function () {
|
|
208
|
+
if (fileInput.files[0]) handleFile(fileInput.files[0]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
function handleFile(file) {
|
|
212
|
+
var url = URL.createObjectURL(file);
|
|
213
|
+
var img = new Image();
|
|
214
|
+
img.onload = function () {
|
|
215
|
+
URL.revokeObjectURL(url);
|
|
216
|
+
|
|
217
|
+
// Very large photos cost time without adding information; cap the long
|
|
218
|
+
// edge so a 48-megapixel phone shot still scans promptly.
|
|
219
|
+
var max = 1600;
|
|
220
|
+
var s = Math.min(1, max / Math.max(img.width, img.height));
|
|
221
|
+
var w = Math.max(1, Math.round(img.width * s));
|
|
222
|
+
var h = Math.max(1, Math.round(img.height * s));
|
|
223
|
+
|
|
224
|
+
var preview = el('preview');
|
|
225
|
+
preview.width = w; preview.height = h;
|
|
226
|
+
preview.getContext('2d').drawImage(img, 0, 0, w, h);
|
|
227
|
+
|
|
228
|
+
work.width = w; work.height = h;
|
|
229
|
+
work.getContext('2d', { willReadFrequently: true }).drawImage(img, 0, 0, w, h);
|
|
230
|
+
|
|
231
|
+
var t0 = (performance && performance.now) ? performance.now() : 0;
|
|
232
|
+
var hits = scanCanvas(work);
|
|
233
|
+
var ms = ((performance && performance.now) ? performance.now() : 0) - t0;
|
|
234
|
+
|
|
235
|
+
el('fileStatus').textContent =
|
|
236
|
+
w + '×' + h + ' · ' + ms.toFixed(0) + ' ms · ' + hits.length + ' found';
|
|
237
|
+
if (!show(hits, 'image')) {
|
|
238
|
+
results.innerHTML = '<div class="status">No barcode found in that image.</div>';
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
img.onerror = function () {
|
|
242
|
+
URL.revokeObjectURL(url);
|
|
243
|
+
el('fileStatus').textContent = 'Could not load that file as an image.';
|
|
244
|
+
};
|
|
245
|
+
img.src = url;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ---------------- camera ---------------- */
|
|
249
|
+
|
|
250
|
+
var video = el('video');
|
|
251
|
+
var stream = null;
|
|
252
|
+
var raf = null;
|
|
253
|
+
|
|
254
|
+
var secure = window.isSecureContext ||
|
|
255
|
+
location.protocol === 'https:' ||
|
|
256
|
+
location.hostname === 'localhost';
|
|
257
|
+
|
|
258
|
+
if (!secure) {
|
|
259
|
+
el('camNote').innerHTML =
|
|
260
|
+
'<div class="note">The camera needs a secure context, so it is unavailable ' +
|
|
261
|
+
'when this page is opened from <code>file://</code>. Serve the folder over ' +
|
|
262
|
+
'http and reload — for example <code>npx serve</code> — or use ' +
|
|
263
|
+
'the image input above, which works anywhere.</div>';
|
|
264
|
+
el('camStart').disabled = true;
|
|
265
|
+
} else if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
266
|
+
el('camNote').innerHTML =
|
|
267
|
+
'<div class="note">This browser does not expose a camera API.</div>';
|
|
268
|
+
el('camStart').disabled = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
el('camStart').addEventListener('click', function () {
|
|
272
|
+
// Called from a click, which is what iOS requires for both getUserMedia
|
|
273
|
+
// and the subsequent play().
|
|
274
|
+
navigator.mediaDevices.getUserMedia({
|
|
275
|
+
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } },
|
|
276
|
+
audio: false
|
|
277
|
+
}).then(function (s) {
|
|
278
|
+
stream = s;
|
|
279
|
+
video.srcObject = s;
|
|
280
|
+
video.hidden = false;
|
|
281
|
+
el('camStart').disabled = true;
|
|
282
|
+
el('camStop').disabled = false;
|
|
283
|
+
return video.play();
|
|
284
|
+
}).then(function () {
|
|
285
|
+
loop();
|
|
286
|
+
}).catch(function (e) {
|
|
287
|
+
el('camNote').innerHTML =
|
|
288
|
+
'<div class="note">Camera unavailable: ' + String(e.message || e) + '</div>';
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
el('camStop').addEventListener('click', stopCamera);
|
|
293
|
+
|
|
294
|
+
function stopCamera() {
|
|
295
|
+
if (raf) { cancelAnimationFrame(raf); raf = null; }
|
|
296
|
+
if (stream) {
|
|
297
|
+
stream.getTracks().forEach(function (t) { t.stop(); });
|
|
298
|
+
stream = null;
|
|
299
|
+
}
|
|
300
|
+
video.hidden = true;
|
|
301
|
+
el('camStart').disabled = false;
|
|
302
|
+
el('camStop').disabled = true;
|
|
303
|
+
el('camStatus').textContent = '';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
var frames = 0;
|
|
307
|
+
var lastReport = 0;
|
|
308
|
+
|
|
309
|
+
function loop() {
|
|
310
|
+
raf = requestAnimationFrame(loop);
|
|
311
|
+
if (!video.videoWidth) return;
|
|
312
|
+
|
|
313
|
+
// Scan at a reduced size: a 720p frame carries far more detail than the
|
|
314
|
+
// decoder needs, and the binarizer cost is what sets the frame rate.
|
|
315
|
+
var scale = Math.min(1, 800 / video.videoWidth);
|
|
316
|
+
work.width = Math.round(video.videoWidth * scale);
|
|
317
|
+
work.height = Math.round(video.videoHeight * scale);
|
|
318
|
+
work.getContext('2d', { willReadFrequently: true })
|
|
319
|
+
.drawImage(video, 0, 0, work.width, work.height);
|
|
320
|
+
|
|
321
|
+
frames++;
|
|
322
|
+
var hits = scanCanvas(work);
|
|
323
|
+
if (hits.length) {
|
|
324
|
+
show(hits, 'camera');
|
|
325
|
+
stopCamera();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
var now = (performance && performance.now) ? performance.now() : 0;
|
|
330
|
+
if (now - lastReport > 500) {
|
|
331
|
+
lastReport = now;
|
|
332
|
+
el('camStatus').textContent =
|
|
333
|
+
'scanning ' + work.width + '×' + work.height + ' · ' + frames + ' frames';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
window.addEventListener('pagehide', stopCamera);
|
|
338
|
+
})();
|
|
339
|
+
</script>
|
|
340
|
+
</body>
|
|
341
|
+
</html>
|