From 07b677be292f6f20a569875c74f5835ef1943506 Mon Sep 17 00:00:00 2001 From: Rodrigo Martins Date: Tue, 22 Apr 2025 10:14:11 -0300 Subject: [PATCH] tenta fix pdfservice.js --- src/server/services/PdfService.ts | 393 ++++++++---------------------- 1 file changed, 108 insertions(+), 285 deletions(-) diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 966afe1..04bbe04 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -256,298 +256,121 @@ export class PdfService { const fontItalic = await pdfDoc.embedFont('Times-Italic'); const fontBoldItalic = fontBold; + // Função para remover todas as tags HTML, exceto as de formatação suportadas + function stripUnsupportedTags(html: string): string { + // Primeiro, simplifica o HTML complexo removendo classes e atributos desnecessários + let simplified = html + // Remove comentários HTML + .replace(//g, '') + // Limpa atributos de estilo complexos, mas preserva text-indent e text-align + .replace(/style="[^"]*?text-indent:\s*(\d+(?:\.\d+)?(?:pt|px|em))[^"]*?"/gi, 'data-indent="$1"') + .replace(/style="[^"]*?text-align:\s*(center|justify|right)[^"]*?"/gi, 'data-align="$1"') + // Remove todos os outros atributos de estilo + .replace(/\s+style="[^"]*?"/gi, '') + // Substitui classes específicas por atributos de dados para preservar formatação + .replace(/class="[^"]*?MsoListParagraph[^"]*?"/gi, 'data-list-para="true"') + // Normaliza blockquote e indentações + .replace(/]*>/gi, '

') + .replace(/<\/blockquote>/gi, '

') + // Trata listas + .replace(/]*>/gi, '

• ') + .replace(/<\/li>/gi, '

'); + + // Remove todas as tags, exceto , , , , ,
,

, + // e mantém os atributos data-* que acabamos de criar para formatação + return simplified + .replace(/<(b|strong|i|em|u|p|span|br)([^>]*)>/gi, (match, tag, attrs) => { + // Preserva apenas data-* atributos + const dataAttrs = attrs.match(/\s+data-[^=]+="[^"]*"/gi) || []; + return `<${tag}${dataAttrs.join('')}>`; + }) + // Remove todas as outras tags não suportadas + .replace(/<(?!\/?(?:b|strong|i|em|u|br|p|span))[^>]+>/gi, '') + // Converte números seguidos de parêntese (como em listas numeradas) em formato padronizado + .replace(/(\d+\))\s+/g, '$1 '); + } + // Função para decodificar entidades HTML comuns (acentuação, símbolos, º, etc) function decodeHtmlEntities(str: string): string { if (!str) return ''; - return str - // Acentos minúsculos e maiúsculos - .replace(/Á/g, 'Á').replace(/á/g, 'á') - .replace(/É/g, 'É').replace(/é/g, 'é') - .replace(/Í/g, 'Í').replace(/í/g, 'í') - .replace(/Ó/g, 'Ó').replace(/ó/g, 'ó') - .replace(/Ú/g, 'Ú').replace(/ú/g, 'ú') - .replace(/À/g, 'À').replace(/à/g, 'à') - .replace(/È/g, 'È').replace(/è/g, 'è') - .replace(/Ì/g, 'Ì').replace(/ì/g, 'ì') - .replace(/Ò/g, 'Ò').replace(/ò/g, 'ò') - .replace(/Ù/g, 'Ù').replace(/ù/g, 'ù') - .replace(/Â/g, 'Â').replace(/â/g, 'â') - .replace(/Ê/g, 'Ê').replace(/ê/g, 'ê') - .replace(/Î/g, 'Î').replace(/î/g, 'î') - .replace(/Ô/g, 'Ô').replace(/ô/g, 'ô') - .replace(/Û/g, 'Û').replace(/û/g, 'û') - .replace(/Ã/g, 'Ã').replace(/ã/g, 'ã') - .replace(/Õ/g, 'Õ').replace(/õ/g, 'õ') - .replace(/Ç/g, 'Ç').replace(/ç/g, 'ç') - .replace(/Ñ/g, 'Ñ').replace(/ñ/g, 'ñ') - .replace(/Ü/g, 'Ü').replace(/ü/g, 'ü') - // Símbolos comuns - .replace(/º/gi, 'º') - .replace(/ª/gi, 'ª') - .replace(/§/gi, '§') - .replace(/°/gi, '°') - .replace(/©/gi, '©') - .replace(/®/gi, '®') - .replace(/¶/gi, '¶') - .replace(/•/gi, '•') - .replace(/–/gi, '–') - .replace(/—/gi, '—') - .replace(/«/gi, '«') - .replace(/»/gi, '»') - .replace(/‘/gi, '‘') - .replace(/’/gi, '’') - .replace(/“/gi, '“') - .replace(/”/gi, '”') - .replace(/€/gi, '€') - .replace(/¥/gi, '¥') - .replace(/£/gi, '£') - .replace(/¢/gi, '¢') - .replace(/ /gi, ' ') - .replace(/&/gi, '&') - .replace(/"/gi, '"') - .replace(/'/g, "'") - .replace(/</gi, '<') - .replace(/>/gi, '>') - // Numéricos - .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); - } - - // Função para remover todas as tags HTML, exceto as de formatação suportadas - function stripUnsupportedTags(html: string): string { - // Remove todas as tags, exceto , , , , ,
,

, , ,

,
    ,
      ,
    1. - // Remove atributos de exceto text-indent e text-align - return html - // Normaliza blockquote para recuo visual - .replace(/]*>/gi, '§§BLOCKQUOTE_START§§') - .replace(/<\/blockquote>/gi, '§§BLOCKQUOTE_END§§') - // Mantém apenas com text-indent ou text-align:center - .replace(/])*?>/gi, '') - // Remove atributos de , , , , ,

      - .replace(/<(b|strong|i|em|u|p)[^>]*>/gi, '<$1>') - // Remove todas as outras tags e atributos, exceto listas - .replace(/<(?!\/?(b|strong|i|em|u|br|p|span|ul|ol|li)[ >])[^>]+>/gi, '') - // Remove spans sem text-indent ou text-align - .replace(/(.*?)<\/span>/gi, '$1') - // Remove comentários HTML - .replace(//g, ''); - } - - // Função para converter listas HTML em texto com marcadores/numeração - function htmlListToText(html: string): string { - //

        e
          para marcadores/numeração simples - let idx = 1; - return html - .replace(/]*>([\s\S]*?)<\/ul>/gi, (_, items) => - items.replace(/]*>([\s\S]*?)<\/li>/gi, (__, li) => `\n• ${li.trim()}`) - ) - .replace(/]*>([\s\S]*?)<\/ol>/gi, (_, items) => - items.replace(/]*>([\s\S]*?)<\/li>/gi, () => `\n${idx++}. `) - ); - } - - // Função para renderizar texto com formatação inline (negrito, itálico, sublinhado) e controle de quebra de linha e recuo - function renderInlineFormattedParagraph( - page, - text: string, - x: number, - y: number, - font, - fontBold, - fontItalic, - fontBoldItalic, - fontSize: number, - color, - align: string, - maxWidth: number, - indent: boolean, - topMargin: number, - bottomMargin: number, - pageHeight: number - ) { - let regex = /(<\/?(b|strong|i|em|u)>)/gi; - let tokens: { text: string, bold: boolean, italic: boolean, underline: boolean }[] = []; - let lastIndex = 0; - let match; - let currentStyle = { bold: false, italic: false, underline: false }; - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - tokens.push({ - text: text.substring(lastIndex, match.index), - bold: currentStyle.bold, - italic: currentStyle.italic, - underline: currentStyle.underline - }); - } - if (/^<(b|strong)>$/i.test(match[0])) currentStyle.bold = true; - if (/^<\/(b|strong)>$/i.test(match[0])) currentStyle.bold = false; - if (/^<(i|em)>$/i.test(match[0])) currentStyle.italic = true; - if (/^<\/(i|em)>$/i.test(match[0])) currentStyle.italic = false; - if (/^$/i.test(match[0])) currentStyle.underline = true; - if (/^<\/u>$/i.test(match[0])) currentStyle.underline = false; - lastIndex = regex.lastIndex; + + // Usa o método nativo do DOM para decodificar entidades HTML + const textarea = document.createElement('textarea'); + textarea.innerHTML = str; + let decoded = textarea.value; + + // Fallback para ambientes sem DOM (Node.js) + if (decoded === str) { + return str + // Acentos minúsculos e maiúsculos + .replace(/Á/g, 'Á').replace(/á/g, 'á') + .replace(/É/g, 'É').replace(/é/g, 'é') + .replace(/Í/g, 'Í').replace(/í/g, 'í') + .replace(/Ó/g, 'Ó').replace(/ó/g, 'ó') + .replace(/Ú/g, 'Ú').replace(/ú/g, 'ú') + .replace(/À/g, 'À').replace(/à/g, 'à') + .replace(/È/g, 'È').replace(/è/g, 'è') + .replace(/Ì/g, 'Ì').replace(/ì/g, 'ì') + .replace(/Ò/g, 'Ò').replace(/ò/g, 'ò') + .replace(/Ù/g, 'Ù').replace(/ù/g, 'ù') + .replace(/Â/g, 'Â').replace(/â/g, 'â') + .replace(/Ê/g, 'Ê').replace(/ê/g, 'ê') + .replace(/Î/g, 'Î').replace(/î/g, 'î') + .replace(/Ô/g, 'Ô').replace(/ô/g, 'ô') + .replace(/Û/g, 'Û').replace(/û/g, 'û') + .replace(/Ã/g, 'Ã').replace(/ã/g, 'ã') + .replace(/Õ/g, 'Õ').replace(/õ/g, 'õ') + .replace(/Ç/g, 'Ç').replace(/ç/g, 'ç') + .replace(/Ñ/g, 'Ñ').replace(/ñ/g, 'ñ') + .replace(/Ü/g, 'Ü').replace(/ü/g, 'ü') + // Símbolos comuns + .replace(/º/gi, 'º') + .replace(/ª/gi, 'ª') + .replace(/§/gi, '§') + .replace(/°/gi, '°') + .replace(/©/gi, '©') + .replace(/®/gi, '®') + .replace(/¶/gi, '¶') + .replace(/•/gi, '•') + .replace(/–/gi, '–') + .replace(/—/gi, '—') + .replace(/«/gi, '«') + .replace(/»/gi, '»') + .replace(/‘/gi, ''') + .replace(/’/gi, ''') + .replace(/“/gi, '"') + .replace(/”/gi, '"') + .replace(/€/gi, '€') + .replace(/¥/gi, '¥') + .replace(/£/gi, '£') + .replace(/¢/gi, '¢') + .replace(/ /gi, ' ') + .replace(/&/gi, '&') + .replace(/"/gi, '"') + .replace(/'/g, "'") + .replace(/</gi, '<') + .replace(/>/gi, '>') + // Numéricos + .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); } - if (lastIndex < text.length) { - tokens.push({ - text: text.substring(lastIndex), - bold: currentStyle.bold, - italic: currentStyle.italic, - underline: currentStyle.underline - }); - } - - let words: { text: string, font: any, underline: boolean, bold: boolean, italic: boolean }[] = []; - for (const token of tokens) { - let usedFont = font; - if (token.bold && token.italic) usedFont = fontBoldItalic; - else if (token.bold) usedFont = fontBold; - else if (token.italic) usedFont = fontItalic; - // Corrige: split por espaço, mas preserva espaços como tokens separados - const split = token.text.split(/(\s+)/); - for (const part of split) { - words.push({ text: part, font: usedFont, underline: token.underline, bold: token.bold, italic: token.italic }); - } - } - - let lines: { tokens: { text: string, font: any, underline: boolean, bold: boolean, italic: boolean }[] }[] = []; - let currentTokens: { text: string, font: any, underline: boolean, bold: boolean, italic: boolean }[] = []; - let currentWidth = 0; - - for (const [i, word] of words.entries()) { - const wordWidth = word.font.widthOfTextAtSize(word.text, fontSize); - if (currentWidth + wordWidth > maxWidth && currentTokens.length > 0) { - lines.push({ tokens: currentTokens }); - currentTokens = [word]; - currentWidth = wordWidth; - } else { - currentTokens.push(word); - currentWidth += wordWidth; - } - } - if (currentTokens.length > 0) { - lines.push({ tokens: currentTokens }); - } - - // Ajuste: Força alinhamento consistente para todas as linhas do parágrafo - // Se align === 'center', todas as linhas centralizadas - // Se align === 'justify', todas as linhas justificadas (exceto a última ou linhas curtas) - // Se align === 'left' ou outro, todas à esquerda - - for (let i = 0; i < lines.length; i++) { - if (y < bottomMargin + fontSize * 1.5) { - return null; - } - let line = lines[i]; - let drawX = x; - - // Remove espaços no início e fim da linha para não justificar esses espaços - let trimmedTokens = [...line.tokens]; - while (trimmedTokens.length && /^\s+$/.test(trimmedTokens[0].text)) trimmedTokens.shift(); - while (trimmedTokens.length && /^\s+$/.test(trimmedTokens[trimmedTokens.length - 1].text)) trimmedTokens.pop(); - - let totalWidth = trimmedTokens.reduce((acc, t) => acc + t.font.widthOfTextAtSize(t.text, fontSize), 0); - - // Corrige centralização: centraliza a linha inteira, inclusive se for a única linha do parágrafo - if (align === 'center') { - drawX = x + (maxWidth - totalWidth) / 2; - for (const t of trimmedTokens) { - page.drawText(t.text, { - x: drawX, - y, - size: fontSize, - font: t.font, - color - }); - if (t.underline && t.text.trim() !== '') { - const underlineY = y - 1.5; - page.drawLine({ - start: { x: drawX, y: underlineY }, - end: { x: drawX + t.font.widthOfTextAtSize(t.text, fontSize), y: underlineY }, - thickness: 0.75, - color - }); - } - drawX += t.font.widthOfTextAtSize(t.text, fontSize); - } - } else if (align === 'justify' && i < lines.length - 1 && trimmedTokens.length > 2) { - // Justify: distribui espaço entre palavras, exceto na última linha ou linhas curtas - const spaces = trimmedTokens.filter(t => /^\s+$/.test(t.text)); - const numSpaces = spaces.length; - const lineTextWidth = trimmedTokens.reduce((acc, t) => acc + t.font.widthOfTextAtSize(t.text, fontSize), 0); - const extraSpace = maxWidth - lineTextWidth; - let spaceWidth = 0; - // Só justifica se houver pelo menos 2 espaços e a linha não for muito curta - if (numSpaces > 0 && extraSpace > 0) { - spaceWidth = extraSpace / numSpaces; - } - let localDrawX = x; - for (const t of trimmedTokens) { - page.drawText(t.text, { - x: localDrawX, - y, - size: fontSize, - font: t.font, - color - }); - if (t.underline && t.text.trim() !== '') { - const underlineY = y - 1.5; - page.drawLine({ - start: { x: localDrawX, y: underlineY }, - end: { x: localDrawX + t.font.widthOfTextAtSize(t.text, fontSize), y: underlineY }, - thickness: 0.75, - color - }); - } - if (/^\s+$/.test(t.text) && spaceWidth > 0) { - localDrawX += t.font.widthOfTextAtSize(t.text, fontSize) + spaceWidth; - } else { - localDrawX += t.font.widthOfTextAtSize(t.text, fontSize); - } - } - } else { - // Alinhamento à esquerda (padrão) ou última linha de parágrafo justificado - for (const t of trimmedTokens) { - page.drawText(t.text, { - x: drawX, - y, - size: fontSize, - font: t.font, - color - }); - if (t.underline && t.text.trim() !== '') { - const underlineY = y - 1.5; - page.drawLine({ - start: { x: drawX, y: underlineY }, - end: { x: drawX + t.font.widthOfTextAtSize(t.text, fontSize), y: underlineY }, - thickness: 0.75, - color - }); - } - drawX += t.font.widthOfTextAtSize(t.text, fontSize); - } - } - y -= fontSize * 1.5; - } - return y; + + return decoded; } const texto = decodeHtmlEntities( - htmlListToText( - stripUnsupportedTags( - this.processarTemplate(conteudo, escritorio) - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n\n') - .replace(/]*>/gi, '') - // Blockquote para recuo visual - .replace(/§§BLOCKQUOTE_START§§/g, '§§INDENT2§§') - .replace(/§§BLOCKQUOTE_END§§/g, '') - .replace(/]*text-indent[^>]*>/gi, '§§INDENT§§') - .replace(/]*text-align\s*:\s*center[^>]*>/gi, '§§CENTER§§') - .replace(/<\/span>/gi, '') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .replace(/\n{3,}/g, '\n\n') - .trim() - ) + stripUnsupportedTags( + this.processarTemplate(conteudo, escritorio) + .replace(//gi, '\n') + .replace(/]*>/gi, '§§INDENT=$1§§') + .replace(/]*>/gi, '§§ALIGN=$1§§') + .replace(/]*>/gi, '• ') + .replace(/]*>/gi, '§§INDENT=20pt§§') + .replace(/<\/p>/gi, '\n\n') + .replace(/]*>/gi, '') + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .trim() ) );