@sythos/js_barcode_universal 1.2.5 → 1.3.1
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 +8 -1
- package/NOTICE.md +3 -0
- package/README.md +548 -463
- package/bundle/sythos-barcode.esm.js +2377 -3
- package/bundle/sythos-barcode.js +2365 -3
- package/examples/create.html +1009 -730
- package/licenses/README.md +35 -58
- package/licenses/aztec-code.license +15 -13
- package/licenses/codabar.license +18 -15
- package/licenses/code-11.license +11 -9
- package/licenses/code-128.license +10 -8
- package/licenses/code-39.license +10 -8
- package/licenses/code-93.license +16 -14
- package/licenses/data-matrix.license +15 -13
- package/licenses/ean-13.license +10 -8
- package/licenses/ean-8.license +10 -8
- package/licenses/frameqr.license +84 -0
- package/licenses/gs1-128.license +10 -8
- package/licenses/isbn.license +10 -8
- package/licenses/itf-14.license +10 -8
- package/licenses/itf.license +9 -7
- package/licenses/micro-qr.license +79 -0
- package/licenses/micropdf417.license +52 -67
- package/licenses/msi-plessey.license +13 -11
- package/licenses/pdf417.license +78 -37
- package/licenses/pharmacode.license +14 -12
- package/licenses/qr-code.license +9 -7
- package/licenses/rmqr.license +79 -0
- package/licenses/upc-a.license +12 -10
- package/licenses/upc-e.license +10 -8
- package/package.json +97 -88
- 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/frameqr/decoder.js +239 -0
- package/src/frameqr/detector.js +192 -0
- package/src/frameqr/encoder.js +156 -0
- package/src/frameqr/index.js +42 -0
- package/src/frameqr/tables.js +270 -0
- package/src/index.js +91 -2
- package/src/microqr/decoder.js +245 -0
- package/src/microqr/detector.js +355 -0
- package/src/microqr/encoder.js +269 -0
- package/src/microqr/index.js +36 -0
- package/src/microqr/tables.js +316 -0
- package/src/oned/index.js +59 -59
- package/src/rmqr/decoder.js +101 -0
- package/src/rmqr/detector.js +90 -0
- package/src/rmqr/encoder.js +172 -0
- package/src/rmqr/index.js +37 -0
- package/src/rmqr/tables.js +154 -0
package/examples/create.html
CHANGED
|
@@ -1,734 +1,1013 @@
|
|
|
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
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
.meta
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#ctFields
|
|
95
|
-
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
</
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</
|
|
179
|
-
</div>
|
|
180
|
-
|
|
181
|
-
<div class="
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
position: relative; padding: 24px; background: #fff; border-radius: 8px; overflow: auto;
|
|
77
|
+
}
|
|
78
|
+
#stage canvas { max-width: 100%; height: auto; image-rendering: pixelated; }
|
|
79
|
+
#stage .frame-art-preview {
|
|
80
|
+
position: absolute; z-index: 2; display: none; pointer-events: none;
|
|
81
|
+
object-fit: cover; object-position: center; transform-origin: center;
|
|
82
|
+
}
|
|
83
|
+
.meta {
|
|
84
|
+
margin-top: 14px; font-family: ui-monospace, monospace; font-size: 12px;
|
|
85
|
+
color: var(--muted); display: flex; gap: 16px; flex-wrap: wrap;
|
|
86
|
+
}
|
|
87
|
+
.meta b { color: var(--ink); font-weight: 600; }
|
|
88
|
+
.error {
|
|
89
|
+
margin-top: 14px; padding: 10px 12px; border-radius: 8px; font-size: 13px;
|
|
90
|
+
background: rgba(255,107,107,0.12); border: 1px solid var(--danger); color: var(--danger);
|
|
91
|
+
}
|
|
92
|
+
.hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
|
93
|
+
|
|
94
|
+
/* Content-type builder. The generated fields live in #ctFields; the wrappers
|
|
95
|
+
below would otherwise trip `label:first-child` and lose their top margin. */
|
|
96
|
+
#plainWrap > label:first-child,
|
|
97
|
+
#ctWrap > label:first-child,
|
|
98
|
+
#ctFields > label:first-child,
|
|
99
|
+
#payloadWrap > label:first-child { margin-top: 14px; }
|
|
100
|
+
#ctFields > .row { margin-top: 14px; }
|
|
101
|
+
.payload {
|
|
102
|
+
font-family: ui-monospace, monospace; font-size: 12px; line-height: 1.45;
|
|
103
|
+
white-space: pre-wrap; word-break: break-all;
|
|
104
|
+
padding: 9px 11px; min-height: 38px; max-height: 168px; overflow: auto;
|
|
105
|
+
background: var(--bg); color: var(--ink);
|
|
106
|
+
border: 1px solid var(--line); border-radius: 8px;
|
|
107
|
+
}
|
|
108
|
+
.payload:empty::before { content: "(empty)"; color: var(--muted); }
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
|
|
113
|
+
<header>
|
|
114
|
+
<h1>Create a barcode</h1>
|
|
115
|
+
<p class="sub">
|
|
116
|
+
Sythos Barcode Suite — 100% original JavaScript, zero dependencies, MIT.
|
|
117
|
+
See also <a href="read.html">read.html</a>.
|
|
118
|
+
</p>
|
|
119
|
+
</header>
|
|
120
|
+
|
|
121
|
+
<div class="layout">
|
|
122
|
+
<div class="panel">
|
|
123
|
+
<label for="format">Format</label>
|
|
124
|
+
<select id="format"></select>
|
|
125
|
+
<p class="hint" id="formatHint"></p>
|
|
126
|
+
|
|
127
|
+
<div id="ctWrap">
|
|
128
|
+
<label for="ctype">Content type</label>
|
|
129
|
+
<select id="ctype"></select>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div id="ctFields"></div>
|
|
133
|
+
|
|
134
|
+
<div id="plainWrap">
|
|
135
|
+
<label for="text">Content</label>
|
|
136
|
+
<textarea id="text">https://www.sythos.net/</textarea>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div id="payloadWrap">
|
|
140
|
+
<label for="payloadOut">Encoded payload</label>
|
|
141
|
+
<div class="payload" id="payloadOut" tabindex="0"></div>
|
|
142
|
+
<p class="hint" id="payloadLen"></p>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div id="qrOpts">
|
|
146
|
+
<div class="row">
|
|
147
|
+
<div>
|
|
148
|
+
<label for="ecc">Error correction</label>
|
|
149
|
+
<select id="ecc">
|
|
150
|
+
<option value="L">L — 7%</option>
|
|
151
|
+
<option value="M" selected>M — 15%</option>
|
|
152
|
+
<option value="Q">Q — 25%</option>
|
|
153
|
+
<option value="H">H — 30%</option>
|
|
154
|
+
</select>
|
|
155
|
+
</div>
|
|
156
|
+
<div>
|
|
157
|
+
<label for="version">Version</label>
|
|
158
|
+
<select id="version"><option value="">Auto</option></select>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div id="frameOpts" style="display:none">
|
|
164
|
+
<p class="hint">Sythos Canvas QR profile: this is not certified DENSO FrameQR interoperability.</p>
|
|
165
|
+
<label for="frameCanvasShape">Canvas shape</label>
|
|
166
|
+
<select id="frameCanvasShape">
|
|
167
|
+
<option value="square" selected>Square</option>
|
|
168
|
+
<option value="circle">Circle</option>
|
|
169
|
+
<option value="diamond">Diamond</option>
|
|
170
|
+
</select>
|
|
171
|
+
<label for="frameCanvasSize">Canvas size (QR modules)</label>
|
|
172
|
+
<input type="number" id="frameCanvasSize" value="5" min="1" step="2">
|
|
173
|
+
<label for="frameCanvasUrl">Remote canvas image</label>
|
|
174
|
+
<input type="url" id="frameCanvasUrl" value="https://www.sythos.net/favicon.ico">
|
|
175
|
+
<p class="hint" id="frameArtworkStatus">
|
|
176
|
+
Fallback: https://www.sythos.net/apple-touch-icon.png. The asset is never copied locally.
|
|
177
|
+
CORS is required to include remote artwork in PNG output.
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="row">
|
|
182
|
+
<div>
|
|
183
|
+
<label for="scale">Scale (px/module)</label>
|
|
184
|
+
<input type="number" id="scale" value="6" min="1" max="40">
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<label for="margin">Quiet zone</label>
|
|
188
|
+
<input type="number" id="margin" value="4" min="0" max="20">
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="row">
|
|
193
|
+
<div>
|
|
194
|
+
<label for="dark">Dark</label>
|
|
195
|
+
<input type="color" id="dark" value="#000000">
|
|
196
|
+
</div>
|
|
197
|
+
<div>
|
|
198
|
+
<label for="light">Light</label>
|
|
199
|
+
<input type="color" id="light" value="#ffffff">
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="btns">
|
|
204
|
+
<button class="primary" id="download-png">Download PNG</button>
|
|
205
|
+
<button id="download-svg">Download SVG</button>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div class="panel">
|
|
210
|
+
<div id="stage">
|
|
211
|
+
<canvas id="canvas"></canvas>
|
|
212
|
+
<img id="frameArtworkPreview" class="frame-art-preview" alt="" aria-hidden="true">
|
|
213
|
+
</div>
|
|
214
|
+
<div class="meta" id="meta"></div>
|
|
215
|
+
<div id="err"></div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<script src="../bundle/sythos-barcode.js"></script>
|
|
220
|
+
<script>
|
|
221
|
+
(function () {
|
|
222
|
+
'use strict';
|
|
223
|
+
var B = window.SythosBarcode;
|
|
224
|
+
|
|
225
|
+
var el = function (id) { return document.getElementById(id); };
|
|
226
|
+
var formatSel = el('format');
|
|
227
|
+
var canvas = el('canvas');
|
|
228
|
+
var stage = el('stage');
|
|
229
|
+
var frameArtworkPreview = el('frameArtworkPreview');
|
|
230
|
+
var frameArtworkState = {
|
|
231
|
+
image: null,
|
|
232
|
+
url: '',
|
|
233
|
+
cors: false,
|
|
234
|
+
status: 'idle',
|
|
235
|
+
request: ''
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Sensible sample content per format, so switching format never lands the
|
|
239
|
+
// user on an "invalid payload" error they have to reverse-engineer.
|
|
206
240
|
var SAMPLES = {
|
|
207
|
-
qr: 'https://
|
|
241
|
+
qr: 'https://www.sythos.net/',
|
|
208
242
|
datamatrix: 'Data Matrix ECC 200',
|
|
209
243
|
aztec: 'Aztec Code — UTF-8 👋',
|
|
210
244
|
pdf417: 'PDF417 sample payload',
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
out =
|
|
257
|
-
out = out.replace(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
var
|
|
267
|
-
|
|
268
|
-
var
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
var
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
var
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
if (ctTrim(v.
|
|
327
|
-
if (ctTrim(v.
|
|
328
|
-
|
|
329
|
-
if (ctTrim(v.
|
|
330
|
-
lines.push('
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (ctTrim(v.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
var
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
var
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
{ id: '
|
|
440
|
-
{ id: '
|
|
441
|
-
{ id: '
|
|
442
|
-
{ id: '
|
|
443
|
-
{ id: '
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
{ id: '
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
{ id: '
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
input = document.createElement('
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
var
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
row.
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
ctFieldsEl.appendChild(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
? '
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
var
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
el('
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
var
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
245
|
+
microqr: '12345',
|
|
246
|
+
rmqr: 'rMQR sample payload',
|
|
247
|
+
frameqr: 'https://www.sythos.net/',
|
|
248
|
+
ean13: '5901234123457',
|
|
249
|
+
ean8: '96385074',
|
|
250
|
+
upca: '036000291452',
|
|
251
|
+
upce: '01234565',
|
|
252
|
+
code128: 'ABC-123456',
|
|
253
|
+
gs1128: '0101234567890128',
|
|
254
|
+
code39: 'SYTHOS-39',
|
|
255
|
+
code93: 'SYTHOS93',
|
|
256
|
+
itf: '12345678',
|
|
257
|
+
itf14: '12345678901231',
|
|
258
|
+
codabar: 'A12345A',
|
|
259
|
+
code11: '123456',
|
|
260
|
+
msi: '1234567',
|
|
261
|
+
pharmacode: '1234'
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/* ---8<--- payload builders --- (unit-tested from a scratch Node script that
|
|
265
|
+
extracts this exact block out of the file, so the tests never drift from
|
|
266
|
+
the shipped code). Everything between the sentinels must stay dependency
|
|
267
|
+
free: no DOM, no closures over outer state.
|
|
268
|
+
|
|
269
|
+
The escaping conventions genuinely differ and must not be shared:
|
|
270
|
+
- WIFI / MECARD backslash-escape \ ; , : "
|
|
271
|
+
- vCard / vEvent backslash-escape \ ; , and newline -> \n, but NOT ':'
|
|
272
|
+
(escaping the colon there would corrupt every "URL:https://..." value).
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
function ctTrim(s) {
|
|
276
|
+
return String(s === null || s === undefined ? '' : s).replace(/^\s+|\s+$/g, '');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function ctStr(s) {
|
|
280
|
+
return String(s === null || s === undefined ? '' : s);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** WIFI: / MECARD: value escaping — backslash before \ ; , : and " */
|
|
284
|
+
function escWifi(s) {
|
|
285
|
+
return ctStr(s).replace(/([\\;,:"])/g, '\\$1');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** vCard 3.0 / iCalendar TEXT value escaping — \ ; , and newlines. No colon. */
|
|
289
|
+
function escVCardText(s) {
|
|
290
|
+
var out = ctStr(s);
|
|
291
|
+
out = out.replace(/\\/g, '\\\\');
|
|
292
|
+
out = out.replace(/;/g, '\\;');
|
|
293
|
+
out = out.replace(/,/g, '\\,');
|
|
294
|
+
out = out.replace(/\r\n|\r|\n/g, '\\n');
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** "2026-09-01T14:00" (or with seconds) -> "20260901T140000" */
|
|
299
|
+
function icalStamp(s) {
|
|
300
|
+
var raw = ctTrim(s);
|
|
301
|
+
if (!raw) return '';
|
|
302
|
+
var cleaned = raw.replace(/[^0-9T]/g, '');
|
|
303
|
+
var parts = cleaned.split('T');
|
|
304
|
+
var date = parts[0] || '';
|
|
305
|
+
var time = parts.length > 1 ? parts[1] : '';
|
|
306
|
+
if (!date) return '';
|
|
307
|
+
while (time.length < 6) { time += '0'; }
|
|
308
|
+
return date + 'T' + time.substring(0, 6);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildPlain(v) {
|
|
312
|
+
return ctStr(v.text);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildUrl(v) {
|
|
316
|
+
var u = ctTrim(v.url);
|
|
317
|
+
if (!u) return '';
|
|
318
|
+
// Scheme test, not a "://" test: mailto:, tel: and geo: have no slashes.
|
|
319
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.\-]*:/.test(u)) u = 'https://' + u;
|
|
320
|
+
return u;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function buildEmail(v) {
|
|
324
|
+
var to = ctTrim(v.to);
|
|
325
|
+
var subject = ctStr(v.subject);
|
|
326
|
+
var body = ctStr(v.body);
|
|
327
|
+
var params = [];
|
|
328
|
+
if (ctTrim(subject)) params.push('subject=' + encodeURIComponent(subject));
|
|
329
|
+
if (ctTrim(body)) params.push('body=' + encodeURIComponent(body));
|
|
330
|
+
// The address itself is not percent-encoded: "@" must survive.
|
|
331
|
+
return 'mailto:' + to + (params.length ? '?' + params.join('&') : '');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildPhone(v) {
|
|
335
|
+
return 'tel:' + ctTrim(v.number);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildSms(v) {
|
|
339
|
+
return 'SMSTO:' + ctTrim(v.number) + ':' + ctStr(v.message);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildWifi(v) {
|
|
343
|
+
var enc = ctTrim(v.encryption) || 'WPA';
|
|
344
|
+
var out = 'WIFI:T:' + enc + ';S:' + escWifi(v.ssid) + ';';
|
|
345
|
+
if (enc !== 'nopass') out += 'P:' + escWifi(v.password) + ';';
|
|
346
|
+
if (v.hidden === true || v.hidden === 'true') out += 'H:true;';
|
|
347
|
+
return out + ';';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildVCard(v) {
|
|
351
|
+
var first = ctTrim(v.first);
|
|
352
|
+
var last = ctTrim(v.last);
|
|
353
|
+
var lines = ['BEGIN:VCARD', 'VERSION:3.0'];
|
|
354
|
+
// The ";" in N: is structural — escape each component, then join.
|
|
355
|
+
lines.push('N:' + escVCardText(last) + ';' + escVCardText(first));
|
|
356
|
+
var fnParts = [];
|
|
357
|
+
if (first) fnParts.push(first);
|
|
358
|
+
if (last) fnParts.push(last);
|
|
359
|
+
lines.push('FN:' + escVCardText(fnParts.join(' ')));
|
|
360
|
+
if (ctTrim(v.org)) lines.push('ORG:' + escVCardText(ctTrim(v.org)));
|
|
361
|
+
if (ctTrim(v.title)) lines.push('TITLE:' + escVCardText(ctTrim(v.title)));
|
|
362
|
+
if (ctTrim(v.phone)) lines.push('TEL:' + escVCardText(ctTrim(v.phone)));
|
|
363
|
+
if (ctTrim(v.email)) lines.push('EMAIL:' + escVCardText(ctTrim(v.email)));
|
|
364
|
+
if (ctTrim(v.url)) lines.push('URL:' + escVCardText(ctTrim(v.url)));
|
|
365
|
+
// ADR is 7 structural components; a free-text address goes in "street".
|
|
366
|
+
if (ctTrim(v.address)) lines.push('ADR:;;' + escVCardText(ctTrim(v.address)) + ';;;;');
|
|
367
|
+
lines.push('END:VCARD');
|
|
368
|
+
return lines.join('\r\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function buildMecard(v) {
|
|
372
|
+
var segs = [];
|
|
373
|
+
if (ctTrim(v.name)) segs.push('N:' + escWifi(ctTrim(v.name)));
|
|
374
|
+
if (ctTrim(v.phone)) segs.push('TEL:' + escWifi(ctTrim(v.phone)));
|
|
375
|
+
if (ctTrim(v.email)) segs.push('EMAIL:' + escWifi(ctTrim(v.email)));
|
|
376
|
+
if (ctTrim(v.url)) segs.push('URL:' + escWifi(ctTrim(v.url)));
|
|
377
|
+
var out = 'MECARD:';
|
|
378
|
+
for (var i = 0; i < segs.length; i++) { out += segs[i] + ';'; }
|
|
379
|
+
return out + ';';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildGeo(v) {
|
|
383
|
+
var lat = ctTrim(v.lat);
|
|
384
|
+
var lon = ctTrim(v.lon);
|
|
385
|
+
var alt = ctTrim(v.alt);
|
|
386
|
+
var out = 'geo:' + lat + ',' + lon;
|
|
387
|
+
if (alt) out += ',' + alt;
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function buildEvent(v) {
|
|
392
|
+
var lines = ['BEGIN:VEVENT'];
|
|
393
|
+
if (ctTrim(v.summary)) lines.push('SUMMARY:' + escVCardText(ctTrim(v.summary)));
|
|
394
|
+
if (ctTrim(v.location)) lines.push('LOCATION:' + escVCardText(ctTrim(v.location)));
|
|
395
|
+
var dtStart = icalStamp(v.start);
|
|
396
|
+
var dtEnd = icalStamp(v.end);
|
|
397
|
+
if (dtStart) lines.push('DTSTART:' + dtStart);
|
|
398
|
+
if (dtEnd) lines.push('DTEND:' + dtEnd);
|
|
399
|
+
lines.push('END:VEVENT');
|
|
400
|
+
return lines.join('\r\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
var PAYLOAD_BUILDERS = {
|
|
404
|
+
text: buildPlain,
|
|
405
|
+
url: buildUrl,
|
|
406
|
+
email: buildEmail,
|
|
407
|
+
phone: buildPhone,
|
|
408
|
+
sms: buildSms,
|
|
409
|
+
wifi: buildWifi,
|
|
410
|
+
vcard: buildVCard,
|
|
411
|
+
mecard: buildMecard,
|
|
412
|
+
geo: buildGeo,
|
|
413
|
+
event: buildEvent
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/* ---8<--- end payload builders --- */
|
|
417
|
+
|
|
418
|
+
// Data-driven field definitions. `kind` maps straight onto the control that
|
|
419
|
+
// gets created; `half` pairs a field with the next one in a .row grid.
|
|
420
|
+
// Every default is a literal (never derived from `new Date()`) so the page
|
|
421
|
+
// and its unit tests agree, and so no type ever opens in an error state.
|
|
422
|
+
var CONTENT_TYPES = [
|
|
423
|
+
{
|
|
424
|
+
type: 'text', label: 'Plain text',
|
|
425
|
+
fields: [
|
|
426
|
+
{ id: 'text', label: 'Text', kind: 'textarea', value: 'https://www.sythos.net/' }
|
|
427
|
+
]
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
type: 'url', label: 'URL',
|
|
431
|
+
fields: [
|
|
432
|
+
{ id: 'url', label: 'URL', kind: 'url', value: 'https://www.sythos.net/',
|
|
433
|
+
placeholder: 'example.com (https:// added if missing)' }
|
|
434
|
+
]
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
type: 'email', label: 'Email',
|
|
438
|
+
fields: [
|
|
439
|
+
{ id: 'to', label: 'To', kind: 'email', value: 'hello@example.com' },
|
|
440
|
+
{ id: 'subject', label: 'Subject', kind: 'text', value: 'Hello from a QR code' },
|
|
441
|
+
{ id: 'body', label: 'Body', kind: 'textarea', value: 'Scanned your code — let’s talk.' }
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
type: 'phone', label: 'Phone',
|
|
446
|
+
fields: [
|
|
447
|
+
{ id: 'number', label: 'Phone number', kind: 'tel', value: '+390212345678' }
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
type: 'sms', label: 'SMS',
|
|
452
|
+
fields: [
|
|
453
|
+
{ id: 'number', label: 'Phone number', kind: 'tel', value: '+390212345678' },
|
|
454
|
+
{ id: 'message', label: 'Message', kind: 'textarea', value: 'Hello!' }
|
|
455
|
+
]
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
type: 'wifi', label: 'Wi-Fi network',
|
|
459
|
+
fields: [
|
|
460
|
+
{ id: 'ssid', label: 'Network name (SSID)', kind: 'text', value: 'Sythos-Guest' },
|
|
461
|
+
{ id: 'password', label: 'Password', kind: 'text', value: 'correct horse' },
|
|
462
|
+
{ id: 'encryption', label: 'Encryption', kind: 'select', value: 'WPA', options: [
|
|
463
|
+
{ value: 'WPA', label: 'WPA / WPA2 / WPA3' },
|
|
464
|
+
{ value: 'WEP', label: 'WEP' },
|
|
465
|
+
{ value: 'nopass', label: 'None (open)' }
|
|
466
|
+
] },
|
|
467
|
+
{ id: 'hidden', label: 'Hidden network', kind: 'checkbox', value: false }
|
|
468
|
+
]
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
type: 'vcard', label: 'Contact (vCard)',
|
|
472
|
+
fields: [
|
|
473
|
+
{ id: 'first', label: 'First name', kind: 'text', value: 'Ada', half: true },
|
|
474
|
+
{ id: 'last', label: 'Last name', kind: 'text', value: 'Lovelace', half: true },
|
|
475
|
+
{ id: 'org', label: 'Organisation', kind: 'text', value: 'Analytical Engines' },
|
|
476
|
+
{ id: 'title', label: 'Title', kind: 'text', value: 'Mathematician' },
|
|
477
|
+
{ id: 'phone', label: 'Phone', kind: 'tel', value: '+390212345678' },
|
|
478
|
+
{ id: 'email', label: 'Email', kind: 'email', value: 'ada@example.com' },
|
|
479
|
+
{ id: 'url', label: 'Website', kind: 'url', value: 'https://example.com' },
|
|
480
|
+
{ id: 'address', label: 'Address', kind: 'text', value: '12 Baker Street, London' }
|
|
481
|
+
]
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
type: 'mecard', label: 'Contact (MeCard)',
|
|
485
|
+
fields: [
|
|
486
|
+
{ id: 'name', label: 'Name', kind: 'text', value: 'Lovelace,Ada' },
|
|
487
|
+
{ id: 'phone', label: 'Phone', kind: 'tel', value: '+390212345678' },
|
|
488
|
+
{ id: 'email', label: 'Email', kind: 'email', value: 'ada@example.com' },
|
|
489
|
+
{ id: 'url', label: 'Website', kind: 'url', value: 'https://example.com' }
|
|
490
|
+
]
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
type: 'geo', label: 'Geo location',
|
|
494
|
+
fields: [
|
|
495
|
+
{ id: 'lat', label: 'Latitude', kind: 'number', step: 'any', value: '45.4642', half: true },
|
|
496
|
+
{ id: 'lon', label: 'Longitude', kind: 'number', step: 'any', value: '9.1900', half: true },
|
|
497
|
+
{ id: 'alt', label: 'Altitude (m, optional)', kind: 'number', step: 'any', value: '' }
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
type: 'event', label: 'Calendar event',
|
|
502
|
+
fields: [
|
|
503
|
+
{ id: 'summary', label: 'Summary', kind: 'text', value: 'Sythos Barcode Suite 1.0' },
|
|
504
|
+
{ id: 'location', label: 'Location', kind: 'text', value: 'Milano, Italy' },
|
|
505
|
+
{ id: 'start', label: 'Start', kind: 'datetime-local', value: '2026-09-01T14:00', half: true },
|
|
506
|
+
{ id: 'end', label: 'End', kind: 'datetime-local', value: '2026-09-01T15:30', half: true }
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
var ctypeSel = el('ctype');
|
|
512
|
+
var ctFieldsEl = el('ctFields');
|
|
513
|
+
var ctType = CONTENT_TYPES[0].type;
|
|
514
|
+
|
|
515
|
+
// One value bag per content type, seeded from the defaults, so switching
|
|
516
|
+
// away and back inside a session never loses what was typed.
|
|
517
|
+
var ctValues = {};
|
|
518
|
+
CONTENT_TYPES.forEach(function (def) {
|
|
519
|
+
var bag = {};
|
|
520
|
+
def.fields.forEach(function (f) {
|
|
521
|
+
bag[f.id] = (f.kind === 'checkbox')
|
|
522
|
+
? (f.value === true)
|
|
523
|
+
: (f.value === null || f.value === undefined ? '' : String(f.value));
|
|
524
|
+
});
|
|
525
|
+
ctValues[def.type] = bag;
|
|
526
|
+
|
|
527
|
+
var o = document.createElement('option');
|
|
528
|
+
o.value = def.type;
|
|
529
|
+
o.textContent = def.label;
|
|
530
|
+
ctypeSel.appendChild(o);
|
|
531
|
+
});
|
|
532
|
+
ctypeSel.value = ctType;
|
|
533
|
+
|
|
534
|
+
function ctDef(typeId) {
|
|
535
|
+
var found = CONTENT_TYPES.filter(function (d) { return d.type === typeId; });
|
|
536
|
+
return found.length ? found[0] : CONTENT_TYPES[0];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function ctFieldId(typeId, fieldId) {
|
|
540
|
+
// Prefixed so generated ids can never collide with the page's own
|
|
541
|
+
// (#text, #scale, #margin, #version, …).
|
|
542
|
+
return 'ct-' + typeId + '-' + fieldId;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function ctLabel(typeId, f) {
|
|
546
|
+
var lab = document.createElement('label');
|
|
547
|
+
lab.setAttribute('for', ctFieldId(typeId, f.id));
|
|
548
|
+
lab.textContent = f.label;
|
|
549
|
+
return lab;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function ctControl(typeId, f, bag) {
|
|
553
|
+
var input;
|
|
554
|
+
if (f.kind === 'textarea') {
|
|
555
|
+
input = document.createElement('textarea');
|
|
556
|
+
input.rows = f.rows || 3;
|
|
557
|
+
} else if (f.kind === 'select') {
|
|
558
|
+
input = document.createElement('select');
|
|
559
|
+
(f.options || []).forEach(function (opt) {
|
|
560
|
+
var o = document.createElement('option');
|
|
561
|
+
o.value = opt.value;
|
|
562
|
+
o.textContent = opt.label;
|
|
563
|
+
input.appendChild(o);
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
input = document.createElement('input');
|
|
567
|
+
// Unknown types (datetime-local on ancient engines) degrade to text.
|
|
568
|
+
try { input.type = f.kind; } catch (e) { input.type = 'text'; }
|
|
569
|
+
if (f.step) input.setAttribute('step', f.step);
|
|
570
|
+
}
|
|
571
|
+
input.id = ctFieldId(typeId, f.id);
|
|
572
|
+
if (f.placeholder) input.setAttribute('placeholder', f.placeholder);
|
|
573
|
+
if (f.kind === 'checkbox') {
|
|
574
|
+
input.checked = bag[f.id] === true;
|
|
575
|
+
} else {
|
|
576
|
+
input.value = bag[f.id] === null || bag[f.id] === undefined ? '' : String(bag[f.id]);
|
|
577
|
+
}
|
|
578
|
+
return input;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function ctCell(typeId, f, bag) {
|
|
582
|
+
var cell = document.createElement('div');
|
|
583
|
+
cell.appendChild(ctLabel(typeId, f));
|
|
584
|
+
cell.appendChild(ctControl(typeId, f, bag));
|
|
585
|
+
return cell;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function ctRender() {
|
|
589
|
+
var def = ctDef(ctType);
|
|
590
|
+
var bag = ctValues[def.type];
|
|
591
|
+
while (ctFieldsEl.firstChild) ctFieldsEl.removeChild(ctFieldsEl.firstChild);
|
|
592
|
+
|
|
593
|
+
var fields = def.fields;
|
|
594
|
+
var i = 0;
|
|
595
|
+
while (i < fields.length) {
|
|
596
|
+
var f = fields[i];
|
|
597
|
+
var next = (i + 1 < fields.length) ? fields[i + 1] : null;
|
|
598
|
+
if (f.half && next && next.half) {
|
|
599
|
+
var row = document.createElement('div');
|
|
600
|
+
row.className = 'row';
|
|
601
|
+
row.appendChild(ctCell(def.type, f, bag));
|
|
602
|
+
row.appendChild(ctCell(def.type, next, bag));
|
|
603
|
+
ctFieldsEl.appendChild(row);
|
|
604
|
+
i += 2;
|
|
605
|
+
} else {
|
|
606
|
+
ctFieldsEl.appendChild(ctLabel(def.type, f));
|
|
607
|
+
ctFieldsEl.appendChild(ctControl(def.type, f, bag));
|
|
608
|
+
i += 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function ctSync() {
|
|
614
|
+
var def = ctDef(ctType);
|
|
615
|
+
var bag = ctValues[def.type];
|
|
616
|
+
def.fields.forEach(function (f) {
|
|
617
|
+
var node = document.getElementById(ctFieldId(def.type, f.id));
|
|
618
|
+
if (!node) return;
|
|
619
|
+
bag[f.id] = (f.kind === 'checkbox') ? !!node.checked : node.value;
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function currentPayload() {
|
|
624
|
+
var isQRLike = formatSel.value === 'qr' || formatSel.value === 'frameqr';
|
|
625
|
+
if (!isQRLike) return el('text').value;
|
|
626
|
+
if (ctType === 'text') return el('text').value;
|
|
627
|
+
var build = PAYLOAD_BUILDERS[ctType];
|
|
628
|
+
return build ? build(ctValues[ctType]) : '';
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
var formats = B.listFormats();
|
|
632
|
+
formats.forEach(function (f) {
|
|
633
|
+
var o = document.createElement('option');
|
|
634
|
+
o.value = f.id;
|
|
635
|
+
o.textContent = f.label + (f.canWrite ? '' : ' (unavailable)');
|
|
636
|
+
o.disabled = !f.canWrite;
|
|
637
|
+
formatSel.appendChild(o);
|
|
638
|
+
});
|
|
639
|
+
formatSel.value = formats.some(function (f) { return f.id === 'qr' && f.canWrite; })
|
|
640
|
+
? 'qr' : 'ean13';
|
|
641
|
+
|
|
642
|
+
var versionSel = el('version');
|
|
643
|
+
for (var v = 1; v <= 40; v++) {
|
|
644
|
+
var o = document.createElement('option');
|
|
645
|
+
o.value = String(v);
|
|
646
|
+
o.textContent = 'Version ' + v + ' (' + (17 + 4 * v) + '×' + (17 + 4 * v) + ')';
|
|
647
|
+
versionSel.appendChild(o);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
var lastMatrix = null;
|
|
651
|
+
|
|
652
|
+
function currentOptions() {
|
|
653
|
+
var options = {
|
|
654
|
+
format: formatSel.value,
|
|
655
|
+
ecc: el('ecc').value,
|
|
656
|
+
version: el('version').value ? Number(el('version').value) : undefined,
|
|
657
|
+
checkDigit: true
|
|
658
|
+
};
|
|
659
|
+
if (formatSel.value === 'frameqr') {
|
|
660
|
+
options.ecc = 'H';
|
|
661
|
+
options.canvas = {
|
|
662
|
+
shape: el('frameCanvasShape').value,
|
|
663
|
+
size: Math.max(1, Number(el('frameCanvasSize').value) || 5)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
return options;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function frameArtworkOptions() {
|
|
670
|
+
var primary = String(el('frameCanvasUrl').value || '').trim();
|
|
671
|
+
return {
|
|
672
|
+
primary: primary || 'https://www.sythos.net/favicon.ico',
|
|
673
|
+
fallback: 'https://www.sythos.net/apple-touch-icon.png'
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function frameArtworkStatus(text) {
|
|
678
|
+
el('frameArtworkStatus').textContent = text;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function loadFrameImage() {
|
|
682
|
+
if (formatSel.value !== 'frameqr') {
|
|
683
|
+
frameArtworkState.image = null;
|
|
684
|
+
frameArtworkState.status = 'idle';
|
|
685
|
+
frameArtworkPreview.style.display = 'none';
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
var urls = frameArtworkOptions();
|
|
690
|
+
var request = urls.primary + '\n' + urls.fallback;
|
|
691
|
+
if (frameArtworkState.request === request && frameArtworkState.image) return;
|
|
692
|
+
frameArtworkState.request = request;
|
|
693
|
+
frameArtworkState.image = null;
|
|
694
|
+
frameArtworkState.url = '';
|
|
695
|
+
frameArtworkState.cors = false;
|
|
696
|
+
frameArtworkState.status = 'loading';
|
|
697
|
+
frameArtworkStatus('Loading remote artwork (CORS first; preview fallback if CORS is unavailable)…');
|
|
698
|
+
|
|
699
|
+
var candidates = [urls.primary, urls.fallback]
|
|
700
|
+
.filter(function (url, index, all) { return url && all.indexOf(url) === index; });
|
|
701
|
+
var candidateIndex = 0;
|
|
702
|
+
|
|
703
|
+
function attempt(url, cors) {
|
|
704
|
+
var image = new Image();
|
|
705
|
+
if (cors) image.crossOrigin = 'anonymous';
|
|
706
|
+
image.onload = function () {
|
|
707
|
+
if (request !== frameArtworkState.request) return;
|
|
708
|
+
frameArtworkState.image = image;
|
|
709
|
+
frameArtworkState.url = url;
|
|
710
|
+
frameArtworkState.cors = cors;
|
|
711
|
+
frameArtworkState.status = cors ? 'ready' : 'preview-only';
|
|
712
|
+
frameArtworkStatus(cors
|
|
713
|
+
? 'Remote artwork loaded with CORS and will be embedded in PNG output.'
|
|
714
|
+
: 'Remote artwork is preview-only because the server did not allow CORS; PNG/SVG downloads keep the QR modules.');
|
|
715
|
+
renderFrameArtwork();
|
|
716
|
+
};
|
|
717
|
+
image.onerror = function () {
|
|
718
|
+
if (request !== frameArtworkState.request) return;
|
|
719
|
+
if (cors) {
|
|
720
|
+
// A no-CORS retry still permits an in-page preview without tainting
|
|
721
|
+
// the QR canvas, while keeping export behaviour deterministic.
|
|
722
|
+
attempt(url, false);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
candidateIndex += 1;
|
|
726
|
+
if (candidateIndex < candidates.length) {
|
|
727
|
+
attempt(candidates[candidateIndex], true);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
frameArtworkState.status = 'missing';
|
|
731
|
+
frameArtworkStatus('Remote artwork could not be loaded; the QR canvas remains usable without a local asset.');
|
|
732
|
+
renderFrameArtwork();
|
|
733
|
+
};
|
|
734
|
+
image.src = url;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (candidates.length) attempt(candidates[0], true);
|
|
738
|
+
else frameArtworkState.status = 'missing';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function renderOptions() {
|
|
742
|
+
return {
|
|
743
|
+
scale: Math.max(1, Number(el('scale').value) || 6),
|
|
744
|
+
margin: Math.max(0, Number(el('margin').value) || 0),
|
|
745
|
+
dark: el('dark').value,
|
|
746
|
+
light: el('light').value
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function frameArtworkGeometry(matrix, options) {
|
|
751
|
+
var frame = matrix && matrix.frameqr && matrix.frameqr.canvas;
|
|
752
|
+
if (!frame) return null;
|
|
753
|
+
var scale = options.scale;
|
|
754
|
+
var margin = options.margin;
|
|
755
|
+
var rotated = frame.angle === 90 || frame.angle === 270;
|
|
756
|
+
var width = (rotated ? frame.height : frame.width) * scale;
|
|
757
|
+
var height = (rotated ? frame.width : frame.height) * scale;
|
|
758
|
+
return {
|
|
759
|
+
frame: frame,
|
|
760
|
+
centerX: (margin + frame.centerX + 0.5) * scale,
|
|
761
|
+
centerY: (margin + frame.centerY + 0.5) * scale,
|
|
762
|
+
width: width,
|
|
763
|
+
height: height,
|
|
764
|
+
angle: frame.angle || 0
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function clipFrameShape(ctx, geometry) {
|
|
769
|
+
var frame = geometry.frame;
|
|
770
|
+
var halfWidth = geometry.width / 2;
|
|
771
|
+
var halfHeight = geometry.height / 2;
|
|
772
|
+
ctx.beginPath();
|
|
773
|
+
if (frame.shape === 'circle') {
|
|
774
|
+
ctx.ellipse(0, 0, halfWidth, halfHeight, 0, 0, Math.PI * 2);
|
|
775
|
+
} else if (frame.shape === 'diamond') {
|
|
776
|
+
ctx.moveTo(0, -halfHeight);
|
|
777
|
+
ctx.lineTo(halfWidth, 0);
|
|
778
|
+
ctx.lineTo(0, halfHeight);
|
|
779
|
+
ctx.lineTo(-halfWidth, 0);
|
|
780
|
+
ctx.closePath();
|
|
781
|
+
} else {
|
|
782
|
+
ctx.rect(-halfWidth, -halfHeight, geometry.width, geometry.height);
|
|
783
|
+
}
|
|
784
|
+
ctx.clip();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function imageIsCorsSafe(image) {
|
|
788
|
+
try {
|
|
789
|
+
var probe = document.createElement('canvas');
|
|
790
|
+
probe.width = 1;
|
|
791
|
+
probe.height = 1;
|
|
792
|
+
var ctx = probe.getContext('2d');
|
|
793
|
+
if (!ctx) return false;
|
|
794
|
+
ctx.drawImage(image, 0, 0, 1, 1);
|
|
795
|
+
ctx.getImageData(0, 0, 1, 1);
|
|
796
|
+
return true;
|
|
797
|
+
} catch (e) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function positionFramePreview(geometry) {
|
|
803
|
+
var image = frameArtworkState.image;
|
|
804
|
+
if (!image || !geometry) {
|
|
805
|
+
frameArtworkPreview.style.display = 'none';
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
var stageRect = stage.getBoundingClientRect();
|
|
809
|
+
var canvasRect = canvas.getBoundingClientRect();
|
|
810
|
+
var scaleX = canvas.width ? canvasRect.width / canvas.width : 1;
|
|
811
|
+
var scaleY = canvas.height ? canvasRect.height / canvas.height : 1;
|
|
812
|
+
var left = canvasRect.left - stageRect.left + (geometry.centerX - geometry.width / 2) * scaleX;
|
|
813
|
+
var top = canvasRect.top - stageRect.top + (geometry.centerY - geometry.height / 2) * scaleY;
|
|
814
|
+
frameArtworkPreview.src = image.src;
|
|
815
|
+
frameArtworkPreview.alt = 'Remote canvas artwork';
|
|
816
|
+
frameArtworkPreview.style.left = left + 'px';
|
|
817
|
+
frameArtworkPreview.style.top = top + 'px';
|
|
818
|
+
frameArtworkPreview.style.width = geometry.width * scaleX + 'px';
|
|
819
|
+
frameArtworkPreview.style.height = geometry.height * scaleY + 'px';
|
|
820
|
+
frameArtworkPreview.style.clipPath = geometry.frame.shape === 'circle'
|
|
821
|
+
? 'ellipse(50% 50% at 50% 50%)'
|
|
822
|
+
: geometry.frame.shape === 'diamond'
|
|
823
|
+
? 'polygon(50% 0, 100% 50%, 50% 100%, 0 50%)'
|
|
824
|
+
: 'inset(0)';
|
|
825
|
+
frameArtworkPreview.style.transform = 'rotate(' + geometry.angle + 'deg)';
|
|
826
|
+
frameArtworkPreview.style.display = 'block';
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function renderFrameArtwork() {
|
|
830
|
+
var isFrameQR = formatSel.value === 'frameqr';
|
|
831
|
+
var geometry = frameArtworkGeometry(lastMatrix, renderOptions());
|
|
832
|
+
if (!isFrameQR || !geometry || !frameArtworkState.image) {
|
|
833
|
+
frameArtworkPreview.style.display = 'none';
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (!frameArtworkState.cors || !imageIsCorsSafe(frameArtworkState.image)) {
|
|
838
|
+
frameArtworkState.cors = false;
|
|
839
|
+
if (frameArtworkState.status !== 'preview-only') {
|
|
840
|
+
frameArtworkState.status = 'preview-only';
|
|
841
|
+
frameArtworkStatus('Remote artwork is preview-only because the server did not allow CORS; PNG/SVG downloads keep the QR modules.');
|
|
842
|
+
}
|
|
843
|
+
positionFramePreview(geometry);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
frameArtworkPreview.style.display = 'none';
|
|
848
|
+
var ctx = canvas.getContext('2d');
|
|
849
|
+
if (!ctx) return;
|
|
850
|
+
var image = frameArtworkState.image;
|
|
851
|
+
var imageWidth = image.naturalWidth || image.width || geometry.width;
|
|
852
|
+
var imageHeight = image.naturalHeight || image.height || geometry.height;
|
|
853
|
+
var coverScale = Math.max(geometry.width / imageWidth, geometry.height / imageHeight);
|
|
854
|
+
ctx.save();
|
|
855
|
+
ctx.translate(geometry.centerX, geometry.centerY);
|
|
856
|
+
ctx.rotate((geometry.angle * Math.PI) / 180);
|
|
857
|
+
clipFrameShape(ctx, geometry);
|
|
858
|
+
ctx.drawImage(
|
|
859
|
+
image,
|
|
860
|
+
-imageWidth * coverScale / 2,
|
|
861
|
+
-imageHeight * coverScale / 2,
|
|
862
|
+
imageWidth * coverScale,
|
|
863
|
+
imageHeight * coverScale
|
|
864
|
+
);
|
|
865
|
+
ctx.restore();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// User-supplied strings never go through innerHTML.
|
|
869
|
+
function showError(msg) {
|
|
870
|
+
var box = el('err');
|
|
871
|
+
while (box.firstChild) box.removeChild(box.firstChild);
|
|
872
|
+
if (!msg) return;
|
|
873
|
+
var d = document.createElement('div');
|
|
874
|
+
d.className = 'error';
|
|
875
|
+
d.textContent = msg;
|
|
876
|
+
box.appendChild(d);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function update() {
|
|
880
|
+
var isQR = formatSel.value === 'qr' || formatSel.value === 'frameqr';
|
|
881
|
+
var isFrameQR = formatSel.value === 'frameqr';
|
|
882
|
+
el('qrOpts').style.display = isQR ? '' : 'none';
|
|
883
|
+
el('frameOpts').style.display = isFrameQR ? '' : 'none';
|
|
884
|
+
el('ctWrap').style.display = isQR ? '' : 'none';
|
|
885
|
+
ctFieldsEl.style.display = isQR ? '' : 'none';
|
|
886
|
+
el('payloadWrap').style.display = isQR ? '' : 'none';
|
|
887
|
+
el('plainWrap').style.display = isQR ? 'none' : '';
|
|
888
|
+
el('ecc').disabled = isFrameQR;
|
|
889
|
+
if (isFrameQR) el('ecc').value = 'H';
|
|
890
|
+
|
|
891
|
+
var info = formats.filter(function (f) { return f.id === formatSel.value; })[0];
|
|
892
|
+
el('formatHint').textContent = info
|
|
893
|
+
? (info.kind + ' · ' + (info.canRead ? 'this suite can also read it' : 'write-only in this build'))
|
|
894
|
+
: '';
|
|
895
|
+
|
|
896
|
+
var payload = currentPayload();
|
|
897
|
+
el('payloadOut').textContent = payload;
|
|
898
|
+
el('payloadLen').textContent = payload.length + ' character' + (payload.length === 1 ? '' : 's');
|
|
899
|
+
|
|
900
|
+
showError('');
|
|
901
|
+
try {
|
|
902
|
+
lastMatrix = B.encode(payload, currentOptions());
|
|
903
|
+
} catch (e) {
|
|
904
|
+
lastMatrix = null;
|
|
905
|
+
showError(String(e && e.message ? e.message : e));
|
|
906
|
+
el('meta').innerHTML = '';
|
|
907
|
+
frameArtworkPreview.style.display = 'none';
|
|
908
|
+
var ctx = canvas.getContext('2d');
|
|
909
|
+
canvas.width = 10; canvas.height = 10;
|
|
910
|
+
if (ctx) ctx.clearRect(0, 0, 10, 10);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Frame artwork is composited through a 2D context; do not commit the
|
|
915
|
+
// canvas to WebGL first because a canvas can own only one context type.
|
|
916
|
+
var renderOpts = renderOptions();
|
|
917
|
+
var out = isFrameQR
|
|
918
|
+
? { backend: B.toCanvas(lastMatrix, canvas, renderOpts) ? '2d' : 'none' }
|
|
919
|
+
: B.renderToCanvasAuto(lastMatrix, canvas, renderOpts);
|
|
920
|
+
|
|
921
|
+
if (isFrameQR) {
|
|
922
|
+
loadFrameImage();
|
|
923
|
+
renderFrameArtwork();
|
|
924
|
+
} else {
|
|
925
|
+
frameArtworkPreview.style.display = 'none';
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
el('meta').innerHTML =
|
|
929
|
+
'<span>modules <b>' + lastMatrix.width + '×' + lastMatrix.height + '</b></span>' +
|
|
930
|
+
'<span>pixels <b>' + canvas.width + '×' + canvas.height + '</b></span>' +
|
|
931
|
+
'<span>backend <b>' + out.backend + '</b></span>' +
|
|
932
|
+
'<span>bytes in <b>' + payload.length + '</b></span>';
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function saveBlob(blob, name) {
|
|
936
|
+
var url = URL.createObjectURL(blob);
|
|
937
|
+
var a = document.createElement('a');
|
|
938
|
+
a.href = url;
|
|
939
|
+
a.download = name;
|
|
940
|
+
document.body.appendChild(a);
|
|
941
|
+
a.click();
|
|
942
|
+
document.body.removeChild(a);
|
|
943
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
el('download-svg').addEventListener('click', function () {
|
|
947
|
+
if (!lastMatrix) return;
|
|
948
|
+
var svg = B.toSVG(lastMatrix, renderOptions());
|
|
949
|
+
if (formatSel.value === 'frameqr' && frameArtworkState.image) {
|
|
950
|
+
frameArtworkStatus('SVG export contains the QR profile only; external artwork remains a remote preview and is not copied locally.');
|
|
951
|
+
}
|
|
952
|
+
saveBlob(new Blob([svg], { type: 'image/svg+xml' }), formatSel.value + '.svg');
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
el('download-png').addEventListener('click', function () {
|
|
956
|
+
if (!lastMatrix) return;
|
|
957
|
+
if (formatSel.value === 'frameqr' && frameArtworkState.cors) {
|
|
958
|
+
try {
|
|
959
|
+
canvas.toBlob(function (blob) {
|
|
960
|
+
if (blob) {
|
|
961
|
+
saveBlob(blob, formatSel.value + '.png');
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
frameArtworkStatus('PNG export could not include remote artwork; exporting the QR profile without a local asset.');
|
|
965
|
+
B.toPNG(lastMatrix, renderOptions()).then(function (bytes) {
|
|
966
|
+
saveBlob(new Blob([bytes], { type: 'image/png' }), formatSel.value + '.png');
|
|
967
|
+
});
|
|
968
|
+
}, 'image/png');
|
|
969
|
+
return;
|
|
970
|
+
} catch (e) {
|
|
971
|
+
frameArtworkStatus('PNG export could not include remote artwork; exporting the QR profile without a local asset.');
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
B.toPNG(lastMatrix, renderOptions()).then(function (bytes) {
|
|
975
|
+
saveBlob(new Blob([bytes], { type: 'image/png' }), formatSel.value + '.png');
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
formatSel.addEventListener('change', function () {
|
|
980
|
+
if (SAMPLES[formatSel.value]) el('text').value = SAMPLES[formatSel.value];
|
|
981
|
+
update();
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
ctypeSel.addEventListener('change', function () {
|
|
985
|
+
ctSync(); // keep whatever was typed under the old type
|
|
986
|
+
ctType = ctypeSel.value;
|
|
987
|
+
ctRender();
|
|
988
|
+
update();
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// Delegated: the generated controls are replaced wholesale on every type
|
|
992
|
+
// switch, so listeners live on the container instead. Both events bubble.
|
|
993
|
+
ctFieldsEl.addEventListener('input', function () { ctSync(); update(); });
|
|
994
|
+
ctFieldsEl.addEventListener('change', function () { ctSync(); update(); });
|
|
995
|
+
|
|
996
|
+
['text', 'ecc', 'version', 'scale', 'margin', 'dark', 'light'].forEach(function (id) {
|
|
997
|
+
el(id).addEventListener('input', update);
|
|
998
|
+
el(id).addEventListener('change', update);
|
|
999
|
+
});
|
|
1000
|
+
['frameCanvasShape', 'frameCanvasSize', 'frameCanvasUrl'].forEach(function (id) {
|
|
1001
|
+
el(id).addEventListener('input', update);
|
|
1002
|
+
el(id).addEventListener('change', update);
|
|
1003
|
+
});
|
|
1004
|
+
window.addEventListener('resize', function () {
|
|
1005
|
+
if (formatSel.value === 'frameqr' && !frameArtworkState.cors) renderFrameArtwork();
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
ctRender();
|
|
1009
|
+
update();
|
|
1010
|
+
})();
|
|
1011
|
+
</script>
|
|
1012
|
+
</body>
|
|
1013
|
+
</html>
|