@forgesworn/qr 0.0.0-seed.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/SPEC.md +136 -0
- package/dist/binarise.d.ts +26 -0
- package/dist/binarise.js +115 -0
- package/dist/canvas.d.ts +29 -0
- package/dist/canvas.js +44 -0
- package/dist/decode.d.ts +42 -0
- package/dist/decode.js +98 -0
- package/dist/encode.d.ts +76 -0
- package/dist/encode.js +495 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +5 -0
- package/dist/locate.d.ts +37 -0
- package/dist/locate.js +489 -0
- package/dist/rs.d.ts +6 -0
- package/dist/rs.js +99 -0
- package/dist/svg.d.ts +17 -0
- package/dist/svg.js +35 -0
- package/dist/symbol.d.ts +25 -0
- package/dist/symbol.js +250 -0
- package/dist/tables.d.ts +8 -0
- package/dist/tables.js +93 -0
- package/dist/terminal.d.ts +18 -0
- package/dist/terminal.js +30 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.js +48 -0
- package/llms.txt +37 -0
- package/package.json +68 -0
package/dist/symbol.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { ALPHANUMERIC, blocksFor, countBand, EC_ORDER, FORMAT_COORDS, formatBits, Grid, maskBit, versionBits, } from './encode.js';
|
|
2
|
+
import { correct } from './rs.js';
|
|
3
|
+
import { fromUtf8, isUtf8 } from './text.js';
|
|
4
|
+
const FORMATS = EC_ORDER.flatMap((ecc) => [0, 1, 2, 3, 4, 5, 6, 7].map((mask) => ({ bits: formatBits(ecc, mask), ecc, mask })));
|
|
5
|
+
const VERSIONS = Array.from({ length: 34 }, (_, i) => ({
|
|
6
|
+
bits: versionBits(i + 7),
|
|
7
|
+
version: i + 7,
|
|
8
|
+
}));
|
|
9
|
+
function popcount(n) {
|
|
10
|
+
let c = 0;
|
|
11
|
+
for (; n !== 0; n &= n - 1)
|
|
12
|
+
c++;
|
|
13
|
+
return c;
|
|
14
|
+
}
|
|
15
|
+
function nearest(table, reads) {
|
|
16
|
+
let best;
|
|
17
|
+
let distance = 4; // both codes correct up to three bit errors
|
|
18
|
+
for (const read of reads) {
|
|
19
|
+
for (const entry of table) {
|
|
20
|
+
const d = popcount(entry.bits ^ read);
|
|
21
|
+
if (d < distance) {
|
|
22
|
+
distance = d;
|
|
23
|
+
best = entry;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
}
|
|
29
|
+
/** The version a symbol of `size` modules declares, when its version blocks can be read. */
|
|
30
|
+
export function readVersion(read, size) {
|
|
31
|
+
if (size < 45)
|
|
32
|
+
return (size - 17) / 4;
|
|
33
|
+
let bottomLeft = 0;
|
|
34
|
+
let topRight = 0;
|
|
35
|
+
for (let k = 17; k >= 0; k--) {
|
|
36
|
+
const i = Math.floor(k / 3);
|
|
37
|
+
const j = k % 3;
|
|
38
|
+
bottomLeft = (bottomLeft << 1) | (read(i, size - 11 + j) ? 1 : 0);
|
|
39
|
+
topRight = (topRight << 1) | (read(size - 11 + j, i) ? 1 : 0);
|
|
40
|
+
}
|
|
41
|
+
return nearest(VERSIONS, [bottomLeft, topRight])?.version;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Decodes a symbol from its modules. `size` must be 17 + 4v; for version 7
|
|
45
|
+
* and up the version blocks decide, and a size that disagrees fails.
|
|
46
|
+
* Returns undefined when the symbol cannot be read.
|
|
47
|
+
*/
|
|
48
|
+
export function readSymbol(read, size) {
|
|
49
|
+
if (size < 21 || size > 177 || (size - 17) % 4 !== 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const version = readVersion(read, size);
|
|
52
|
+
if (version === undefined || 17 + 4 * version !== size)
|
|
53
|
+
return undefined;
|
|
54
|
+
let first = 0;
|
|
55
|
+
let second = 0;
|
|
56
|
+
for (let i = 14; i >= 0; i--) {
|
|
57
|
+
const [x, y] = FORMAT_COORDS[i];
|
|
58
|
+
first = (first << 1) | (read(x, y) ? 1 : 0);
|
|
59
|
+
second = (second << 1) | (read(i < 8 ? size - 1 - i : 8, i < 8 ? 8 : size - 7 + (i - 8)) ? 1 : 0);
|
|
60
|
+
}
|
|
61
|
+
const format = nearest(FORMATS, [first, second]);
|
|
62
|
+
if (format === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
const grid = new Grid(version);
|
|
65
|
+
const [ecPerBlock, g1, d1, g2] = blocksFor(version, format.ecc);
|
|
66
|
+
const blockCount = g1 + g2;
|
|
67
|
+
const total = g1 * d1 + g2 * (d1 + 1) + blockCount * ecPerBlock;
|
|
68
|
+
const stream = new Uint8Array(total);
|
|
69
|
+
let bit = 0;
|
|
70
|
+
let upward = true;
|
|
71
|
+
for (let right = size - 1; right >= 1 && bit < total * 8; right -= 2) {
|
|
72
|
+
if (right === 6)
|
|
73
|
+
right = 5;
|
|
74
|
+
for (let step = 0; step < size; step++) {
|
|
75
|
+
const y = upward ? size - 1 - step : step;
|
|
76
|
+
for (let i = 0; i < 2; i++) {
|
|
77
|
+
const x = right - i;
|
|
78
|
+
if (grid.isReserved(x, y) || bit >= total * 8)
|
|
79
|
+
continue;
|
|
80
|
+
if (read(x, y) !== maskBit(format.mask, x, y))
|
|
81
|
+
stream[bit >>> 3] |= 0x80 >>> (bit & 7);
|
|
82
|
+
bit++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
upward = !upward;
|
|
86
|
+
}
|
|
87
|
+
const blocks = [];
|
|
88
|
+
for (let b = 0; b < blockCount; b++)
|
|
89
|
+
blocks.push(new Uint8Array((b < g1 ? d1 : d1 + 1) + ecPerBlock));
|
|
90
|
+
let k = 0;
|
|
91
|
+
for (let i = 0; i <= d1; i++) {
|
|
92
|
+
for (let b = 0; b < blockCount; b++)
|
|
93
|
+
if (i < (b < g1 ? d1 : d1 + 1))
|
|
94
|
+
blocks[b][i] = stream[k++];
|
|
95
|
+
}
|
|
96
|
+
for (let i = 0; i < ecPerBlock; i++) {
|
|
97
|
+
for (let b = 0; b < blockCount; b++)
|
|
98
|
+
blocks[b][(b < g1 ? d1 : d1 + 1) + i] = stream[k++];
|
|
99
|
+
}
|
|
100
|
+
const data = new Uint8Array(g1 * d1 + g2 * (d1 + 1));
|
|
101
|
+
let corrected = 0;
|
|
102
|
+
for (let b = 0, offset = 0; b < blockCount; b++) {
|
|
103
|
+
const fixed = correct(blocks[b], ecPerBlock);
|
|
104
|
+
if (fixed < 0)
|
|
105
|
+
return undefined;
|
|
106
|
+
corrected += fixed;
|
|
107
|
+
const length = b < g1 ? d1 : d1 + 1;
|
|
108
|
+
data.set(blocks[b].subarray(0, length), offset);
|
|
109
|
+
offset += length;
|
|
110
|
+
}
|
|
111
|
+
const parsed = parse(data, version);
|
|
112
|
+
if (parsed === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
return { ...parsed, version, ecc: format.ecc, mask: format.mask, corrected };
|
|
115
|
+
}
|
|
116
|
+
class Bits {
|
|
117
|
+
data;
|
|
118
|
+
position = 0;
|
|
119
|
+
constructor(data) {
|
|
120
|
+
this.data = data;
|
|
121
|
+
}
|
|
122
|
+
get available() {
|
|
123
|
+
return this.data.length * 8 - this.position;
|
|
124
|
+
}
|
|
125
|
+
read(count) {
|
|
126
|
+
if (count > this.available)
|
|
127
|
+
throw new RangeError('Ran off the end of the data');
|
|
128
|
+
let value = 0;
|
|
129
|
+
for (let i = 0; i < count; i++, this.position++) {
|
|
130
|
+
value = (value << 1) | ((this.data[this.position >>> 3] >>> (7 - (this.position & 7))) & 1);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const KANJI_COUNT_BITS = [8, 10, 12];
|
|
136
|
+
function parse(data, version) {
|
|
137
|
+
const bits = new Bits(data);
|
|
138
|
+
const band = countBand(version);
|
|
139
|
+
const runs = [{ eci: undefined, bytes: [] }];
|
|
140
|
+
const eci = [];
|
|
141
|
+
const out = [];
|
|
142
|
+
const emit = (byte) => {
|
|
143
|
+
runs[runs.length - 1].bytes.push(byte);
|
|
144
|
+
out.push(byte);
|
|
145
|
+
};
|
|
146
|
+
try {
|
|
147
|
+
while (bits.available >= 4) {
|
|
148
|
+
const mode = bits.read(4);
|
|
149
|
+
if (mode === 0)
|
|
150
|
+
break;
|
|
151
|
+
if (mode === 1) {
|
|
152
|
+
let count = bits.read([10, 12, 14][band]);
|
|
153
|
+
for (; count >= 3; count -= 3) {
|
|
154
|
+
const v = bits.read(10);
|
|
155
|
+
if (v > 999)
|
|
156
|
+
return undefined;
|
|
157
|
+
for (const c of String(v).padStart(3, '0'))
|
|
158
|
+
emit(c.charCodeAt(0));
|
|
159
|
+
}
|
|
160
|
+
if (count > 0) {
|
|
161
|
+
const v = bits.read(count === 2 ? 7 : 4);
|
|
162
|
+
if (v >= (count === 2 ? 100 : 10))
|
|
163
|
+
return undefined;
|
|
164
|
+
for (const c of String(v).padStart(count, '0'))
|
|
165
|
+
emit(c.charCodeAt(0));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (mode === 2) {
|
|
169
|
+
let count = bits.read([9, 11, 13][band]);
|
|
170
|
+
for (; count >= 2; count -= 2) {
|
|
171
|
+
const v = bits.read(11);
|
|
172
|
+
if (v >= 45 * 45)
|
|
173
|
+
return undefined;
|
|
174
|
+
emit(ALPHANUMERIC.charCodeAt(Math.floor(v / 45)));
|
|
175
|
+
emit(ALPHANUMERIC.charCodeAt(v % 45));
|
|
176
|
+
}
|
|
177
|
+
if (count === 1) {
|
|
178
|
+
const v = bits.read(6);
|
|
179
|
+
if (v >= 45)
|
|
180
|
+
return undefined;
|
|
181
|
+
emit(ALPHANUMERIC.charCodeAt(v));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (mode === 4) {
|
|
185
|
+
const count = bits.read([8, 16, 16][band]);
|
|
186
|
+
for (let i = 0; i < count; i++)
|
|
187
|
+
emit(bits.read(8));
|
|
188
|
+
}
|
|
189
|
+
else if (mode === 8) {
|
|
190
|
+
// Kanji: kept as its Shift_JIS bytes; the text shows U+FFFD for each.
|
|
191
|
+
const count = bits.read(KANJI_COUNT_BITS[band]);
|
|
192
|
+
for (let i = 0; i < count; i++) {
|
|
193
|
+
const v = bits.read(13);
|
|
194
|
+
let sjis = (Math.floor(v / 0xc0) << 8) | v % 0xc0;
|
|
195
|
+
sjis += sjis < 0x1f00 ? 0x8140 : 0xc140;
|
|
196
|
+
out.push(sjis >>> 8, sjis & 0xff);
|
|
197
|
+
runs.push({ eci: -1, bytes: [] }, { eci: runs[runs.length - 1].eci, bytes: [] });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (mode === 7) {
|
|
201
|
+
const first = bits.read(8);
|
|
202
|
+
const value = (first & 0x80) === 0
|
|
203
|
+
? first
|
|
204
|
+
: (first & 0xc0) === 0x80
|
|
205
|
+
? ((first & 0x3f) << 8) | bits.read(8)
|
|
206
|
+
: (first & 0xe0) === 0xc0
|
|
207
|
+
? ((first & 0x1f) << 16) | bits.read(16)
|
|
208
|
+
: -1;
|
|
209
|
+
if (value < 0)
|
|
210
|
+
return undefined;
|
|
211
|
+
eci.push(value);
|
|
212
|
+
runs.push({ eci: value, bytes: [] });
|
|
213
|
+
}
|
|
214
|
+
else if (mode === 3) {
|
|
215
|
+
bits.read(16); // structured append: position, total and parity; one symbol is read alone
|
|
216
|
+
}
|
|
217
|
+
else if (mode === 5) {
|
|
218
|
+
// FNC1 in first position: GS1 data follows in ordinary segments
|
|
219
|
+
}
|
|
220
|
+
else if (mode === 9) {
|
|
221
|
+
bits.read(8); // FNC1 in second position: application indicator
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
let text = '';
|
|
232
|
+
for (const run of runs)
|
|
233
|
+
text += run.eci === -1 ? '�' : decodeRun(run);
|
|
234
|
+
return { text, bytes: Uint8Array.from(out), eci };
|
|
235
|
+
}
|
|
236
|
+
/** ECI 26 is UTF-8; 3 and 1 are Latin-1. Without ECI, valid UTF-8 is taken as UTF-8, else Latin-1. */
|
|
237
|
+
function decodeRun(run) {
|
|
238
|
+
const bytes = Uint8Array.from(run.bytes);
|
|
239
|
+
if (run.eci === 3 || run.eci === 1)
|
|
240
|
+
return latin1(bytes);
|
|
241
|
+
if (run.eci === 26 || isUtf8(bytes))
|
|
242
|
+
return fromUtf8(bytes);
|
|
243
|
+
return latin1(bytes);
|
|
244
|
+
}
|
|
245
|
+
function latin1(bytes) {
|
|
246
|
+
let s = '';
|
|
247
|
+
for (const b of bytes)
|
|
248
|
+
s += String.fromCharCode(b);
|
|
249
|
+
return s;
|
|
250
|
+
}
|
package/dist/tables.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Alignment pattern centre coordinates, indexed by version - 1. */
|
|
2
|
+
export declare const ALIGNMENT_CENTRES: readonly (readonly number[])[];
|
|
3
|
+
/**
|
|
4
|
+
* Error correction blocks, indexed by version - 1 then level (L, M, Q, H):
|
|
5
|
+
* [EC codewords per block, group 1 blocks, group 1 data codewords per block,
|
|
6
|
+
* group 2 blocks]. Group 2 blocks each hold one more data codeword.
|
|
7
|
+
*/
|
|
8
|
+
export declare const EC_BLOCKS: readonly (readonly (readonly [number, number, number, number])[])[];
|
package/dist/tables.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Values from ISO/IEC 18004:2015 Table 9 (error correction blocks) and Annex E
|
|
2
|
+
// (alignment pattern centres). test/tables.test.ts checks every row against
|
|
3
|
+
// the module count of its version.
|
|
4
|
+
/** Alignment pattern centre coordinates, indexed by version - 1. */
|
|
5
|
+
export const ALIGNMENT_CENTRES = [
|
|
6
|
+
[],
|
|
7
|
+
[6, 18],
|
|
8
|
+
[6, 22],
|
|
9
|
+
[6, 26],
|
|
10
|
+
[6, 30],
|
|
11
|
+
[6, 34],
|
|
12
|
+
[6, 22, 38],
|
|
13
|
+
[6, 24, 42],
|
|
14
|
+
[6, 26, 46],
|
|
15
|
+
[6, 28, 50],
|
|
16
|
+
[6, 30, 54],
|
|
17
|
+
[6, 32, 58],
|
|
18
|
+
[6, 34, 62],
|
|
19
|
+
[6, 26, 46, 66],
|
|
20
|
+
[6, 26, 48, 70],
|
|
21
|
+
[6, 26, 50, 74],
|
|
22
|
+
[6, 30, 54, 78],
|
|
23
|
+
[6, 30, 56, 82],
|
|
24
|
+
[6, 30, 58, 86],
|
|
25
|
+
[6, 34, 62, 90],
|
|
26
|
+
[6, 28, 50, 72, 94],
|
|
27
|
+
[6, 26, 50, 74, 98],
|
|
28
|
+
[6, 30, 54, 78, 102],
|
|
29
|
+
[6, 28, 54, 80, 106],
|
|
30
|
+
[6, 32, 58, 84, 110],
|
|
31
|
+
[6, 30, 58, 86, 114],
|
|
32
|
+
[6, 34, 62, 90, 118],
|
|
33
|
+
[6, 26, 50, 74, 98, 122],
|
|
34
|
+
[6, 30, 54, 78, 102, 126],
|
|
35
|
+
[6, 26, 52, 78, 104, 130],
|
|
36
|
+
[6, 30, 56, 82, 108, 134],
|
|
37
|
+
[6, 34, 60, 86, 112, 138],
|
|
38
|
+
[6, 30, 58, 86, 114, 142],
|
|
39
|
+
[6, 34, 62, 90, 118, 146],
|
|
40
|
+
[6, 30, 54, 78, 102, 126, 150],
|
|
41
|
+
[6, 24, 50, 76, 102, 128, 154],
|
|
42
|
+
[6, 28, 54, 80, 106, 132, 158],
|
|
43
|
+
[6, 32, 58, 84, 110, 136, 162],
|
|
44
|
+
[6, 26, 54, 82, 110, 138, 166],
|
|
45
|
+
[6, 30, 58, 86, 114, 142, 170],
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Error correction blocks, indexed by version - 1 then level (L, M, Q, H):
|
|
49
|
+
* [EC codewords per block, group 1 blocks, group 1 data codewords per block,
|
|
50
|
+
* group 2 blocks]. Group 2 blocks each hold one more data codeword.
|
|
51
|
+
*/
|
|
52
|
+
export const EC_BLOCKS = [
|
|
53
|
+
[[7, 1, 19, 0], [10, 1, 16, 0], [13, 1, 13, 0], [17, 1, 9, 0]],
|
|
54
|
+
[[10, 1, 34, 0], [16, 1, 28, 0], [22, 1, 22, 0], [28, 1, 16, 0]],
|
|
55
|
+
[[15, 1, 55, 0], [26, 1, 44, 0], [18, 2, 17, 0], [22, 2, 13, 0]],
|
|
56
|
+
[[20, 1, 80, 0], [18, 2, 32, 0], [26, 2, 24, 0], [16, 4, 9, 0]],
|
|
57
|
+
[[26, 1, 108, 0], [24, 2, 43, 0], [18, 2, 15, 2], [22, 2, 11, 2]],
|
|
58
|
+
[[18, 2, 68, 0], [16, 4, 27, 0], [24, 4, 19, 0], [28, 4, 15, 0]],
|
|
59
|
+
[[20, 2, 78, 0], [18, 4, 31, 0], [18, 2, 14, 4], [26, 4, 13, 1]],
|
|
60
|
+
[[24, 2, 97, 0], [22, 2, 38, 2], [22, 4, 18, 2], [26, 4, 14, 2]],
|
|
61
|
+
[[30, 2, 116, 0], [22, 3, 36, 2], [20, 4, 16, 4], [24, 4, 12, 4]],
|
|
62
|
+
[[18, 2, 68, 2], [26, 4, 43, 1], [24, 6, 19, 2], [28, 6, 15, 2]],
|
|
63
|
+
[[20, 4, 81, 0], [30, 1, 50, 4], [28, 4, 22, 4], [24, 3, 12, 8]],
|
|
64
|
+
[[24, 2, 92, 2], [22, 6, 36, 2], [26, 4, 20, 6], [28, 7, 14, 4]],
|
|
65
|
+
[[26, 4, 107, 0], [22, 8, 37, 1], [24, 8, 20, 4], [22, 12, 11, 4]],
|
|
66
|
+
[[30, 3, 115, 1], [24, 4, 40, 5], [20, 11, 16, 5], [24, 11, 12, 5]],
|
|
67
|
+
[[22, 5, 87, 1], [24, 5, 41, 5], [30, 5, 24, 7], [24, 11, 12, 7]],
|
|
68
|
+
[[24, 5, 98, 1], [28, 7, 45, 3], [24, 15, 19, 2], [30, 3, 15, 13]],
|
|
69
|
+
[[28, 1, 107, 5], [28, 10, 46, 1], [28, 1, 22, 15], [28, 2, 14, 17]],
|
|
70
|
+
[[30, 5, 120, 1], [26, 9, 43, 4], [28, 17, 22, 1], [28, 2, 14, 19]],
|
|
71
|
+
[[28, 3, 113, 4], [26, 3, 44, 11], [26, 17, 21, 4], [26, 9, 13, 16]],
|
|
72
|
+
[[28, 3, 107, 5], [26, 3, 41, 13], [30, 15, 24, 5], [28, 15, 15, 10]],
|
|
73
|
+
[[28, 4, 116, 4], [26, 17, 42, 0], [28, 17, 22, 6], [30, 19, 16, 6]],
|
|
74
|
+
[[28, 2, 111, 7], [28, 17, 46, 0], [30, 7, 24, 16], [24, 34, 13, 0]],
|
|
75
|
+
[[30, 4, 121, 5], [28, 4, 47, 14], [30, 11, 24, 14], [30, 16, 15, 14]],
|
|
76
|
+
[[30, 6, 117, 4], [28, 6, 45, 14], [30, 11, 24, 16], [30, 30, 16, 2]],
|
|
77
|
+
[[26, 8, 106, 4], [28, 8, 47, 13], [30, 7, 24, 22], [30, 22, 15, 13]],
|
|
78
|
+
[[28, 10, 114, 2], [28, 19, 46, 4], [28, 28, 22, 6], [30, 33, 16, 4]],
|
|
79
|
+
[[30, 8, 122, 4], [28, 22, 45, 3], [30, 8, 23, 26], [30, 12, 15, 28]],
|
|
80
|
+
[[30, 3, 117, 10], [28, 3, 45, 23], [30, 4, 24, 31], [30, 11, 15, 31]],
|
|
81
|
+
[[30, 7, 116, 7], [28, 21, 45, 7], [30, 1, 23, 37], [30, 19, 15, 26]],
|
|
82
|
+
[[30, 5, 115, 10], [28, 19, 47, 10], [30, 15, 24, 25], [30, 23, 15, 25]],
|
|
83
|
+
[[30, 13, 115, 3], [28, 2, 46, 29], [30, 42, 24, 1], [30, 23, 15, 28]],
|
|
84
|
+
[[30, 17, 115, 0], [28, 10, 46, 23], [30, 10, 24, 35], [30, 19, 15, 35]],
|
|
85
|
+
[[30, 17, 115, 1], [28, 14, 46, 21], [30, 29, 24, 19], [30, 11, 15, 46]],
|
|
86
|
+
[[30, 13, 115, 6], [28, 14, 46, 23], [30, 44, 24, 7], [30, 59, 16, 1]],
|
|
87
|
+
[[30, 12, 121, 7], [28, 12, 47, 26], [30, 39, 24, 14], [30, 22, 15, 41]],
|
|
88
|
+
[[30, 6, 121, 14], [28, 6, 47, 34], [30, 46, 24, 10], [30, 2, 15, 64]],
|
|
89
|
+
[[30, 17, 122, 4], [28, 29, 46, 14], [30, 49, 24, 10], [30, 24, 15, 46]],
|
|
90
|
+
[[30, 4, 122, 18], [28, 13, 46, 32], [30, 48, 24, 14], [30, 42, 15, 32]],
|
|
91
|
+
[[30, 20, 117, 4], [28, 40, 47, 7], [30, 43, 24, 22], [30, 10, 15, 67]],
|
|
92
|
+
[[30, 19, 118, 6], [28, 18, 47, 31], [30, 34, 24, 34], [30, 20, 15, 61]],
|
|
93
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type QrCode } from './encode.js';
|
|
2
|
+
export interface TerminalOptions {
|
|
3
|
+
/** Quiet zone in modules on every side. Default 4, the standard's minimum. */
|
|
4
|
+
margin?: number;
|
|
5
|
+
/**
|
|
6
|
+
* Which modules the block characters draw. `'light'` (the default) suits a
|
|
7
|
+
* terminal with a dark background: blocks paint the light modules and the
|
|
8
|
+
* quiet zone, and the background shows through as dark modules. Use
|
|
9
|
+
* `'dark'` on a light background.
|
|
10
|
+
*/
|
|
11
|
+
ink?: 'light' | 'dark';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Renders a code as text, two module rows per line with half-block
|
|
15
|
+
* characters, one line per row pair and no trailing newline. Needs a
|
|
16
|
+
* monospace font whose block characters fill the cell.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toTerminal(qr: QrCode, options?: TerminalOptions): string;
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const BLOCKS = [' ', '▀', '▄', '█']; // none, upper half, lower half, full
|
|
2
|
+
/**
|
|
3
|
+
* Renders a code as text, two module rows per line with half-block
|
|
4
|
+
* characters, one line per row pair and no trailing newline. Needs a
|
|
5
|
+
* monospace font whose block characters fill the cell.
|
|
6
|
+
*/
|
|
7
|
+
export function toTerminal(qr, options = {}) {
|
|
8
|
+
const margin = options.margin ?? 4;
|
|
9
|
+
if (!Number.isInteger(margin) || margin < 0)
|
|
10
|
+
throw new RangeError('Margin must be a whole number of modules');
|
|
11
|
+
const inkIsLight = (options.ink ?? 'light') === 'light';
|
|
12
|
+
const side = qr.size + margin * 2;
|
|
13
|
+
const inked = (x, y) => {
|
|
14
|
+
const mx = x - margin;
|
|
15
|
+
const my = y - margin;
|
|
16
|
+
const dark = mx >= 0 && my >= 0 && mx < qr.size && my < qr.size && qr.modules[my * qr.size + mx] === 1;
|
|
17
|
+
return dark !== inkIsLight;
|
|
18
|
+
};
|
|
19
|
+
const lines = [];
|
|
20
|
+
for (let y = 0; y < side; y += 2) {
|
|
21
|
+
let line = '';
|
|
22
|
+
for (let x = 0; x < side; x++) {
|
|
23
|
+
// An odd side leaves the last line's lower half blank (the terminal background).
|
|
24
|
+
const lower = y + 1 < side && inked(x, y + 1);
|
|
25
|
+
line += BLOCKS[(inked(x, y) ? 1 : 0) | (lower ? 2 : 0)];
|
|
26
|
+
}
|
|
27
|
+
lines.push(line);
|
|
28
|
+
}
|
|
29
|
+
return lines.join('\n');
|
|
30
|
+
}
|
package/dist/text.d.ts
ADDED
package/dist/text.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// UTF-8 decoding without TextDecoder, which ES2022 does not guarantee.
|
|
2
|
+
/** Length of the well-formed UTF-8 sequence at `i`, or 0 if it is not one. */
|
|
3
|
+
function sequence(bytes, i) {
|
|
4
|
+
const b = bytes[i];
|
|
5
|
+
if (b < 0x80)
|
|
6
|
+
return 1;
|
|
7
|
+
const length = b >= 0xc2 && b <= 0xdf ? 2 : b >= 0xe0 && b <= 0xef ? 3 : b >= 0xf0 && b <= 0xf4 ? 4 : 0;
|
|
8
|
+
if (length === 0 || i + length > bytes.length)
|
|
9
|
+
return 0;
|
|
10
|
+
const second = bytes[i + 1];
|
|
11
|
+
const low = b === 0xe0 ? 0xa0 : b === 0xf0 ? 0x90 : 0x80;
|
|
12
|
+
const high = b === 0xed ? 0x9f : b === 0xf4 ? 0x8f : 0xbf;
|
|
13
|
+
if (second < low || second > high)
|
|
14
|
+
return 0;
|
|
15
|
+
for (let k = 2; k < length; k++)
|
|
16
|
+
if ((bytes[i + k] & 0xc0) !== 0x80)
|
|
17
|
+
return 0;
|
|
18
|
+
return length;
|
|
19
|
+
}
|
|
20
|
+
/** True when `bytes` is entirely well-formed UTF-8. */
|
|
21
|
+
export function isUtf8(bytes) {
|
|
22
|
+
for (let i = 0; i < bytes.length;) {
|
|
23
|
+
const n = sequence(bytes, i);
|
|
24
|
+
if (n === 0)
|
|
25
|
+
return false;
|
|
26
|
+
i += n;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
/** Decodes UTF-8, replacing each ill-formed byte with U+FFFD. */
|
|
31
|
+
export function fromUtf8(bytes) {
|
|
32
|
+
let out = '';
|
|
33
|
+
for (let i = 0; i < bytes.length;) {
|
|
34
|
+
const n = sequence(bytes, i);
|
|
35
|
+
if (n === 0) {
|
|
36
|
+
out += '�';
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const b = bytes[i];
|
|
41
|
+
let cp = n === 1 ? b : b & (0xff >>> (n + 1));
|
|
42
|
+
for (let k = 1; k < n; k++)
|
|
43
|
+
cp = (cp << 6) | (bytes[i + k] & 0x3f);
|
|
44
|
+
out += String.fromCodePoint(cp);
|
|
45
|
+
i += n;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @forgesworn/qr
|
|
2
|
+
|
|
3
|
+
> QR code encoder and decoder with no dependencies, for browsers, workers and Node. Versions 1-40, levels L/M/Q/H, numeric, alphanumeric and byte modes, optional UTF-8 ECI, SVG, canvas and terminal output. The decoder reads RGBA or greyscale camera frames, including rotated, tilted, mirrored and inverted codes. Tests compare 114 symbols module for module with ZXing, read output back with jsQR, and require the decoder to read at least as many synthetic distorted frames as jsQR.
|
|
4
|
+
|
|
5
|
+
ESM only, Node.js 22.13+.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @forgesworn/qr
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
- `encode(data: string | Uint8Array, options?: EncodeOptions): QrCode` - options `ecc` ('L'|'M'|'Q'|'H', default 'M'), `version` (1-40), `minVersion`, `mask` (0-7), `mode` ('numeric'|'alphanumeric'|'byte'), `eci` (boolean). Throws `QrCapacityError` (a RangeError) when the data does not fit.
|
|
16
|
+
- `QrCode` - `{ version, ecc, mask, mode, size, modules }`; `modules` is a row-major Uint8Array, 1 = dark, no quiet zone.
|
|
17
|
+
- `isDark(qr, x, y): boolean`
|
|
18
|
+
- `toSvg(qr, { margin = 4, dark = '#000', light = '#fff' | null, title? }): string`
|
|
19
|
+
- `drawCanvas(canvas, qr, { width = 8 px per module, margin = 4, dark, light | null }): void` - any HTMLCanvasElement or OffscreenCanvas; resizes it to width square.
|
|
20
|
+
- `toTerminal(qr, { margin = 4, ink = 'light' | 'dark' }): string` - half-block text; 'light' ink for dark terminal backgrounds.
|
|
21
|
+
- `decode(image: { data, width, height }, { rowStride?, channels?: 1 | 4, inversion?: 'normal' | 'inverted' | 'both' }): DecodeResult | undefined` - data is RGBA (ImageData) or greyscale; result `{ text, bytes, version, ecc, mask, eci, corrected, mirrored, inverted, location }`; undefined when no readable code is found.
|
|
22
|
+
- `decodeModules(modules, size)` - reads a row-major module grid (no quiet zone), straight or mirrored.
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { encode, toSvg } from '@forgesworn/qr'
|
|
28
|
+
const svg = toSvg(encode('https://example.com/', { ecc: 'Q' }))
|
|
29
|
+
|
|
30
|
+
import { decode } from '@forgesworn/qr'
|
|
31
|
+
const text = decode(ctx.getImageData(0, 0, w, h))?.text
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Docs
|
|
35
|
+
|
|
36
|
+
- [README](https://github.com/forgesworn/qr/blob/main/README.md)
|
|
37
|
+
- [SPEC](https://github.com/forgesworn/qr/blob/main/SPEC.md): exact encoder behaviour for ports, and the decoder's pipeline
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgesworn/qr",
|
|
3
|
+
"version": "0.0.0-seed.0",
|
|
4
|
+
"description": "QR code encoder and decoder with no dependencies: versions 1-40, all error correction levels, SVG output, camera-frame reading, checked against independent test vectors.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"SPEC.md",
|
|
20
|
+
"llms.txt",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
27
|
+
"generate-vectors": "sh scripts/generate-vectors.sh",
|
|
28
|
+
"check": "npm run typecheck && npm test",
|
|
29
|
+
"prepare": "npm run build",
|
|
30
|
+
"prepublishOnly": "npm run check"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"qr",
|
|
34
|
+
"qrcode",
|
|
35
|
+
"qr-code",
|
|
36
|
+
"encoder",
|
|
37
|
+
"svg",
|
|
38
|
+
"zero-dependency"
|
|
39
|
+
],
|
|
40
|
+
"author": "forgesworn",
|
|
41
|
+
"funding": {
|
|
42
|
+
"type": "lightning",
|
|
43
|
+
"url": "lightning:profusemeat89@walletofsatoshi.com"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.13"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^22",
|
|
52
|
+
"jsqr": "1.4.0",
|
|
53
|
+
"typescript": "^5.6.0",
|
|
54
|
+
"vitest": "^4.1.11"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public",
|
|
58
|
+
"provenance": true
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/forgesworn/qr#readme",
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/forgesworn/qr/issues"
|
|
63
|
+
},
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/forgesworn/qr.git"
|
|
67
|
+
}
|
|
68
|
+
}
|