@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,731 @@
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 — Create</title>
7
+ <!--
8
+ Sythos Barcode Suite — example
9
+ Copyright (c) 2026 Sythos
10
+ SPDX-License-Identifier: MIT
11
+
12
+ Loads the IIFE bundle with a plain <script> tag, so this page works when
13
+ opened straight from disk (file://). No server, no build step, no modules.
14
+ -->
15
+ <style>
16
+ :root {
17
+ --bg: #10131a;
18
+ --panel: #181c26;
19
+ --line: #262c3a;
20
+ --ink: #e6e9f0;
21
+ --muted: #8b93a7;
22
+ --accent: #5ac8fa;
23
+ --danger: #ff6b6b;
24
+ color-scheme: dark;
25
+ }
26
+ @media (prefers-color-scheme: light) {
27
+ :root {
28
+ --bg: #f5f6f8; --panel: #ffffff; --line: #dfe3ea;
29
+ --ink: #171a21; --muted: #666e80; --accent: #0a84c4;
30
+ color-scheme: light;
31
+ }
32
+ }
33
+ * { box-sizing: border-box; }
34
+ body {
35
+ margin: 0; padding: 24px;
36
+ font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
37
+ background: var(--bg); color: var(--ink);
38
+ }
39
+ header { max-width: 1100px; margin: 0 auto 20px; }
40
+ h1 { font-size: 20px; margin: 0 0 4px; letter-spacing: -0.01em; }
41
+ .sub { color: var(--muted); font-size: 13px; }
42
+ .sub a { color: var(--accent); }
43
+ .layout {
44
+ max-width: 1100px; margin: 0 auto;
45
+ display: grid; grid-template-columns: 340px 1fr; gap: 20px;
46
+ align-items: start;
47
+ }
48
+ @media (max-width: 800px) { .layout { grid-template-columns: 1fr; } }
49
+ .panel {
50
+ background: var(--panel); border: 1px solid var(--line);
51
+ border-radius: 12px; padding: 18px;
52
+ }
53
+ label { display: block; font-size: 12px; color: var(--muted); margin: 14px 0 5px;
54
+ text-transform: uppercase; letter-spacing: 0.05em; }
55
+ label:first-child { margin-top: 0; }
56
+ input, select, textarea {
57
+ width: 100%; padding: 9px 11px; font: inherit; font-size: 14px;
58
+ background: var(--bg); color: var(--ink);
59
+ border: 1px solid var(--line); border-radius: 8px;
60
+ }
61
+ input[type=color] { padding: 3px; height: 38px; }
62
+ input[type=checkbox] { width: auto; height: 18px; padding: 0; margin: 4px 0 0;
63
+ accent-color: var(--accent); }
64
+ textarea { resize: vertical; min-height: 74px; font-family: ui-monospace, monospace; font-size: 13px; }
65
+ .row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
66
+ .btns { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 18px; }
67
+ button {
68
+ flex: 1; min-width: 108px; padding: 10px 14px; font: inherit; font-weight: 600;
69
+ font-size: 13px; cursor: pointer; border-radius: 8px;
70
+ border: 1px solid var(--line); background: var(--bg); color: var(--ink);
71
+ }
72
+ button.primary { background: var(--accent); border-color: var(--accent); color: #051019; }
73
+ button:hover { filter: brightness(1.12); }
74
+ #stage {
75
+ min-height: 300px; display: flex; align-items: center; justify-content: center;
76
+ padding: 24px; background: #fff; border-radius: 8px; overflow: auto;
77
+ }
78
+ #stage canvas { max-width: 100%; height: auto; image-rendering: pixelated; }
79
+ .meta {
80
+ margin-top: 14px; font-family: ui-monospace, monospace; font-size: 12px;
81
+ color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap;
82
+ }
83
+ .meta b { color: var(--ink); font-weight: 600; }
84
+ .error {
85
+ margin-top: 14px; padding: 10px 12px; border-radius: 8px; font-size: 13px;
86
+ background: rgba(255,107,107,0.12); border: 1px solid var(--danger); color: var(--danger);
87
+ }
88
+ .hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
89
+
90
+ /* Content-type builder. The generated fields live in #ctFields; the wrappers
91
+ below would otherwise trip `label:first-child` and lose their top margin. */
92
+ #plainWrap > label:first-child,
93
+ #ctWrap > label:first-child,
94
+ #ctFields > label:first-child,
95
+ #payloadWrap > label:first-child { margin-top: 14px; }
96
+ #ctFields > .row { margin-top: 14px; }
97
+ .payload {
98
+ font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.45;
99
+ white-space: pre-wrap; word-break: break-all;
100
+ padding: 9px 11px; min-height: 38px; max-height: 168px; overflow: auto;
101
+ background: var(--bg); color: var(--ink);
102
+ border: 1px solid var(--line); border-radius: 8px;
103
+ }
104
+ .payload:empty::before { content: "(empty)"; color: var(--muted); }
105
+ </style>
106
+ </head>
107
+ <body>
108
+
109
+ <header>
110
+ <h1>Create a barcode</h1>
111
+ <p class="sub">
112
+ Sythos Barcode Suite &mdash; 100% original JavaScript, zero dependencies, MIT.
113
+ See also <a href="read.html">read.html</a>.
114
+ </p>
115
+ </header>
116
+
117
+ <div class="layout">
118
+ <div class="panel">
119
+ <label for="format">Format</label>
120
+ <select id="format"></select>
121
+ <p class="hint" id="formatHint"></p>
122
+
123
+ <div id="ctWrap">
124
+ <label for="ctype">Content type</label>
125
+ <select id="ctype"></select>
126
+ </div>
127
+
128
+ <div id="ctFields"></div>
129
+
130
+ <div id="plainWrap">
131
+ <label for="text">Content</label>
132
+ <textarea id="text">https://example.com</textarea>
133
+ </div>
134
+
135
+ <div id="payloadWrap">
136
+ <label for="payloadOut">Encoded payload</label>
137
+ <div class="payload" id="payloadOut" tabindex="0"></div>
138
+ <p class="hint" id="payloadLen"></p>
139
+ </div>
140
+
141
+ <div id="qrOpts">
142
+ <div class="row">
143
+ <div>
144
+ <label for="ecc">Error correction</label>
145
+ <select id="ecc">
146
+ <option value="L">L &mdash; 7%</option>
147
+ <option value="M" selected>M &mdash; 15%</option>
148
+ <option value="Q">Q &mdash; 25%</option>
149
+ <option value="H">H &mdash; 30%</option>
150
+ </select>
151
+ </div>
152
+ <div>
153
+ <label for="version">Version</label>
154
+ <select id="version"><option value="">Auto</option></select>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="row">
160
+ <div>
161
+ <label for="scale">Scale (px/module)</label>
162
+ <input type="number" id="scale" value="6" min="1" max="40">
163
+ </div>
164
+ <div>
165
+ <label for="margin">Quiet zone</label>
166
+ <input type="number" id="margin" value="4" min="0" max="20">
167
+ </div>
168
+ </div>
169
+
170
+ <div class="row">
171
+ <div>
172
+ <label for="dark">Dark</label>
173
+ <input type="color" id="dark" value="#000000">
174
+ </div>
175
+ <div>
176
+ <label for="light">Light</label>
177
+ <input type="color" id="light" value="#ffffff">
178
+ </div>
179
+ </div>
180
+
181
+ <div class="btns">
182
+ <button class="primary" id="download-png">Download PNG</button>
183
+ <button id="download-svg">Download SVG</button>
184
+ </div>
185
+ </div>
186
+
187
+ <div class="panel">
188
+ <div id="stage"><canvas id="canvas"></canvas></div>
189
+ <div class="meta" id="meta"></div>
190
+ <div id="err"></div>
191
+ </div>
192
+ </div>
193
+
194
+ <script src="../bundle/sythos-barcode.js"></script>
195
+ <script>
196
+ (function () {
197
+ 'use strict';
198
+ var B = window.SythosBarcode;
199
+
200
+ var el = function (id) { return document.getElementById(id); };
201
+ var formatSel = el('format');
202
+ var canvas = el('canvas');
203
+
204
+ // Sensible sample content per format, so switching format never lands the
205
+ // user on an "invalid payload" error they have to reverse-engineer.
206
+ var SAMPLES = {
207
+ qr: 'https://example.com',
208
+ ean13: '5901234123457',
209
+ ean8: '96385074',
210
+ upca: '036000291452',
211
+ upce: '01234565',
212
+ code128: 'ABC-123456',
213
+ gs1128: '0101234567890128',
214
+ code39: 'SYTHOS-39',
215
+ code93: 'SYTHOS93',
216
+ itf: '12345678',
217
+ itf14: '12345678901231',
218
+ codabar: 'A12345A',
219
+ code11: '123456',
220
+ msi: '1234567',
221
+ pharmacode: '1234'
222
+ };
223
+
224
+ /* ---8<--- payload builders --- (unit-tested from a scratch Node script that
225
+ extracts this exact block out of the file, so the tests never drift from
226
+ the shipped code). Everything between the sentinels must stay dependency
227
+ free: no DOM, no closures over outer state.
228
+
229
+ The escaping conventions genuinely differ and must not be shared:
230
+ - WIFI / MECARD backslash-escape \ ; , : "
231
+ - vCard / vEvent backslash-escape \ ; , and newline -> \n, but NOT ':'
232
+ (escaping the colon there would corrupt every "URL:https://..." value).
233
+ */
234
+
235
+ function ctTrim(s) {
236
+ return String(s === null || s === undefined ? '' : s).replace(/^\s+|\s+$/g, '');
237
+ }
238
+
239
+ function ctStr(s) {
240
+ return String(s === null || s === undefined ? '' : s);
241
+ }
242
+
243
+ /** WIFI: / MECARD: value escaping — backslash before \ ; , : and " */
244
+ function escWifi(s) {
245
+ return ctStr(s).replace(/([\\;,:"])/g, '\\$1');
246
+ }
247
+
248
+ /** vCard 3.0 / iCalendar TEXT value escaping — \ ; , and newlines. No colon. */
249
+ function escVCardText(s) {
250
+ var out = ctStr(s);
251
+ out = out.replace(/\\/g, '\\\\');
252
+ out = out.replace(/;/g, '\\;');
253
+ out = out.replace(/,/g, '\\,');
254
+ out = out.replace(/\r\n|\r|\n/g, '\\n');
255
+ return out;
256
+ }
257
+
258
+ /** "2026-09-01T14:00" (or with seconds) -> "20260901T140000" */
259
+ function icalStamp(s) {
260
+ var raw = ctTrim(s);
261
+ if (!raw) return '';
262
+ var cleaned = raw.replace(/[^0-9T]/g, '');
263
+ var parts = cleaned.split('T');
264
+ var date = parts[0] || '';
265
+ var time = parts.length > 1 ? parts[1] : '';
266
+ if (!date) return '';
267
+ while (time.length < 6) { time += '0'; }
268
+ return date + 'T' + time.substring(0, 6);
269
+ }
270
+
271
+ function buildPlain(v) {
272
+ return ctStr(v.text);
273
+ }
274
+
275
+ function buildUrl(v) {
276
+ var u = ctTrim(v.url);
277
+ if (!u) return '';
278
+ // Scheme test, not a "://" test: mailto:, tel: and geo: have no slashes.
279
+ if (!/^[a-zA-Z][a-zA-Z0-9+.\-]*:/.test(u)) u = 'https://' + u;
280
+ return u;
281
+ }
282
+
283
+ function buildEmail(v) {
284
+ var to = ctTrim(v.to);
285
+ var subject = ctStr(v.subject);
286
+ var body = ctStr(v.body);
287
+ var params = [];
288
+ if (ctTrim(subject)) params.push('subject=' + encodeURIComponent(subject));
289
+ if (ctTrim(body)) params.push('body=' + encodeURIComponent(body));
290
+ // The address itself is not percent-encoded: "@" must survive.
291
+ return 'mailto:' + to + (params.length ? '?' + params.join('&') : '');
292
+ }
293
+
294
+ function buildPhone(v) {
295
+ return 'tel:' + ctTrim(v.number);
296
+ }
297
+
298
+ function buildSms(v) {
299
+ return 'SMSTO:' + ctTrim(v.number) + ':' + ctStr(v.message);
300
+ }
301
+
302
+ function buildWifi(v) {
303
+ var enc = ctTrim(v.encryption) || 'WPA';
304
+ var out = 'WIFI:T:' + enc + ';S:' + escWifi(v.ssid) + ';';
305
+ if (enc !== 'nopass') out += 'P:' + escWifi(v.password) + ';';
306
+ if (v.hidden === true || v.hidden === 'true') out += 'H:true;';
307
+ return out + ';';
308
+ }
309
+
310
+ function buildVCard(v) {
311
+ var first = ctTrim(v.first);
312
+ var last = ctTrim(v.last);
313
+ var lines = ['BEGIN:VCARD', 'VERSION:3.0'];
314
+ // The ";" in N: is structural — escape each component, then join.
315
+ lines.push('N:' + escVCardText(last) + ';' + escVCardText(first));
316
+ var fnParts = [];
317
+ if (first) fnParts.push(first);
318
+ if (last) fnParts.push(last);
319
+ lines.push('FN:' + escVCardText(fnParts.join(' ')));
320
+ if (ctTrim(v.org)) lines.push('ORG:' + escVCardText(ctTrim(v.org)));
321
+ if (ctTrim(v.title)) lines.push('TITLE:' + escVCardText(ctTrim(v.title)));
322
+ if (ctTrim(v.phone)) lines.push('TEL:' + escVCardText(ctTrim(v.phone)));
323
+ if (ctTrim(v.email)) lines.push('EMAIL:' + escVCardText(ctTrim(v.email)));
324
+ if (ctTrim(v.url)) lines.push('URL:' + escVCardText(ctTrim(v.url)));
325
+ // ADR is 7 structural components; a free-text address goes in "street".
326
+ if (ctTrim(v.address)) lines.push('ADR:;;' + escVCardText(ctTrim(v.address)) + ';;;;');
327
+ lines.push('END:VCARD');
328
+ return lines.join('\r\n');
329
+ }
330
+
331
+ function buildMecard(v) {
332
+ var segs = [];
333
+ if (ctTrim(v.name)) segs.push('N:' + escWifi(ctTrim(v.name)));
334
+ if (ctTrim(v.phone)) segs.push('TEL:' + escWifi(ctTrim(v.phone)));
335
+ if (ctTrim(v.email)) segs.push('EMAIL:' + escWifi(ctTrim(v.email)));
336
+ if (ctTrim(v.url)) segs.push('URL:' + escWifi(ctTrim(v.url)));
337
+ var out = 'MECARD:';
338
+ for (var i = 0; i < segs.length; i++) { out += segs[i] + ';'; }
339
+ return out + ';';
340
+ }
341
+
342
+ function buildGeo(v) {
343
+ var lat = ctTrim(v.lat);
344
+ var lon = ctTrim(v.lon);
345
+ var alt = ctTrim(v.alt);
346
+ var out = 'geo:' + lat + ',' + lon;
347
+ if (alt) out += ',' + alt;
348
+ return out;
349
+ }
350
+
351
+ function buildEvent(v) {
352
+ var lines = ['BEGIN:VEVENT'];
353
+ if (ctTrim(v.summary)) lines.push('SUMMARY:' + escVCardText(ctTrim(v.summary)));
354
+ if (ctTrim(v.location)) lines.push('LOCATION:' + escVCardText(ctTrim(v.location)));
355
+ var dtStart = icalStamp(v.start);
356
+ var dtEnd = icalStamp(v.end);
357
+ if (dtStart) lines.push('DTSTART:' + dtStart);
358
+ if (dtEnd) lines.push('DTEND:' + dtEnd);
359
+ lines.push('END:VEVENT');
360
+ return lines.join('\r\n');
361
+ }
362
+
363
+ var PAYLOAD_BUILDERS = {
364
+ text: buildPlain,
365
+ url: buildUrl,
366
+ email: buildEmail,
367
+ phone: buildPhone,
368
+ sms: buildSms,
369
+ wifi: buildWifi,
370
+ vcard: buildVCard,
371
+ mecard: buildMecard,
372
+ geo: buildGeo,
373
+ event: buildEvent
374
+ };
375
+
376
+ /* ---8<--- end payload builders --- */
377
+
378
+ // Data-driven field definitions. `kind` maps straight onto the control that
379
+ // gets created; `half` pairs a field with the next one in a .row grid.
380
+ // Every default is a literal (never derived from `new Date()`) so the page
381
+ // and its unit tests agree, and so no type ever opens in an error state.
382
+ var CONTENT_TYPES = [
383
+ {
384
+ type: 'text', label: 'Plain text',
385
+ fields: [
386
+ { id: 'text', label: 'Text', kind: 'textarea', value: 'https://example.com' }
387
+ ]
388
+ },
389
+ {
390
+ type: 'url', label: 'URL',
391
+ fields: [
392
+ { id: 'url', label: 'URL', kind: 'url', value: 'https://sythos.net',
393
+ placeholder: 'example.com (https:// added if missing)' }
394
+ ]
395
+ },
396
+ {
397
+ type: 'email', label: 'Email',
398
+ fields: [
399
+ { id: 'to', label: 'To', kind: 'email', value: 'hello@example.com' },
400
+ { id: 'subject', label: 'Subject', kind: 'text', value: 'Hello from a QR code' },
401
+ { id: 'body', label: 'Body', kind: 'textarea', value: 'Scanned your code — let’s talk.' }
402
+ ]
403
+ },
404
+ {
405
+ type: 'phone', label: 'Phone',
406
+ fields: [
407
+ { id: 'number', label: 'Phone number', kind: 'tel', value: '+390212345678' }
408
+ ]
409
+ },
410
+ {
411
+ type: 'sms', label: 'SMS',
412
+ fields: [
413
+ { id: 'number', label: 'Phone number', kind: 'tel', value: '+390212345678' },
414
+ { id: 'message', label: 'Message', kind: 'textarea', value: 'Hello!' }
415
+ ]
416
+ },
417
+ {
418
+ type: 'wifi', label: 'Wi-Fi network',
419
+ fields: [
420
+ { id: 'ssid', label: 'Network name (SSID)', kind: 'text', value: 'Sythos-Guest' },
421
+ { id: 'password', label: 'Password', kind: 'text', value: 'correct horse' },
422
+ { id: 'encryption', label: 'Encryption', kind: 'select', value: 'WPA', options: [
423
+ { value: 'WPA', label: 'WPA / WPA2 / WPA3' },
424
+ { value: 'WEP', label: 'WEP' },
425
+ { value: 'nopass', label: 'None (open)' }
426
+ ] },
427
+ { id: 'hidden', label: 'Hidden network', kind: 'checkbox', value: false }
428
+ ]
429
+ },
430
+ {
431
+ type: 'vcard', label: 'Contact (vCard)',
432
+ fields: [
433
+ { id: 'first', label: 'First name', kind: 'text', value: 'Ada', half: true },
434
+ { id: 'last', label: 'Last name', kind: 'text', value: 'Lovelace', half: true },
435
+ { id: 'org', label: 'Organisation', kind: 'text', value: 'Analytical Engines' },
436
+ { id: 'title', label: 'Title', kind: 'text', value: 'Mathematician' },
437
+ { id: 'phone', label: 'Phone', kind: 'tel', value: '+390212345678' },
438
+ { id: 'email', label: 'Email', kind: 'email', value: 'ada@example.com' },
439
+ { id: 'url', label: 'Website', kind: 'url', value: 'https://example.com' },
440
+ { id: 'address', label: 'Address', kind: 'text', value: '12 Baker Street, London' }
441
+ ]
442
+ },
443
+ {
444
+ type: 'mecard', label: 'Contact (MeCard)',
445
+ fields: [
446
+ { id: 'name', label: 'Name', kind: 'text', value: 'Lovelace,Ada' },
447
+ { id: 'phone', label: 'Phone', kind: 'tel', value: '+390212345678' },
448
+ { id: 'email', label: 'Email', kind: 'email', value: 'ada@example.com' },
449
+ { id: 'url', label: 'Website', kind: 'url', value: 'https://example.com' }
450
+ ]
451
+ },
452
+ {
453
+ type: 'geo', label: 'Geo location',
454
+ fields: [
455
+ { id: 'lat', label: 'Latitude', kind: 'number', step: 'any', value: '45.4642', half: true },
456
+ { id: 'lon', label: 'Longitude', kind: 'number', step: 'any', value: '9.1900', half: true },
457
+ { id: 'alt', label: 'Altitude (m, optional)', kind: 'number', step: 'any', value: '' }
458
+ ]
459
+ },
460
+ {
461
+ type: 'event', label: 'Calendar event',
462
+ fields: [
463
+ { id: 'summary', label: 'Summary', kind: 'text', value: 'Sythos Barcode Suite 1.0' },
464
+ { id: 'location', label: 'Location', kind: 'text', value: 'Milano, Italy' },
465
+ { id: 'start', label: 'Start', kind: 'datetime-local', value: '2026-09-01T14:00', half: true },
466
+ { id: 'end', label: 'End', kind: 'datetime-local', value: '2026-09-01T15:30', half: true }
467
+ ]
468
+ }
469
+ ];
470
+
471
+ var ctypeSel = el('ctype');
472
+ var ctFieldsEl = el('ctFields');
473
+ var ctType = CONTENT_TYPES[0].type;
474
+
475
+ // One value bag per content type, seeded from the defaults, so switching
476
+ // away and back inside a session never loses what was typed.
477
+ var ctValues = {};
478
+ CONTENT_TYPES.forEach(function (def) {
479
+ var bag = {};
480
+ def.fields.forEach(function (f) {
481
+ bag[f.id] = (f.kind === 'checkbox')
482
+ ? (f.value === true)
483
+ : (f.value === null || f.value === undefined ? '' : String(f.value));
484
+ });
485
+ ctValues[def.type] = bag;
486
+
487
+ var o = document.createElement('option');
488
+ o.value = def.type;
489
+ o.textContent = def.label;
490
+ ctypeSel.appendChild(o);
491
+ });
492
+ ctypeSel.value = ctType;
493
+
494
+ function ctDef(typeId) {
495
+ var found = CONTENT_TYPES.filter(function (d) { return d.type === typeId; });
496
+ return found.length ? found[0] : CONTENT_TYPES[0];
497
+ }
498
+
499
+ function ctFieldId(typeId, fieldId) {
500
+ // Prefixed so generated ids can never collide with the page's own
501
+ // (#text, #scale, #margin, #version, …).
502
+ return 'ct-' + typeId + '-' + fieldId;
503
+ }
504
+
505
+ function ctLabel(typeId, f) {
506
+ var lab = document.createElement('label');
507
+ lab.setAttribute('for', ctFieldId(typeId, f.id));
508
+ lab.textContent = f.label;
509
+ return lab;
510
+ }
511
+
512
+ function ctControl(typeId, f, bag) {
513
+ var input;
514
+ if (f.kind === 'textarea') {
515
+ input = document.createElement('textarea');
516
+ input.rows = f.rows || 3;
517
+ } else if (f.kind === 'select') {
518
+ input = document.createElement('select');
519
+ (f.options || []).forEach(function (opt) {
520
+ var o = document.createElement('option');
521
+ o.value = opt.value;
522
+ o.textContent = opt.label;
523
+ input.appendChild(o);
524
+ });
525
+ } else {
526
+ input = document.createElement('input');
527
+ // Unknown types (datetime-local on ancient engines) degrade to text.
528
+ try { input.type = f.kind; } catch (e) { input.type = 'text'; }
529
+ if (f.step) input.setAttribute('step', f.step);
530
+ }
531
+ input.id = ctFieldId(typeId, f.id);
532
+ if (f.placeholder) input.setAttribute('placeholder', f.placeholder);
533
+ if (f.kind === 'checkbox') {
534
+ input.checked = bag[f.id] === true;
535
+ } else {
536
+ input.value = bag[f.id] === null || bag[f.id] === undefined ? '' : String(bag[f.id]);
537
+ }
538
+ return input;
539
+ }
540
+
541
+ function ctCell(typeId, f, bag) {
542
+ var cell = document.createElement('div');
543
+ cell.appendChild(ctLabel(typeId, f));
544
+ cell.appendChild(ctControl(typeId, f, bag));
545
+ return cell;
546
+ }
547
+
548
+ function ctRender() {
549
+ var def = ctDef(ctType);
550
+ var bag = ctValues[def.type];
551
+ while (ctFieldsEl.firstChild) ctFieldsEl.removeChild(ctFieldsEl.firstChild);
552
+
553
+ var fields = def.fields;
554
+ var i = 0;
555
+ while (i < fields.length) {
556
+ var f = fields[i];
557
+ var next = (i + 1 < fields.length) ? fields[i + 1] : null;
558
+ if (f.half && next && next.half) {
559
+ var row = document.createElement('div');
560
+ row.className = 'row';
561
+ row.appendChild(ctCell(def.type, f, bag));
562
+ row.appendChild(ctCell(def.type, next, bag));
563
+ ctFieldsEl.appendChild(row);
564
+ i += 2;
565
+ } else {
566
+ ctFieldsEl.appendChild(ctLabel(def.type, f));
567
+ ctFieldsEl.appendChild(ctControl(def.type, f, bag));
568
+ i += 1;
569
+ }
570
+ }
571
+ }
572
+
573
+ function ctSync() {
574
+ var def = ctDef(ctType);
575
+ var bag = ctValues[def.type];
576
+ def.fields.forEach(function (f) {
577
+ var node = document.getElementById(ctFieldId(def.type, f.id));
578
+ if (!node) return;
579
+ bag[f.id] = (f.kind === 'checkbox') ? !!node.checked : node.value;
580
+ });
581
+ }
582
+
583
+ function currentPayload() {
584
+ if (formatSel.value !== 'qr') return el('text').value;
585
+ var build = PAYLOAD_BUILDERS[ctType];
586
+ return build ? build(ctValues[ctType]) : '';
587
+ }
588
+
589
+ var formats = B.listFormats();
590
+ formats.forEach(function (f) {
591
+ var o = document.createElement('option');
592
+ o.value = f.id;
593
+ o.textContent = f.label + (f.canWrite ? '' : ' (unavailable)');
594
+ o.disabled = !f.canWrite;
595
+ formatSel.appendChild(o);
596
+ });
597
+ formatSel.value = formats.some(function (f) { return f.id === 'qr' && f.canWrite; })
598
+ ? 'qr' : 'ean13';
599
+
600
+ var versionSel = el('version');
601
+ for (var v = 1; v <= 40; v++) {
602
+ var o = document.createElement('option');
603
+ o.value = String(v);
604
+ o.textContent = 'Version ' + v + ' (' + (17 + 4 * v) + '×' + (17 + 4 * v) + ')';
605
+ versionSel.appendChild(o);
606
+ }
607
+
608
+ var lastMatrix = null;
609
+
610
+ function currentOptions() {
611
+ return {
612
+ format: formatSel.value,
613
+ ecc: el('ecc').value,
614
+ version: el('version').value ? Number(el('version').value) : undefined,
615
+ checkDigit: true
616
+ };
617
+ }
618
+
619
+ function renderOptions() {
620
+ return {
621
+ scale: Math.max(1, Number(el('scale').value) || 6),
622
+ margin: Math.max(0, Number(el('margin').value) || 0),
623
+ dark: el('dark').value,
624
+ light: el('light').value
625
+ };
626
+ }
627
+
628
+ // User-supplied strings never go through innerHTML.
629
+ function showError(msg) {
630
+ var box = el('err');
631
+ while (box.firstChild) box.removeChild(box.firstChild);
632
+ if (!msg) return;
633
+ var d = document.createElement('div');
634
+ d.className = 'error';
635
+ d.textContent = msg;
636
+ box.appendChild(d);
637
+ }
638
+
639
+ function update() {
640
+ var isQR = formatSel.value === 'qr';
641
+ el('qrOpts').style.display = isQR ? '' : 'none';
642
+ el('ctWrap').style.display = isQR ? '' : 'none';
643
+ ctFieldsEl.style.display = isQR ? '' : 'none';
644
+ el('payloadWrap').style.display = isQR ? '' : 'none';
645
+ el('plainWrap').style.display = isQR ? 'none' : '';
646
+
647
+ var info = formats.filter(function (f) { return f.id === formatSel.value; })[0];
648
+ el('formatHint').textContent = info
649
+ ? (info.kind + ' · ' + (info.canRead ? 'this suite can also read it' : 'write-only in this build'))
650
+ : '';
651
+
652
+ var payload = currentPayload();
653
+ el('payloadOut').textContent = payload;
654
+ el('payloadLen').textContent = payload.length + ' character' + (payload.length === 1 ? '' : 's');
655
+
656
+ showError('');
657
+ try {
658
+ lastMatrix = B.encode(payload, currentOptions());
659
+ } catch (e) {
660
+ lastMatrix = null;
661
+ showError(String(e && e.message ? e.message : e));
662
+ el('meta').innerHTML = '';
663
+ var ctx = canvas.getContext('2d');
664
+ canvas.width = 10; canvas.height = 10;
665
+ if (ctx) ctx.clearRect(0, 0, 10, 10);
666
+ return;
667
+ }
668
+
669
+ // renderToCanvasAuto tries WebGL2 first and falls back to the 2D context,
670
+ // reporting which one actually ran.
671
+ var out = B.renderToCanvasAuto(lastMatrix, canvas, renderOptions());
672
+
673
+ el('meta').innerHTML =
674
+ '<span>modules <b>' + lastMatrix.width + '×' + lastMatrix.height + '</b></span>' +
675
+ '<span>pixels <b>' + canvas.width + '×' + canvas.height + '</b></span>' +
676
+ '<span>backend <b>' + out.backend + '</b></span>' +
677
+ '<span>bytes in <b>' + payload.length + '</b></span>';
678
+ }
679
+
680
+ function saveBlob(blob, name) {
681
+ var url = URL.createObjectURL(blob);
682
+ var a = document.createElement('a');
683
+ a.href = url;
684
+ a.download = name;
685
+ document.body.appendChild(a);
686
+ a.click();
687
+ document.body.removeChild(a);
688
+ setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
689
+ }
690
+
691
+ el('download-svg').addEventListener('click', function () {
692
+ if (!lastMatrix) return;
693
+ var svg = B.toSVG(lastMatrix, renderOptions());
694
+ saveBlob(new Blob([svg], { type: 'image/svg+xml' }), formatSel.value + '.svg');
695
+ });
696
+
697
+ el('download-png').addEventListener('click', function () {
698
+ if (!lastMatrix) return;
699
+ B.toPNG(lastMatrix, renderOptions()).then(function (bytes) {
700
+ saveBlob(new Blob([bytes], { type: 'image/png' }), formatSel.value + '.png');
701
+ });
702
+ });
703
+
704
+ formatSel.addEventListener('change', function () {
705
+ if (SAMPLES[formatSel.value]) el('text').value = SAMPLES[formatSel.value];
706
+ update();
707
+ });
708
+
709
+ ctypeSel.addEventListener('change', function () {
710
+ ctSync(); // keep whatever was typed under the old type
711
+ ctType = ctypeSel.value;
712
+ ctRender();
713
+ update();
714
+ });
715
+
716
+ // Delegated: the generated controls are replaced wholesale on every type
717
+ // switch, so listeners live on the container instead. Both events bubble.
718
+ ctFieldsEl.addEventListener('input', function () { ctSync(); update(); });
719
+ ctFieldsEl.addEventListener('change', function () { ctSync(); update(); });
720
+
721
+ ['text', 'ecc', 'version', 'scale', 'margin', 'dark', 'light'].forEach(function (id) {
722
+ el(id).addEventListener('input', update);
723
+ el(id).addEventListener('change', update);
724
+ });
725
+
726
+ ctRender();
727
+ update();
728
+ })();
729
+ </script>
730
+ </body>
731
+ </html>