From 76fa2c91ab4654891b16d9c539056b98b14a830f Mon Sep 17 00:00:00 2001 From: Rodrigo Martins Date: Mon, 21 Apr 2025 20:35:21 -0300 Subject: [PATCH] tenta fix pdfservice.js --- src/server/services/PdfService.ts | 185 ++++++++++++++++++------------ 1 file changed, 111 insertions(+), 74 deletions(-) diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 3970d1f..fbee94b 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -287,10 +287,10 @@ export class PdfService { if (!fontItalic) fontItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique); if (!fontBoldItalic) fontBoldItalic = fontBold; - // --- PARSER HTML TinyMCE SEM DEPENDÊNCIAS EXTERNAS --- - // Suporte básico a blocos, alinhamento, recuo, negrito, itálico, sublinhado, listas, títulos - function simpleHtmlToBlocks(html: string): any[] { - // Remove tags não suportadas e preserva apenas blocos essenciais + // --- PARSER HTML TinyMCE INLINE E BLOCOS (LEGADO MELHORADO) --- + // Suporte a blocos, alinhamento, recuo, negrito, itálico, sublinhado, listas, títulos, font-family, centralização, tags inline + function parseHtmlToBlocks(html: string): any[] { + // Normaliza blocos html = html.replace(/]*class=["'][^"']*MsoNormal[^"']*["'][^>]*>/gi, '

'); html = html.replace(/]*>/gi, '

'); html = html.replace(/<\/div>/gi, '

