@sythos/js_barcode_universal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +215 -0
- package/NOTICE.md +106 -0
- package/README.md +433 -0
- package/bundle/sythos-barcode.esm.js +7998 -0
- package/bundle/sythos-barcode.js +7948 -0
- package/examples/create.html +731 -0
- package/examples/read.html +341 -0
- package/licenses/README.md +42 -0
- package/licenses/codabar.license +74 -0
- package/licenses/code-11.license +69 -0
- package/licenses/code-128.license +69 -0
- package/licenses/code-39.license +70 -0
- package/licenses/code-93.license +71 -0
- package/licenses/ean-13.license +70 -0
- package/licenses/ean-8.license +70 -0
- package/licenses/gs1-128.license +71 -0
- package/licenses/isbn.license +76 -0
- package/licenses/itf-14.license +69 -0
- package/licenses/itf.license +70 -0
- package/licenses/msi-plessey.license +72 -0
- package/licenses/pharmacode.license +71 -0
- package/licenses/qr-code.license +75 -0
- package/licenses/upc-a.license +72 -0
- package/licenses/upc-e.license +69 -0
- package/package.json +89 -0
- package/src/core/bit-buffer.js +174 -0
- package/src/core/bit-matrix.js +241 -0
- package/src/core/errors.js +61 -0
- package/src/core/galois-field.js +204 -0
- package/src/core/index.js +56 -0
- package/src/core/reed-solomon.js +313 -0
- package/src/image/binarizer.js +270 -0
- package/src/image/grid-sampler.js +164 -0
- package/src/image/index.js +40 -0
- package/src/image/luminance.js +196 -0
- package/src/image/perspective.js +195 -0
- package/src/index.js +240 -0
- package/src/oned/index.js +89 -0
- package/src/oned/patterns.js +384 -0
- package/src/oned/reader.js +918 -0
- package/src/oned/writers.js +741 -0
- package/src/qr/decoder.js +575 -0
- package/src/qr/detector.js +630 -0
- package/src/qr/encoder.js +958 -0
- package/src/qr/index.js +44 -0
- package/src/qr/tables.js +737 -0
- package/src/render/image-data.js +125 -0
- package/src/render/index.js +130 -0
- package/src/render/options.js +160 -0
- package/src/render/png.js +295 -0
- package/src/render/svg.js +120 -0
- package/src/render/webgl.js +206 -0
- package/src/render/webgpu.js +369 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* QR Code encoder.
|
|
33
|
+
*
|
|
34
|
+
* Pipeline: analyse the text into mode segments, pick the smallest version that
|
|
35
|
+
* holds them, serialise the bitstream, split it into Reed-Solomon blocks,
|
|
36
|
+
* interleave data and parity, lay the result into the module grid along the
|
|
37
|
+
* zig-zag path, then choose the mask that scores best under the four penalty
|
|
38
|
+
* rules.
|
|
39
|
+
*
|
|
40
|
+
* Segment selection is a shortest-path problem, not a greedy scan. "1234ABCD"
|
|
41
|
+
* is cheaper as one alphanumeric segment than as numeric plus alphanumeric,
|
|
42
|
+
* because a mode switch costs a mode indicator plus a character count field;
|
|
43
|
+
* whether that trade pays depends on run lengths that a left-to-right scan
|
|
44
|
+
* cannot see yet. The dynamic program below weighs it properly.
|
|
45
|
+
*
|
|
46
|
+
* It also has to run *per version band*, because the character count field
|
|
47
|
+
* widens at versions 10 and 27 — so the cheapest segmentation depends on the
|
|
48
|
+
* version, and the smallest sufficient version depends on the segmentation.
|
|
49
|
+
* {@link encodeQR} resolves the circularity by solving each band independently
|
|
50
|
+
* and taking the first version that fits.
|
|
51
|
+
*
|
|
52
|
+
* @module qr/encoder
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
56
|
+
import { BitWriter } from '../core/bit-buffer.js';
|
|
57
|
+
import { EncodeError } from '../core/errors.js';
|
|
58
|
+
import { GF256_QR } from '../core/galois-field.js';
|
|
59
|
+
import { rsEncode } from '../core/reed-solomon.js';
|
|
60
|
+
import {
|
|
61
|
+
ECC_LEVELS,
|
|
62
|
+
ECC_LEVEL_BITS,
|
|
63
|
+
MAX_VERSION,
|
|
64
|
+
MIN_VERSION,
|
|
65
|
+
MODE,
|
|
66
|
+
VERSION_INFO_MIN,
|
|
67
|
+
alignmentCentres,
|
|
68
|
+
blockLayout,
|
|
69
|
+
countBits,
|
|
70
|
+
dataBitCapacity,
|
|
71
|
+
dataModuleOrder,
|
|
72
|
+
formatInfoPositions,
|
|
73
|
+
maskBit,
|
|
74
|
+
versionSize,
|
|
75
|
+
} from './tables.js';
|
|
76
|
+
|
|
77
|
+
/** Alphanumeric mode character set; a character's index is its encoded value. */
|
|
78
|
+
export const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
|
79
|
+
|
|
80
|
+
/** ECI designator for UTF-8. */
|
|
81
|
+
export const ECI_UTF8 = 26;
|
|
82
|
+
|
|
83
|
+
/** BCH(15,5) generator for the format information: x^10+x^8+x^5+x^4+x^2+x+1. */
|
|
84
|
+
const FORMAT_GENERATOR = 0x537;
|
|
85
|
+
|
|
86
|
+
/** Applied to the format information so an all-zero payload is not all-zero. */
|
|
87
|
+
const FORMAT_MASK = 0x5412;
|
|
88
|
+
|
|
89
|
+
/** BCH(18,6) generator for the version information. */
|
|
90
|
+
const VERSION_GENERATOR = 0x1f25;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {object} EncodeOptions
|
|
94
|
+
* @property {'L'|'M'|'Q'|'H'} [ecc] Error correction level. Default 'M'.
|
|
95
|
+
* @property {number} [version] Force a version 1-40 instead of the smallest fit.
|
|
96
|
+
* @property {number} [mask] Force a mask 0-7 instead of the best-scoring one.
|
|
97
|
+
* @property {'auto'|'utf-8'|'iso-8859-1'} [charset] Byte mode interpretation.
|
|
98
|
+
* 'auto' uses ISO-8859-1 when the text allows it and UTF-8 with an ECI
|
|
99
|
+
* header otherwise.
|
|
100
|
+
* @property {boolean} [kanji] Allow kanji mode. Default true; ignored when the
|
|
101
|
+
* platform cannot supply a Shift_JIS codec.
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/* ------------------------------------------------------------------ *
|
|
105
|
+
* Character classification
|
|
106
|
+
* ------------------------------------------------------------------ */
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {string} ch Single code point.
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
function isNumeric(ch) {
|
|
113
|
+
return ch >= '0' && ch <= '9';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} ch Single code point.
|
|
118
|
+
* @returns {number} Alphanumeric value, or -1.
|
|
119
|
+
*/
|
|
120
|
+
function alphanumericValue(ch) {
|
|
121
|
+
return ALPHANUMERIC_CHARS.indexOf(ch);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* UTF-8 length of a code point, without allocating.
|
|
126
|
+
*
|
|
127
|
+
* @param {number} cp
|
|
128
|
+
* @returns {number} 1-4.
|
|
129
|
+
*/
|
|
130
|
+
function utf8Length(cp) {
|
|
131
|
+
if (cp < 0x80) return 1;
|
|
132
|
+
if (cp < 0x800) return 2;
|
|
133
|
+
if (cp < 0x10000) return 3;
|
|
134
|
+
return 4;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ------------------------------------------------------------------ *
|
|
138
|
+
* Shift_JIS
|
|
139
|
+
* ------------------------------------------------------------------ */
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Pack a Shift_JIS double byte into the 13-bit kanji mode value.
|
|
143
|
+
*
|
|
144
|
+
* The two covered ranges are folded onto a single contiguous space by
|
|
145
|
+
* subtracting a different offset from each, then re-basing the low byte to 0xC0
|
|
146
|
+
* values per high byte.
|
|
147
|
+
*
|
|
148
|
+
* @param {number} sjis 16-bit Shift_JIS value.
|
|
149
|
+
* @returns {number} 13-bit value, or -1 if outside the kanji mode ranges.
|
|
150
|
+
*/
|
|
151
|
+
export function sjisToThirteenBits(sjis) {
|
|
152
|
+
// The trail byte must be a real Shift_JIS one. This is not defensive
|
|
153
|
+
// decoration: the packing is only injective while the rebased low byte stays
|
|
154
|
+
// below 0xC0, which holds for trail bytes 0x40-0xFC and fails for anything
|
|
155
|
+
// below 0x40. Accepting those would silently map two inputs to one value.
|
|
156
|
+
const trail = sjis & 0xff;
|
|
157
|
+
if (trail < 0x40 || trail === 0x7f || trail > 0xfc) return -1;
|
|
158
|
+
|
|
159
|
+
let v;
|
|
160
|
+
if (sjis >= 0x8140 && sjis <= 0x9ffc) v = sjis - 0x8140;
|
|
161
|
+
else if (sjis >= 0xe040 && sjis <= 0xebbf) v = sjis - 0xc140;
|
|
162
|
+
else return -1;
|
|
163
|
+
|
|
164
|
+
const packed = ((v >> 8) * 0xc0) + (v & 0xff);
|
|
165
|
+
return packed > 0x1fff ? -1 : packed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @type {Map<string, number> | null} Unicode code point -> Shift_JIS. */
|
|
169
|
+
let sjisReverseMap = null;
|
|
170
|
+
/** @type {boolean} True once we have tried and know whether it worked. */
|
|
171
|
+
let sjisReverseTried = false;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build the Unicode -> Shift_JIS map by inverting the platform's decoder.
|
|
175
|
+
*
|
|
176
|
+
* There is no `TextEncoder` for legacy encodings, so the map is derived by
|
|
177
|
+
* decoding every double byte in the two kanji ranges once and recording what
|
|
178
|
+
* comes back. That is around ten thousand two-byte decodes, done lazily and
|
|
179
|
+
* cached, and only ever paid by text that actually contains kanji.
|
|
180
|
+
*
|
|
181
|
+
* @returns {Map<string, number> | null} Null when the platform has no
|
|
182
|
+
* Shift_JIS decoder, in which case kanji mode is simply not offered.
|
|
183
|
+
*/
|
|
184
|
+
function getSjisReverseMap() {
|
|
185
|
+
if (sjisReverseTried) return sjisReverseMap;
|
|
186
|
+
sjisReverseTried = true;
|
|
187
|
+
|
|
188
|
+
let decoder;
|
|
189
|
+
try {
|
|
190
|
+
decoder = new TextDecoder('shift_jis', { fatal: true });
|
|
191
|
+
// Some runtimes accept the label and then decode everything to U+FFFD.
|
|
192
|
+
if (decoder.decode(new Uint8Array([0x82, 0xa0])) !== 'あ') return null;
|
|
193
|
+
} catch (e) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const map = new Map();
|
|
198
|
+
const buf = new Uint8Array(2);
|
|
199
|
+
const ranges = [[0x8140, 0x9ffc], [0xe040, 0xebbf]];
|
|
200
|
+
|
|
201
|
+
for (let r = 0; r < ranges.length; r++) {
|
|
202
|
+
for (let sjis = ranges[r][0]; sjis <= ranges[r][1]; sjis++) {
|
|
203
|
+
const lo = sjis & 0xff;
|
|
204
|
+
// Shift_JIS trail bytes never take these values.
|
|
205
|
+
if (lo < 0x40 || lo === 0x7f || lo > 0xfc) continue;
|
|
206
|
+
if (sjisToThirteenBits(sjis) < 0) continue;
|
|
207
|
+
buf[0] = sjis >> 8;
|
|
208
|
+
buf[1] = lo;
|
|
209
|
+
let text;
|
|
210
|
+
try {
|
|
211
|
+
text = decoder.decode(buf);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Reject anything that did not decode to exactly one code point, and
|
|
216
|
+
// keep the first (lowest) encoding when a character has several.
|
|
217
|
+
if (text.length === 0 || Array.from(text).length !== 1) continue;
|
|
218
|
+
if (!map.has(text)) map.set(text, sjis);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
sjisReverseMap = map;
|
|
223
|
+
return map;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ------------------------------------------------------------------ *
|
|
227
|
+
* Segment analysis
|
|
228
|
+
* ------------------------------------------------------------------ */
|
|
229
|
+
|
|
230
|
+
/** Modes considered by the segmentation search, in table order. */
|
|
231
|
+
const SEARCH_MODES = [MODE.NUMERIC, MODE.ALPHANUMERIC, MODE.BYTE, MODE.KANJI];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Cost unit: one sixth of a bit, so numeric (10 bits per 3 characters) and
|
|
235
|
+
* alphanumeric (11 bits per 2) are both exact integers.
|
|
236
|
+
*/
|
|
237
|
+
const UNIT = 6;
|
|
238
|
+
const COST_NUMERIC = 20; // 10/3 bits
|
|
239
|
+
const COST_ALPHANUMERIC = 33; // 11/2 bits
|
|
240
|
+
const COST_KANJI = 78; // 13 bits
|
|
241
|
+
const INFEASIBLE = Number.MAX_SAFE_INTEGER / 4;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @typedef {object} CharInfo
|
|
245
|
+
* @property {string[]} points Code points.
|
|
246
|
+
* @property {Uint8Array} numeric
|
|
247
|
+
* @property {Int32Array} alnum Alphanumeric value, or -1.
|
|
248
|
+
* @property {Int32Array} byteLen Bytes this code point costs in byte mode.
|
|
249
|
+
* @property {Int32Array} kanji 13-bit kanji value, or -1.
|
|
250
|
+
* @property {boolean} utf8
|
|
251
|
+
*/
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {string} text
|
|
255
|
+
* @param {boolean} utf8
|
|
256
|
+
* @param {boolean} allowKanji
|
|
257
|
+
* @returns {CharInfo}
|
|
258
|
+
*/
|
|
259
|
+
function classify(text, utf8, allowKanji) {
|
|
260
|
+
const points = Array.from(text);
|
|
261
|
+
const n = points.length;
|
|
262
|
+
const numeric = new Uint8Array(n);
|
|
263
|
+
const alnum = new Int32Array(n);
|
|
264
|
+
const byteLen = new Int32Array(n);
|
|
265
|
+
const kanji = new Int32Array(n);
|
|
266
|
+
|
|
267
|
+
const reverse = allowKanji ? getSjisReverseMap() : null;
|
|
268
|
+
|
|
269
|
+
for (let i = 0; i < n; i++) {
|
|
270
|
+
const ch = points[i];
|
|
271
|
+
const cp = ch.codePointAt(0);
|
|
272
|
+
numeric[i] = isNumeric(ch) ? 1 : 0;
|
|
273
|
+
alnum[i] = alphanumericValue(ch);
|
|
274
|
+
byteLen[i] = utf8 ? utf8Length(cp) : 1;
|
|
275
|
+
|
|
276
|
+
kanji[i] = -1;
|
|
277
|
+
if (reverse) {
|
|
278
|
+
const sjis = reverse.get(ch);
|
|
279
|
+
if (sjis !== undefined) kanji[i] = sjisToThirteenBits(sjis);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { points, numeric, alnum, byteLen, kanji, utf8 };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Cost of one character in a mode, in sixths of a bit.
|
|
288
|
+
*
|
|
289
|
+
* @param {CharInfo} info @param {number} i @param {number} mode
|
|
290
|
+
* @returns {number}
|
|
291
|
+
*/
|
|
292
|
+
function charCost(info, i, mode) {
|
|
293
|
+
switch (mode) {
|
|
294
|
+
case MODE.NUMERIC: return info.numeric[i] ? COST_NUMERIC : INFEASIBLE;
|
|
295
|
+
case MODE.ALPHANUMERIC: return info.alnum[i] >= 0 ? COST_ALPHANUMERIC : INFEASIBLE;
|
|
296
|
+
case MODE.BYTE: return info.byteLen[i] * 8 * UNIT;
|
|
297
|
+
case MODE.KANJI: return info.kanji[i] >= 0 ? COST_KANJI : INFEASIBLE;
|
|
298
|
+
default: return INFEASIBLE;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @typedef {object} Segment
|
|
304
|
+
* @property {number} mode
|
|
305
|
+
* @property {number} start Inclusive index into the code point array.
|
|
306
|
+
* @property {number} end Exclusive.
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Cheapest segmentation for a given version band.
|
|
311
|
+
*
|
|
312
|
+
* Shortest path over (character index, current mode): staying in a mode costs
|
|
313
|
+
* the character, switching costs the character plus a fresh mode indicator and
|
|
314
|
+
* count field.
|
|
315
|
+
*
|
|
316
|
+
* @param {CharInfo} info
|
|
317
|
+
* @param {number} version Any version in the band; only the band matters.
|
|
318
|
+
* @returns {Segment[]}
|
|
319
|
+
*/
|
|
320
|
+
function segmentize(info, version) {
|
|
321
|
+
const n = info.points.length;
|
|
322
|
+
if (n === 0) return [{ mode: MODE.BYTE, start: 0, end: 0 }];
|
|
323
|
+
|
|
324
|
+
const M = SEARCH_MODES.length;
|
|
325
|
+
const header = new Array(M);
|
|
326
|
+
for (let m = 0; m < M; m++) {
|
|
327
|
+
header[m] = (4 + countBits(SEARCH_MODES[m], version)) * UNIT;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let cost = new Array(M);
|
|
331
|
+
for (let m = 0; m < M; m++) cost[m] = header[m];
|
|
332
|
+
|
|
333
|
+
// from[i * M + m] is the mode we were in before character i-1 was appended
|
|
334
|
+
// in mode m. Int8Array is plenty for four modes and keeps this cheap on
|
|
335
|
+
// long payloads.
|
|
336
|
+
const from = new Int8Array((n + 1) * M).fill(-1);
|
|
337
|
+
|
|
338
|
+
for (let i = 0; i < n; i++) {
|
|
339
|
+
const next = new Array(M);
|
|
340
|
+
for (let m = 0; m < M; m++) {
|
|
341
|
+
const cc = charCost(info, i, SEARCH_MODES[m]);
|
|
342
|
+
if (cc >= INFEASIBLE) {
|
|
343
|
+
next[m] = INFEASIBLE;
|
|
344
|
+
from[(i + 1) * M + m] = -1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let bestCost = cost[m]; // stay in this mode
|
|
349
|
+
let bestPrev = m;
|
|
350
|
+
for (let p = 0; p < M; p++) {
|
|
351
|
+
if (p === m) continue;
|
|
352
|
+
const candidate = cost[p] + header[m];
|
|
353
|
+
if (candidate < bestCost) {
|
|
354
|
+
bestCost = candidate;
|
|
355
|
+
bestPrev = p;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
next[m] = bestCost >= INFEASIBLE ? INFEASIBLE : bestCost + cc;
|
|
360
|
+
from[(i + 1) * M + m] = bestPrev;
|
|
361
|
+
}
|
|
362
|
+
cost = next;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let bestMode = -1;
|
|
366
|
+
let bestCost = INFEASIBLE;
|
|
367
|
+
for (let m = 0; m < M; m++) {
|
|
368
|
+
if (cost[m] < bestCost) {
|
|
369
|
+
bestCost = cost[m];
|
|
370
|
+
bestMode = m;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (bestMode < 0) {
|
|
374
|
+
throw new EncodeError('QR: no mode can represent this text');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Walk the parent pointers back, collecting mode runs.
|
|
378
|
+
const modeAt = new Int8Array(n);
|
|
379
|
+
let m = bestMode;
|
|
380
|
+
for (let i = n; i > 0; i--) {
|
|
381
|
+
modeAt[i - 1] = m;
|
|
382
|
+
m = from[i * M + m];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const segments = [];
|
|
386
|
+
let start = 0;
|
|
387
|
+
for (let i = 1; i <= n; i++) {
|
|
388
|
+
if (i === n || modeAt[i] !== modeAt[start]) {
|
|
389
|
+
segments.push({ mode: SEARCH_MODES[modeAt[start]], start, end: i });
|
|
390
|
+
start = i;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return segments;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Exact encoded length of a segment.
|
|
398
|
+
*
|
|
399
|
+
* @param {Segment} seg @param {CharInfo} info @param {number} version
|
|
400
|
+
* @returns {number} Bits, including the mode indicator and count field.
|
|
401
|
+
*/
|
|
402
|
+
function segmentBits(seg, info, version) {
|
|
403
|
+
const n = seg.end - seg.start;
|
|
404
|
+
let payload;
|
|
405
|
+
switch (seg.mode) {
|
|
406
|
+
case MODE.NUMERIC:
|
|
407
|
+
payload = 10 * Math.floor(n / 3) + [0, 4, 7][n % 3];
|
|
408
|
+
break;
|
|
409
|
+
case MODE.ALPHANUMERIC:
|
|
410
|
+
payload = 11 * Math.floor(n / 2) + (n % 2) * 6;
|
|
411
|
+
break;
|
|
412
|
+
case MODE.KANJI:
|
|
413
|
+
payload = 13 * n;
|
|
414
|
+
break;
|
|
415
|
+
default: {
|
|
416
|
+
let bytes = 0;
|
|
417
|
+
for (let i = seg.start; i < seg.end; i++) bytes += info.byteLen[i];
|
|
418
|
+
payload = bytes * 8;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return 4 + countBits(seg.mode, version) + payload;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Character count field value — bytes for byte mode, characters otherwise.
|
|
427
|
+
*
|
|
428
|
+
* @param {Segment} seg @param {CharInfo} info
|
|
429
|
+
* @returns {number}
|
|
430
|
+
*/
|
|
431
|
+
function segmentCount(seg, info) {
|
|
432
|
+
if (seg.mode !== MODE.BYTE) return seg.end - seg.start;
|
|
433
|
+
let bytes = 0;
|
|
434
|
+
for (let i = seg.start; i < seg.end; i++) bytes += info.byteLen[i];
|
|
435
|
+
return bytes;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* ------------------------------------------------------------------ *
|
|
439
|
+
* Bitstream
|
|
440
|
+
* ------------------------------------------------------------------ */
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Serialise segments into the data codewords for a version and level.
|
|
444
|
+
*
|
|
445
|
+
* @param {Segment[]} segments @param {CharInfo} info
|
|
446
|
+
* @param {number} version @param {string} ecc @param {boolean} withEci
|
|
447
|
+
* @returns {Uint8Array} Exactly `dataCodewords(version, ecc)` bytes.
|
|
448
|
+
*/
|
|
449
|
+
function writeBitstream(segments, info, version, ecc, withEci) {
|
|
450
|
+
const writer = new BitWriter();
|
|
451
|
+
const capacity = dataBitCapacity(version, ecc);
|
|
452
|
+
const encoder = info.utf8 ? new TextEncoder() : null;
|
|
453
|
+
|
|
454
|
+
if (withEci) {
|
|
455
|
+
writer.put(MODE.ECI, 4);
|
|
456
|
+
writer.put(ECI_UTF8, 8); // single-byte designator form, values 0-127
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
for (let s = 0; s < segments.length; s++) {
|
|
460
|
+
const seg = segments[s];
|
|
461
|
+
const count = segmentCount(seg, info);
|
|
462
|
+
const width = countBits(seg.mode, version);
|
|
463
|
+
if (count >= (1 << width)) {
|
|
464
|
+
throw new EncodeError(
|
|
465
|
+
`QR: segment of ${count} does not fit a ${width}-bit count field at version ${version}`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
writer.put(seg.mode, 4);
|
|
470
|
+
writer.put(count, width);
|
|
471
|
+
|
|
472
|
+
switch (seg.mode) {
|
|
473
|
+
case MODE.NUMERIC: {
|
|
474
|
+
let i = seg.start;
|
|
475
|
+
while (i + 2 < seg.end) {
|
|
476
|
+
writer.put(
|
|
477
|
+
+info.points[i] * 100 + +info.points[i + 1] * 10 + +info.points[i + 2],
|
|
478
|
+
10
|
|
479
|
+
);
|
|
480
|
+
i += 3;
|
|
481
|
+
}
|
|
482
|
+
const left = seg.end - i;
|
|
483
|
+
if (left === 2) writer.put(+info.points[i] * 10 + +info.points[i + 1], 7);
|
|
484
|
+
else if (left === 1) writer.put(+info.points[i], 4);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
case MODE.ALPHANUMERIC: {
|
|
489
|
+
let i = seg.start;
|
|
490
|
+
while (i + 1 < seg.end) {
|
|
491
|
+
writer.put(info.alnum[i] * 45 + info.alnum[i + 1], 11);
|
|
492
|
+
i += 2;
|
|
493
|
+
}
|
|
494
|
+
if (i < seg.end) writer.put(info.alnum[i], 6);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
case MODE.KANJI: {
|
|
499
|
+
for (let i = seg.start; i < seg.end; i++) writer.put(info.kanji[i], 13);
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
default: {
|
|
504
|
+
for (let i = seg.start; i < seg.end; i++) {
|
|
505
|
+
const ch = info.points[i];
|
|
506
|
+
if (encoder) {
|
|
507
|
+
writer.putBytes(encoder.encode(ch));
|
|
508
|
+
} else {
|
|
509
|
+
writer.put(ch.codePointAt(0) & 0xff, 8);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (writer.length > capacity) {
|
|
518
|
+
throw new EncodeError(
|
|
519
|
+
`QR: ${writer.length} bits exceed the ${capacity}-bit capacity of version ${version}-${ecc}`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Terminator: up to four zero bits, truncated if the symbol is nearly full.
|
|
524
|
+
const terminator = Math.min(4, capacity - writer.length);
|
|
525
|
+
if (terminator > 0) writer.put(0, terminator);
|
|
526
|
+
writer.padToByte();
|
|
527
|
+
|
|
528
|
+
const bytes = writer.toBytes();
|
|
529
|
+
const target = capacity / 8;
|
|
530
|
+
const out = new Uint8Array(target);
|
|
531
|
+
out.set(bytes.subarray(0, Math.min(bytes.length, target)));
|
|
532
|
+
|
|
533
|
+
// Pad with the specified alternating filler.
|
|
534
|
+
for (let i = bytes.length; i < target; i++) {
|
|
535
|
+
out[i] = (i - bytes.length) % 2 === 0 ? 0xec : 0x11;
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Split into blocks, add Reed-Solomon parity, and interleave.
|
|
542
|
+
*
|
|
543
|
+
* Interleaving is what makes the error correction useful against real damage:
|
|
544
|
+
* a scratch that destroys twenty consecutive codewords in the symbol spreads
|
|
545
|
+
* across every block as one or two errors each, well inside what each block can
|
|
546
|
+
* repair on its own.
|
|
547
|
+
*
|
|
548
|
+
* @param {Uint8Array} data @param {import('./tables.js').BlockLayout} layout
|
|
549
|
+
* @returns {Uint8Array}
|
|
550
|
+
*/
|
|
551
|
+
function interleave(data, layout) {
|
|
552
|
+
const blocks = [];
|
|
553
|
+
let offset = 0;
|
|
554
|
+
for (let b = 0; b < layout.blockCount; b++) {
|
|
555
|
+
const count = b < layout.group1Blocks ? layout.group1DataCount : layout.group2DataCount;
|
|
556
|
+
const chunk = data.subarray(offset, offset + count);
|
|
557
|
+
offset += count;
|
|
558
|
+
blocks.push({ data: chunk, ecc: rsEncode(chunk, layout.eccPerBlock, GF256_QR, 0) });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const out = new Uint8Array(layout.totalCodewords);
|
|
562
|
+
let n = 0;
|
|
563
|
+
|
|
564
|
+
const maxData = layout.group2Blocks > 0 ? layout.group2DataCount : layout.group1DataCount;
|
|
565
|
+
for (let i = 0; i < maxData; i++) {
|
|
566
|
+
for (let b = 0; b < blocks.length; b++) {
|
|
567
|
+
if (i < blocks[b].data.length) out[n++] = blocks[b].data[i];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (let i = 0; i < layout.eccPerBlock; i++) {
|
|
571
|
+
for (let b = 0; b < blocks.length; b++) out[n++] = blocks[b].ecc[i];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* ------------------------------------------------------------------ *
|
|
578
|
+
* BCH
|
|
579
|
+
* ------------------------------------------------------------------ */
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @param {number} v
|
|
583
|
+
* @returns {number} Position of the highest set bit, plus one.
|
|
584
|
+
*/
|
|
585
|
+
function bitLength(v) {
|
|
586
|
+
let n = 0;
|
|
587
|
+
while (v !== 0) {
|
|
588
|
+
n++;
|
|
589
|
+
v >>>= 1;
|
|
590
|
+
}
|
|
591
|
+
return n;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Remainder of `value` modulo a BCH generator polynomial, over GF(2).
|
|
596
|
+
*
|
|
597
|
+
* @param {number} value @param {number} generator
|
|
598
|
+
* @returns {number}
|
|
599
|
+
*/
|
|
600
|
+
function bchRemainder(value, generator) {
|
|
601
|
+
const degree = bitLength(generator) - 1;
|
|
602
|
+
let v = value;
|
|
603
|
+
while (bitLength(v) > degree) {
|
|
604
|
+
v ^= generator << (bitLength(v) - degree - 1);
|
|
605
|
+
}
|
|
606
|
+
return v;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* The 15-bit masked format information.
|
|
611
|
+
*
|
|
612
|
+
* @param {string} ecc @param {number} mask
|
|
613
|
+
* @returns {number}
|
|
614
|
+
*/
|
|
615
|
+
export function formatInfoBits(ecc, mask) {
|
|
616
|
+
const level = ECC_LEVEL_BITS[ecc];
|
|
617
|
+
if (level === undefined) throw new EncodeError(`QR: unknown error correction level "${ecc}"`);
|
|
618
|
+
const data = (level << 3) | mask;
|
|
619
|
+
return ((data << 10) | bchRemainder(data << 10, FORMAT_GENERATOR)) ^ FORMAT_MASK;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* The 18-bit version information, for versions 7 and up.
|
|
624
|
+
*
|
|
625
|
+
* @param {number} version
|
|
626
|
+
* @returns {number}
|
|
627
|
+
*/
|
|
628
|
+
export function versionInfoBits(version) {
|
|
629
|
+
return (version << 12) | bchRemainder(version << 12, VERSION_GENERATOR);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/* ------------------------------------------------------------------ *
|
|
633
|
+
* Module layout
|
|
634
|
+
* ------------------------------------------------------------------ */
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Clear a rectangle. The counterpart of `setRegion`, which BitMatrix does not
|
|
638
|
+
* need often enough to carry.
|
|
639
|
+
*
|
|
640
|
+
* @param {BitMatrix} m @param {number} x @param {number} y
|
|
641
|
+
* @param {number} w @param {number} h
|
|
642
|
+
*/
|
|
643
|
+
function clearRegion(m, x, y, w, h) {
|
|
644
|
+
for (let j = y; j < y + h; j++) {
|
|
645
|
+
for (let i = x; i < x + w; i++) m.unset(i, j);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Draw finders, separators, timing, alignment and the dark module.
|
|
651
|
+
*
|
|
652
|
+
* @param {BitMatrix} m @param {number} version
|
|
653
|
+
*/
|
|
654
|
+
function drawFunctionPatterns(m, version) {
|
|
655
|
+
const size = versionSize(version);
|
|
656
|
+
|
|
657
|
+
// Finder patterns: 7x7 dark ring, light ring, 3x3 dark core. Separators are
|
|
658
|
+
// simply left light, which they already are.
|
|
659
|
+
const corners = [[0, 0], [size - 7, 0], [0, size - 7]];
|
|
660
|
+
for (let c = 0; c < corners.length; c++) {
|
|
661
|
+
const [x, y] = corners[c];
|
|
662
|
+
m.setRegion(x, y, 7, 7);
|
|
663
|
+
clearRegion(m, x + 1, y + 1, 5, 5);
|
|
664
|
+
m.setRegion(x + 2, y + 2, 3, 3);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Timing patterns: dark on even coordinates, which keeps them in phase with
|
|
668
|
+
// the finder edges they run between.
|
|
669
|
+
for (let i = 8; i < size - 8; i++) {
|
|
670
|
+
if ((i & 1) === 0) {
|
|
671
|
+
m.set(i, 6);
|
|
672
|
+
m.set(6, i);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Alignment patterns: 5x5 dark ring, light ring, single dark centre.
|
|
677
|
+
const centres = alignmentCentres(version);
|
|
678
|
+
for (let c = 0; c < centres.length; c++) {
|
|
679
|
+
const [cx, cy] = centres[c];
|
|
680
|
+
m.setRegion(cx - 2, cy - 2, 5, 5);
|
|
681
|
+
clearRegion(m, cx - 1, cy - 1, 3, 3);
|
|
682
|
+
m.set(cx, cy);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// The dark module, which is always set and carries no information.
|
|
686
|
+
m.set(8, size - 8);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* @param {BitMatrix} m @param {number} version @param {string} ecc @param {number} mask
|
|
691
|
+
*/
|
|
692
|
+
function drawFormatInfo(m, version, ecc, mask) {
|
|
693
|
+
const size = versionSize(version);
|
|
694
|
+
const bits = formatInfoBits(ecc, mask);
|
|
695
|
+
const [a, b] = formatInfoPositions(size);
|
|
696
|
+
for (let i = 0; i < 15; i++) {
|
|
697
|
+
const on = ((bits >> i) & 1) === 1;
|
|
698
|
+
m.setValue(a[i][0], a[i][1], on);
|
|
699
|
+
m.setValue(b[i][0], b[i][1], on);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* @param {BitMatrix} m @param {number} version
|
|
705
|
+
*/
|
|
706
|
+
function drawVersionInfo(m, version) {
|
|
707
|
+
if (version < VERSION_INFO_MIN) return;
|
|
708
|
+
const size = versionSize(version);
|
|
709
|
+
const bits = versionInfoBits(version);
|
|
710
|
+
for (let i = 0; i < 18; i++) {
|
|
711
|
+
const on = ((bits >> i) & 1) === 1;
|
|
712
|
+
const major = Math.floor(i / 3);
|
|
713
|
+
const minor = i % 3;
|
|
714
|
+
m.setValue(major, size - 11 + minor, on); // bottom-left block
|
|
715
|
+
m.setValue(size - 11 + minor, major, on); // top-right block, transposed
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Lay the interleaved codewords along the zig-zag path, applying the mask.
|
|
721
|
+
*
|
|
722
|
+
* @param {BitMatrix} m @param {number} version @param {Uint8Array} codewords
|
|
723
|
+
* @param {number} mask
|
|
724
|
+
*/
|
|
725
|
+
function placeData(m, version, codewords, mask) {
|
|
726
|
+
const order = dataModuleOrder(version);
|
|
727
|
+
const available = codewords.length * 8;
|
|
728
|
+
|
|
729
|
+
for (let p = 0, bit = 0; p < order.length; p += 2, bit++) {
|
|
730
|
+
const x = order[p];
|
|
731
|
+
const y = order[p + 1];
|
|
732
|
+
// Past the last codeword are the remainder bits, which are always zero.
|
|
733
|
+
let dark = bit < available &&
|
|
734
|
+
((codewords[bit >> 3] >> (7 - (bit & 7))) & 1) === 1;
|
|
735
|
+
if (maskBit(mask, x, y)) dark = !dark;
|
|
736
|
+
m.setValue(x, y, dark);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* ------------------------------------------------------------------ *
|
|
741
|
+
* Mask scoring
|
|
742
|
+
* ------------------------------------------------------------------ */
|
|
743
|
+
|
|
744
|
+
/** The 11-module finder-lookalike runs that rule 3 punishes. */
|
|
745
|
+
const RULE3_A = 0b10111010000;
|
|
746
|
+
const RULE3_B = 0b00001011101;
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The four penalty rules. Lower is better.
|
|
750
|
+
*
|
|
751
|
+
* These exist to keep a symbol readable: long uniform runs and large blocks
|
|
752
|
+
* confuse the binarizer, finder lookalikes confuse the detector, and a symbol
|
|
753
|
+
* far from half dark loses contrast headroom. Scoring is a heuristic, not a
|
|
754
|
+
* correctness surface — any of the eight masks decodes, because the format
|
|
755
|
+
* information says which one was used.
|
|
756
|
+
*
|
|
757
|
+
* @param {BitMatrix} m
|
|
758
|
+
* @returns {number}
|
|
759
|
+
*/
|
|
760
|
+
export function maskPenalty(m) {
|
|
761
|
+
const size = m.width;
|
|
762
|
+
let penalty = 0;
|
|
763
|
+
|
|
764
|
+
// Rule 1: runs of five or more identical modules, per row and per column.
|
|
765
|
+
for (let axis = 0; axis < 2; axis++) {
|
|
766
|
+
for (let a = 0; a < size; a++) {
|
|
767
|
+
let run = 1;
|
|
768
|
+
let prev = axis === 0 ? m.get(0, a) : m.get(a, 0);
|
|
769
|
+
for (let b = 1; b < size; b++) {
|
|
770
|
+
const cur = axis === 0 ? m.get(b, a) : m.get(a, b);
|
|
771
|
+
if (cur === prev) {
|
|
772
|
+
run++;
|
|
773
|
+
} else {
|
|
774
|
+
if (run >= 5) penalty += 3 + (run - 5);
|
|
775
|
+
prev = cur;
|
|
776
|
+
run = 1;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (run >= 5) penalty += 3 + (run - 5);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Rule 2: every 2x2 block of one colour.
|
|
784
|
+
for (let y = 0; y < size - 1; y++) {
|
|
785
|
+
for (let x = 0; x < size - 1; x++) {
|
|
786
|
+
const v = m.get(x, y);
|
|
787
|
+
if (v === m.get(x + 1, y) && v === m.get(x, y + 1) && v === m.get(x + 1, y + 1)) {
|
|
788
|
+
penalty += 3;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Rule 3: the 1:1:3:1:1 finder ratio with four light modules on one side.
|
|
794
|
+
// An 11-module sliding window in each direction catches both orientations.
|
|
795
|
+
for (let axis = 0; axis < 2; axis++) {
|
|
796
|
+
for (let a = 0; a < size; a++) {
|
|
797
|
+
let window = 0;
|
|
798
|
+
for (let b = 0; b < size; b++) {
|
|
799
|
+
const bit = (axis === 0 ? m.get(b, a) : m.get(a, b)) ? 1 : 0;
|
|
800
|
+
window = ((window << 1) | bit) & 0x7ff;
|
|
801
|
+
if (b >= 10 && (window === RULE3_A || window === RULE3_B)) penalty += 40;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Rule 4: departure from an even split of dark and light.
|
|
807
|
+
let dark = 0;
|
|
808
|
+
for (let y = 0; y < size; y++) {
|
|
809
|
+
for (let x = 0; x < size; x++) {
|
|
810
|
+
if (m.get(x, y)) dark++;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const percent = (dark * 100) / (size * size);
|
|
814
|
+
penalty += Math.floor(Math.abs(percent - 50) / 5) * 10;
|
|
815
|
+
|
|
816
|
+
return penalty;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* ------------------------------------------------------------------ *
|
|
820
|
+
* Public API
|
|
821
|
+
* ------------------------------------------------------------------ */
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Build the module grid for a version, level, mask and payload.
|
|
825
|
+
*
|
|
826
|
+
* @param {number} version @param {string} ecc @param {number} mask
|
|
827
|
+
* @param {Uint8Array} codewords
|
|
828
|
+
* @returns {BitMatrix}
|
|
829
|
+
*/
|
|
830
|
+
function buildMatrix(version, ecc, mask, codewords) {
|
|
831
|
+
const m = new BitMatrix(versionSize(version));
|
|
832
|
+
drawFunctionPatterns(m, version);
|
|
833
|
+
placeData(m, version, codewords, mask);
|
|
834
|
+
drawFormatInfo(m, version, ecc, mask);
|
|
835
|
+
drawVersionInfo(m, version);
|
|
836
|
+
return m;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Encode text as a QR Code.
|
|
841
|
+
*
|
|
842
|
+
* The result carries no quiet zone; callers add one with
|
|
843
|
+
* `matrix.withMargin(4)`. Keeping the margin out of the encoder means the
|
|
844
|
+
* renderer decides it, which is where the decision belongs — a symbol embedded
|
|
845
|
+
* in a design and a symbol printed on a label want different borders.
|
|
846
|
+
*
|
|
847
|
+
* @param {string} text
|
|
848
|
+
* @param {EncodeOptions} [options]
|
|
849
|
+
* @returns {BitMatrix} Set bit = dark module.
|
|
850
|
+
* @throws {EncodeError} If the text does not fit, or the options are invalid.
|
|
851
|
+
*/
|
|
852
|
+
export function encodeQR(text, options = {}) {
|
|
853
|
+
if (typeof text !== 'string') {
|
|
854
|
+
throw new EncodeError('QR: text must be a string');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const ecc = options.ecc ?? 'M';
|
|
858
|
+
if (ECC_LEVELS.indexOf(ecc) === -1) {
|
|
859
|
+
throw new EncodeError(`QR: error correction level must be L, M, Q or H, got "${ecc}"`);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const forcedVersion = options.version;
|
|
863
|
+
if (forcedVersion !== undefined) {
|
|
864
|
+
if (!Number.isInteger(forcedVersion) || forcedVersion < MIN_VERSION || forcedVersion > MAX_VERSION) {
|
|
865
|
+
throw new EncodeError(`QR: version must be an integer 1-40, got ${forcedVersion}`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const forcedMask = options.mask;
|
|
870
|
+
if (forcedMask !== undefined && (!Number.isInteger(forcedMask) || forcedMask < 0 || forcedMask > 7)) {
|
|
871
|
+
throw new EncodeError(`QR: mask must be an integer 0-7, got ${forcedMask}`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Byte mode interpretation. ISO-8859-1 is the default ECI, so Latin-1 text
|
|
875
|
+
// needs no header; anything else goes out as UTF-8 with ECI 26 announced.
|
|
876
|
+
const charset = options.charset ?? 'auto';
|
|
877
|
+
let utf8;
|
|
878
|
+
if (charset === 'utf-8') utf8 = true;
|
|
879
|
+
else if (charset === 'iso-8859-1') utf8 = false;
|
|
880
|
+
else if (charset === 'auto') utf8 = !isLatin1(text);
|
|
881
|
+
else throw new EncodeError(`QR: unknown charset "${charset}"`);
|
|
882
|
+
|
|
883
|
+
if (!utf8 && !isLatin1(text)) {
|
|
884
|
+
throw new EncodeError('QR: text contains characters outside ISO-8859-1');
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const allowKanji = options.kanji !== false;
|
|
888
|
+
const info = classify(text, utf8, allowKanji);
|
|
889
|
+
|
|
890
|
+
// Solve each version band once — the count field widths, and therefore the
|
|
891
|
+
// cheapest segmentation, are constant within a band.
|
|
892
|
+
const bands = [[1, 9], [10, 26], [27, 40]];
|
|
893
|
+
let chosen = null;
|
|
894
|
+
|
|
895
|
+
for (let b = 0; b < bands.length && !chosen; b++) {
|
|
896
|
+
const [lo, hi] = bands[b];
|
|
897
|
+
if (forcedVersion !== undefined && (forcedVersion < lo || forcedVersion > hi)) continue;
|
|
898
|
+
|
|
899
|
+
const segments = segmentize(info, lo);
|
|
900
|
+
const needsEci = utf8 && segments.some((s) => s.mode === MODE.BYTE);
|
|
901
|
+
|
|
902
|
+
let bits = needsEci ? 12 : 0; // ECI mode indicator plus one designator byte
|
|
903
|
+
for (let s = 0; s < segments.length; s++) bits += segmentBits(segments[s], info, lo);
|
|
904
|
+
|
|
905
|
+
const from = forcedVersion !== undefined ? forcedVersion : lo;
|
|
906
|
+
const to = forcedVersion !== undefined ? forcedVersion : hi;
|
|
907
|
+
for (let v = from; v <= to; v++) {
|
|
908
|
+
if (bits <= dataBitCapacity(v, ecc)) {
|
|
909
|
+
chosen = { version: v, segments, needsEci, bits };
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (!chosen) {
|
|
916
|
+
if (forcedVersion !== undefined) {
|
|
917
|
+
throw new EncodeError(
|
|
918
|
+
`QR: text does not fit version ${forcedVersion}-${ecc} ` +
|
|
919
|
+
`(capacity ${dataBitCapacity(forcedVersion, ecc)} bits)`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
throw new EncodeError(
|
|
923
|
+
`QR: text is too long for any version at error correction level ${ecc}`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const { version, segments, needsEci } = chosen;
|
|
928
|
+
const layout = blockLayout(version, ecc);
|
|
929
|
+
const data = writeBitstream(segments, info, version, ecc, needsEci);
|
|
930
|
+
const codewords = interleave(data, layout);
|
|
931
|
+
|
|
932
|
+
if (forcedMask !== undefined) {
|
|
933
|
+
return buildMatrix(version, ecc, forcedMask, codewords);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let best = null;
|
|
937
|
+
let bestScore = Infinity;
|
|
938
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
939
|
+
const candidate = buildMatrix(version, ecc, mask, codewords);
|
|
940
|
+
const score = maskPenalty(candidate);
|
|
941
|
+
if (score < bestScore) {
|
|
942
|
+
bestScore = score;
|
|
943
|
+
best = candidate;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return best;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* @param {string} text
|
|
951
|
+
* @returns {boolean} True if every code point fits one ISO-8859-1 byte.
|
|
952
|
+
*/
|
|
953
|
+
function isLatin1(text) {
|
|
954
|
+
for (let i = 0; i < text.length; i++) {
|
|
955
|
+
if (text.charCodeAt(i) > 0xff) return false;
|
|
956
|
+
}
|
|
957
|
+
return true;
|
|
958
|
+
}
|