From 2cc0b586e98776259daf4f5556b5df891102f7f6 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Soares Date: Sat, 15 Aug 2026 11:12:05 -0300 Subject: [PATCH] fix(danfse): normaliza CR/tab/controle antes de desenhar no PDF CR (\r) e tab soltos no XML apareciam como glifo visivel no DANFSe em vez de virar quebra de linha/espaco - achado em producao real com descricao de servico colada de fora. --- src/danfse/desenho.ts | 5 +++-- src/danfse/formatadores.teste.ts | 23 +++++++++++++++++++++++ src/danfse/formatadores.ts | 16 ++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/danfse/formatadores.teste.ts diff --git a/src/danfse/desenho.ts b/src/danfse/desenho.ts index 29cc9ed..73da46f 100644 --- a/src/danfse/desenho.ts +++ b/src/danfse/desenho.ts @@ -33,6 +33,7 @@ import { formatarPercentual, formatarTelefone, juntar, + limparTextoPdf, reticencias, ufDoCodigoMunicipio, } from './formatadores.js'; @@ -117,7 +118,7 @@ export async function desenharDanfse(dados: NfseLegivel, opcoes: OpcoesGerarDanf // nesses tamanhos de fonte. const semLigadura = { liga: false } as unknown as PDFKit.Mixins.OpenTypeFeatures[]; function escrever(texto: string, x: number, y: number, opcoes: PDFKit.Mixins.TextOptions = {}): void { - documento.text(texto, x, y, { ...opcoes, features: semLigadura }); + documento.text(limparTextoPdf(texto), x, y, { ...opcoes, features: semLigadura }); } function larguraDoTexto(texto: string): number { return documento.widthOfString(texto, { features: semLigadura }); @@ -417,7 +418,7 @@ export async function desenharDanfse(dados: NfseLegivel, opcoes: OpcoesGerarDanf const alturaLinha = tamanhoFonte * 1.15; const disponivel = margemSup + alturaUtil - alturaReservadaAposServico() - (y + 3.0 * MM); const maxLinhas = Math.max(Math.floor(disponivel / alturaLinha), 1); - let descricao = s.descricao; + let descricao = limparTextoPdf(s.descricao); const linhas = contarLinhas(descricao, bw - 1.6 * MM, tamanhoFonte); if (linhas > maxLinhas) { const aproximado = Math.floor((descricao.length * maxLinhas) / linhas) - 4; diff --git a/src/danfse/formatadores.teste.ts b/src/danfse/formatadores.teste.ts new file mode 100644 index 0000000..588e2d5 --- /dev/null +++ b/src/danfse/formatadores.teste.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { limparTextoPdf } from './formatadores.js'; + +test('limparTextoPdf troca CRLF e CR solto por LF', () => { + assert.equal(limparTextoPdf('linha1\r\nlinha2\rlinha3'), 'linha1\nlinha2\nlinha3'); +}); + +test('limparTextoPdf troca tab por espaco', () => { + assert.equal(limparTextoPdf('coluna1\tcoluna2'), 'coluna1 coluna2'); +}); + +test('limparTextoPdf remove outros caracteres de controle sem apagar o texto ao redor', () => { + assert.equal(limparTextoPdf('abc\x00\x01\x1fdef'), 'abcdef'); +}); + +test('limparTextoPdf preserva LF (quebra de linha legitima)', () => { + assert.equal(limparTextoPdf('linha1\nlinha2'), 'linha1\nlinha2'); +}); + +test('limparTextoPdf devolve string vazia para undefined', () => { + assert.equal(limparTextoPdf(undefined), ''); +}); diff --git a/src/danfse/formatadores.ts b/src/danfse/formatadores.ts index d668237..068ed20 100644 --- a/src/danfse/formatadores.ts +++ b/src/danfse/formatadores.ts @@ -72,3 +72,19 @@ export function reticencias(texto: string | undefined, limite: number): string { const ultimoEspaco = cortado.lastIndexOf(' '); return (ultimoEspaco > 0 ? cortado.slice(0, ultimoEspaco) : cortado) + '...'; } + +/** + * Normaliza texto vindo do XML antes de desenhar no PDF: CR/CRLF viram LF, + * tab vira espaço e outros caracteres de controle são removidos. Sem isso, + * um \r (retorno de carro) ou \t soltos no XML aparecem como glifo visível + * no DANFSe em vez de virar quebra de linha ou espaço - visto em descrições + * de serviço e informações complementares com texto colado de fora. + */ +export function limparTextoPdf(texto: string | undefined): string { + if (!texto) return ''; + return texto + .replace(/\r\n?/g, '\n') + .replace(/\t/g, ' ') + // eslint-disable-next-line no-control-regex + .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, ''); +}