kafox-cli 0.9.9 → 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.
@@ -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
+ }