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;
}