primocode 11.1.0 → 11.2.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.
@@ -215,7 +215,7 @@ const OPS = {
215
215
  ate = Math.min(asset.seconds, ate);
216
216
  if (!(ate > de + 0.5)) throw T.erro('o momento 3D precisa de pelo menos meio segundo');
217
217
  }
218
- return require('./ops.js').mutar(ctx, 'separate_layers', (tl) => {
218
+ return require('./mutar.js').mutar(ctx, 'separate_layers', (tl) => {
219
219
  const pessoa = camadaDaPessoa(tl, asset, rs);
220
220
  const out = { layers: { pessoa: pessoa.length, fundo: 'a gravação embaixo' } };
221
221
  if (a.show3d !== undefined) { C.limparPapel(tl, 'camadas3d', asset.id); C.limparGerados(tl); }
@@ -261,7 +261,7 @@ const OPS = {
261
261
  const seg = a.at === undefined ? await momento(ctx, asset, 'snap').catch(() => 0) : await momento(ctx, asset, a.at);
262
262
  const medido = await r().medirAsset(ctx.tl, arquivo);
263
263
  const ganho = ganhoDeCor(await corMedia(rs.arquivos.fundo), await corMedia(arquivo));
264
- return require('./ops.js').mutar(ctx, 'replace_background', (tl) => {
264
+ return require('./mutar.js').mutar(ctx, 'replace_background', (tl) => {
265
265
  if (!temPessoa(tl, asset.id)) camadaDaPessoa(tl, asset, rs);
266
266
  const F = quadro({ tl }, asset, seg) ?? 0;
267
267
  const fim = a.until !== undefined ? Math.round(Number(a.until)) : T.duracao(tl);
@@ -334,7 +334,7 @@ const OPS = {
334
334
  if (de === null) de = t[0];
335
335
  const ate = a.to !== undefined ? await momento(ctx, asset, a.to) : Math.min(asset.seconds, Math.max(t[1] + 0.3, de + 1.5));
336
336
  if (!(ate > de)) throw T.erro('"to" antes de "from"');
337
- return require('./ops.js').mutar(ctx, 'attach_3d', (tl) => {
337
+ return require('./mutar.js').mutar(ctx, 'attach_3d', (tl) => {
338
338
  const f = fxDe(tl, asset.id);
339
339
  const o = { id: `o${f.objetos.length + 1}`, modelo, mao, de: r2(de * 1000) / 1000, ate: r2(ate * 1000) / 1000 };
340
340
  if (a.color) o.cor = C.cor(a.color);
@@ -365,7 +365,7 @@ const OPS = {
365
365
  }
366
366
  const ateFusao = a.until !== undefined ? await momento(ctx, asset, a.until) : null;
367
367
  const depois = a.then === 'none' || a.then === false ? null : C.modeloDe(a.then || 'brain') || 'cerebro';
368
- return require('./ops.js').mutar(ctx, 'merge_on_join', (tl) => {
368
+ return require('./mutar.js').mutar(ctx, 'merge_on_join', (tl) => {
369
369
  const f = fxDe(tl, asset.id);
370
370
  // Os objetos vivos na junção ficam até ela (e se fundem).
371
371
  for (const o of f.objetos) if (o.de < junta && o.ate < junta + 0.5) o.ate = r2((junta + 0.6) * 1000) / 1000;
@@ -400,7 +400,7 @@ const OPS = {
400
400
  if (i1 > i0) ranges.push([T.origemParaQuadro(ctx.tl, c, i0), T.origemParaQuadro(ctx.tl, c, i1)]);
401
401
  }
402
402
  if (!ranges.length) return { ok: false, error: 'esse silêncio já foi cortado' };
403
- return require('./ops.js').mutar(ctx, 'cut_silence_at', (tl) => ({ silence: { de: r2(de), ate: r2(ate) }, ...E.apagarTrechos(tl, ranges) }));
403
+ return require('./mutar.js').mutar(ctx, 'cut_silence_at', (tl) => ({ silence: { de: r2(de), ate: r2(ate) }, ...E.apagarTrechos(tl, ranges) }));
404
404
  },
405
405
  },
406
406
  style_captions: {
@@ -423,7 +423,7 @@ const OPS = {
423
423
  const CY = { bottom: 0.78, baixo: 0.78, top: 0.14, topo: 0.14, center: 0.5, centro: 0.5, middle: 0.5 };
424
424
  if (pos && !seguir && CY[pos] === undefined) throw T.erro(`position "${a.position}" (use above_head, bottom, top ou center)`);
425
425
  const corPedida = a.color !== undefined ? C.cor(a.color) : undefined;
426
- return require('./ops.js').mutar(ctx, 'style_captions', (t) => {
426
+ return require('./mutar.js').mutar(ctx, 'style_captions', (t) => {
427
427
  for (const ref of alvo) {
428
428
  const c = T.achar(t, ref.id).clip;
429
429
  const st = { ...(c.text.style || {}) };
@@ -17,6 +17,7 @@ const T = require('./timeline.js');
17
17
  const E = require('./edits.js');
18
18
  const F = require('./filters.js');
19
19
  const { diff } = require('./delta.js');
20
+ const { mutar } = require('./mutar.js');
20
21
  const P = require('./projeto.js');
21
22
  const TR = require('./transcript.js');
22
23
  const A = require('./analise.js');
@@ -35,19 +36,6 @@ function alvos(tl, args, { tipos = ['video', 'image'] } = {}) {
35
36
  return refs.map((ref) => T.achar(tl, ref).clip.id);
36
37
  }
37
38
 
38
- async function mutar(ctx, op, fn) {
39
- const antes = T.clone(ctx.tl);
40
- const extra = (await fn(ctx.tl)) || {};
41
- T.ordenar(ctx.tl);
42
- const probs = T.problemas(ctx.tl);
43
- if (probs.length) throw T.erro(`edição recusada (nada mudou): ${probs.slice(0, 4).join('; ')}`);
44
- P.fotografar(ctx.p, antes, op);
45
- P.salvar(ctx.p, ctx.tl);
46
- const d = diff(antes, ctx.tl, extra);
47
- P.anotar(ctx.p, op, { dur: d.dur, added: (d.added || []).length, removed: (d.removed || []).length });
48
- return d;
49
- }
50
-
51
39
  function precisaProjeto(ctx) {
52
40
  if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."} (cria o projeto) ou new');
53
41
  }
@@ -311,6 +299,7 @@ const PED = require('./pedido.js');
311
299
  const AUTO = require('./auto.js');
312
300
  const ZOOM = require('./zoom.js'); // o zoom que segue o cursor (gravação de tela): fica na ajuda principal
313
301
  Object.assign(OPS, FXO.OPS, PED.OPS, AUTO.OPS, ZOOM.OPS);
302
+ require('./catalogo.js').registrar(OPS); // auto e pedido encadeiam operações por aqui
314
303
 
315
304
  const ALIAS = { timeline: 'get_timeline', transcript: 'get_transcript', captions: 'add_captions', texts: 'add_texts', render: 'export', pedido: 'do', analyze: 'track' };
316
305
 
@@ -152,7 +152,7 @@ const OPS = {
152
152
  const resumo = plano.passos.map((p) => ({ op: p.op, args: p.args }));
153
153
  if (a.dryRun) return { ok: true, plano: resumo, ...(plano.avisos.length ? { avisos: plano.avisos } : {}) };
154
154
  if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
155
- const { OPS: TODAS } = require('./ops.js');
155
+ const TODAS = require('./catalogo.js').catalogo();
156
156
  // O que é lento e não depende de nada roda JUNTO antes do primeiro passo.
157
157
  const C2 = require('./camadas.js');
158
158
  const asset = C2.base(tl);
@@ -231,7 +231,7 @@ const OPS = {
231
231
  async run(ctx, a) {
232
232
  const T = require('./timeline.js');
233
233
  const E = require('./edits.js');
234
- const { mutar } = require('./ops.js');
234
+ const { mutar } = require('./mutar.js');
235
235
  if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."}');
236
236
  const primeiro = T.todosOsClipes(ctx.tl).find((x) => x.clip.type === 'video');
237
237
  if (!a.id && !primeiro) throw T.erro('não há clipe de vídeo');
@@ -0,0 +1,90 @@
1
+ // Projetos do app: pasta em ~/primocode/<projeto>/ + .primocode/projeto.json
2
+ // (nome, descrição, ícone, cor). docs/estrutura.md, seção 7. Node puro.
3
+ // "Apagar" só arquiva (marca no projeto.json): os arquivos da pessoa nunca são removidos.
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const ICONES = ['folder', 'film', 'palette', 'workflow', 'pen', 'home', 'shield', 'bell', 'sliders'];
10
+ const COR_PADRAO = '#22d3ee';
11
+ const hexOk = (h) => /^#[0-9a-f]{6}$/i.test(String(h || ''));
12
+ const cfg = () => require('./config/config');
13
+ const raiz = () => cfg().ROOT_DIR;
14
+ const metaDe = (dir) => path.join(dir, '.primocode', 'projeto.json');
15
+
16
+ function lerMeta(dir) { try { return JSON.parse(fs.readFileSync(metaDe(dir), 'utf8')); } catch { return {}; } }
17
+ function gravarMeta(dir, meta) {
18
+ fs.mkdirSync(path.join(dir, '.primocode'), { recursive: true });
19
+ fs.writeFileSync(metaDe(dir), JSON.stringify(meta, null, 2) + '\n');
20
+ }
21
+
22
+ /* id = nome da pasta. Só vale se a pasta existe dentro da raiz. */
23
+ function pastaDe(id) {
24
+ const s = String(id || '');
25
+ if (!/^[\w][\w.-]{0,80}$/.test(s)) return null;
26
+ const dir = path.join(raiz(), s);
27
+ try { return fs.statSync(dir).isDirectory() ? dir : null; } catch { return null; }
28
+ }
29
+
30
+ /* Se a pasta é um repositório git, devolve { git: true, ramo }: lido do .git/HEAD, sem rodar o git. */
31
+ function ramoGit(dir) {
32
+ try {
33
+ const cabeca = fs.readFileSync(path.join(dir, '.git', 'HEAD'), 'utf8').trim();
34
+ const m = /^ref: refs\/heads\/(.+)$/.exec(cabeca);
35
+ return { git: true, ramo: m ? m[1] : cabeca.slice(0, 7) };
36
+ } catch { return { git: false, ramo: '' }; }
37
+ }
38
+
39
+ function resumo(id) {
40
+ const dir = path.join(raiz(), id);
41
+ const m = lerMeta(dir);
42
+ return {
43
+ id, nome: m.nome || id, descricao: m.descricao || '',
44
+ icone: ICONES.includes(m.icone) ? m.icone : 'folder', cor: hexOk(m.cor) ? m.cor : COR_PADRAO,
45
+ caminho: dir, arquivado: !!m.arquivado, criado: m.criado || null,
46
+ ...ramoGit(dir),
47
+ };
48
+ }
49
+
50
+ function listar({ arquivados = false } = {}) {
51
+ return cfg().listProjects().map(resumo).filter((p) => arquivados || !p.arquivado);
52
+ }
53
+
54
+ function limpar(c, { criando = false } = {}) {
55
+ const out = {};
56
+ if ('nome' in c) { out.nome = String(c.nome || '').trim().slice(0, 80); if (!out.nome) throw new Error('falta o nome'); }
57
+ else if (criando) throw new Error('falta o nome');
58
+ if ('descricao' in c) out.descricao = String(c.descricao || '').trim().slice(0, 240);
59
+ if ('icone' in c && c.icone) { if (!ICONES.includes(c.icone)) throw new Error(`ícone inválido (use: ${ICONES.join(', ')})`); out.icone = c.icone; }
60
+ if ('cor' in c && c.cor) { if (!hexOk(c.cor)) throw new Error('cor inválida (use #rrggbb)'); out.cor = String(c.cor).toLowerCase(); }
61
+ return out;
62
+ }
63
+
64
+ function criar(c = {}) {
65
+ const campos = limpar(c, { criando: true });
66
+ const dir = cfg().createProjectDir(campos.nome);
67
+ const id = path.basename(dir);
68
+ const atual = lerMeta(dir);
69
+ if (atual.nome && !atual.arquivado) throw new Error(`já existe o projeto "${atual.nome}"`);
70
+ gravarMeta(dir, { ...atual, ...campos, arquivado: false, criado: atual.criado || new Date().toISOString() });
71
+ return resumo(id);
72
+ }
73
+
74
+ function atualizar(id, c = {}) {
75
+ const dir = pastaDe(id);
76
+ if (!dir) throw new Error(`não achei o projeto ${id}`);
77
+ const campos = limpar(c);
78
+ if (!Object.keys(campos).length) throw new Error('nada para mudar (nome, descricao, icone, cor)');
79
+ gravarMeta(dir, { ...lerMeta(dir), ...campos });
80
+ return resumo(path.basename(dir));
81
+ }
82
+
83
+ function arquivar(id) {
84
+ const dir = pastaDe(id);
85
+ if (!dir) throw new Error(`não achei o projeto ${id}`);
86
+ gravarMeta(dir, { ...lerMeta(dir), arquivado: true });
87
+ return { id: path.basename(dir), arquivado: true, arquivos: 'mantidos' };
88
+ }
89
+
90
+ module.exports = { ICONES, listar, criar, atualizar, arquivar, pastaDe, resumo };
package/lib/qr.js ADDED
@@ -0,0 +1,209 @@
1
+ // Gerador de QR code mínimo, sem dependência: modo byte (UTF-8), correção L ou M, versões 1 a 10
2
+ // (até 213 bytes no M, 271 no L). Serve para o pareamento do celular (docs/estrutura.md, seção 15).
3
+ // Algoritmo da ISO/IEC 18004: Reed-Solomon em GF(256), 8 máscaras, escolhe a de menor penalidade.
4
+ 'use strict';
5
+
6
+ const ECC_POR_BLOCO = { L: [7, 10, 15, 20, 26, 18, 20, 24, 30, 18], M: [10, 16, 26, 18, 24, 16, 18, 22, 22, 26] };
7
+ const NUM_BLOCOS = { L: [1, 1, 1, 1, 1, 2, 2, 2, 2, 4], M: [1, 1, 1, 2, 2, 4, 4, 4, 5, 5] };
8
+ const FORMATO_ECC = { L: 1, M: 0 };
9
+ const VERSAO_MAX = 10;
10
+
11
+ /* Módulos que sobram para dados + correção, descontados padrões de função. */
12
+ function modulosBrutos(v) {
13
+ let r = (16 * v + 128) * v + 64;
14
+ if (v >= 2) {
15
+ const n = Math.floor(v / 7) + 2;
16
+ r -= (25 * n - 10) * n - 55;
17
+ if (v >= 7) r -= 36;
18
+ }
19
+ return r;
20
+ }
21
+ const dadosCodewords = (v, ecc) => Math.floor(modulosBrutos(v) / 8) - ECC_POR_BLOCO[ecc][v - 1] * NUM_BLOCOS[ecc][v - 1];
22
+
23
+ /* GF(256) com o polinômio 0x11D. */
24
+ function multiplicar(x, y) {
25
+ let z = 0;
26
+ for (let i = 7; i >= 0; i--) { z = (z << 1) ^ ((z >>> 7) * 0x11D); z ^= ((y >>> i) & 1) * x; }
27
+ return z;
28
+ }
29
+ function divisor(grau) {
30
+ const r = new Array(grau).fill(0);
31
+ r[grau - 1] = 1;
32
+ let raiz = 1;
33
+ for (let i = 0; i < grau; i++) {
34
+ for (let j = 0; j < r.length; j++) { r[j] = multiplicar(r[j], raiz); if (j + 1 < r.length) r[j] ^= r[j + 1]; }
35
+ raiz = multiplicar(raiz, 2);
36
+ }
37
+ return r;
38
+ }
39
+ function resto(dados, div) {
40
+ const r = new Array(div.length).fill(0);
41
+ for (const b of dados) {
42
+ const f = b ^ r.shift();
43
+ r.push(0);
44
+ div.forEach((c, i) => { r[i] ^= multiplicar(c, f); });
45
+ }
46
+ return r;
47
+ }
48
+
49
+ function bitsDoTexto(bytes, v, ecc) {
50
+ const bits = [];
51
+ const poe = (valor, n) => { for (let i = n - 1; i >= 0; i--) bits.push((valor >>> i) & 1); };
52
+ poe(0b0100, 4);
53
+ poe(bytes.length, v <= 9 ? 8 : 16);
54
+ for (const b of bytes) poe(b, 8);
55
+ const cap = dadosCodewords(v, ecc) * 8;
56
+ poe(0, Math.min(4, cap - bits.length));
57
+ poe(0, (8 - (bits.length % 8)) % 8);
58
+ for (let pad = 0xEC; bits.length < cap; pad ^= 0xEC ^ 0x11) poe(pad, 8);
59
+ const cw = [];
60
+ for (let i = 0; i < bits.length; i += 8) cw.push(parseInt(bits.slice(i, i + 8).join(''), 2));
61
+ return cw;
62
+ }
63
+
64
+ /* Divide em blocos, calcula a correção e intercala. */
65
+ function intercalar(dados, v, ecc) {
66
+ const nb = NUM_BLOCOS[ecc][v - 1];
67
+ const eb = ECC_POR_BLOCO[ecc][v - 1];
68
+ const total = Math.floor(modulosBrutos(v) / 8);
69
+ const curtos = nb - (total % nb);
70
+ const lenCurto = Math.floor(total / nb) - eb;
71
+ const div = divisor(eb);
72
+ const blocos = [];
73
+ for (let i = 0, k = 0; i < nb; i++) {
74
+ const dat = dados.slice(k, k + lenCurto + (i < curtos ? 0 : 1));
75
+ k += dat.length;
76
+ blocos.push({ dat, ecc: resto(dat, div) });
77
+ }
78
+ const out = [];
79
+ for (let i = 0; i <= lenCurto; i++) for (const b of blocos) if (i < b.dat.length) out.push(b.dat[i]);
80
+ for (let i = 0; i < eb; i++) for (const b of blocos) out.push(b.ecc[i]);
81
+ return out;
82
+ }
83
+
84
+ function criarMatriz(v) {
85
+ const n = v * 4 + 17;
86
+ return { n, m: Array.from({ length: n }, () => new Array(n).fill(false)), f: Array.from({ length: n }, () => new Array(n).fill(false)) };
87
+ }
88
+ function posAlinhamento(v) {
89
+ if (v === 1) return [];
90
+ const n = Math.floor(v / 7) + 2;
91
+ const passo = v === 32 ? 26 : Math.ceil((v * 4 + 4) / (n * 2 - 2)) * 2;
92
+ const r = [6];
93
+ for (let p = v * 4 + 10; r.length < n; p -= passo) r.splice(1, 0, p);
94
+ return r;
95
+ }
96
+ function desenharFuncoes(Q, v, ecc) {
97
+ const { n, m, f } = Q;
98
+ const set = (x, y, cor) => { if (x >= 0 && x < n && y >= 0 && y < n) { m[y][x] = cor; f[y][x] = true; } };
99
+ for (let i = 0; i < n; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
100
+ const busca = (cx, cy) => { for (let dy = -4; dy <= 4; dy++) for (let dx = -4; dx <= 4; dx++) { const d = Math.max(Math.abs(dx), Math.abs(dy)); set(cx + dx, cy + dy, d !== 2 && d !== 4); } };
101
+ busca(3, 3); busca(n - 4, 3); busca(3, n - 4);
102
+ const al = posAlinhamento(v);
103
+ for (let i = 0; i < al.length; i++) for (let j = 0; j < al.length; j++) {
104
+ if ((i === 0 && j === 0) || (i === 0 && j === al.length - 1) || (i === al.length - 1 && j === 0)) continue;
105
+ for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(al[i] + dx, al[j] + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
106
+ }
107
+ desenharFormato(Q, ecc, 0);
108
+ if (v >= 7) {
109
+ let r = v;
110
+ for (let i = 0; i < 12; i++) r = (r << 1) ^ ((r >>> 11) * 0x1F25);
111
+ const bits = (v << 12) | r;
112
+ for (let i = 0; i < 18; i++) {
113
+ const cor = ((bits >>> i) & 1) !== 0;
114
+ const a = n - 11 + (i % 3);
115
+ const b = Math.floor(i / 3);
116
+ set(a, b, cor); set(b, a, cor);
117
+ }
118
+ }
119
+ }
120
+ function desenharFormato(Q, ecc, mascara) {
121
+ const { n, m, f } = Q;
122
+ const dados = (FORMATO_ECC[ecc] << 3) | mascara;
123
+ let r = dados;
124
+ for (let i = 0; i < 10; i++) r = (r << 1) ^ ((r >>> 9) * 0x537);
125
+ const bits = ((dados << 10) | r) ^ 0x5412;
126
+ const bit = (i) => ((bits >>> i) & 1) !== 0;
127
+ const set = (x, y, cor) => { m[y][x] = cor; f[y][x] = true; };
128
+ for (let i = 0; i <= 5; i++) set(8, i, bit(i));
129
+ set(8, 7, bit(6)); set(8, 8, bit(7)); set(7, 8, bit(8));
130
+ for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i));
131
+ for (let i = 0; i < 8; i++) set(n - 1 - i, 8, bit(i));
132
+ for (let i = 8; i < 15; i++) set(8, n - 15 + i, bit(i));
133
+ set(8, n - 8, true);
134
+ }
135
+ function colocarDados(Q, cw) {
136
+ const { n, m, f } = Q;
137
+ let i = 0;
138
+ for (let direita = n - 1; direita >= 1; direita -= 2) {
139
+ if (direita === 6) direita = 5;
140
+ for (let vert = 0; vert < n; vert++) for (let j = 0; j < 2; j++) {
141
+ const x = direita - j;
142
+ const y = ((direita + 1) & 2) === 0 ? n - 1 - vert : vert;
143
+ if (!f[y][x] && i < cw.length * 8) { m[y][x] = ((cw[i >>> 3] >>> (7 - (i & 7))) & 1) !== 0; i++; }
144
+ }
145
+ }
146
+ }
147
+ const MASCARAS = [
148
+ (x, y) => (x + y) % 2 === 0, (x, y) => y % 2 === 0, (x) => x % 3 === 0, (x, y) => (x + y) % 3 === 0,
149
+ (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
150
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
151
+ ];
152
+ function aplicarMascara(Q, k) {
153
+ for (let y = 0; y < Q.n; y++) for (let x = 0; x < Q.n; x++) if (!Q.f[y][x] && MASCARAS[k](x, y)) Q.m[y][x] = !Q.m[y][x];
154
+ }
155
+ function penalidade(Q) {
156
+ const { n, m } = Q;
157
+ let p = 0;
158
+ const corrida = (linha) => {
159
+ let s = 0; let run = 1;
160
+ for (let i = 1; i <= linha.length; i++) {
161
+ if (i < linha.length && linha[i] === linha[i - 1]) run++;
162
+ else { if (run >= 5) s += run - 2; run = 1; }
163
+ }
164
+ const str = linha.map((b) => (b ? 1 : 0)).join('');
165
+ for (const pad of ['10111010000', '00001011101']) { let at = str.indexOf(pad); while (at >= 0) { s += 40; at = str.indexOf(pad, at + 1); } }
166
+ return s;
167
+ };
168
+ for (let y = 0; y < n; y++) p += corrida(m[y]);
169
+ for (let x = 0; x < n; x++) p += corrida(m.map((l) => l[x]));
170
+ for (let y = 0; y < n - 1; y++) for (let x = 0; x < n - 1; x++) if (m[y][x] === m[y][x + 1] && m[y][x] === m[y + 1][x] && m[y][x] === m[y + 1][x + 1]) p += 3;
171
+ const escuros = m.reduce((a, l) => a + l.filter(Boolean).length, 0);
172
+ p += (Math.ceil(Math.abs(escuros * 20 - n * n * 10) / (n * n)) - 1) * 10;
173
+ return p;
174
+ }
175
+
176
+ /* Devolve { tamanho, modulos: boolean[][], versao }. Lança se o texto não cabe. */
177
+ function gerarQR(texto, { ecc = 'M' } = {}) {
178
+ if (!ECC_POR_BLOCO[ecc]) throw new Error('ecc deve ser L ou M');
179
+ const bytes = Buffer.from(String(texto), 'utf8');
180
+ let v = 1;
181
+ for (; v <= VERSAO_MAX; v++) {
182
+ const bitsLen = 4 + (v <= 9 ? 8 : 16) + bytes.length * 8;
183
+ if (bitsLen <= dadosCodewords(v, ecc) * 8) break;
184
+ }
185
+ if (v > VERSAO_MAX) throw new Error(`texto grande demais para o QR (${bytes.length} bytes; máximo ${dadosCodewords(VERSAO_MAX, ecc) - 3})`);
186
+ const cw = intercalar(bitsDoTexto(bytes, v, ecc), v, ecc);
187
+ let melhor = null;
188
+ for (let k = 0; k < 8; k++) {
189
+ const Q = criarMatriz(v);
190
+ desenharFuncoes(Q, v, ecc);
191
+ colocarDados(Q, cw);
192
+ aplicarMascara(Q, k);
193
+ desenharFormato(Q, ecc, k);
194
+ const pen = penalidade(Q);
195
+ if (!melhor || pen < melhor.pen) melhor = { pen, Q };
196
+ }
197
+ return { tamanho: melhor.Q.n, modulos: melhor.Q.m, versao: v };
198
+ }
199
+
200
+ /* SVG pronto (módulos escuros em `cor`, fundo em `fundo`, com a zona de silêncio de 4 módulos). */
201
+ function qrSvg(texto, { ecc = 'M', cor = '#000', fundo = '#fff' } = {}) {
202
+ const { tamanho, modulos } = gerarQR(texto, { ecc });
203
+ const t = tamanho + 8;
204
+ let d = '';
205
+ for (let y = 0; y < tamanho; y++) for (let x = 0; x < tamanho; x++) if (modulos[y][x]) d += `M${x + 4} ${y + 4}h1v1h-1z`;
206
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${t} ${t}" shape-rendering="crispEdges" role="img" aria-label="QR code de pareamento"><rect width="${t}" height="${t}" fill="${fundo}"/><path d="${d}" fill="${cor}"/></svg>`;
207
+ }
208
+
209
+ module.exports = { gerarQR, qrSvg };