diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 25a2694..12427e7 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -329,38 +329,56 @@ export class PdfService { .replace(//g, ''); } - // Função para renderizar texto com formatação inline (negrito, itálico, ambos) e controle de quebra de linha - function renderInlineFormattedParagraph(page, text: string, x: number, y: number, font, fontBold, fontItalic, fontBoldItalic, fontSize: number, color, align: string, maxWidth: number) { - // Divide o texto em tokens de formatação (, , etc) - let regex = /(<\/?(b|strong|i|em)>)/gi; - let tokens: { text: string, bold: boolean, italic: boolean }[] = []; + // 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 + ) { + // Divide o texto em tokens de formatação (, , , etc) + 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 }; + 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 + 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; } if (lastIndex < text.length) { tokens.push({ text: text.substring(lastIndex), bold: currentStyle.bold, - italic: currentStyle.italic + italic: currentStyle.italic, + underline: currentStyle.underline }); } // Junta todos os tokens em uma lista de palavras com formatação - let words: { text: string, font: any }[] = []; + let words: { text: string, font: any, underline: boolean }[] = []; for (const token of tokens) { let usedFont = font; if (token.bold && token.italic) usedFont = fontBoldItalic; @@ -370,19 +388,24 @@ export class PdfService { const split = token.text.split(/(\s+)/); for (const part of split) { if (part.length > 0) { - words.push({ text: part, font: usedFont }); + words.push({ text: part, font: usedFont, underline: token.underline }); } } } // Monta linhas respeitando largura máxima - let lines: { text: string, tokens: { text: string, font: any }[] }[] = []; + let lines: { text: string, tokens: { text: string, font: any, underline: boolean }[] }[] = []; let currentLine = ''; - let currentTokens: { text: string, font: any }[] = []; + let currentTokens: { text: string, font: any, underline: boolean }[] = []; let currentWidth = 0; - for (const word of words) { + for (const [i, word] of words.entries()) { + // Aplica recuo de parágrafo na primeira linha + let recuo = 0; + if (indent && lines.length === 0 && currentLine === '') { + recuo = fontSize * 2; + } const wordWidth = word.font.widthOfTextAtSize(word.text, fontSize); - if (currentWidth + wordWidth > maxWidth && currentLine.trim() !== '') { + if (currentWidth + wordWidth + recuo > maxWidth && currentLine.trim() !== '') { lines.push({ text: currentLine, tokens: currentTokens }); currentLine = word.text; currentTokens = [word]; @@ -403,6 +426,13 @@ export class PdfService { let drawX = x; let totalWidth = line.tokens.reduce((acc, t) => acc + t.font.widthOfTextAtSize(t.text, fontSize), 0); + // Aplica recuo de parágrafo na primeira linha + let recuo = 0; + if (indent && i === 0) { + recuo = fontSize * 2; + drawX += recuo; + } + // Centralizado if (align === 'center') { drawX = x + (maxWidth - totalWidth) / 2; @@ -420,13 +450,23 @@ export class PdfService { font: t.font, color }); + // Sublinhado + 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 + }); + } if (/^\s+$/.test(t.text)) { drawX += t.font.widthOfTextAtSize(t.text, fontSize) + extraSpace; } else { drawX += t.font.widthOfTextAtSize(t.text, fontSize); } } - y -= fontSize * 1.25; + y -= fontSize * 1.5; continue; } @@ -439,9 +479,19 @@ export class PdfService { font: t.font, color }); + // Sublinhado + 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.25; + y -= fontSize * 1.5; } return y; } @@ -512,12 +562,8 @@ export class PdfService { // 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) e controle de linhas + // Renderiza texto com formatação inline (negrito, itálico, sublinhado, ambos) e controle de linhas let yAfter = renderInlineFormattedParagraph( page, text, @@ -530,12 +576,13 @@ export class PdfService { FONT_SIZE, rgb(0, 0, 0), align, - parWidth + parWidth, + indent ); y = yAfter - FONT_SIZE * 0.5; currentPar++; - if (y < margens.bottom + FONT_SIZE * 1.25) break; + if (y < margens.bottom + FONT_SIZE * 1.5) break; } pageIndex++; }