kafox-cli 0.9.8 → 0.10.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/bin/kafox.js +78 -0
- package/package.json +18 -1
- package/src/agent.js +24 -0
- package/src/auth.js +25 -6
- package/src/cli.js +3 -0
- package/src/compact.js +346 -0
- package/src/gateway.js +119 -53
- package/src/mobile/auth.js +352 -0
- package/src/mobile/bridge.js +285 -0
- package/src/mobile/index.js +538 -0
- package/src/mobile/protocol.js +51 -0
- package/src/mobile/qr.js +487 -0
- package/src/mobile/server.js +297 -0
- package/src/mobile/tui-bridge.js +289 -0
- package/src/mobile/view.js +734 -0
- package/src/models.js +19 -0
- package/src/providers-cli.js +82 -0
- package/src/providers.js +87 -0
- package/src/storage.js +39 -0
- package/src/tools.js +13 -2
- package/src/tui.js +549 -44
package/src/mobile/qr.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QR Code em ASCII, sem dependencia externa.
|
|
3
|
+
*
|
|
4
|
+
* Escopo de proposito: URLs curtas do pareamento mobile (byte mode, ECC M,
|
|
5
|
+
* versoes 1..10 = ate 213 bytes). Nao e um QR generico: e exatamente o que o
|
|
6
|
+
* `kafox mobile` precisa e nada mais — zero `npm install`.
|
|
7
|
+
*
|
|
8
|
+
* A saida usa meios blocos ("#" / espaco) para o terminal renderizar com boa
|
|
9
|
+
* densidade e o celular conseguir ler a tela.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Capacidade (bytes) em ECC-M por versao 1..10.
|
|
13
|
+
const CAPACITY_M = { 1: 14, 2: 26, 3: 42, 4: 62, 5: 84, 6: 106, 7: 122, 8: 152, 9: 180, 10: 213 };
|
|
14
|
+
|
|
15
|
+
// Config de blocos ECC-M por versao: [eccPorBloco, blocosG1, dadosG1, blocosG2, dadosG2].
|
|
16
|
+
const BLOCKS_M = {
|
|
17
|
+
1: [10, 1, 16, 0, 0],
|
|
18
|
+
2: [16, 1, 28, 0, 0],
|
|
19
|
+
3: [26, 1, 44, 0, 0],
|
|
20
|
+
4: [18, 2, 32, 0, 0],
|
|
21
|
+
5: [24, 2, 43, 0, 0],
|
|
22
|
+
6: [16, 4, 27, 0, 0],
|
|
23
|
+
7: [18, 4, 31, 0, 0],
|
|
24
|
+
8: [22, 2, 38, 2, 39],
|
|
25
|
+
9: [22, 3, 36, 2, 37],
|
|
26
|
+
10: [26, 4, 43, 1, 44],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ALIGNMENT = {
|
|
30
|
+
1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30],
|
|
31
|
+
6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function sizeFor(version) {
|
|
35
|
+
return version * 4 + 17;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pickVersion(byteLength) {
|
|
39
|
+
for (let v = 1; v <= 10; v += 1) if (CAPACITY_M[v] >= byteLength) return v;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- Galois field GF(256) para Reed-Solomon ----
|
|
44
|
+
const EXP = new Array(512);
|
|
45
|
+
const LOG = new Array(256);
|
|
46
|
+
(() => {
|
|
47
|
+
let x = 1;
|
|
48
|
+
for (let i = 0; i < 255; i += 1) {
|
|
49
|
+
EXP[i] = x;
|
|
50
|
+
LOG[x] = i;
|
|
51
|
+
x <<= 1;
|
|
52
|
+
if (x & 0x100) x ^= 0x11d;
|
|
53
|
+
}
|
|
54
|
+
for (let i = 255; i < 512; i += 1) EXP[i] = EXP[i - 255];
|
|
55
|
+
})();
|
|
56
|
+
|
|
57
|
+
function mul(a, b) {
|
|
58
|
+
if (a === 0 || b === 0) return 0;
|
|
59
|
+
return EXP[LOG[a] + LOG[b]];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rsGenerator(degree) {
|
|
63
|
+
let poly = [1];
|
|
64
|
+
for (let i = 0; i < degree; i += 1) {
|
|
65
|
+
const next = new Array(poly.length + 1).fill(0);
|
|
66
|
+
for (let j = 0; j < poly.length; j += 1) {
|
|
67
|
+
next[j] ^= mul(poly[j], 1);
|
|
68
|
+
next[j + 1] ^= mul(poly[j], EXP[i]);
|
|
69
|
+
}
|
|
70
|
+
poly = next;
|
|
71
|
+
}
|
|
72
|
+
return poly;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function rsEncode(data, eccLen) {
|
|
76
|
+
const gen = rsGenerator(eccLen);
|
|
77
|
+
const res = new Array(eccLen).fill(0);
|
|
78
|
+
for (const byte of data) {
|
|
79
|
+
const factor = byte ^ res[0];
|
|
80
|
+
res.shift();
|
|
81
|
+
res.push(0);
|
|
82
|
+
for (let i = 0; i < eccLen; i += 1) res[i] ^= mul(gen[i + 1], factor);
|
|
83
|
+
}
|
|
84
|
+
return res;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- Bit buffer ----
|
|
88
|
+
class Bits {
|
|
89
|
+
constructor() {
|
|
90
|
+
this.bits = [];
|
|
91
|
+
}
|
|
92
|
+
put(value, length) {
|
|
93
|
+
for (let i = length - 1; i >= 0; i -= 1) this.bits.push((value >> i) & 1);
|
|
94
|
+
}
|
|
95
|
+
get length() {
|
|
96
|
+
return this.bits.length;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function encodeData(text, version) {
|
|
101
|
+
const bytes = Buffer.from(text, "utf8");
|
|
102
|
+
const bits = new Bits();
|
|
103
|
+
bits.put(0b0100, 4); // byte mode
|
|
104
|
+
// contador de caracteres: 8 bits para versoes 1..9, 16 bits na 10.
|
|
105
|
+
bits.put(bytes.length, version <= 9 ? 8 : 16);
|
|
106
|
+
for (const byte of bytes) bits.put(byte, 8);
|
|
107
|
+
|
|
108
|
+
const spec = BLOCKS_M[version];
|
|
109
|
+
const eccPerBlock = spec[0];
|
|
110
|
+
const totalBlocks = spec[1] + spec[3];
|
|
111
|
+
const dataBlocks = spec[1] + spec[3];
|
|
112
|
+
const totalDataBytes = spec[1] * spec[2] + spec[3] * spec[4];
|
|
113
|
+
|
|
114
|
+
// terminador (ate 4 bits) + padding ate fechar o byte.
|
|
115
|
+
const capacityBits = totalDataBytes * 8;
|
|
116
|
+
const terminate = Math.min(4, capacityBits - bits.length);
|
|
117
|
+
bits.put(0, terminate);
|
|
118
|
+
while (bits.length % 8 !== 0) bits.put(0, 1);
|
|
119
|
+
|
|
120
|
+
const dataBytes = [];
|
|
121
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
122
|
+
let byte = 0;
|
|
123
|
+
for (let j = 0; j < 8; j += 1) byte = (byte << 1) | bits.bits[i + j];
|
|
124
|
+
dataBytes.push(byte);
|
|
125
|
+
}
|
|
126
|
+
// bytes de preenchimento alternados.
|
|
127
|
+
const PAD = [0xec, 0x11];
|
|
128
|
+
let padIndex = 0;
|
|
129
|
+
while (dataBytes.length < totalDataBytes) {
|
|
130
|
+
dataBytes.push(PAD[padIndex % 2]);
|
|
131
|
+
padIndex += 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// divide em blocos.
|
|
135
|
+
const blocks = [];
|
|
136
|
+
let offset = 0;
|
|
137
|
+
for (let i = 0; i < spec[1]; i += 1) {
|
|
138
|
+
blocks.push(dataBytes.slice(offset, offset + spec[2]));
|
|
139
|
+
offset += spec[2];
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < spec[3]; i += 1) {
|
|
142
|
+
blocks.push(dataBytes.slice(offset, offset + spec[4]));
|
|
143
|
+
offset += spec[4];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const eccBlocks = blocks.map((block) => rsEncode(block, eccPerBlock));
|
|
147
|
+
|
|
148
|
+
// interleave dos dados, depois do ECC.
|
|
149
|
+
const interleaved = [];
|
|
150
|
+
const maxData = Math.max(spec[2], spec[4]);
|
|
151
|
+
for (let i = 0; i < maxData; i += 1) {
|
|
152
|
+
for (const block of blocks) if (i < block.length) interleaved.push(block[i]);
|
|
153
|
+
}
|
|
154
|
+
for (let i = 0; i < eccPerBlock; i += 1) {
|
|
155
|
+
for (const block of eccBlocks) interleaved.push(block[i]);
|
|
156
|
+
}
|
|
157
|
+
return interleaved;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- Matriz ----
|
|
161
|
+
function createMatrix(version) {
|
|
162
|
+
const size = sizeFor(version);
|
|
163
|
+
const modules = Array.from({ length: size }, () => new Array(size).fill(null));
|
|
164
|
+
const reserved = Array.from({ length: size }, () => new Array(size).fill(false));
|
|
165
|
+
|
|
166
|
+
const setFinder = (row, col) => {
|
|
167
|
+
for (let r = -1; r <= 7; r += 1) {
|
|
168
|
+
for (let c = -1; c <= 7; c += 1) {
|
|
169
|
+
const rr = row + r;
|
|
170
|
+
const cc = col + c;
|
|
171
|
+
if (rr < 0 || cc < 0 || rr >= size || cc >= size) continue;
|
|
172
|
+
const inRing = r >= 0 && r <= 6 && c >= 0 && c <= 6;
|
|
173
|
+
const dark = inRing && (r === 0 || r === 6 || c === 0 || c === 6 || (r >= 2 && r <= 4 && c >= 2 && c <= 4));
|
|
174
|
+
modules[rr][cc] = dark ? 1 : 0;
|
|
175
|
+
reserved[rr][cc] = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
setFinder(0, 0);
|
|
180
|
+
setFinder(0, size - 7);
|
|
181
|
+
setFinder(size - 7, 0);
|
|
182
|
+
|
|
183
|
+
// timing patterns
|
|
184
|
+
for (let i = 8; i < size - 8; i += 1) {
|
|
185
|
+
if (modules[6][i] === null) {
|
|
186
|
+
modules[6][i] = i % 2 === 0 ? 1 : 0;
|
|
187
|
+
reserved[6][i] = true;
|
|
188
|
+
}
|
|
189
|
+
if (modules[i][6] === null) {
|
|
190
|
+
modules[i][6] = i % 2 === 0 ? 1 : 0;
|
|
191
|
+
reserved[i][6] = true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// alignment patterns
|
|
196
|
+
const positions = ALIGNMENT[version];
|
|
197
|
+
for (const row of positions) {
|
|
198
|
+
for (const col of positions) {
|
|
199
|
+
if (reserved[row][col]) continue;
|
|
200
|
+
for (let r = -2; r <= 2; r += 1) {
|
|
201
|
+
for (let c = -2; c <= 2; c += 1) {
|
|
202
|
+
const dark = Math.max(Math.abs(r), Math.abs(c)) !== 1;
|
|
203
|
+
modules[row + r][col + c] = dark ? 1 : 0;
|
|
204
|
+
reserved[row + r][col + c] = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Reserva a area da info de formato (15 bits x 2 copias), contornando os
|
|
211
|
+
// finders. Copia 1: em volta do finder superior esquerdo (linha 8 e coluna 8,
|
|
212
|
+
// pulando a coluna/linha de timing em 6). Copia 2: sob o finder superior
|
|
213
|
+
// direito (linha 8, ultimas 8 colunas) e a direita do finder inferior
|
|
214
|
+
// esquerdo (coluna 8, ultimas 7 linhas).
|
|
215
|
+
for (let i = 0; i <= 8; i += 1) {
|
|
216
|
+
if (i !== 6) {
|
|
217
|
+
reserved[8][i] = true;
|
|
218
|
+
reserved[i][8] = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (let i = 0; i < 8; i += 1) {
|
|
222
|
+
reserved[8][size - 1 - i] = true;
|
|
223
|
+
}
|
|
224
|
+
for (let i = 0; i < 7; i += 1) {
|
|
225
|
+
reserved[size - 1 - i][8] = true;
|
|
226
|
+
}
|
|
227
|
+
// dark module (sempre escuro, nao carrega dado)
|
|
228
|
+
modules[size - 8][8] = 1;
|
|
229
|
+
reserved[size - 8][8] = true;
|
|
230
|
+
|
|
231
|
+
return { size, modules, reserved };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function placeData(matrix, codewords) {
|
|
235
|
+
const { size, modules, reserved } = matrix;
|
|
236
|
+
let bitIndex = 0;
|
|
237
|
+
let upward = true;
|
|
238
|
+
for (let col = size - 1; col > 0; col -= 2) {
|
|
239
|
+
if (col === 6) col -= 1; // pula a coluna de timing
|
|
240
|
+
for (let i = 0; i < size; i += 1) {
|
|
241
|
+
const row = upward ? size - 1 - i : i;
|
|
242
|
+
for (let c = 0; c < 2; c += 1) {
|
|
243
|
+
const cc = col - c;
|
|
244
|
+
if (reserved[row][cc]) continue;
|
|
245
|
+
const bit =
|
|
246
|
+
bitIndex < codewords.length * 8
|
|
247
|
+
? (codewords[bitIndex >> 3] >> (7 - (bitIndex & 7))) & 1
|
|
248
|
+
: 0;
|
|
249
|
+
modules[row][cc] = bit;
|
|
250
|
+
bitIndex += 1;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
upward = !upward;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function applyMask(modules, reserved, size, maskId) {
|
|
258
|
+
const maskFn = {
|
|
259
|
+
0: (r, c) => (r + c) % 2 === 0,
|
|
260
|
+
1: (r) => r % 2 === 0,
|
|
261
|
+
2: (_, c) => c % 3 === 0,
|
|
262
|
+
3: (r, c) => (r + c) % 3 === 0,
|
|
263
|
+
4: (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
|
|
264
|
+
5: (r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
265
|
+
6: (r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
266
|
+
7: (r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0,
|
|
267
|
+
}[maskId];
|
|
268
|
+
for (let r = 0; r < size; r += 1) {
|
|
269
|
+
for (let c = 0; c < size; c += 1) {
|
|
270
|
+
if (!reserved[r][c] && maskFn(r, c)) modules[r][c] ^= 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function penalty(modules, size) {
|
|
276
|
+
let score = 0;
|
|
277
|
+
// regra 1: sequencias iguais
|
|
278
|
+
for (let r = 0; r < size; r += 1) {
|
|
279
|
+
let run = 1;
|
|
280
|
+
for (let c = 1; c < size; c += 1) {
|
|
281
|
+
if (modules[r][c] === modules[r][c - 1]) run += 1;
|
|
282
|
+
else {
|
|
283
|
+
if (run >= 5) score += run - 2;
|
|
284
|
+
run = 1;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (run >= 5) score += run - 2;
|
|
288
|
+
}
|
|
289
|
+
for (let c = 0; c < size; c += 1) {
|
|
290
|
+
let run = 1;
|
|
291
|
+
for (let r = 1; r < size; r += 1) {
|
|
292
|
+
if (modules[r][c] === modules[r - 1][c]) run += 1;
|
|
293
|
+
else {
|
|
294
|
+
if (run >= 5) score += run - 2;
|
|
295
|
+
run = 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (run >= 5) score += run - 2;
|
|
299
|
+
}
|
|
300
|
+
// regra 2: blocos 2x2
|
|
301
|
+
for (let r = 0; r < size - 1; r += 1) {
|
|
302
|
+
for (let c = 0; c < size - 1; c += 1) {
|
|
303
|
+
const v = modules[r][c];
|
|
304
|
+
if (v === modules[r][c + 1] && v === modules[r + 1][c] && v === modules[r + 1][c + 1]) score += 3;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// regra 3: padrao 1:1:3:1:1 intercalado com 4 claros
|
|
308
|
+
const pattern = [1, 0, 1, 1, 1, 0, 1];
|
|
309
|
+
for (let r = 0; r < size; r += 1) {
|
|
310
|
+
for (let i = 0; i <= size - 7; i += 1) {
|
|
311
|
+
let match = true;
|
|
312
|
+
for (let j = 0; j < 7; j += 1) if (modules[r][i + j] !== pattern[j]) { match = false; break; }
|
|
313
|
+
if (match) {
|
|
314
|
+
const before = i >= 4 && [0, 1, 2, 3].every((k) => modules[r][i - 4 + k] === 0);
|
|
315
|
+
const after = i + 11 <= size && [0, 1, 2, 3].every((k) => modules[r][i + 7 + k] === 0);
|
|
316
|
+
if (before || after) score += 40;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (let c = 0; c < size; c += 1) {
|
|
321
|
+
for (let i = 0; i <= size - 7; i += 1) {
|
|
322
|
+
let match = true;
|
|
323
|
+
for (let j = 0; j < 7; j += 1) if (modules[i + j][c] !== pattern[j]) { match = false; break; }
|
|
324
|
+
if (match) {
|
|
325
|
+
const before = i >= 4 && [0, 1, 2, 3].every((k) => modules[i - 4 + k][c] === 0);
|
|
326
|
+
const after = i + 11 <= size && [0, 1, 2, 3].every((k) => modules[i + 7 + k][c] === 0);
|
|
327
|
+
if (before || after) score += 40;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// regra 4: proporcao de escuros
|
|
332
|
+
let dark = 0;
|
|
333
|
+
for (let r = 0; r < size; r += 1) for (let c = 0; c < size; c += 1) if (modules[r][c]) dark += 1;
|
|
334
|
+
const ratio = (dark * 100) / (size * size);
|
|
335
|
+
score += Math.floor(Math.abs(ratio - 50) / 5) * 10;
|
|
336
|
+
return score;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function drawFormat(modules, size, maskId) {
|
|
340
|
+
const ECC_M = 0b00;
|
|
341
|
+
const data = (ECC_M << 3) | maskId;
|
|
342
|
+
let rem = data;
|
|
343
|
+
for (let i = 0; i < 10; i += 1) {
|
|
344
|
+
rem = (rem << 1) ^ ((rem >> 9) & 1 ? 0x537 : 0);
|
|
345
|
+
}
|
|
346
|
+
const bits = ((data << 10) | rem) ^ 0x5412;
|
|
347
|
+
// A spec coloca o bit MAIS significativo (14) na primeira posicao da copia-1
|
|
348
|
+
// e assim por diante: a posicao i recebe o bit (14 - i). Escrever LSB-first
|
|
349
|
+
// gera um QR que parece certo mas nao decodifica em nenhuma camera.
|
|
350
|
+
const b = (i) => (bits >> (14 - i)) & 1;
|
|
351
|
+
|
|
352
|
+
for (let i = 0; i <= 5; i += 1) modules[8][i] = b(i);
|
|
353
|
+
modules[8][7] = b(6);
|
|
354
|
+
modules[8][8] = b(7);
|
|
355
|
+
modules[7][8] = b(8);
|
|
356
|
+
for (let i = 9; i < 15; i += 1) modules[14 - i][8] = b(i);
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i < 8; i += 1) modules[size - 1 - i][8] = b(i);
|
|
359
|
+
for (let i = 8; i < 15; i += 1) modules[8][size - 15 + i] = b(i);
|
|
360
|
+
modules[size - 8][8] = 1;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Gera a matriz final do QR (0/1) para o texto.
|
|
365
|
+
* @param {string} text
|
|
366
|
+
* @returns {{size:number, modules:number[][]}}
|
|
367
|
+
*/
|
|
368
|
+
function generate(text) {
|
|
369
|
+
const byteLength = Buffer.byteLength(text, "utf8");
|
|
370
|
+
const version = pickVersion(byteLength);
|
|
371
|
+
if (!version) throw new Error(`texto grande demais para o QR (${byteLength} bytes > 213)`);
|
|
372
|
+
|
|
373
|
+
const codewords = encodeData(text, version);
|
|
374
|
+
const matrix = createMatrix(version);
|
|
375
|
+
placeData(matrix, codewords);
|
|
376
|
+
|
|
377
|
+
let best = null;
|
|
378
|
+
let bestScore = Infinity;
|
|
379
|
+
for (let maskId = 0; maskId < 8; maskId += 1) {
|
|
380
|
+
const trial = matrix.modules.map((row) => row.slice());
|
|
381
|
+
applyMask(trial, matrix.reserved, matrix.size, maskId);
|
|
382
|
+
// escreve o formato numa copia para o calculo de penalidade
|
|
383
|
+
const withFormat = trial.map((row) => row.slice());
|
|
384
|
+
drawFormat(withFormat, matrix.size, maskId);
|
|
385
|
+
const score = penalty(withFormat, matrix.size);
|
|
386
|
+
if (score < bestScore) {
|
|
387
|
+
bestScore = score;
|
|
388
|
+
best = { maskId, modules: withFormat };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { size: matrix.size, modules: best.modules };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Renderiza o QR em ASCII usando meios blocos Unicode.
|
|
396
|
+
*
|
|
397
|
+
* Por que meios blocos e nao "#"/espaco: o terminal do usuario costuma ter
|
|
398
|
+
* fundo escuro, e um QR desenhado com caracteres claros "inverte" a leitura.
|
|
399
|
+
* Com "▀ ▄ █" cada linha de texto carrega DUAS linhas de modulos, o modulo
|
|
400
|
+
* claro e o proprio fundo do terminal e o escuro e o glifo — entao funciona
|
|
401
|
+
* em fundo claro E escuro, e cabe na tela pela metade.
|
|
402
|
+
*
|
|
403
|
+
* LEGIBILIDADE POR CAMERA (o que mais importa):
|
|
404
|
+
* - quietZone padrao 4 modulos: e o minimo da especificacao do QR. Com menos
|
|
405
|
+
* que isso, muitas cameras de celular simplesmente nao acham o codigo.
|
|
406
|
+
* - `scale` multiplica a largura: cada modulo vira `scale` caracteres na
|
|
407
|
+
* horizontal. Como o caractere do terminal e ~2x mais alto que largo e
|
|
408
|
+
* cada linha de texto carrega 2 linhas de modulos, `scale=2` deixa cada
|
|
409
|
+
* modulo QUADRADO (2 chars de largura ≈ 2 linhas de altura) e sem vao
|
|
410
|
+
* entre modulos vizinhos — que e o que a camera consegue travar.
|
|
411
|
+
*
|
|
412
|
+
* @param {string} text
|
|
413
|
+
* @param {{quietZone?:number, scale?:number}} [options]
|
|
414
|
+
*/
|
|
415
|
+
function toAscii(text, { quietZone = 4, scale = 1, color = false } = {}) {
|
|
416
|
+
const { size, modules } = generate(text);
|
|
417
|
+
// Geometria do terminal: uma celula (char) e ~1 de largura por ~2 de altura.
|
|
418
|
+
// Uma linha de texto carrega DUAS meias-linhas de modulo (via meios-blocos).
|
|
419
|
+
//
|
|
420
|
+
// `scale = N` faz cada modulo ocupar N chars de LARGURA e N meias-linhas de
|
|
421
|
+
// ALTURA, ou seja, multiplica o QR inteiro por N mantendo o modulo QUADRADO:
|
|
422
|
+
// - scale=1: 1 char x 1 meia-linha (menor; cada linha de texto = 2 fileiras)
|
|
423
|
+
// - scale=2: 2 chars x 2 meias-linhas (1 linha de texto por fileira, bloco cheio)
|
|
424
|
+
// Nunca estica: largura e altura crescem juntas.
|
|
425
|
+
// `scale` precisa ser 1 ou par (par = N chars largura + N meias-linhas altura).
|
|
426
|
+
let N = Math.max(1, Math.floor(scale));
|
|
427
|
+
if (N !== 1 && N % 2 !== 0) N = N + 1; // arredonda impar pra par
|
|
428
|
+
const total = size + quietZone * 2; // linhas/colunas de modulos (com margem)
|
|
429
|
+
const bit = (row, col) => {
|
|
430
|
+
const rr = row - quietZone;
|
|
431
|
+
const cc = col - quietZone;
|
|
432
|
+
if (rr < 0 || cc < 0 || rr >= size || cc >= size) return 0;
|
|
433
|
+
return modules[rr][cc];
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Cor ANSI (preto no branco) quando color=true: camera de celular costuma
|
|
437
|
+
// NAO decodificar QR invertido (claro no fundo escuro do terminal). Sem cor,
|
|
438
|
+
// o modulo escuro e o proprio glifo e o claro e o fundo do terminal.
|
|
439
|
+
const ON = color ? "\x1b[30;47m" : ""; // preto sobre branco
|
|
440
|
+
const OFF = color ? "\x1b[0m" : "";
|
|
441
|
+
|
|
442
|
+
const lines = [];
|
|
443
|
+
if (N === 1) {
|
|
444
|
+
// Duas fileiras de modulos por linha de texto (meios-blocos). Modulo = 1
|
|
445
|
+
// char de largura x 1 meia-linha de altura => quadrado.
|
|
446
|
+
for (let row = 0; row < total; row += 2) {
|
|
447
|
+
let line = color ? ON : "";
|
|
448
|
+
for (let col = 0; col < total; col += 1) {
|
|
449
|
+
const a = bit(row, col);
|
|
450
|
+
const b = row + 1 < total ? bit(row + 1, col) : 0;
|
|
451
|
+
line += (a ? (b ? "█" : "▀") : b ? "▄" : " ").repeat(1);
|
|
452
|
+
}
|
|
453
|
+
if (color) line += OFF;
|
|
454
|
+
lines.push(line);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
// Cada fileira de modulos vira N/2 linhas de texto, cada modulo com N
|
|
458
|
+
// chars de largura => quadrado. Blocos cheios (preto) ou espaco (claro).
|
|
459
|
+
const linesPerRow = N / 2;
|
|
460
|
+
for (let row = 0; row < total; row += 1) {
|
|
461
|
+
let line = color ? ON : "";
|
|
462
|
+
for (let col = 0; col < total; col += 1) {
|
|
463
|
+
line += (bit(row, col) ? "█" : " ").repeat(N);
|
|
464
|
+
}
|
|
465
|
+
if (color) line += OFF;
|
|
466
|
+
for (let k = 0; k < linesPerRow; k += 1) lines.push(line);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return lines.join("\n");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
module.exports = { generate, toAscii, pickVersion, CAPACITY_M, reservedFor };
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Mapa de modulos "reservados" (finder/timing/alignment/formato/dark) para o
|
|
476
|
+
* texto dado. Exposto para os testes de ida-e-volta: quem decodifica precisa
|
|
477
|
+
* saber exatamente quais modulos NAO carregam dados, e a fonte da verdade e o
|
|
478
|
+
* proprio encoder (`createMatrix`) — duplicar essa logica num teste e o jeito
|
|
479
|
+
* mais facil de o teste mentir.
|
|
480
|
+
* @param {string} text
|
|
481
|
+
* @returns {boolean[][]}
|
|
482
|
+
*/
|
|
483
|
+
function reservedFor(text) {
|
|
484
|
+
const version = pickVersion(Buffer.byteLength(text, "utf8"));
|
|
485
|
+
if (!version) throw new Error(`texto grande demais para o QR`);
|
|
486
|
+
return createMatrix(version).reserved.map((row) => row.slice());
|
|
487
|
+
}
|