diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index d202f5c..0ae8afa 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -355,6 +355,259 @@ export class PdfService { .replace(/>/g, '>'); } + /** + * Renderiza um parágrafo com formatação inline (negrito, itálico) + * @param page - Página do PDF + * @param text - Texto a ser renderizado + * @param x - Posição X inicial + * @param y - Posição Y inicial + * @param regularFont - Fonte normal + * @param boldFont - Fonte negrito + * @param italicFont - Fonte itálica + * @param boldItalicFont - Fonte negrito+itálica + * @param fontSize - Tamanho da fonte + * @param color - Cor do texto + * @param align - Alinhamento do texto + * @param maxWidth - Largura máxima + * @param debug - Modo debug + * @param marginTop - Margem superior + * @param marginBottom - Margem inferior + * @param pageHeight - Altura da página + * @returns Nova posição Y ou null se não couber na página + */ + function renderInlineFormattedParagraph( + page: any, + text: string, + x: number, + y: number, + regularFont: any, + boldFont: any, + italicFont: any, + boldItalicFont: any, + fontSize: number, + color: any, + align: 'left' | 'center' | 'right' | 'justify' = 'left', + maxWidth: number, + debug: boolean = false, + marginTop: number, + marginBottom: number, + pageHeight: number + ): number | null { + if (y < marginBottom || y > pageHeight - marginTop) { + return null; // Fora dos limites da página + } + + // Considera a linha inteira + const lineHeight = fontSize * 1.2; + + // Separa o texto em segmentos de formatação + const segments: { text: string; isBold: boolean; isItalic: boolean }[] = []; + let remaining = text; + + // Padrões para encontrar tags de formatação inline + const boldStartPattern = //i; + const boldEndPattern = /<\/b>/i; + const italicStartPattern = //i; + const italicEndPattern = /<\/i>/i; + + let boldActive = false; + let italicActive = false; + + // Extrai segmentos formatados do texto + while (remaining.length > 0) { + const boldStartMatch = remaining.search(boldStartPattern); + const boldEndMatch = remaining.search(boldEndPattern); + const italicStartMatch = remaining.search(italicStartPattern); + const italicEndMatch = remaining.search(italicEndPattern); + + // Encontra a próxima tag + let nextTagPos = -1; + let nextTagType = ''; + + if (boldStartMatch !== -1 && (nextTagPos === -1 || boldStartMatch < nextTagPos)) { + nextTagPos = boldStartMatch; + nextTagType = 'boldStart'; + } + + if (boldEndMatch !== -1 && (nextTagPos === -1 || boldEndMatch < nextTagPos)) { + nextTagPos = boldEndMatch; + nextTagType = 'boldEnd'; + } + + if (italicStartMatch !== -1 && (nextTagPos === -1 || italicStartMatch < nextTagPos)) { + nextTagPos = italicStartMatch; + nextTagType = 'italicStart'; + } + + if (italicEndMatch !== -1 && (nextTagPos === -1 || italicEndMatch < nextTagPos)) { + nextTagPos = italicEndMatch; + nextTagType = 'italicEnd'; + } + + if (nextTagPos === -1) { + // Não há mais tags, adicione o texto restante como um único segmento + segments.push({ + text: remaining, + isBold: boldActive, + isItalic: italicActive, + }); + break; + } else { + // Adiciona texto antes da tag + if (nextTagPos > 0) { + segments.push({ + text: remaining.substring(0, nextTagPos), + isBold: boldActive, + isItalic: italicActive, + }); + } + + // Atualiza o estado de formatação com base na tag + if (nextTagType === 'boldStart') { + boldActive = true; + remaining = remaining.substring(nextTagPos + 3); // tem 3 caracteres + } else if (nextTagType === 'boldEnd') { + boldActive = false; + remaining = remaining.substring(nextTagPos + 4); // tem 4 caracteres + } else if (nextTagType === 'italicStart') { + italicActive = true; + remaining = remaining.substring(nextTagPos + 3); // tem 3 caracteres + } else if (nextTagType === 'italicEnd') { + italicActive = false; + remaining = remaining.substring(nextTagPos + 4); // tem 4 caracteres + } + } + } + + // Função para quebrar um texto em palavras + const getWords = (text: string): string[] => { + return text.split(/(\s+)/).filter(word => word.length > 0); + }; + + // Calcular largura de texto + const getTextWidth = (text: string, font: any): number => { + return font.widthOfTextAtSize(text, fontSize); + }; + + // Organiza palavras em linhas, respeitando a formatação + const lines: { + segments: { text: string; isBold: boolean; isItalic: boolean }[]; + width: number; + }[] = []; + + let currentLine: { + segments: { text: string; isBold: boolean; isItalic: boolean }[]; + width: number; + } = { segments: [], width: 0 }; + + for (const segment of segments) { + const words = getWords(segment.text); + + for (const word of words) { + // Para espaços, apenas adicione-os diretamente + if (/^\s+$/.test(word)) { + if (currentLine.segments.length > 0) { + const lastSegment = currentLine.segments[currentLine.segments.length - 1]; + + // Apenas adicione espaço se não for o primeiro segmento da linha + lastSegment.text += word; + const font = getFont(lastSegment.isBold, lastSegment.isItalic); + currentLine.width += getTextWidth(word, font); + } + continue; + } + + const font = getFont(segment.isBold, segment.isItalic); + const wordWidth = getTextWidth(word, font); + + // Verifica se a palavra cabe na linha atual + if (currentLine.segments.length === 0 || currentLine.width + wordWidth <= maxWidth) { + // Tenta adicionar à última parte do mesmo tipo de formatação + if ( + currentLine.segments.length > 0 && + currentLine.segments[currentLine.segments.length - 1].isBold === segment.isBold && + currentLine.segments[currentLine.segments.length - 1].isItalic === segment.isItalic + ) { + currentLine.segments[currentLine.segments.length - 1].text += word; + } else { + // Adiciona novo segmento + currentLine.segments.push({ + text: word, + isBold: segment.isBold, + isItalic: segment.isItalic, + }); + } + + currentLine.width += wordWidth; + } else { + // A palavra não cabe, iniciar nova linha + lines.push(currentLine); + currentLine = { + segments: [ + { + text: word, + isBold: segment.isBold, + isItalic: segment.isItalic, + }, + ], + width: wordWidth, + }; + } + } + } + + // Adiciona a última linha se não estiver vazia + if (currentLine.segments.length > 0) { + lines.push(currentLine); + } + + // Função auxiliar para obter a fonte correta + function getFont(isBold: boolean, isItalic: boolean): any { + if (isBold && isItalic) return boldItalicFont; + if (isBold) return boldFont; + if (isItalic) return italicFont; + return regularFont; + } + + // Renderiza as linhas + let currentY = y; + + for (const line of lines) { + // Se não couber mais na página, retorna null + if (currentY - lineHeight < marginBottom) { + return null; + } + + let lineX = x; + + // Ajusta a posição X com base no alinhamento + if (align === 'center') { + lineX = x + (maxWidth - line.width) / 2; + } else if (align === 'right') { + lineX = x + maxWidth - line.width; + } + + // Renderiza cada segmento da linha + for (const segment of line.segments) { + const font = getFont(segment.isBold, segment.isItalic); + page.drawText(segment.text, { + x: lineX, + y: currentY, + font, + size: fontSize, + color, + }); + + lineX += getTextWidth(segment.text, font); + } + + // Move para a próxima linha + currentY -= lineHeight; + } + + return currentY; + } + // Processe o texto com as novas funções melhoradas const textoLimpo = stripUnsupportedTags( this.processarTemplate(conteudo, escritorio)