diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 8b68c33..006a39f 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -247,7 +247,13 @@ export class PdfService { const LINE_HEIGHT = FONT_SIZE * 1.5; const MAX_WIDTH = PAGE_WIDTH - margens.left - margens.right; - // Função para decodificar entidades HTML comuns (acentuação e outros) + // Fonte padrão do pdf-lib: TimesRoman, TimesBold, TimesItalic + const font = await pdfDoc.embedFont('Times-Roman'); + const fontBold = await pdfDoc.embedFont('Times-Bold'); + const fontItalic = await pdfDoc.embedFont('Times-Italic'); + const fontBoldItalic = fontBold; + + // Função para decodificar entidades HTML comuns (acentuação, símbolos, º, etc) function decodeHtmlEntities(str: string): string { if (!str) return ''; return str @@ -272,15 +278,34 @@ export class PdfService { .replace(/Ç/g, 'Ç').replace(/ç/g, 'ç') .replace(/Ñ/g, 'Ñ').replace(/ñ/g, 'ñ') .replace(/Ü/g, 'Ü').replace(/ü/g, 'ü') - // Outras entidades comuns + // 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, '>') - .replace(/–/gi, '–') - .replace(/—/gi, '—') + // Numéricos .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); } @@ -301,6 +326,107 @@ export class PdfService { .replace(//g, ''); } + // Função para renderizar texto com formatação inline (negrito, itálico, ambos) + function renderInlineFormattedText(page, text: string, x: number, y: number, font, fontBold, fontItalic, fontBoldItalic, fontSize: number, color, align: string, maxWidth: number) { + // Divide por tags , , , e combinações + // Exemplo: "Texto itálico negrito-itálico normal" + let cursor = x; + let tokens: { text: string, bold: boolean, italic: boolean }[] = []; + let regex = /(<\/?(b|strong|i|em)>)/gi; + let stack: { bold: boolean, italic: boolean }[] = []; + let lastIndex = 0; + let match; + let currentStyle = { bold: false, italic: 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 + }); + } + 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; + lastIndex = regex.lastIndex; + } + if (lastIndex < text.length) { + tokens.push({ + text: text.substring(lastIndex), + bold: currentStyle.bold, + italic: currentStyle.italic + }); + } + // Renderiza cada token com a fonte correta + let line = ''; + let lineTokens: { text: string, font: any }[] = []; + 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; + // Quebra em palavras para respeitar largura máxima + const words = token.text.split(' '); + for (let i = 0; i < words.length; i++) { + let word = words[i]; + let testLine = line ? line + ' ' + word : word; + let testWidth = 0; + let tempLineTokens = [...lineTokens, { text: (line ? ' ' : '') + word, font: usedFont }]; + for (const t of tempLineTokens) { + testWidth += t.font.widthOfTextAtSize(t.text, fontSize); + } + if (testWidth > maxWidth && line) { + // Renderiza a linha atual + let drawX = x; + if (align === 'center') { + drawX = x + (maxWidth - testWidth + usedFont.widthOfTextAtSize(word, fontSize)) / 2; + } + for (const t of lineTokens) { + page.drawText(t.text, { + x: drawX, + y, + size: fontSize, + font: t.font, + color + }); + drawX += t.font.widthOfTextAtSize(t.text, fontSize); + } + y -= fontSize * 1.25; + line = word; + lineTokens = [{ text: word, font: usedFont }]; + } else { + if (line) { + line += ' ' + word; + lineTokens.push({ text: ' ' + word, font: usedFont }); + } else { + line = word; + lineTokens.push({ text: word, font: usedFont }); + } + } + } + } + // Renderiza última linha + if (line) { + let drawX = x; + let totalWidth = lineTokens.reduce((acc, t) => acc + t.font.widthOfTextAtSize(t.text, fontSize), 0); + if (align === 'center') { + drawX = x + (maxWidth - totalWidth) / 2; + } + for (const t of lineTokens) { + page.drawText(t.text, { + x: drawX, + y, + size: fontSize, + font: t.font, + color + }); + drawX += t.font.widthOfTextAtSize(t.text, fontSize); + } + } + return y; + } + // Processar variáveis do template e decodificar entidades HTML // Mantém tags , , , , ,
,

, , const texto = decodeHtmlEntities( @@ -340,53 +466,6 @@ export class PdfService { } } - // Fonte padrão do pdf-lib: TimesRoman, TimesBold, TimesItalic - // Usa apenas as fontes padrão do pdf-lib, sem try/catch, e SEM VARIÁVEL UNDEFINED - const font = await pdfDoc.embedFont('Times-Roman'); - const fontBold = await pdfDoc.embedFont('Times-Bold'); - const fontItalic = await pdfDoc.embedFont('Times-Italic'); - const fontBoldItalic = fontBold; - - // Função para quebrar texto em linhas respeitando largura máxima e manter quebras de linha simples - function wrapTextStyled(text: string, font: any, fontBold: any, fontItalic: any, fontBoldItalic: any, fontSize: number, maxWidth: number, indent: boolean, align: string, bold: boolean, italic: boolean): { text: string, font: any, indent: boolean, align: string, bold: boolean, italic: boolean }[] { - // Mantém quebras de linha simples para espaçamento - const rawLines = text.split('\n'); - const lines: { text: string, font: any, indent: boolean, align: string, bold: boolean, italic: boolean }[] = []; - for (const rawLine of rawLines) { - let line = rawLine; - // Detecta negrito e itálico inline - let isBold = bold, isItalic = italic; - if (/^|^/.test(line)) { isBold = true; line = line.replace(/^|^/, ''); } - if (/^|^/.test(line)) { isItalic = true; line = line.replace(/^|^/, ''); } - if (/<\/b>$|<\/strong>$/.test(line)) { isBold = true; line = line.replace(/<\/b>$|<\/strong>$/, ''); } - if (/<\/i>$|<\/em>$/.test(line)) { isItalic = true; line = line.replace(/<\/i>$|<\/em>$/, ''); } - line = line.replace(/<\/?(b|strong|i|em)>/gi, ''); - - // Seleciona fonte correta - let usedFont = font; - if (isBold && isItalic && fontBoldItalic) usedFont = fontBoldItalic; - else if (isBold && fontBold) usedFont = fontBold; - else if (isItalic && fontItalic) usedFont = fontItalic; - - // Quebra em palavras para justificar - const words = line.split(' '); - let currentLine = ''; - for (let i = 0; i < words.length; i++) { - const word = words[i]; - const testLine = currentLine ? currentLine + ' ' + word : word; - const width = usedFont.widthOfTextAtSize(testLine, fontSize); - if (width > maxWidth && currentLine) { - lines.push({ text: currentLine, font: usedFont, indent, align, bold: isBold, italic: isItalic }); - currentLine = word; - } else { - currentLine = testLine; - } - } - if (currentLine) lines.push({ text: currentLine, font: usedFont, indent, align, bold: isBold, italic: isItalic }); - } - return lines; - } - // Quebrar o texto em páginas let currentPar = 0; let currentLine = 0; @@ -404,78 +483,42 @@ export class PdfService { }); } let y = PAGE_HEIGHT - margens.top; - let firstLine = true; while (currentPar < paragrafos.length) { - // Detecta recuo e alinhamento por marcação let indent = paragrafos[currentPar].startsWith('§§INDENT§§'); let center = paragrafos[currentPar].startsWith('§§CENTER§§'); let align = center ? 'center' : 'justify'; let text = paragrafos[currentPar].replace(/^§§INDENT§§/, '').replace(/^§§CENTER§§/, '').trim(); - // Detecta negrito/itálico no parágrafo inteiro - let bold = /^|^/.test(text); - let italic = /^|^/.test(text); + // Aplica decode de entidades HTML e símbolos + text = decodeHtmlEntities(text); - const styledLines = wrapTextStyled(text, font, fontBold, fontItalic, fontBoldItalic, FONT_SIZE, MAX_WIDTH - (indent ? FONT_SIZE * 2 : 0), indent, align, bold, italic); - let lineIdx = 0; - if (currentLine > 0) lineIdx = currentLine; - for (; lineIdx < styledLines.length; lineIdx++) { - if (y < margens.bottom + LINE_HEIGHT) { - currentLine = lineIdx; - break; - } - let { text, font: usedFont, indent, align, bold, italic } = styledLines[lineIdx]; - let x = margens.left + (indent ? FONT_SIZE * 2 : 0); + // Renderiza o parágrafo inteiro com recuo (não só a primeira linha) + let x = margens.left; + let parWidth = MAX_WIDTH; + if (indent) { + x += FONT_SIZE * 2; + parWidth -= FONT_SIZE * 2; + } + + // Renderiza texto com formatação inline (negrito, itálico, ambos) + y = renderInlineFormattedText( + page, + text, + x, + y, + font, + fontBold, + fontItalic, + fontBoldItalic, + FONT_SIZE, + rgb(0, 0, 0), + align, + parWidth + ); + y -= FONT_SIZE * 0.5; - // Centralizado - if (align === 'center') { - const textWidth = usedFont.widthOfTextAtSize(text, FONT_SIZE); - x = margens.left + ((MAX_WIDTH - (indent ? FONT_SIZE * 2 : 0) - textWidth) / 2) + (indent ? FONT_SIZE * 2 : 0); - page.drawText(text, { - x, - y, - size: FONT_SIZE, - font: usedFont, - color: rgb(0, 0, 0) - }); - } - // Justificado (exceto última linha do parágrafo e centralizado) - else if (text.trim().length > 0 && lineIdx < styledLines.length - 1 && align === 'justify') { - const words = text.split(' '); - if (words.length > 1) { - const textWidth = usedFont.widthOfTextAtSize(text, FONT_SIZE); - const spaceWidth = usedFont.widthOfTextAtSize(' ', FONT_SIZE); - const totalSpaces = words.length - 1; - const extraSpace = (MAX_WIDTH - (indent ? FONT_SIZE * 2 : 0) - textWidth) / totalSpaces; - let cursor = x; - for (let w = 0; w < words.length; w++) { - page.drawText(words[w], { - x: cursor, - y, - size: FONT_SIZE, - font: usedFont - }); - if (w < words.length - 1) { - cursor += usedFont.widthOfTextAtSize(words[w], FONT_SIZE) + spaceWidth + extraSpace; - } - } - } else { - page.drawText(text, { x, y, size: FONT_SIZE, font: usedFont }); - } - } else { - page.drawText(text, { x, y, size: FONT_SIZE, font: usedFont }); - } - y -= FONT_SIZE * 1.25; // espaçamento entre linhas 1,25 - currentLine = 0; - firstLine = false; - } - if (lineIdx >= styledLines.length) { - y -= FONT_SIZE * 0.5; - } - if (lineIdx < styledLines.length) { - break; - } currentPar++; + if (y < margens.bottom + FONT_SIZE * 1.25) break; } pageIndex++; }