diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index e2c6935..f30b48f 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -260,374 +260,6 @@ 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 ''; - // Como estamos em Node.js, usamos apenas o método de substituição - return str - .replace(/\r\n/g, '\n') - // 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(/º/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, '>') - // Numéricos - .replace(/(\d+);/g, (m, code) => String.fromCharCode(code)); - } - - const texto = decodeHtmlEntities( - stripUnsupportedTags( - this.processarTemplate(conteudo, escritorio) - ) - ); - - // Process paragraph alignment and formatting - const paragrafos = texto.split(/\n{2,}/).map(p => { - let paragraph = p.trim(); - if (!paragraph) return ''; - - // Process paragraph alignment - let align = 'justify'; // default - const alignMatch = paragraph.match(/^§§ALIGN=(center|justify|right)§§/); - if (alignMatch) { - align = alignMatch[1]; - paragraph = paragraph.substring(alignMatch[0].length); - } - - // Process indentation - let indent = 0; - const indentMatch = paragraph.match(/^§§INDENT=([0-9.]+)(?:pt|px|em)§§/); - if (indentMatch) { - indent = parseFloat(indentMatch[1]); - paragraph = paragraph.substring(indentMatch[0].length); - } - - // Mark paragraph with alignment and indentation - return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph}`; - }).filter(Boolean); - - /** - * Renderiza um parágrafo com formatação inline (negrito, itálico, sublinhado) - */ - const renderInlineFormattedParagraph = ( - page: any, - text: string, - x: number, - y: number, - normalFont: any, - boldFont: any, - italicFont: any, - boldItalicFont: any, - fontSize: number, - color: any, - align: 'left' | 'center' | 'justify' = 'left', - maxWidth: number = MAX_WIDTH, - isContinuation: boolean = false, - marginTop: number = margens.top, - marginBottom: number = margens.bottom, - pageHeight: number = PAGE_HEIGHT - ): number | null => { - // Se não houver espaço suficiente na página atual para pelo menos uma linha de texto - if (y < marginBottom + fontSize) { - return null; // Indica que precisa criar nova página - } - - const words = text.split(/\s+/); - let currentLine = ''; - let currentX = x; - let currentY = y; - const lineHeight = fontSize * 1.2; - - // Processamento de formatação inline básica - let formattedChunks: { text: string, isItalic: boolean, isBold: boolean }[] = []; - - // Pré-processamento do texto para detectar formatação - let remaining = text; - let inBold = false; - let inItalic = false; - let currentChunk = ''; - - while (remaining.length > 0) { - if (remaining.startsWith('') || remaining.startsWith('')) { - // Adiciona o texto acumulado antes da tag - if (currentChunk) { - formattedChunks.push({ text: currentChunk, isItalic: inItalic, isBold: inBold }); - currentChunk = ''; - } - - inBold = true; - remaining = remaining.replace(/^<(b|strong)>/, ''); - } - else if (remaining.startsWith('') || remaining.startsWith('
')) { - // Adiciona o texto acumulado em negrito - if (currentChunk) { - formattedChunks.push({ text: currentChunk, isItalic: inItalic, isBold: true }); - currentChunk = ''; - } - - inBold = false; - remaining = remaining.replace(/^<\/(b|strong)>/, ''); - } - else if (remaining.startsWith('') || remaining.startsWith('')) { - // Adiciona o texto acumulado antes da tag - if (currentChunk) { - formattedChunks.push({ text: currentChunk, isItalic: inItalic, isBold: inBold }); - currentChunk = ''; - } - - inItalic = true; - remaining = remaining.replace(/^<(i|em)>/, ''); - } - else if (remaining.startsWith('') || remaining.startsWith('')) { - // Adiciona o texto acumulado em itálico - if (currentChunk) { - formattedChunks.push({ text: currentChunk, isItalic: true, isBold: inBold }); - currentChunk = ''; - } - - inItalic = false; - remaining = remaining.replace(/^<\/(i|em)>/, ''); - } - else if (remaining.startsWith('
') || remaining.startsWith('
') || remaining.startsWith('
')) { - // Adiciona o texto acumulado e então adiciona uma quebra de linha - if (currentChunk) { - formattedChunks.push({ text: currentChunk, isItalic: inItalic, isBold: inBold }); - currentChunk = ''; - } - - formattedChunks.push({ text: '\n', isItalic: false, isBold: false }); - remaining = remaining.replace(/^
/i, ''); - } - else { - // Acumula caractere a caractere - currentChunk += remaining[0]; - remaining = remaining.substring(1); - } - } - - // Adiciona qualquer texto restante - if (currentChunk) { - formattedChunks.push({ text: currentChunk, isItalic: inItalic, isBold: inBold }); - } - - let lines: { chunks: { text: string, isItalic: boolean, isBold: boolean }[], width: number }[] = [{ chunks: [], width: 0 }]; - let currentLineIndex = 0; - - for (const chunk of formattedChunks) { - if (chunk.text === '\n') { - // Forçar quebra de linha - lines.push({ chunks: [], width: 0 }); - currentLineIndex++; - continue; - } - - const words = chunk.text.split(/\s+/); - - for (let i = 0; i < words.length; i++) { - const word = words[i]; - if (!word) continue; - - const font = chunk.isBold && chunk.isItalic ? boldItalicFont : - chunk.isBold ? boldFont : - chunk.isItalic ? italicFont : normalFont; - - const wordWidth = font.widthOfTextAtSize(word, fontSize); - const spaceWidth = font.widthOfTextAtSize(' ', fontSize); - - // Verificar se precisamos adicionar espaço antes da palavra - let wordWithSpace = word; - let totalWidth = wordWidth; - - // Adicionar espaço se não for a primeira palavra da linha e não estivermos após uma quebra forçada - if (lines[currentLineIndex].chunks.length > 0) { - wordWithSpace = ' ' + word; - totalWidth = wordWidth + spaceWidth; - } - - // Verificar se a palavra cabe na linha atual - if (lines[currentLineIndex].width + totalWidth <= maxWidth) { - // Adicionar à linha atual - lines[currentLineIndex].chunks.push({ - text: wordWithSpace, - isItalic: chunk.isItalic, - isBold: chunk.isBold - }); - lines[currentLineIndex].width += totalWidth; - } else { - // Criar nova linha - lines.push({ - chunks: [{ text: word, isItalic: chunk.isItalic, isBold: chunk.isBold }], - width: wordWidth - }); - currentLineIndex++; - - // Verificar se temos espaço para mais uma linha - if (currentY - lineHeight < marginBottom) { - // Não há espaço suficiente para mais uma linha - return null; // Indica que precisa criar nova página - } - } - } - } - - // Desenhar as linhas com formatação - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - currentY -= lineHeight; - - // Verificar se temos espaço para esta linha - if (currentY < marginBottom) { - return null; // Indica que precisa criar nova página - } - - // Ajustar posição X baseada no alinhamento - let startX = x; - - if (align === 'center') { - startX = x + (maxWidth - line.width) / 2; - } else if (align === 'justify' && i < lines.length - 1 && line.chunks.length > 1) { - // Justificar texto (exceto última linha do parágrafo) - let wordCount = 0; - let textWidth = 0; - - // Contar palavras e determinar largura total do texto - for (const chunk of line.chunks) { - const text = chunk.text.trim(); - if (text) { - wordCount += text.split(/\s+/).length; - const font = chunk.isBold && chunk.isItalic ? boldItalicFont : - chunk.isBold ? boldFont : - chunk.isItalic ? italicFont : normalFont; - textWidth += font.widthOfTextAtSize(text, fontSize); - } - } - - // Calcular espaçamento extra entre palavras para justificar - const totalSpaces = wordCount - 1; - const spaceWidth = totalSpaces > 0 ? (maxWidth - textWidth) / totalSpaces : 0; - - // Desenhar cada chunk com espaçamento justificado - let runningX = x; - for (const chunk of line.chunks) { - const font = chunk.isBold && chunk.isItalic ? boldItalicFont : - chunk.isBold ? boldFont : - chunk.isItalic ? italicFont : normalFont; - - // Desenhar texto do chunk - page.drawText(chunk.text, { - x: runningX, - y: currentY, - size: fontSize, - font: font, - color: color - }); - - // Avançar posição X - runningX += font.widthOfTextAtSize(chunk.text, fontSize) + spaceWidth; - } - - // Pular para a próxima linha - continue; - } - - // Desenhar cada chunk - let runningX = startX; - for (const chunk of line.chunks) { - const font = chunk.isBold && chunk.isItalic ? boldItalicFont : - chunk.isBold ? boldFont : - chunk.isItalic ? italicFont : normalFont; - - page.drawText(chunk.text, { - x: runningX, - y: currentY, - size: fontSize, - font: font, - color: color - }); - - runningX += font.widthOfTextAtSize(chunk.text, fontSize); - } - } - - return currentY; // Retorna a posição Y final - }; - let timbradoPdf: PDFDocument | null = null; let timbradoPageCount = 0; let timbradoPages: any[] = []; @@ -652,6 +284,147 @@ export class PdfService { } } + // Melhorar a função que remove tags HTML + function stripUnsupportedTags(html: string): string { + // Primeiro, substituir entidades e tags comuns + const cleanHtml = html + // Preserva quebras de linha HTML como marcadores especiais + .replace(/
|<\/p>|<\/div>
/gi, '\n') + // Substitui parágrafos, divs e blockquote por marcadores de bloco + quebra de linha + .replace(/]*>|
]*>|]*>/gi, '') + .replace(/<\/p>|<\/div>|<\/blockquote>/gi, '\n\n') + // Marca formatação de texto + .replace(/|/gi, '§§BOLD_START§§') + .replace(/<\/b>|<\/strong>/gi, '§§BOLD_END§§') + .replace(/|/gi, '§§ITALIC_START§§') + .replace(/<\/i>|<\/em>/gi, '§§ITALIC_END§§') + // Armazena alinhamento e indentação + .replace(/style="[^"]*text-align\s*:\s*(center|right|justify)[^"]*"/gi, (_, align) => `§§ALIGN=${align}§§`) + .replace(/style="[^"]*text-indent\s*:\s*(\d+(?:pt|px|em)[^"]*)"/gi, (_, indent) => `§§INDENT=${indent}§§`) + // Remove todas as outras tags HTML + .replace(/<[^>]+>/g, '') + // Restaura formatação de texto + .replace(/§§BOLD_START§§/g, '') + .replace(/§§BOLD_END§§/g, '') + .replace(/§§ITALIC_START§§/g, '') + .replace(/§§ITALIC_END§§/g, ''); + + return cleanHtml + // Normaliza espaços extras e quebras de linha + .replace(/\n{3,}/g, '\n\n') + .replace(/[ \t]+/g, ' '); + } + + // Função para decodificar entidades HTML comuns + function decodeHtmlEntities(str: string): string { + if (!str) return ''; + const textarea = document.createElement('textarea'); + textarea.innerHTML = str; + let result = textarea.value; + + // Substitui manualmente as entidades mais comuns para funcionar em Node.js + return result + .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, 'ª'); + } + + // Corrigir manualmente a função decodeHtmlEntities para ambiente Node.js + function decodeHtmlEntities(str: string): string { + if (!str) return ''; + + return str + .replace(/\r\n/g, '\n') + // Acentos e caracteres especiais + .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 e caracteres especiais + .replace(/º/g, 'º') + .replace(/ª/g, 'ª') + .replace(/§/g, '§') + .replace(/°/g, '°') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>'); + } + + // Processe o texto com as novas funções melhoradas + const textoLimpo = stripUnsupportedTags( + this.processarTemplate(conteudo, escritorio) + ); + + const texto = decodeHtmlEntities(textoLimpo); + + console.log('Texto processado para renderização:', texto.substring(0, 100) + '...'); + + // Corrigir o processamento de parágrafos para lidar melhor com formatações + const paragrafos = texto.split(/\n{2,}/).map(p => { + let paragraph = p.trim(); + if (!paragraph) return ''; + + // Extrair informações de formatação + let align = 'left'; // padrão + const alignMatch = paragraph.match(/§§ALIGN=(center|right|justify)§§/); + if (alignMatch) { + align = alignMatch[1]; + paragraph = paragraph.replace(/§§ALIGN=(center|right|justify)§§/, ''); + } + + let indent = 0; + const indentMatch = paragraph.match(/§§INDENT=(\d+)(pt|px|em)§§/); + if (indentMatch) { + indent = parseInt(indentMatch[1]); + if (indentMatch[2] === 'em') indent *= 12; // Converter em para pontos aproximadamente + paragraph = paragraph.replace(/§§INDENT=\d+(pt|px|em)§§/, ''); + } + + return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph.trim()}`; + }).filter(Boolean); + let currentPar = 0; let pageIndex = 0; console.log(`Gerando PDF com ${paragrafos.length} parágrafos`);