diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 6f523e3..8fd63a2 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -348,6 +348,193 @@ export class PdfService { .replace(/>/g, '>'); } + /** + * Renderiza um parágrafo com formatação inline (negrito, itálico) + * @returns A posição Y após renderizar ou null se não coube na página + */ + function renderInlineFormattedParagraph( + page: any, + text: string, + x: number, + startY: number, + normalFont: any, + boldFont: any, + italicFont: any, + boldItalicFont: any, + fontSize: number, + color: any, + align: 'left' | 'center' | 'right' | 'justify', + maxWidth: number, + debug: boolean = false, + marginTop: number, + marginBottom: number, + pageHeight: number + ): number | null { + const lineHeight = fontSize * 1.5; + let y = startY; + let currentX = x; + let words: Array<{text: string, font: any, width: number}> = []; + let currentLine: Array<{text: string, font: any, width: number}> = []; + let currentLineWidth = 0; + let textParts = []; + + // Detectar formatação inline + let remainingText = text; + let currentFont = normalFont; + let boldActive = false; + let italicActive = false; + + // Processa texto com formatação + while (remainingText.length > 0) { + if (remainingText.startsWith('')) { + // Começa negrito + boldActive = true; + remainingText = remainingText.substring(3); + currentFont = italicActive ? boldItalicFont : boldFont; + } else if (remainingText.startsWith('')) { + // Termina negrito + boldActive = false; + remainingText = remainingText.substring(4); + currentFont = italicActive ? italicFont : normalFont; + } else if (remainingText.startsWith('')) { + // Começa itálico + italicActive = true; + remainingText = remainingText.substring(3); + currentFont = boldActive ? boldItalicFont : italicFont; + } else if (remainingText.startsWith('')) { + // Termina itálico + italicActive = false; + remainingText = remainingText.substring(4); + currentFont = boldActive ? boldFont : normalFont; + } else { + // Texto normal - procura pela próxima tag de formatação + let nextTag = remainingText.search(/<\/?[bi]>/); + if (nextTag === -1) { + // Não há mais tags, adiciona todo o texto restante + textParts.push({ + text: remainingText, + font: currentFont + }); + remainingText = ''; + } else { + // Adiciona texto até a próxima tag + textParts.push({ + text: remainingText.substring(0, nextTag), + font: currentFont + }); + remainingText = remainingText.substring(nextTag); + } + } + } + + // Processa partes de texto em palavras para quebra de linha + for (const part of textParts) { + const partWords = part.text.split(/\s+/); + + for (let i = 0; i < partWords.length; i++) { + if (partWords[i].length === 0) continue; + + const wordText = i < partWords.length - 1 ? partWords[i] + ' ' : partWords[i]; + const wordWidth = part.font.widthOfTextAtSize(wordText, fontSize); + + words.push({ + text: wordText, + font: part.font, + width: wordWidth + }); + } + } + + // Processa palavras em linhas + for (let i = 0; i < words.length; i++) { + const word = words[i]; + + if (currentLineWidth + word.width <= maxWidth) { + // A palavra cabe na linha atual + currentLine.push(word); + currentLineWidth += word.width; + } else { + // A palavra não cabe, desenha a linha atual e começa nova linha + // Primeiro verifica se há espaço vertical suficiente + if (y - lineHeight < marginBottom) { + return null; // Não há espaço suficiente na página + } + + // Desenha a linha com o alinhamento apropriado + y = drawFormattedLine(page, currentLine, x, y, maxWidth, align, fontSize, color); + + // Começa uma nova linha com a palavra atual + currentLine = [word]; + currentLineWidth = word.width; + } + } + + // Desenha a última linha se houver palavras restantes + if (currentLine.length > 0) { + if (y - lineHeight < marginBottom) { + return null; // Não há espaço suficiente na página + } + + y = drawFormattedLine(page, currentLine, x, y, maxWidth, align, fontSize, color); + } + + return y; + } + + /** + * Desenha uma linha formatada com alinhamento específico + */ + function drawFormattedLine( + page: any, + line: Array<{text: string, font: any, width: number}>, + x: number, + y: number, + maxWidth: number, + align: 'left' | 'center' | 'right' | 'justify', + fontSize: number, + color: any + ): number { + const lineHeight = fontSize * 1.5; + let currentX = x; + let totalWidth = line.reduce((sum, word) => sum + word.width, 0); + + // Calcula o espaço adicional entre palavras para justificação + let extraSpacing = 0; + if (align === 'justify' && line.length > 1 && totalWidth < maxWidth * 0.8) { + extraSpacing = (maxWidth - totalWidth) / (line.length - 1); + } + + // Ajusta a posição x inicial baseada no alinhamento + if (align === 'center') { + currentX = x + (maxWidth - totalWidth) / 2; + } else if (align === 'right') { + currentX = x + maxWidth - totalWidth; + } + + // Desenha cada palavra + for (let i = 0; i < line.length; i++) { + const word = line[i]; + page.drawText(word.text, { + x: currentX, + y: y, + font: word.font, + size: fontSize, + color: color + }); + + // Avança X para a próxima palavra + currentX += word.width; + + // Adiciona espaço extra para justificação, exceto na última palavra + if (align === 'justify' && i < line.length - 1) { + currentX += extraSpacing; + } + } + + // Retorna a nova posição Y após desenhar a linha + return y - lineHeight; + } + // Processe o texto com as novas funções melhoradas const textoLimpo = stripUnsupportedTags( this.processarTemplate(conteudo, escritorio)