nfe-lib-node 1.7.0 → 1.8.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/dist/Danfe.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { inflateSync, deflateSync } from 'node:zlib';
1
2
  import { DOMParser } from '@xmldom/xmldom';
2
3
  const CODE128_PATTERNS = [
3
4
  '212222', '222122', '222221', '121223', '121322', '131222', '122213', '122312', '132212', '221213',
@@ -13,23 +14,30 @@ const CODE128_PATTERNS = [
13
14
  '114131', '311141', '411131', '211412', '211214', '211232', '2331112',
14
15
  ];
15
16
  const COLUNAS_ITENS = [
16
- { x: 20, w: 42, label: 'COD. PROD' },
17
- { x: 62, w: 155, label: 'DESCRICAO DO PRODUTO / SERVICO' },
18
- { x: 217, w: 36, label: 'NCM/SH' },
19
- { x: 253, w: 28, label: 'O/CSOSN' },
20
- { x: 281, w: 24, label: 'CFOP' },
21
- { x: 305, w: 18, label: 'UN' },
22
- { x: 323, w: 34, label: 'QUANT.' },
23
- { x: 357, w: 36, label: 'V. UNIT.' },
24
- { x: 393, w: 36, label: 'V. TOTAL' },
25
- { x: 429, w: 34, label: 'B.C. ICMS' },
26
- { x: 463, w: 32, label: 'V. ICMS' },
27
- { x: 495, w: 28, label: 'V. IPI' },
28
- { x: 523, w: 26, label: 'AL. ICMS' },
29
- { x: 549, w: 26, label: 'AL. IPI' },
17
+ { x: 20, w: 44, label: 'CÓDIGO PROD.', align: 'left' },
18
+ { x: 64, w: 153, label: 'DESCRIÇÃO DO PRODUTO / SERVIÇO', align: 'left' },
19
+ { x: 217, w: 36, label: 'NCM/SH', align: 'center' },
20
+ { x: 253, w: 28, label: 'O/CSOSN', align: 'center' },
21
+ { x: 281, w: 24, label: 'CFOP', align: 'center' },
22
+ { x: 305, w: 18, label: 'UN', align: 'center' },
23
+ { x: 323, w: 34, label: 'QUANT.', align: 'right' },
24
+ { x: 357, w: 36, label: 'VALOR UNIT.', align: 'right' },
25
+ { x: 393, w: 36, label: 'VALOR TOTAL', align: 'right' },
26
+ { x: 429, w: 34, label: 'B.CÁLC ICMS', align: 'right' },
27
+ { x: 463, w: 32, label: 'VALOR ICMS', align: 'right' },
28
+ { x: 495, w: 28, label: 'VALOR IPI', align: 'right' },
29
+ { x: 523, w: 26, label: 'ALÍQ. ICMS', align: 'right' },
30
+ { x: 549, w: 26, label: 'ALÍQ. IPI', align: 'right' },
30
31
  ];
31
32
  export class Danfe {
32
- static fromXml(xmlNFe, xmlRetornoSefaz) {
33
+ static fromXml(xmlNFe, xmlRetornoSefazOuOpcoes, opcoes) {
34
+ const xmlRetornoSefaz = typeof xmlRetornoSefazOuOpcoes === 'string' ? xmlRetornoSefazOuOpcoes : undefined;
35
+ const opcoesResolvidas = typeof xmlRetornoSefazOuOpcoes === 'object' && xmlRetornoSefazOuOpcoes !== null
36
+ ? xmlRetornoSefazOuOpcoes
37
+ : opcoes;
38
+ const logoEmbed = opcoesResolvidas?.logo
39
+ ? this.parseLogoImage(opcoesResolvidas.logo)
40
+ : undefined;
33
41
  const doc = new DOMParser().parseFromString(xmlNFe, 'text/xml');
34
42
  const docRetorno = xmlRetornoSefaz
35
43
  ? new DOMParser().parseFromString(xmlRetornoSefaz, 'text/xml')
@@ -43,7 +51,11 @@ export class Danfe {
43
51
  const icmsTot = doc.getElementsByTagName('ICMSTot')[0];
44
52
  const transp = doc.getElementsByTagName('transp')[0];
45
53
  const transporta = transp?.getElementsByTagName('transporta')[0];
54
+ const veicTransp = transp?.getElementsByTagName('veicTransp')[0];
46
55
  const vol = transp?.getElementsByTagName('vol')[0];
56
+ const cobr = doc.getElementsByTagName('cobr')[0];
57
+ const fat = cobr?.getElementsByTagName('fat')[0];
58
+ const dupNodes = cobr?.getElementsByTagName('dup');
47
59
  const detPag = doc.getElementsByTagName('detPag')[0];
48
60
  const infAdic = doc.getElementsByTagName('infAdic')[0];
49
61
  const infProt = docRetorno.getElementsByTagName('infProt')[0] ??
@@ -59,16 +71,17 @@ export class Danfe {
59
71
  const dhEmi = this.formatDateTime(this.getTag(ide, 'dhEmi', ''));
60
72
  const dhSaiEnt = this.formatDateTime(this.getTag(ide, 'dhSaiEnt', this.getTag(ide, 'dhEmi', '')));
61
73
  const emitNome = this.getTag(emit, 'xNome', '');
74
+ const emitFant = this.getTag(emit, 'xFant', '');
62
75
  const emitCnpj = this.formatDoc(this.getTag(emit, 'CNPJ', this.getTag(emit, 'CPF', '')));
63
76
  const emitIe = this.getTag(emit, 'IE', '');
64
77
  const emitIeSt = this.getTag(emit, 'IEST', '');
65
78
  const emitEnd = `${this.getTag(enderEmit, 'xLgr', '')}, ${this.getTag(enderEmit, 'nro', '')} - ${this.getTag(enderEmit, 'xBairro', '')}`;
66
- const emitMunUfCep = `${this.getTag(enderEmit, 'xMun', '')}/${this.getTag(enderEmit, 'UF', '')} - CEP ${this.getTag(enderEmit, 'CEP', '')} - Fone ${this.getTag(enderEmit, 'fone', '')}`;
79
+ const emitMunUfCep = `${this.getTag(enderEmit, 'xMun', '')}/${this.getTag(enderEmit, 'UF', '')} - CEP ${this.formatCep(this.getTag(enderEmit, 'CEP', ''))}${this.getTag(enderEmit, 'fone', '') ? ' - Fone: ' + this.getTag(enderEmit, 'fone', '') : ''}`;
67
80
  const destNome = this.getTag(dest, 'xNome', '');
68
81
  const destDoc = this.formatDoc(this.getTag(dest, 'CNPJ', this.getTag(dest, 'CPF', '')));
69
82
  const destEnd = `${this.getTag(enderDest, 'xLgr', '')}, ${this.getTag(enderDest, 'nro', '')}`;
70
83
  const destBairro = this.getTag(enderDest, 'xBairro', '');
71
- const destCep = this.getTag(enderDest, 'CEP', '');
84
+ const destCep = this.formatCep(this.getTag(enderDest, 'CEP', ''));
72
85
  const destMun = this.getTag(enderDest, 'xMun', '');
73
86
  const destUf = this.getTag(enderDest, 'UF', '');
74
87
  const destFone = this.getTag(enderDest, 'fone', '');
@@ -79,7 +92,9 @@ export class Danfe {
79
92
  const dhRecbto = this.formatDateTime(this.getTag(infProt, 'dhRecbto', this.getTag(docRetorno, 'dhRecbto', '')));
80
93
  const protocoloTexto = nProt
81
94
  ? `${nProt} - ${dhRecbto}`
82
- : `cStat ${cStat} - ${xMotivo.slice(0, 38)} (${dhRecbto})`;
95
+ : cStat
96
+ ? `Status ${cStat}: ${xMotivo.slice(0, 36)}`
97
+ : 'NFe sem Autorização de Uso da SEFAZ';
83
98
  const detNodes = doc.getElementsByTagName('det');
84
99
  const itens = [];
85
100
  for (let i = 0; i < detNodes.length; i++) {
@@ -108,142 +123,408 @@ export class Danfe {
108
123
  pIPI: this.formatDecimal(this.getTag(ipi, 'pIPI', '0.00')),
109
124
  });
110
125
  }
111
- const cmds = [];
112
- cmds.push('0.5 w');
113
- this.drawBox(cmds, 20, 782, 450, 38, 'RECEBEMOS DE ' + emitNome.slice(0, 65) + ' OS PRODUTOS CONSTANTES DA NOTA FISCAL INDICADA AO LADO');
114
- this.drawBox(cmds, 20, 782, 110, 20, 'DATA DE RECEBIMENTO');
115
- this.drawBox(cmds, 130, 782, 340, 20, 'IDENTIFICACAO E ASSINATURA DO RECEBEDOR');
116
- this.drawBox(cmds, 470, 782, 105, 38, 'NF-e', `No. ${nNF.padStart(9, '0')} / Serie ${serie}`, 7.5, true);
117
- cmds.push('[2 2] 0 d 0.4 w 20 776 m 575 776 l S [] 0 d 0.5 w');
118
- this.drawBox(cmds, 20, 688, 220, 82, 'IDENTIFICACAO DO EMITENTE');
119
- this.drawText(cmds, 24, 752, emitNome.slice(0, 38), 7.5, true);
120
- this.drawText(cmds, 24, 739, emitEnd.slice(0, 45), 6.5, false);
121
- this.drawText(cmds, 24, 728, emitMunUfCep.slice(0, 47), 6.5, false);
122
- if (tpAmb === '2') {
123
- this.drawText(cmds, 24, 695, 'AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL', 6.5, true);
126
+ const itensPagina1 = 20;
127
+ const itensPorPaginaExtra = 40;
128
+ const paginasItens = [];
129
+ if (itens.length <= itensPagina1) {
130
+ paginasItens.push(itens);
124
131
  }
125
- this.drawBox(cmds, 240, 688, 95, 82, '');
126
- this.drawText(cmds, 268, 756, 'DANFE', 11, true);
127
- this.drawText(cmds, 246, 746, 'Documento Auxiliar da', 6, false);
128
- this.drawText(cmds, 245, 738, 'Nota Fiscal Eletronica', 6, false);
129
- this.drawText(cmds, 246, 725, '0 - ENTRADA', 6, false);
130
- this.drawText(cmds, 246, 717, '1 - SAIDA', 6, false);
131
- this.drawBox(cmds, 312, 716, 15, 15, '');
132
- this.drawText(cmds, 317, 720, tpNF, 8.5, true);
133
- this.drawText(cmds, 246, 703, `No. ${nNF.padStart(9, '0')}`, 7.5, true);
134
- this.drawText(cmds, 246, 693, `SERIE: ${serie.padStart(3, '0')} FL 1/1`, 6.5, true);
135
- this.drawBox(cmds, 335, 729, 240, 41, 'CONTROLE DO FISCO');
136
- this.drawCode128C(cmds, chave, 348, 733, 214, 26);
137
- this.drawBox(cmds, 335, 688, 240, 41, 'CHAVE DE ACESSO', chaveFormatada, 7, true);
138
- this.drawBox(cmds, 20, 666, 315, 22, 'NATUREZA DA OPERACAO', natOp, 7.5, false);
139
- this.drawBox(cmds, 335, 666, 240, 22, 'PROTOCOLO DE AUTORIZACAO DE USO', protocoloTexto, 6.5, true);
140
- this.drawBox(cmds, 20, 644, 185, 22, 'INSCRICAO ESTADUAL', emitIe, 7.5, false);
141
- this.drawBox(cmds, 205, 644, 185, 22, 'INSC. ESTADUAL DO SUBST. TRIBUTARIO', emitIeSt, 7.5, false);
142
- this.drawBox(cmds, 390, 644, 185, 22, 'CNPJ / CPF', emitCnpj, 7.5, true);
143
- this.drawSectionHeader(cmds, 20, 633, 'DESTINATARIO / REMETENTE');
144
- this.drawBox(cmds, 20, 609, 340, 22, 'NOME / RAZAO SOCIAL', destNome.slice(0, 64), 7.5, false);
145
- this.drawBox(cmds, 360, 609, 130, 22, 'CNPJ / CPF', destDoc, 7.5, true);
146
- this.drawBox(cmds, 490, 609, 85, 22, 'DATA DA EMISSAO', dhEmi.slice(0, 10), 7.5, false);
147
- this.drawBox(cmds, 20, 587, 270, 22, 'ENDERECO', destEnd.slice(0, 48), 7.5, false);
148
- this.drawBox(cmds, 290, 587, 120, 22, 'BAIRRO / DISTRITO', destBairro.slice(0, 24), 7.5, false);
149
- this.drawBox(cmds, 410, 587, 80, 22, 'CEP', destCep, 7.5, false);
150
- this.drawBox(cmds, 490, 587, 85, 22, 'DATA SAIDA/ENTRADA', dhSaiEnt.slice(0, 10), 7.5, false);
151
- this.drawBox(cmds, 20, 565, 220, 22, 'MUNICIPIO', destMun, 7.5, false);
152
- this.drawBox(cmds, 240, 565, 95, 22, 'FONE / FAX', destFone, 7.5, false);
153
- this.drawBox(cmds, 335, 565, 30, 22, 'UF', destUf, 7.5, false);
154
- this.drawBox(cmds, 365, 565, 125, 22, 'INSCRICAO ESTADUAL', destIe, 7.5, false);
155
- this.drawBox(cmds, 490, 565, 85, 22, 'HORA DA SAIDA', dhSaiEnt.slice(11, 19), 7.5, false);
156
- this.drawSectionHeader(cmds, 20, 554, 'FATURA / PAGAMENTO');
157
- const tPag = this.getTag(detPag, 'tPag', '01');
158
- const vPag = this.formatDecimal(this.getTag(detPag, 'vPag', '0.00'));
159
- this.drawBox(cmds, 20, 530, 185, 22, 'FORMA DE PAGAMENTO', this.getDescricaoPagamento(tPag), 7.5, false);
160
- this.drawBox(cmds, 205, 530, 185, 22, 'VALOR DO PAGAMENTO (R$)', vPag, 7.5, true);
161
- this.drawBox(cmds, 390, 530, 185, 22, 'TROCO (R$)', this.formatDecimal(this.getTag(doc.getElementsByTagName('pag')[0], 'vTroco', '0.00')), 7.5, false);
162
- this.drawSectionHeader(cmds, 20, 519, 'CALCULO DO IMPOSTO');
163
- this.drawBox(cmds, 20, 495, 70, 22, 'BASE CALC. ICMS', this.formatDecimal(this.getTag(icmsTot, 'vBC', '0.00')), 7.5, false);
164
- this.drawBox(cmds, 90, 495, 68, 22, 'VALOR DO ICMS', this.formatDecimal(this.getTag(icmsTot, 'vICMS', '0.00')), 7.5, false);
165
- this.drawBox(cmds, 158, 495, 68, 22, 'BASE CALC. ICMS ST', this.formatDecimal(this.getTag(icmsTot, 'vBCST', '0.00')), 7.5, false);
166
- this.drawBox(cmds, 226, 495, 68, 22, 'VALOR ICMS SUBST.', this.formatDecimal(this.getTag(icmsTot, 'vST', '0.00')), 7.5, false);
167
- this.drawBox(cmds, 294, 495, 65, 22, 'V. IMP. IMPORTACAO', this.formatDecimal(this.getTag(icmsTot, 'vII', '0.00')), 7.5, false);
168
- this.drawBox(cmds, 359, 495, 65, 22, 'VALOR DO FCP', this.formatDecimal(this.getTag(icmsTot, 'vFCP', '0.00')), 7.5, false);
169
- this.drawBox(cmds, 424, 495, 65, 22, 'VALOR DO PIS', this.formatDecimal(this.getTag(icmsTot, 'vPIS', '0.00')), 7.5, false);
170
- this.drawBox(cmds, 489, 495, 86, 22, 'V. TOTAL PRODUTOS', this.formatDecimal(this.getTag(icmsTot, 'vProd', '0.00')), 7.5, true);
171
- this.drawBox(cmds, 20, 473, 70, 22, 'VALOR DO FRETE', this.formatDecimal(this.getTag(icmsTot, 'vFrete', '0.00')), 7.5, false);
172
- this.drawBox(cmds, 90, 473, 68, 22, 'VALOR DO SEGURO', this.formatDecimal(this.getTag(icmsTot, 'vSeg', '0.00')), 7.5, false);
173
- this.drawBox(cmds, 158, 473, 68, 22, 'DESCONTO', this.formatDecimal(this.getTag(icmsTot, 'vDesc', '0.00')), 7.5, false);
174
- this.drawBox(cmds, 226, 473, 68, 22, 'OUTRAS DESPESAS', this.formatDecimal(this.getTag(icmsTot, 'vOutro', '0.00')), 7.5, false);
175
- this.drawBox(cmds, 294, 473, 65, 22, 'VALOR TOTAL IPI', this.formatDecimal(this.getTag(icmsTot, 'vIPI', '0.00')), 7.5, false);
176
- this.drawBox(cmds, 359, 473, 65, 22, 'V. APROX. TRIBUTOS', this.formatDecimal(this.getTag(icmsTot, 'vTotTrib', '0.00')), 7.5, false);
177
- this.drawBox(cmds, 424, 473, 65, 22, 'VALOR DA COFINS', this.formatDecimal(this.getTag(icmsTot, 'vCOFINS', '0.00')), 7.5, false);
178
- this.drawBox(cmds, 489, 473, 86, 22, 'V. TOTAL DA NOTA', this.formatDecimal(this.getTag(icmsTot, 'vNF', '0.00')), 8.5, true);
179
- this.drawSectionHeader(cmds, 20, 462, 'TRANSPORTADOR / VOLUMES TRANSPORTADOS');
180
- const modFrete = this.getTag(transp, 'modFrete', '9');
181
- this.drawBox(cmds, 20, 438, 210, 22, 'NOME / RAZAO SOCIAL', this.getTag(transporta, 'xNome', ''), 7.5, false);
182
- this.drawBox(cmds, 230, 438, 95, 22, 'FRETE POR CONTA', this.getDescricaoFrete(modFrete), 7, false);
183
- this.drawBox(cmds, 325, 438, 65, 22, 'CODIGO ANTT', '', 7.5, false);
184
- this.drawBox(cmds, 390, 438, 65, 22, 'PLACA DO VEICULO', '', 7.5, false);
185
- this.drawBox(cmds, 455, 438, 25, 22, 'UF', '', 7.5, false);
186
- this.drawBox(cmds, 480, 438, 95, 22, 'CNPJ / CPF', this.formatDoc(this.getTag(transporta, 'CNPJ', '')), 7.5, false);
187
- this.drawBox(cmds, 20, 416, 85, 22, 'QUANTIDADE', this.getTag(vol, 'qVol', ''), 7.5, false);
188
- this.drawBox(cmds, 105, 416, 110, 22, 'ESPECIE', this.getTag(vol, 'esp', ''), 7.5, false);
189
- this.drawBox(cmds, 215, 416, 100, 22, 'MARCA', this.getTag(vol, 'marca', ''), 7.5, false);
190
- this.drawBox(cmds, 315, 416, 90, 22, 'NUMERACAO', this.getTag(vol, 'nVol', ''), 7.5, false);
191
- this.drawBox(cmds, 405, 416, 85, 22, 'PESO BRUTO', this.getTag(vol, 'pesoB', ''), 7.5, false);
192
- this.drawBox(cmds, 490, 416, 85, 22, 'PESO LIQUIDO', this.getTag(vol, 'pesoL', ''), 7.5, false);
193
- this.drawSectionHeader(cmds, 20, 405, 'DADOS DOS PRODUTOS / SERVICOS');
194
- const headerY = 388;
195
- const headerH = 14;
196
- const tableBottomY = 112;
197
- const tableBodyH = headerY - tableBottomY;
198
- for (const col of COLUNAS_ITENS) {
199
- cmds.push(`${col.x} ${headerY} ${col.w} ${headerH} re S`);
200
- this.drawText(cmds, col.x + 2, headerY + 4, col.label, 5, true);
201
- cmds.push(`${col.x} ${tableBottomY} ${col.w} ${tableBodyH} re S`);
132
+ else {
133
+ paginasItens.push(itens.slice(0, itensPagina1));
134
+ for (let idx = itensPagina1; idx < itens.length; idx += itensPorPaginaExtra) {
135
+ paginasItens.push(itens.slice(idx, idx + itensPorPaginaExtra));
136
+ }
137
+ }
138
+ const totalPaginas = paginasItens.length;
139
+ const duplicatasResumo = [];
140
+ if (dupNodes) {
141
+ for (let i = 0; i < Math.min(dupNodes.length, 4); i++) {
142
+ const dup = dupNodes[i];
143
+ if (!dup)
144
+ continue;
145
+ const nDup = this.getTag(dup, 'nDup', String(i + 1).padStart(3, '0'));
146
+ const dVencRaw = this.getTag(dup, 'dVenc', '');
147
+ const dVenc = dVencRaw.length >= 10
148
+ ? `${dVencRaw.slice(8, 10)}/${dVencRaw.slice(5, 7)}/${dVencRaw.slice(0, 4)}`
149
+ : dVencRaw;
150
+ const vDup = this.formatDecimal(this.getTag(dup, 'vDup', '0.00'));
151
+ duplicatasResumo.push(`${nDup} ${dVenc} R$ ${vDup}`);
152
+ }
202
153
  }
203
- let rowY = headerY - 11;
204
- for (const item of itens) {
205
- if (rowY < tableBottomY + 8)
206
- break;
207
- this.drawText(cmds, 22, rowY, item.cProd.slice(0, 9), 6, false);
208
- this.drawText(cmds, 64, rowY, item.xProd.slice(0, 36), 6, false);
209
- this.drawText(cmds, 219, rowY, item.ncm, 6, false);
210
- this.drawText(cmds, 255, rowY, item.origCsosnCst, 6, false);
211
- this.drawText(cmds, 283, rowY, item.cfop, 6, false);
212
- this.drawText(cmds, 307, rowY, item.uCom.slice(0, 4), 6, false);
213
- this.drawText(cmds, 325, rowY, item.qCom, 6, false);
214
- this.drawText(cmds, 359, rowY, item.vUnCom, 6, false);
215
- this.drawText(cmds, 395, rowY, item.vProd, 6, false);
216
- this.drawText(cmds, 431, rowY, item.vBC, 6, false);
217
- this.drawText(cmds, 465, rowY, item.vICMS, 6, false);
218
- this.drawText(cmds, 497, rowY, item.vIPI, 6, false);
219
- this.drawText(cmds, 525, rowY, item.pICMS, 6, false);
220
- this.drawText(cmds, 551, rowY, item.pIPI, 6, false);
221
- cmds.push(`[1 2] 0 d 0.25 w 20 ${rowY - 4} m 575 ${rowY - 4} l S [] 0 d 0.5 w`);
222
- rowY -= 13;
154
+ const numeroFormatado = this.formatNumeroNF(nNF);
155
+ const now = new Date();
156
+ const pad2 = (n) => String(n).padStart(2, '0');
157
+ const agoraStr = `${pad2(now.getDate())}/${pad2(now.getMonth() + 1)}/${now.getFullYear()} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
158
+ const streamsPaginas = [];
159
+ for (let numPag = 1; numPag <= totalPaginas; numPag++) {
160
+ const cmds = [];
161
+ const itensDaPagina = paginasItens[numPag - 1] ?? [];
162
+ // Estilo NFePHP (sped-da): 100% monocromático (preto/branco), traço fino 0.4pt e cantos suavizados
163
+ cmds.push('1 j 1 J 0.4 w 0 G 0 g');
164
+ // =========================================================================
165
+ // 1. CANHOTO DESTACÁVEL (Apenas na 1ª página)
166
+ // =========================================================================
167
+ if (numPag === 1) {
168
+ this.drawFieldBox(cmds, 20, 801, 450, 19, `RECEBEMOS DE ${emitNome.slice(0, 62)} OS PRODUTOS E/OU SERVIÇOS CONSTANTES DA NOTA FISCAL ELETRÔNICA INDICADA ABAIXO`);
169
+ this.drawFieldBox(cmds, 20, 782, 110, 19, 'DATA DE RECEBIMENTO');
170
+ this.drawFieldBox(cmds, 130, 782, 340, 19, 'IDENTIFICAÇÃO E ASSINATURA DO RECEBEDOR');
171
+ this.drawFieldBox(cmds, 470, 782, 105, 38, '');
172
+ this.drawTextCentered(cmds, 470, 105, 808, 'NF-e', 9, true);
173
+ this.drawTextCentered(cmds, 470, 105, 796, `Nº. ${numeroFormatado}`, 7.5, true);
174
+ this.drawTextCentered(cmds, 470, 105, 786, `Série ${serie.padStart(3, '0')}`, 6.8, true);
175
+ // Linha pontilhada de destaque do canhoto
176
+ cmds.push('[2 2] 0 d 0.35 w 20 776 m 575 776 l S [] 0 d 0.4 w');
177
+ }
178
+ // =========================================================================
179
+ // 2. CABEÇALHO PRINCIPAL (Emitente + DANFE + Código de Barras / Chave)
180
+ // =========================================================================
181
+ this.drawFieldBox(cmds, 20, 688, 220, 82, 'IDENTIFICAÇÃO DO EMITENTE');
182
+ if (logoEmbed) {
183
+ const slotX = 25;
184
+ const slotY = 703;
185
+ const slotW = 54;
186
+ const slotH = 54;
187
+ const scale = Math.min(slotW / logoEmbed.width, slotH / logoEmbed.height);
188
+ const drawW = logoEmbed.width * scale;
189
+ const drawH = logoEmbed.height * scale;
190
+ const imgX = slotX + (slotW - drawW) / 2;
191
+ const imgY = slotY + (slotH - drawH) / 2;
192
+ cmds.push(`q ${drawW.toFixed(2)} 0 0 ${drawH.toFixed(2)} ${imgX.toFixed(2)} ${imgY.toFixed(2)} cm /Im1 Do Q`);
193
+ const textBoxX = 83;
194
+ const textBoxW = 154;
195
+ const linhasNomeEmit = this.wrapText(emitNome, 26);
196
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 749, linhasNomeEmit[0] ?? '', 7.4, true);
197
+ if (linhasNomeEmit[1]) {
198
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 739, linhasNomeEmit[1].slice(0, 28), 7, true);
199
+ }
200
+ else if (emitFant) {
201
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 739, emitFant.slice(0, 28), 6.6, false);
202
+ }
203
+ const linhasEndEmit = this.wrapText(emitEnd, 34);
204
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 726, linhasEndEmit[0] ?? '', 5.8, false);
205
+ if (linhasEndEmit[1]) {
206
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 718, linhasEndEmit[1].slice(0, 36), 5.8, false);
207
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 709, emitMunUfCep.slice(0, 50), 5.2, false);
208
+ }
209
+ else {
210
+ this.drawTextCentered(cmds, textBoxX, textBoxW, 716, emitMunUfCep.slice(0, 50), 5.3, false);
211
+ }
212
+ }
213
+ else {
214
+ const linhasNomeEmit = this.wrapText(emitNome, 33);
215
+ this.drawTextCentered(cmds, 20, 220, 747, linhasNomeEmit[0] ?? '', 8, true);
216
+ if (linhasNomeEmit[1]) {
217
+ this.drawTextCentered(cmds, 20, 220, 737, linhasNomeEmit[1], 7.5, true);
218
+ }
219
+ else if (emitFant) {
220
+ this.drawTextCentered(cmds, 20, 220, 737, emitFant.slice(0, 36), 7, false);
221
+ }
222
+ this.drawTextCentered(cmds, 20, 220, 722, emitEnd.slice(0, 44), 6.3, false);
223
+ this.drawTextCentered(cmds, 20, 220, 712, emitMunUfCep.slice(0, 46), 6.2, false);
224
+ }
225
+ if (tpAmb === '2') {
226
+ this.drawTextCentered(cmds, 20, 220, 693, 'EMITIDA EM AMBIENTE DE HOMOLOGAÇÃO - SEM VALOR FISCAL', 5.8, true);
227
+ }
228
+ // Quadro Central DANFE (estilo sped-da)
229
+ this.drawFieldBox(cmds, 240, 688, 95, 82, '');
230
+ this.drawTextCentered(cmds, 240, 95, 755, 'DANFE', 12, true);
231
+ this.drawTextCentered(cmds, 240, 95, 745, 'Documento Auxiliar da', 6, false);
232
+ this.drawTextCentered(cmds, 240, 95, 737, 'Nota Fiscal Eletrônica', 6, false);
233
+ this.drawText(cmds, 246, 724, '0 - ENTRADA', 6, false);
234
+ this.drawText(cmds, 246, 715, '1 - SAÍDA', 6, false);
235
+ this.drawFieldBox(cmds, 310, 714, 16, 16, '');
236
+ this.drawTextCentered(cmds, 310, 16, 719, tpNF, 8.5, true);
237
+ this.drawTextCentered(cmds, 240, 95, 702, `Nº. ${numeroFormatado}`, 7.5, true);
238
+ this.drawTextCentered(cmds, 240, 95, 692, `Série ${serie.padStart(3, '0')} Folha ${numPag}/${totalPaginas}`, 6.5, true);
239
+ // Código de Barras e Chave de Acesso
240
+ this.drawFieldBox(cmds, 335, 688, 240, 82, '');
241
+ this.drawCode128C(cmds, chave, 348, 738, 214, 25);
242
+ this.drawHLine(cmds, 335, 575, 732);
243
+ this.drawLabel(cmds, 339, 725, 'CHAVE DE ACESSO');
244
+ this.drawTextCentered(cmds, 335, 240, 714, chaveFormatada, 6.8, true);
245
+ this.drawHLine(cmds, 335, 575, 709);
246
+ this.drawTextCentered(cmds, 335, 240, 699, 'Consulta de autenticidade no portal nacional da NF-e', 6, false);
247
+ this.drawTextCentered(cmds, 335, 240, 691, 'www.nfe.fazenda.gov.br/portal ou no site da Sefaz Autorizadora', 5.5, false);
248
+ // Natureza da Operação e Protocolo
249
+ this.drawFieldBox(cmds, 20, 666, 315, 22, 'NATUREZA DA OPERAÇÃO', natOp.slice(0, 56), 7.5, false);
250
+ this.drawFieldBox(cmds, 335, 666, 240, 22, 'PROTOCOLO DE AUTORIZAÇÃO DE USO', protocoloTexto, 6.5, true, 'center');
251
+ // IE / IE ST / CNPJ
252
+ this.drawFieldBox(cmds, 20, 644, 185, 22, 'INSCRIÇÃO ESTADUAL', emitIe, 7.5, false);
253
+ this.drawFieldBox(cmds, 205, 644, 185, 22, 'INSCRIÇÃO ESTADUAL DO SUBST. TRIBUT.', emitIeSt, 7.5, false);
254
+ this.drawFieldBox(cmds, 390, 644, 185, 22, 'CNPJ / CPF', emitCnpj, 7.5, false);
255
+ let headerY = 385;
256
+ let tableBottomY = 108;
257
+ if (numPag === 1) {
258
+ // =========================================================================
259
+ // 3. DESTINATÁRIO / REMETENTE
260
+ // =========================================================================
261
+ this.drawSectionTitle(cmds, 20, 634, 'DESTINATÁRIO / REMETENTE');
262
+ this.drawFieldBox(cmds, 20, 610, 340, 22, 'NOME / RAZÃO SOCIAL', destNome.slice(0, 64), 7.5, false);
263
+ this.drawFieldBox(cmds, 360, 610, 130, 22, 'CNPJ / CPF', destDoc, 7.5, true);
264
+ this.drawFieldBox(cmds, 490, 610, 85, 22, 'DATA DA EMISSÃO', dhEmi.slice(0, 10), 7.5, false, 'center');
265
+ this.drawFieldBox(cmds, 20, 588, 270, 22, 'ENDEREÇO', destEnd.slice(0, 48), 7.5, false);
266
+ this.drawFieldBox(cmds, 290, 588, 120, 22, 'BAIRRO / DISTRITO', destBairro.slice(0, 24), 7.5, false);
267
+ this.drawFieldBox(cmds, 410, 588, 80, 22, 'CEP', destCep, 7.5, false, 'center');
268
+ this.drawFieldBox(cmds, 490, 588, 85, 22, 'DATA DA SAÍDA/ENTRADA', dhSaiEnt.slice(0, 10), 7.5, false, 'center');
269
+ this.drawFieldBox(cmds, 20, 566, 220, 22, 'MUNICÍPIO', destMun, 7.5, false);
270
+ this.drawFieldBox(cmds, 240, 566, 95, 22, 'FONE / FAX', destFone, 7.5, false);
271
+ this.drawFieldBox(cmds, 335, 566, 30, 22, 'UF', destUf, 7.5, false, 'center');
272
+ this.drawFieldBox(cmds, 365, 566, 125, 22, 'INSCRIÇÃO ESTADUAL', destIe, 7.5, false);
273
+ this.drawFieldBox(cmds, 490, 566, 85, 22, 'HORA DA SAÍDA/ENTRADA', dhSaiEnt.slice(11, 19), 7.5, false, 'center');
274
+ // =========================================================================
275
+ // 4. FATURA / DUPLICATAS / PAGAMENTO
276
+ // =========================================================================
277
+ this.drawSectionTitle(cmds, 20, 556, 'FATURA / DUPLICATAS');
278
+ const tPag = this.getTag(detPag, 'tPag', '01');
279
+ const vPag = this.formatDecimal(this.getTag(detPag, 'vPag', '0.00'));
280
+ if (duplicatasResumo.length > 0 || fat) {
281
+ const nFat = this.getTag(fat, 'nFat', '');
282
+ const vLiq = this.getTag(fat, 'vLiq', '');
283
+ const textoFatura = nFat ? `Nº ${nFat} - Líq: R$ ${this.formatDecimal(vLiq)}` : this.getDescricaoPagamento(tPag);
284
+ this.drawFieldBox(cmds, 20, 532, 145, 22, 'FATURA / FORMA PAGTO', textoFatura.slice(0, 28), 6.8, false);
285
+ this.drawFieldBox(cmds, 165, 532, 295, 22, 'DUPLICATAS (NÚMERO / VENCIMENTO / VALOR)', duplicatasResumo.join(' | ').slice(0, 64), 6.3, false);
286
+ this.drawFieldBox(cmds, 460, 532, 115, 22, 'VALOR DO PAGAMENTO', vPag, 7.5, true, 'right');
287
+ }
288
+ else {
289
+ this.drawFieldBox(cmds, 20, 532, 185, 22, 'FORMA DE PAGAMENTO', this.getDescricaoPagamento(tPag), 7.5, false);
290
+ this.drawFieldBox(cmds, 205, 532, 185, 22, 'VALOR DO PAGAMENTO', vPag, 7.5, true, 'right');
291
+ this.drawFieldBox(cmds, 390, 532, 185, 22, 'TROCO', this.formatDecimal(this.getTag(doc.getElementsByTagName('pag')[0], 'vTroco', '0.00')), 7.5, false, 'right');
292
+ }
293
+ // =========================================================================
294
+ // 5. CÁLCULO DO IMPOSTO
295
+ // =========================================================================
296
+ this.drawSectionTitle(cmds, 20, 522, 'CÁLCULO DO IMPOSTO');
297
+ this.drawFieldBox(cmds, 20, 498, 70, 22, 'BASE DE CÁLC. DO ICMS', this.formatDecimal(this.getTag(icmsTot, 'vBC', '0.00')), 7.2, false, 'right');
298
+ this.drawFieldBox(cmds, 90, 498, 68, 22, 'VALOR DO ICMS', this.formatDecimal(this.getTag(icmsTot, 'vICMS', '0.00')), 7.2, false, 'right');
299
+ this.drawFieldBox(cmds, 158, 498, 68, 22, 'BASE DE CÁLC. ICMS S.T.', this.formatDecimal(this.getTag(icmsTot, 'vBCST', '0.00')), 7.2, false, 'right');
300
+ this.drawFieldBox(cmds, 226, 498, 68, 22, 'VALOR DO ICMS SUBST.', this.formatDecimal(this.getTag(icmsTot, 'vST', '0.00')), 7.2, false, 'right');
301
+ this.drawFieldBox(cmds, 294, 498, 65, 22, 'V. IMP. IMPORTAÇÃO', this.formatDecimal(this.getTag(icmsTot, 'vII', '0.00')), 7.2, false, 'right');
302
+ this.drawFieldBox(cmds, 359, 498, 65, 22, 'VALOR DO FCP', this.formatDecimal(this.getTag(icmsTot, 'vFCP', '0.00')), 7.2, false, 'right');
303
+ this.drawFieldBox(cmds, 424, 498, 65, 22, 'VALOR DO PIS', this.formatDecimal(this.getTag(icmsTot, 'vPIS', '0.00')), 7.2, false, 'right');
304
+ this.drawFieldBox(cmds, 489, 498, 86, 22, 'V. TOTAL PRODUTOS', this.formatDecimal(this.getTag(icmsTot, 'vProd', '0.00')), 7.5, true, 'right');
305
+ this.drawFieldBox(cmds, 20, 476, 70, 22, 'VALOR DO FRETE', this.formatDecimal(this.getTag(icmsTot, 'vFrete', '0.00')), 7.2, false, 'right');
306
+ this.drawFieldBox(cmds, 90, 476, 68, 22, 'VALOR DO SEGURO', this.formatDecimal(this.getTag(icmsTot, 'vSeg', '0.00')), 7.2, false, 'right');
307
+ this.drawFieldBox(cmds, 158, 476, 68, 22, 'DESCONTO', this.formatDecimal(this.getTag(icmsTot, 'vDesc', '0.00')), 7.2, false, 'right');
308
+ this.drawFieldBox(cmds, 226, 476, 68, 22, 'OUTRAS DESPESAS', this.formatDecimal(this.getTag(icmsTot, 'vOutro', '0.00')), 7.2, false, 'right');
309
+ this.drawFieldBox(cmds, 294, 476, 65, 22, 'VALOR TOTAL IPI', this.formatDecimal(this.getTag(icmsTot, 'vIPI', '0.00')), 7.2, false, 'right');
310
+ this.drawFieldBox(cmds, 359, 476, 65, 22, 'V. APROX. TRIBUTOS', this.formatDecimal(this.getTag(icmsTot, 'vTotTrib', '0.00')), 7.2, false, 'right');
311
+ this.drawFieldBox(cmds, 424, 476, 65, 22, 'VALOR DA COFINS', this.formatDecimal(this.getTag(icmsTot, 'vCOFINS', '0.00')), 7.2, false, 'right');
312
+ this.drawFieldBox(cmds, 489, 476, 86, 22, 'V. TOTAL DA NOTA', this.formatDecimal(this.getTag(icmsTot, 'vNF', '0.00')), 8.2, true, 'right');
313
+ // =========================================================================
314
+ // 6. TRANSPORTADOR / VOLUMES TRANSPORTADOS
315
+ // =========================================================================
316
+ this.drawSectionTitle(cmds, 20, 466, 'TRANSPORTADOR / VOLUMES TRANSPORTADOS');
317
+ const modFrete = this.getTag(transp, 'modFrete', '9');
318
+ const rntc = this.getTag(veicTransp, 'RNTC', '');
319
+ const placa = this.getTag(veicTransp, 'placa', '');
320
+ const ufVeic = this.getTag(veicTransp, 'UF', this.getTag(transporta, 'UF', ''));
321
+ const docTransp = this.formatDoc(this.getTag(transporta, 'CNPJ', this.getTag(transporta, 'CPF', '')));
322
+ this.drawFieldBox(cmds, 20, 442, 210, 22, 'NOME / RAZÃO SOCIAL', this.getTag(transporta, 'xNome', ''), 7.2, false);
323
+ this.drawFieldBox(cmds, 230, 442, 95, 22, 'FRETE POR CONTA', this.getDescricaoFrete(modFrete), 6.8, false);
324
+ this.drawFieldBox(cmds, 325, 442, 65, 22, 'CÓDIGO ANTT', rntc, 7.2, false, 'center');
325
+ this.drawFieldBox(cmds, 390, 442, 65, 22, 'PLACA DO VEÍCULO', placa, 7.2, false, 'center');
326
+ this.drawFieldBox(cmds, 455, 442, 25, 22, 'UF', ufVeic, 7.2, false, 'center');
327
+ this.drawFieldBox(cmds, 480, 442, 95, 22, 'CNPJ / CPF', docTransp, 7.2, false);
328
+ this.drawFieldBox(cmds, 20, 420, 85, 22, 'QUANTIDADE', this.getTag(vol, 'qVol', ''), 7.2, false, 'right');
329
+ this.drawFieldBox(cmds, 105, 420, 110, 22, 'ESPÉCIE', this.getTag(vol, 'esp', ''), 7.2, false);
330
+ this.drawFieldBox(cmds, 215, 420, 100, 22, 'MARCA', this.getTag(vol, 'marca', ''), 7.2, false);
331
+ this.drawFieldBox(cmds, 315, 420, 90, 22, 'NUMERAÇÃO', this.getTag(vol, 'nVol', ''), 7.2, false);
332
+ this.drawFieldBox(cmds, 405, 420, 85, 22, 'PESO BRUTO', this.getTag(vol, 'pesoB', ''), 7.2, false, 'right');
333
+ this.drawFieldBox(cmds, 490, 420, 85, 22, 'PESO LÍQUIDO', this.getTag(vol, 'pesoL', ''), 7.2, false, 'right');
334
+ this.drawSectionTitle(cmds, 20, 410, 'DADOS DOS PRODUTOS / SERVIÇOS');
335
+ headerY = 407;
336
+ }
337
+ else {
338
+ this.drawSectionTitle(cmds, 20, 634, `DADOS DOS PRODUTOS / SERVIÇOS (CONTINUAÇÃO - FOLHA ${numPag}/${totalPaginas})`);
339
+ headerY = 631;
340
+ tableBottomY = 42;
341
+ }
342
+ // =========================================================================
343
+ // 7. TABELA DE ITENS E MARCA D'ÁGUA "SEM VALOR FISCAL" ESTILO NFEPHP
344
+ // =========================================================================
345
+ const headerH = 13;
346
+ const topTableY = headerY - headerH;
347
+ // Marca d'água em grande evidência no centro da tabela (exatamente como no NFePHP sped-da)
348
+ if (tpAmb === '2' || !nProt) {
349
+ const centerY = (topTableY + tableBottomY) / 2;
350
+ cmds.push('0.68 g');
351
+ this.drawTextCentered(cmds, 20, 555, centerY + 6, 'SEM VALOR FISCAL', 34, true);
352
+ if (tpAmb === '2') {
353
+ this.drawTextCentered(cmds, 20, 555, centerY - 14, 'EMITIDA EM AMBIENTE DE HOMOLOGAÇÃO', 13, true);
354
+ }
355
+ if (!nProt) {
356
+ this.drawTextCentered(cmds, 20, 555, centerY - 28, 'FALTA PROTOCOLO DE AUTORIZAÇÃO DA SEFAZ', 10, true);
357
+ }
358
+ cmds.push('0 g');
359
+ }
360
+ // Contorno arredondado da tabela e divisórias de colunas
361
+ this.drawRoundedRect(cmds, 20, tableBottomY, 555, headerY - tableBottomY, 2.5, 'S');
362
+ this.drawHLine(cmds, 20, 575, topTableY);
363
+ for (let i = 1; i < COLUNAS_ITENS.length; i++) {
364
+ const col = COLUNAS_ITENS[i];
365
+ this.drawVLine(cmds, col.x, tableBottomY, headerY);
366
+ }
367
+ // Títulos das colunas da tabela
368
+ for (const col of COLUNAS_ITENS) {
369
+ if (col.align === 'left') {
370
+ this.drawText(cmds, col.x + 2.5, topTableY + 4, col.label, 4.4, true);
371
+ }
372
+ else {
373
+ this.drawTextCentered(cmds, col.x, col.w, topTableY + 4, col.label, 4.3, true);
374
+ }
375
+ }
376
+ // Linhas de itens
377
+ let rowY = topTableY - 10;
378
+ for (let idxItem = 0; idxItem < itensDaPagina.length; idxItem++) {
379
+ const item = itensDaPagina[idxItem];
380
+ if (!item || rowY < tableBottomY + 6)
381
+ break;
382
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[0], rowY, item.cProd.slice(0, 10));
383
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[1], rowY, item.xProd.slice(0, 37));
384
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[2], rowY, item.ncm);
385
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[3], rowY, item.origCsosnCst);
386
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[4], rowY, item.cfop);
387
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[5], rowY, item.uCom.slice(0, 4));
388
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[6], rowY, item.qCom);
389
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[7], rowY, item.vUnCom);
390
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[8], rowY, item.vProd);
391
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[9], rowY, item.vBC);
392
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[10], rowY, item.vICMS);
393
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[11], rowY, item.vIPI);
394
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[12], rowY, item.pICMS);
395
+ this.drawAlignedItemCell(cmds, COLUNAS_ITENS[13], rowY, item.pIPI);
396
+ cmds.push(`[1 2] 0 d 0.25 w 20 ${(rowY - 3.5).toFixed(2)} m 575 ${(rowY - 3.5).toFixed(2)} l S [] 0 d 0.4 w`);
397
+ rowY -= 13;
398
+ }
399
+ // =========================================================================
400
+ // 8. DADOS ADICIONAIS (Apenas na 1ª página)
401
+ // =========================================================================
402
+ if (numPag === 1) {
403
+ this.drawSectionTitle(cmds, 20, 98, 'DADOS ADICIONAIS');
404
+ this.drawFieldBox(cmds, 20, 42, 370, 53, 'INFORMAÇÕES COMPLEMENTARES');
405
+ this.drawFieldBox(cmds, 390, 42, 185, 53, 'RESERVADO AO FISCO');
406
+ const infCpl = this.getTag(infAdic, 'infCpl', '');
407
+ const linhaStatus = `Ambiente: ${tpAmb === '2' ? '2 - Homologação (Sem Valor Fiscal)' : '1 - Produção'}${cStat ? ` | Status SEFAZ: ${cStat} - ${xMotivo}` : ''}`;
408
+ const linhasCompl = infCpl
409
+ ? [...this.wrapText(infCpl, 74).slice(0, 2), linhaStatus.slice(0, 78)]
410
+ : [linhaStatus.slice(0, 78)];
411
+ let yCompl = 76;
412
+ for (const linha of linhasCompl) {
413
+ this.drawText(cmds, 24, yCompl, linha, 6.3, false);
414
+ yCompl -= 10;
415
+ }
416
+ }
417
+ // =========================================================================
418
+ // 9. RODAPÉ ESTILO NFEPHP (Monocromático e nítido)
419
+ // =========================================================================
420
+ this.drawText(cmds, 20, 30, `Impresso em ${agoraStr.slice(0, 10)} às ${agoraStr.slice(11, 19)}`, 6.2, false);
421
+ this.drawTextRight(cmds, 575, 30, 'Powered by nfe-lib-node', 7.2, true);
422
+ streamsPaginas.push(cmds.join('\n'));
223
423
  }
224
- if (tpAmb === '2') {
225
- cmds.push('0.78 g');
226
- this.drawText(cmds, 115, 245, 'SEM VALOR FISCAL - HOMOLOGACAO', 18, true);
227
- cmds.push('0 g');
424
+ return this.buildPdfBuffer(streamsPaginas, logoEmbed);
425
+ }
426
+ static drawSectionTitle(cmds, x, y, title) {
427
+ this.drawText(cmds, x + 1, y, title, 5.8, true);
428
+ }
429
+ static drawFieldBox(cmds, x, y, w, h, label, value = '', valueSize = 7.5, boldValue = false, align = 'left') {
430
+ this.drawRoundedRect(cmds, x, y, w, h, 2, 'S');
431
+ if (label) {
432
+ this.drawLabel(cmds, x + 3, y + h - 6.2, label);
433
+ }
434
+ if (value) {
435
+ if (align === 'right') {
436
+ this.drawTextRight(cmds, x + w - 3.5, y + 4.2, value, valueSize, boldValue);
437
+ }
438
+ else if (align === 'center') {
439
+ this.drawTextCentered(cmds, x, w, y + 4.2, value, valueSize, boldValue);
440
+ }
441
+ else {
442
+ this.drawText(cmds, x + 3.5, y + 4.2, value, valueSize, boldValue);
443
+ }
444
+ }
445
+ }
446
+ static drawRoundedRect(cmds, x, y, w, h, r, style) {
447
+ const radius = Math.min(r, w / 2, h / 2);
448
+ const k = 0.55228475 * radius;
449
+ cmds.push(`${(x + radius).toFixed(2)} ${y.toFixed(2)} m`, `${(x + w - radius).toFixed(2)} ${y.toFixed(2)} l`, `${(x + w - radius + k).toFixed(2)} ${y.toFixed(2)} ${(x + w).toFixed(2)} ${(y + radius - k).toFixed(2)} ${(x + w).toFixed(2)} ${(y + radius).toFixed(2)} c`, `${(x + w).toFixed(2)} ${(y + h - radius).toFixed(2)} l`, `${(x + w).toFixed(2)} ${(y + h - radius + k).toFixed(2)} ${(x + w - radius + k).toFixed(2)} ${(y + h).toFixed(2)} ${(x + w - radius).toFixed(2)} ${(y + h).toFixed(2)} c`, `${(x + radius).toFixed(2)} ${(y + h).toFixed(2)} l`, `${(x + radius - k).toFixed(2)} ${(y + h).toFixed(2)} ${x.toFixed(2)} ${(y + h - radius + k).toFixed(2)} ${x.toFixed(2)} ${(y + h - radius).toFixed(2)} c`, `${x.toFixed(2)} ${(y + radius).toFixed(2)} l`, `${x.toFixed(2)} ${(y + radius - k).toFixed(2)} ${(x + radius - k).toFixed(2)} ${y.toFixed(2)} ${(x + radius).toFixed(2)} ${y.toFixed(2)} c`, `h ${style}`);
450
+ }
451
+ static drawHLine(cmds, x1, x2, y) {
452
+ cmds.push(`${x1.toFixed(2)} ${y.toFixed(2)} m ${x2.toFixed(2)} ${y.toFixed(2)} l S`);
453
+ }
454
+ static drawVLine(cmds, x, y1, y2) {
455
+ cmds.push(`${x.toFixed(2)} ${y1.toFixed(2)} m ${x.toFixed(2)} ${y2.toFixed(2)} l S`);
456
+ }
457
+ static drawLabel(cmds, x, y, label) {
458
+ this.drawText(cmds, x, y, label, 4.8, false);
459
+ }
460
+ static drawAlignedItemCell(cmds, col, y, text) {
461
+ if (!text)
462
+ return;
463
+ if (col.align === 'right') {
464
+ this.drawTextRight(cmds, col.x + col.w - 2.5, y, text, 5.8, false);
465
+ }
466
+ else if (col.align === 'center') {
467
+ this.drawTextCentered(cmds, col.x, col.w, y, text, 5.8, false);
468
+ }
469
+ else {
470
+ this.drawText(cmds, col.x + 2.5, y, text, 5.8, false);
471
+ }
472
+ }
473
+ static estimateTextWidth(text, size, bold) {
474
+ const factor = bold ? 0.54 : 0.50;
475
+ return text.length * size * factor;
476
+ }
477
+ static drawText(cmds, x, y, text, size, bold) {
478
+ const font = bold ? '/F2' : '/F1';
479
+ const escaped = this.escapePdfText(text);
480
+ cmds.push(`BT ${font} ${size} Tf ${x.toFixed(2)} ${y.toFixed(2)} Td (${escaped}) Tj ET`);
481
+ }
482
+ static drawTextRight(cmds, rightX, y, text, size, bold) {
483
+ const w = this.estimateTextWidth(text, size, bold);
484
+ this.drawText(cmds, rightX - w, y, text, size, bold);
485
+ }
486
+ static drawTextCentered(cmds, boxX, boxW, y, text, size, bold) {
487
+ const w = this.estimateTextWidth(text, size, bold);
488
+ const x = boxX + Math.max(2, (boxW - w) / 2);
489
+ this.drawText(cmds, x, y, text, size, bold);
490
+ }
491
+ static wrapText(text, maxCharsPerLine) {
492
+ const words = text.trim().split(/\s+/);
493
+ const lines = [];
494
+ let current = '';
495
+ for (const word of words) {
496
+ if (!current) {
497
+ current = word;
498
+ }
499
+ else if (current.length + 1 + word.length <= maxCharsPerLine) {
500
+ current += ' ' + word;
501
+ }
502
+ else {
503
+ lines.push(current);
504
+ current = word;
505
+ }
228
506
  }
229
- this.drawSectionHeader(cmds, 20, 100, 'DADOS ADICIONAIS');
230
- const infCpl = this.getTag(infAdic, 'infCpl', '');
231
- const textoAdicional = infCpl
232
- ? `${infCpl} | Status SEFAZ: ${cStat} - ${xMotivo}`
233
- : `Ambiente: ${tpAmb === '2' ? '2 - Homologacao (Sem Valor Fiscal)' : '1 - Producao'} | Status SEFAZ: ${cStat} - ${xMotivo}`;
234
- this.drawBox(cmds, 20, 40, 370, 57, 'INFORMACOES COMPLEMENTARES', textoAdicional.slice(0, 82), 6.5, false);
235
- this.drawBox(cmds, 390, 40, 185, 57, 'RESERVADO AO FISCO', '', 6.5, false);
236
- this.drawText(cmds, 543, 33, 'nfe-lib-node', 5.5, false);
237
- return this.buildPdfBuffer(cmds.join('\n'));
507
+ if (current)
508
+ lines.push(current);
509
+ return lines;
510
+ }
511
+ static escapePdfText(text) {
512
+ return text
513
+ .replace(/•/g, '-')
514
+ .replace(/—/g, '-')
515
+ .replace(/[^\x20-\xFF]/g, '')
516
+ .replace(/\\/g, '\\\\')
517
+ .replace(/\(/g, '\\(')
518
+ .replace(/\)/g, '\\)');
238
519
  }
239
520
  static getDescricaoFrete(modFrete) {
240
521
  const mapa = {
241
- '0': '0-Remetente (CIF)',
242
- '1': '1-Destinatario (FOB)',
243
- '2': '2-Terceiros',
244
- '3': '3-Proprio Remet.',
245
- '4': '4-Proprio Dest.',
246
- '9': '9-Sem Transporte',
522
+ '0': '0 - Remetente (CIF)',
523
+ '1': '1 - Destinatário (FOB)',
524
+ '2': '2 - Terceiros',
525
+ '3': '3 - Próprio Remet.',
526
+ '4': '4 - Próprio Dest.',
527
+ '9': '9 - Sem Transporte',
247
528
  };
248
529
  return mapa[modFrete] ?? modFrete;
249
530
  }
@@ -251,14 +532,14 @@ export class Danfe {
251
532
  const mapa = {
252
533
  '01': '01 - Dinheiro',
253
534
  '02': '02 - Cheque',
254
- '03': '03 - Cartao de Credito',
255
- '04': '04 - Cartao de Debito',
256
- '15': '15 - Boleto Bancario',
535
+ '03': '03 - Cartão de Crédito',
536
+ '04': '04 - Cartão de Débito',
537
+ '15': '15 - Boleto Bancário',
257
538
  '17': '17 - PIX',
258
539
  '90': '90 - Sem Pagamento',
259
540
  '99': '99 - Outros',
260
541
  };
261
- return mapa[tPag] ?? `Codigo ${tPag}`;
542
+ return mapa[tPag] ?? `Código ${tPag}`;
262
543
  }
263
544
  static formatDecimal(val) {
264
545
  const num = Number(val.replace(',', '.'));
@@ -266,6 +547,17 @@ export class Danfe {
266
547
  return val;
267
548
  return num.toFixed(2).replace('.', ',');
268
549
  }
550
+ static formatNumeroNF(nNF) {
551
+ const padded = nNF.replace(/\D/g, '').padStart(9, '0');
552
+ return `${padded.slice(0, 3)}.${padded.slice(3, 6)}.${padded.slice(6, 9)}`;
553
+ }
554
+ static formatCep(cep) {
555
+ const digits = cep.replace(/\D/g, '');
556
+ if (digits.length === 8) {
557
+ return `${digits.slice(0, 5)}-${digits.slice(5)}`;
558
+ }
559
+ return cep;
560
+ }
269
561
  static getTag(parent, tagName, fallback) {
270
562
  if (!parent || typeof parent.getElementsByTagName !== 'function') {
271
563
  return fallback;
@@ -290,31 +582,6 @@ export class Danfe {
290
582
  const timePart = iso.slice(11, 19);
291
583
  return `${datePart} ${timePart}`;
292
584
  }
293
- static drawSectionHeader(cmds, x, y, title) {
294
- this.drawText(cmds, x, y, title, 6, true);
295
- }
296
- static drawBox(cmds, x, y, w, h, label, value = '', valueSize = 7.5, boldValue = false) {
297
- cmds.push(`${x} ${y} ${w} ${h} re S`);
298
- if (label) {
299
- this.drawText(cmds, x + 2.5, y + h - 6.5, label, 5, false);
300
- }
301
- if (value) {
302
- this.drawText(cmds, x + 2.5, y + 4.5, value, valueSize, boldValue);
303
- }
304
- }
305
- static drawText(cmds, x, y, text, size, bold) {
306
- const font = bold ? '/F2' : '/F1';
307
- const escaped = this.escapePdfText(text);
308
- cmds.push(`BT ${font} ${size} Tf ${x} ${y} Td (${escaped}) Tj ET`);
309
- }
310
- static escapePdfText(text) {
311
- return text
312
- .normalize('NFD')
313
- .replace(/[\u0300-\u036f]/g, '')
314
- .replace(/\\/g, '\\\\')
315
- .replace(/\(/g, '\\(')
316
- .replace(/\)/g, '\\)');
317
- }
318
585
  static drawCode128C(cmds, digits, x, y, width, height) {
319
586
  const clean = digits.replace(/\D/g, '').padStart(44, '0').slice(0, 44);
320
587
  const codes = [105];
@@ -341,16 +608,226 @@ export class Danfe {
341
608
  }
342
609
  }
343
610
  }
344
- static buildPdfBuffer(contentStream) {
345
- const streamBytes = Buffer.byteLength(contentStream, 'latin1');
611
+ static parseLogoImage(logo) {
612
+ const buf = Buffer.isBuffer(logo)
613
+ ? logo
614
+ : Buffer.from(logo.replace(/^data:image\/[a-zA-Z0-9.+-]+;base64,/, '').trim(), 'base64');
615
+ if (buf.length < 16)
616
+ return undefined;
617
+ // 1. JPEG (FF D8)
618
+ if (buf[0] === 0xff && buf[1] === 0xd8) {
619
+ let offset = 2;
620
+ while (offset + 9 < buf.length) {
621
+ if (buf[offset] !== 0xff) {
622
+ offset++;
623
+ continue;
624
+ }
625
+ let marker = buf[offset + 1] ?? 0;
626
+ while (marker === 0xff && offset + 2 < buf.length) {
627
+ offset++;
628
+ marker = buf[offset + 1] ?? 0;
629
+ }
630
+ if (marker === 0xd8 || marker === 0xd9 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) {
631
+ offset += 2;
632
+ continue;
633
+ }
634
+ const segLen = buf.readUInt16BE(offset + 2);
635
+ if (marker === 0xc0 || marker === 0xc1 || marker === 0xc2) {
636
+ const bitsPerComponent = buf[offset + 4] ?? 8;
637
+ const height = buf.readUInt16BE(offset + 5);
638
+ const width = buf.readUInt16BE(offset + 7);
639
+ const numComponents = buf[offset + 9] ?? 3;
640
+ const colorSpace = numComponents === 1
641
+ ? '/DeviceGray'
642
+ : numComponents === 4
643
+ ? '/DeviceCMYK'
644
+ : '/DeviceRGB';
645
+ const hexStream = buf.toString('hex') + '>';
646
+ return {
647
+ width,
648
+ height,
649
+ colorSpace,
650
+ bitsPerComponent,
651
+ filter: '[/ASCIIHexDecode /DCTDecode]',
652
+ hexStream,
653
+ };
654
+ }
655
+ offset += 2 + segLen;
656
+ }
657
+ return undefined;
658
+ }
659
+ // 2. PNG (89 50 4E 47 0D 0A 1A 0A)
660
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
661
+ let offset = 8;
662
+ let width = 0;
663
+ let height = 0;
664
+ let bitDepth = 8;
665
+ let colorType = 2;
666
+ let palette;
667
+ let trns;
668
+ const idatChunks = [];
669
+ while (offset + 8 <= buf.length) {
670
+ const length = buf.readUInt32BE(offset);
671
+ const type = buf.toString('ascii', offset + 4, offset + 8);
672
+ const dataStart = offset + 8;
673
+ const dataEnd = dataStart + length;
674
+ if (dataEnd > buf.length)
675
+ break;
676
+ const chunkData = buf.subarray(dataStart, dataEnd);
677
+ if (type === 'IHDR' && length >= 13) {
678
+ width = chunkData.readUInt32BE(0);
679
+ height = chunkData.readUInt32BE(4);
680
+ bitDepth = chunkData[8] ?? 8;
681
+ colorType = chunkData[9] ?? 2;
682
+ }
683
+ else if (type === 'PLTE') {
684
+ palette = chunkData;
685
+ }
686
+ else if (type === 'tRNS') {
687
+ trns = chunkData;
688
+ }
689
+ else if (type === 'IDAT') {
690
+ idatChunks.push(chunkData);
691
+ }
692
+ else if (type === 'IEND') {
693
+ break;
694
+ }
695
+ offset = dataEnd + 4;
696
+ }
697
+ if (width <= 0 || height <= 0 || bitDepth !== 8 || idatChunks.length === 0) {
698
+ return undefined;
699
+ }
700
+ const bpp = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 4 ? 2 : 1;
701
+ const stride = width * bpp;
702
+ const rawData = inflateSync(Buffer.concat(idatChunks));
703
+ const unfiltered = Buffer.alloc(height * stride);
704
+ const paethPredictor = (a, b, c) => {
705
+ const p = a + b - c;
706
+ const pa = Math.abs(p - a);
707
+ const pb = Math.abs(p - b);
708
+ const pc = Math.abs(p - c);
709
+ if (pa <= pb && pa <= pc)
710
+ return a;
711
+ if (pb <= pc)
712
+ return b;
713
+ return c;
714
+ };
715
+ for (let y = 0; y < height; y++) {
716
+ const filterType = rawData[y * (stride + 1)] ?? 0;
717
+ const srcRowStart = y * (stride + 1) + 1;
718
+ const dstRowStart = y * stride;
719
+ for (let x = 0; x < stride; x++) {
720
+ const rawByte = rawData[srcRowStart + x] ?? 0;
721
+ const left = x >= bpp ? (unfiltered[dstRowStart + x - bpp] ?? 0) : 0;
722
+ const up = y > 0 ? (unfiltered[dstRowStart - stride + x] ?? 0) : 0;
723
+ const upLeft = y > 0 && x >= bpp ? (unfiltered[dstRowStart - stride + x - bpp] ?? 0) : 0;
724
+ let recon = rawByte;
725
+ if (filterType === 1) {
726
+ recon = (rawByte + left) & 0xff;
727
+ }
728
+ else if (filterType === 2) {
729
+ recon = (rawByte + up) & 0xff;
730
+ }
731
+ else if (filterType === 3) {
732
+ recon = (rawByte + Math.floor((left + up) / 2)) & 0xff;
733
+ }
734
+ else if (filterType === 4) {
735
+ recon = (rawByte + paethPredictor(left, up, upLeft)) & 0xff;
736
+ }
737
+ unfiltered[dstRowStart + x] = recon;
738
+ }
739
+ }
740
+ // Converte qualquer PNG (RGB, RGBA, Escala de Cinza, Paleta) em RGB de 24 bits sobre fundo branco (#FFFFFF)
741
+ const rgbOut = Buffer.alloc(width * height * 3);
742
+ for (let i = 0; i < width * height; i++) {
743
+ let r = 255;
744
+ let g = 255;
745
+ let b = 255;
746
+ let a = 255;
747
+ if (colorType === 6) {
748
+ r = unfiltered[i * 4] ?? 0;
749
+ g = unfiltered[i * 4 + 1] ?? 0;
750
+ b = unfiltered[i * 4 + 2] ?? 0;
751
+ a = unfiltered[i * 4 + 3] ?? 255;
752
+ }
753
+ else if (colorType === 2) {
754
+ r = unfiltered[i * 3] ?? 0;
755
+ g = unfiltered[i * 3 + 1] ?? 0;
756
+ b = unfiltered[i * 3 + 2] ?? 0;
757
+ }
758
+ else if (colorType === 4) {
759
+ const gray = unfiltered[i * 2] ?? 0;
760
+ r = gray;
761
+ g = gray;
762
+ b = gray;
763
+ a = unfiltered[i * 2 + 1] ?? 255;
764
+ }
765
+ else if (colorType === 0) {
766
+ const gray = unfiltered[i] ?? 0;
767
+ r = gray;
768
+ g = gray;
769
+ b = gray;
770
+ }
771
+ else if (colorType === 3 && palette) {
772
+ const idx = unfiltered[i] ?? 0;
773
+ r = palette[idx * 3] ?? 0;
774
+ g = palette[idx * 3 + 1] ?? 0;
775
+ b = palette[idx * 3 + 2] ?? 0;
776
+ if (trns && idx < trns.length) {
777
+ a = trns[idx] ?? 255;
778
+ }
779
+ }
780
+ if (a < 255) {
781
+ const alpha = a / 255;
782
+ r = Math.round(r * alpha + 255 * (1 - alpha));
783
+ g = Math.round(g * alpha + 255 * (1 - alpha));
784
+ b = Math.round(b * alpha + 255 * (1 - alpha));
785
+ }
786
+ rgbOut[i * 3] = r;
787
+ rgbOut[i * 3 + 1] = g;
788
+ rgbOut[i * 3 + 2] = b;
789
+ }
790
+ const compressedRgb = deflateSync(rgbOut);
791
+ const hexStream = compressedRgb.toString('hex') + '>';
792
+ return {
793
+ width,
794
+ height,
795
+ colorSpace: '/DeviceRGB',
796
+ bitsPerComponent: 8,
797
+ filter: '[/ASCIIHexDecode /FlateDecode]',
798
+ hexStream,
799
+ };
800
+ }
801
+ return undefined;
802
+ }
803
+ static buildPdfBuffer(contentStreams, logoEmbed) {
804
+ const pageObjNums = [];
805
+ const pageObjects = [];
806
+ // Objetos 1..4: Catalog (1), Pages (2), Font Regular (3), Font Bold (4)
807
+ // Objeto 5 (opcional): XObject Image /Im1 (quando logoEmbed estiver presente)
808
+ const imageObjNum = logoEmbed ? 5 : undefined;
809
+ let nextObjNum = logoEmbed ? 6 : 5;
810
+ for (const stream of contentStreams) {
811
+ const pageObjNum = nextObjNum++;
812
+ const contentObjNum = nextObjNum++;
813
+ pageObjNums.push(pageObjNum);
814
+ const xObjectResource = imageObjNum ? ` /XObject << /Im1 ${imageObjNum} 0 R >>` : '';
815
+ const streamBytes = Buffer.byteLength(stream, 'latin1');
816
+ pageObjects.push(`${pageObjNum} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 3 0 R /F2 4 0 R >>${xObjectResource} >> /Contents ${contentObjNum} 0 R >>\nendobj\n`);
817
+ pageObjects.push(`${contentObjNum} 0 obj\n<< /Length ${streamBytes} >>\nstream\n${stream}\nendstream\nendobj\n`);
818
+ }
819
+ const kidsRefs = pageObjNums.map((n) => `${n} 0 R`).join(' ');
346
820
  const objects = [
347
821
  '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n',
348
- '2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n',
349
- '3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>\nendobj\n',
350
- '4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n',
351
- '5 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n',
352
- `6 0 obj\n<< /Length ${streamBytes} >>\nstream\n${contentStream}\nendstream\nendobj\n`,
822
+ `2 0 obj\n<< /Type /Pages /Kids [${kidsRefs}] /Count ${pageObjNums.length} >>\nendobj\n`,
823
+ '3 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>\nendobj\n',
824
+ '4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>\nendobj\n',
353
825
  ];
826
+ if (logoEmbed && imageObjNum) {
827
+ const imgStreamLen = Buffer.byteLength(logoEmbed.hexStream, 'latin1');
828
+ objects.push(`${imageObjNum} 0 obj\n<< /Type /XObject /Subtype /Image /Width ${logoEmbed.width} /Height ${logoEmbed.height} /ColorSpace ${logoEmbed.colorSpace} /BitsPerComponent ${logoEmbed.bitsPerComponent} /Filter ${logoEmbed.filter} /Length ${imgStreamLen} >>\nstream\n${logoEmbed.hexStream}\nendstream\nendobj\n`);
829
+ }
830
+ objects.push(...pageObjects);
354
831
  let pdf = '%PDF-1.4\n';
355
832
  const offsets = [0];
356
833
  for (const obj of objects) {