'); @@ -300,7 +300,6 @@ export class PdfService { html = html.replace(/<\/h[1-6]>/gi, m => m + '\n'); html = html.replace(/<\/ul>/gi, '\n'); html = html.replace(/<\/ol>/gi, '\n'); - // Divide por linhas const lines = html.split('\n'); const blocks: any[] = []; let inList = false, listType = ''; @@ -313,13 +312,17 @@ export class PdfService { if (line.match(/^<\/ul>/i) || line.match(/^<\/ol>/i)) { inList = false; listType = ''; continue; } if (line.match(/^]*>/i)) { let text = line.replace(/^]*>/i, '').replace(/<\/li>$/i, ''); - blocks.push({ text: (listType === 'ol' ? '1. ' : '• ') + text, tag: 'li', styles: {} }); + blocks.push({ inlines: parseInline(text), tag: 'li', styles: {} }); continue; } // Títulos - let headingMatch = line.match(/^]*>(.*?)<\/h\1>/i); + let headingMatch = line.match(/^]*)>(.*?)<\/h\1>/i); if (headingMatch) { - blocks.push({ text: headingMatch[2], tag: `h${headingMatch[1]}`, styles: { heading: `h${headingMatch[1]}` } }); + let styles: any = { heading: `h${headingMatch[1]}` }; + if (/text-align:\s*center/i.test(headingMatch[2])) styles.align = 'center'; + if (/text-align:\s*right/i.test(headingMatch[2])) styles.align = 'right'; + if (/font-family:\s*['"]?Fira Sans/i.test(headingMatch[2])) styles.fontFamily = 'Fira Sans'; + blocks.push({ inlines: parseInline(headingMatch[3]), tag: `h${headingMatch[1]}`, styles }); continue; } // Parágrafo @@ -330,29 +333,62 @@ export class PdfService { if (/text-align:\s*center/i.test(styleAttr)) styles.align = 'center'; if (/text-align:\s*right/i.test(styleAttr)) styles.align = 'right'; if (/text-indent:\s*2em/i.test(styleAttr)) styles.indent = true; - let text = pMatch[2]; - // Inline styles - if (/|/i.test(text)) styles.bold = true; - if (/|/i.test(text)) styles.italic = true; - if (//i.test(text)) styles.underline = true; - text = text.replace(/<\/?(b|strong|i|em|u)>/gi, ''); - blocks.push({ text, tag: 'p', styles }); + if (/font-family:\s*['"]?Fira Sans/i.test(styleAttr)) styles.fontFamily = 'Fira Sans'; + blocks.push({ inlines: parseInline(pMatch[2]), tag: 'p', styles }); continue; } // Centralizado let centerMatch = line.match(/^]*>(.*?)<\/center>$/i); if (centerMatch) { - blocks.push({ text: centerMatch[1], tag: 'p', styles: { align: 'center' } }); + blocks.push({ inlines: parseInline(centerMatch[1]), tag: 'p', styles: { align: 'center' } }); continue; } // Texto puro - blocks.push({ text: line.replace(/<\/?[^>]+>/g, ''), tag: 'span', styles: {} }); + blocks.push({ inlines: parseInline(line.replace(/<\/?[^>]+>/g, '')), tag: 'span', styles: {} }); } return blocks; } + // Parser inline para negrito, itálico, sublinhado, font-family, combinações + function parseInline(html: string): any[] { + const result: any[] = []; + let regex = /(<(b|strong|i|em|u|span)[^>]*>|<\/(b|strong|i|em|u|span)>)/gi; + let stack: any[] = []; + let lastIndex = 0; + let match; + while ((match = regex.exec(html)) !== null) { + if (match.index > lastIndex) { + result.push({ + text: html.substring(lastIndex, match.index), + ...stack.reduce((acc, s) => ({ ...acc, ...s }), {}) + }); + } + if (match[0][1] === '/') { + stack.pop(); + } else { + let style: any = {}; + if (/^<(b|strong)/i.test(match[0])) style.bold = true; + if (/^<(i|em)/i.test(match[0])) style.italic = true; + if (/^ ({ ...acc, ...s }), {}) + }); + } + return result.filter(i => i.text && i.text.replace(/\s+/g, '').length > 0); + } + // Gera os blocos do HTML do TinyMCE - const blocks = simpleHtmlToBlocks(conteudo); + const blocks = parseHtmlToBlocks(conteudo); // Renderização dos blocos no PDF let pageIndex = 0; @@ -373,87 +409,88 @@ export class PdfService { y -= LINE_HEIGHT * 0.5; continue; } - let usedFont = font; - if (block.styles.bold && block.styles.italic && fontBoldItalic) usedFont = fontBoldItalic; - else if (block.styles.bold && fontBold) usedFont = fontBold; - else if (block.styles.italic && fontItalic) usedFont = fontItalic; - let blockFontSize = FONT_SIZE; if (block.styles.heading === 'h1') blockFontSize = 22; if (block.styles.heading === 'h2') blockFontSize = 18; if (block.styles.heading === 'h3') blockFontSize = 16; - // Remove quebras de linha do texto antes de medir e desenhar - const cleanText = (block.text || '').replace(/\r?\n/g, ' '); + let x = margens.left + (block.styles.indent ? FONT_SIZE * 2 : 0); + let line = ''; + let lineWidth = 0; + for (const inline of block.inlines) { + let usedFont = font; + if (inline.bold && inline.italic && fontBoldItalic) usedFont = fontBoldItalic; + else if (inline.bold && fontBold) usedFont = fontBold; + else if (inline.italic && fontItalic) usedFont = fontItalic; + if (inline.fontFamily && inline.fontFamily.toLowerCase().includes('fira')) usedFont = font; - // Quebra em múltiplas linhas se necessário - const lines = (() => { - const words = cleanText.split(' '); - const result: string[] = []; - let currentLine = ''; - for (const word of words) { - const testLine = currentLine ? currentLine + ' ' + word : word; - const width = usedFont.widthOfTextAtSize(testLine, blockFontSize); - if (width > MAX_WIDTH && currentLine) { - result.push(currentLine); - currentLine = word; + const words = (inline.text || '').split(' '); + for (let i = 0; i < words.length; i++) { + let word = words[i]; + let testLine = line ? line + ' ' + word : word; + let testWidth = usedFont.widthOfTextAtSize(testLine, blockFontSize); + if (testWidth > MAX_WIDTH && line) { + let drawX = x; + if (block.styles.align === 'center' || block.styles.heading === 'h1') { + drawX = margens.left + ((MAX_WIDTH - lineWidth) / 2); + } + if (block.styles.align === 'right') { + drawX = margens.left + (MAX_WIDTH - lineWidth); + } + page.drawText(line, { + x: drawX, + y, + size: blockFontSize, + font: usedFont, + color: rgb(0, 0, 0) + }); + if (inline.underline) { + page.drawLine({ + start: { x: drawX, y: y - 2 }, + end: { x: drawX + lineWidth, y: y - 2 }, + thickness: 0.7, + color: rgb(0, 0, 0) + }); + } + y -= blockFontSize * 1.5; + line = word; + lineWidth = usedFont.widthOfTextAtSize(word, blockFontSize); } else { - currentLine = testLine; + if (line) { + line += ' ' + word; + lineWidth = usedFont.widthOfTextAtSize(line, blockFontSize); + } else { + line = word; + lineWidth = usedFont.widthOfTextAtSize(line, blockFontSize); + } } } - if (currentLine) result.push(currentLine); - return result; - })(); - - for (const line of lines) { - let x = margens.left + (block.styles.indent ? FONT_SIZE * 2 : 0); - let width = usedFont.widthOfTextAtSize(line, blockFontSize); - - // Centralização + } + if (line) { + let drawX = x; if (block.styles.align === 'center' || block.styles.heading === 'h1') { - x = margens.left + ((MAX_WIDTH - width) / 2); + drawX = margens.left + ((MAX_WIDTH - lineWidth) / 2); } if (block.styles.align === 'right') { - x = margens.left + (MAX_WIDTH - width); + drawX = margens.left + (MAX_WIDTH - lineWidth); } - - // Quebra de página se necessário - if (y < margens.bottom + blockFontSize * 1.2) { - pageIndex++; - page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]); - if (timbradoPages && timbradoPages.length > 0) { - const embedded = timbradoPages[pageIndex % timbradoPages.length]; - page.drawPage(embedded, { - x: 0, - y: 0, - width: PAGE_WIDTH, - height: PAGE_HEIGHT - }); - } - y = PAGE_HEIGHT - margens.top; - } - page.drawText(line, { - x, + x: drawX, y, size: blockFontSize, - font: usedFont, + font: font, color: rgb(0, 0, 0) }); - - // Sublinhado - if (block.styles.underline) { + if (block.inlines.some(i => i.underline)) { page.drawLine({ - start: { x, y: y - 2 }, - end: { x: x + width, y: y - 2 }, + start: { x: drawX, y: y - 2 }, + end: { x: drawX + lineWidth, y: y - 2 }, thickness: 0.7, color: rgb(0, 0, 0) }); } - y -= blockFontSize * 1.5; } - // Espaço extra entre parágrafos if (block.tag === 'p' || block.styles.heading) { y -= blockFontSize * 0.3; }