diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index a90112f..f73d36c 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -348,6 +348,254 @@ export class PdfService { .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); } + /** + * Renderiza um parágrafo com formatação inline (negrito, itálico, etc) + * Retorna a nova posição Y após renderizar o parágrafo, ou null se precisar de nova página + */ + function renderInlineFormattedParagraph( + page: any, + text: string, + x: number, + y: number, + font: any, + fontBold: any, + fontItalic: any, + fontBoldItalic: any, + fontSize: number, + color: any, + align: 'left' | 'center' | 'justify' = 'left', + maxWidth: number, + forceNewPage: boolean = false, + marginTop: number, + marginBottom: number, + pageHeight: number + ): number | null { + if (forceNewPage || y < marginBottom + fontSize * 2) { + return null; // Indica necessidade de nova página + } + + // Dividir o texto em palavras preservando as tags + const segments: { text: string; isBold: boolean; isItalic: boolean }[] = []; + let currentSegment = { text: '', isBold: false, isItalic: false }; + + // Regex para encontrar tags , , , , , , , + const regex = /<\/?(?:b|strong|i|em)>/gi; + let lastIndex = 0; + let match; + + // Clone do texto para manipulação + let textToProcess = text; + + // Processar tags de indentação e alinhamento + let indentPt = 0; + let textAlign = align; + + const indentMatch = textToProcess.match(/^§§INDENT=([0-9.]+(?:pt|px|em))§§/); + if (indentMatch) { + const indentValue = indentMatch[1]; + indentPt = parseInt(indentValue); + if (isNaN(indentPt)) indentPt = 20; // Padrão: 20pt + textToProcess = textToProcess.substring(indentMatch[0].length); + x += indentPt; + maxWidth -= indentPt; + } + + const alignMatch = textToProcess.match(/^§§ALIGN=(center|justify|right)§§/); + if (alignMatch) { + textAlign = alignMatch[1] as 'center' | 'justify' | 'left'; + textToProcess = textToProcess.substring(alignMatch[0].length); + } + + while ((match = regex.exec(textToProcess)) !== null) { + // Adiciona texto entre as tags + if (match.index > lastIndex) { + currentSegment.text += textToProcess.substring(lastIndex, match.index); + } + + // Processa a tag + const tag = match[0].toLowerCase(); + if (tag === '' || tag === '') { + // Finaliza o segmento atual se não vazio + if (currentSegment.text) { + segments.push({ ...currentSegment }); + currentSegment.text = ''; + } + currentSegment.isBold = true; + } else if (tag === '' || tag === '') { + // Finaliza o segmento atual se não vazio + if (currentSegment.text) { + segments.push({ ...currentSegment }); + currentSegment.text = ''; + } + currentSegment.isBold = false; + } else if (tag === '' || tag === '') { + // Finaliza o segmento atual se não vazio + if (currentSegment.text) { + segments.push({ ...currentSegment }); + currentSegment.text = ''; + } + currentSegment.isItalic = true; + } else if (tag === '' || tag === '') { + // Finaliza o segmento atual se não vazio + if (currentSegment.text) { + segments.push({ ...currentSegment }); + currentSegment.text = ''; + } + currentSegment.isItalic = false; + } + + lastIndex = match.index + match[0].length; + } + + // Adiciona o texto restante + if (lastIndex < textToProcess.length) { + currentSegment.text += textToProcess.substring(lastIndex); + } + + // Adiciona o último segmento se não estiver vazio + if (currentSegment.text) { + segments.push(currentSegment); + } + + // Processa cada segmento e divide em linhas + const lines: { segments: { text: string; isBold: boolean; isItalic: boolean; width: number; font: any }[]; totalWidth: number }[] = []; + let currentLine: { segments: { text: string; isBold: boolean; isItalic: boolean; width: number; font: any }[]; totalWidth: number } = { + segments: [], + totalWidth: 0 + }; + + for (const segment of segments) { + const words = segment.text.split(/\s+/); + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + if (!word) continue; + + // Adiciona espaço antes se não for a primeira palavra da linha + const wordWithSpace = currentLine.segments.length > 0 ? ' ' + word : word; + // Seleciona a fonte correta com base na formatação + let segmentFont; + if (segment.isBold && segment.isItalic) { + segmentFont = fontBoldItalic; + } else if (segment.isBold) { + segmentFont = fontBold; + } else if (segment.isItalic) { + segmentFont = fontItalic; + } else { + segmentFont = font; + } + + const wordWidth = segmentFont.widthOfTextAtSize(wordWithSpace, fontSize); + + // Se a palavra cabe na linha atual + if (currentLine.totalWidth + wordWidth <= maxWidth) { + currentLine.segments.push({ + text: wordWithSpace, + isBold: segment.isBold, + isItalic: segment.isItalic, + width: wordWidth, + font: segmentFont + }); + currentLine.totalWidth += wordWidth; + } else { + // Se a linha atual não está vazia, finalize-a + if (currentLine.segments.length > 0) { + lines.push(currentLine); + currentLine = { segments: [], totalWidth: 0 }; + } + + // Se a palavra for maior que a largura máxima, divida-a + if (segmentFont.widthOfTextAtSize(word, fontSize) > maxWidth) { + // Implementação simplificada: apenas adiciona a palavra muito longa + // em uma nova linha, mesmo que ultrapasse a largura máxima + currentLine.segments.push({ + text: word, + isBold: segment.isBold, + isItalic: segment.isItalic, + width: segmentFont.widthOfTextAtSize(word, fontSize), + font: segmentFont + }); + currentLine.totalWidth = segmentFont.widthOfTextAtSize(word, fontSize); + lines.push(currentLine); + currentLine = { segments: [], totalWidth: 0 }; + } else { + // Caso contrário, adicione a palavra na nova linha + currentLine.segments.push({ + text: word, + isBold: segment.isBold, + isItalic: segment.isItalic, + width: segmentFont.widthOfTextAtSize(word, fontSize), + font: segmentFont + }); + currentLine.totalWidth = segmentFont.widthOfTextAtSize(word, fontSize); + } + } + } + } + + // Adiciona a última linha se não estiver vazia + if (currentLine.segments.length > 0) { + lines.push(currentLine); + } + + // Se não houver linhas após o processamento, retorne a posição Y atual + if (lines.length === 0) { + return y; + } + + // Verifica se todas as linhas cabem na página atual + const totalHeight = lines.length * (fontSize * 1.2); + if (y - totalHeight < marginBottom) { + return null; // Precisa de uma nova página + } + + // Renderiza as linhas + let currentY = y; + const lineHeight = fontSize * 1.2; + + for (const line of lines) { + let currentX = x; + + // Ajusta o X inicial com base no alinhamento + if (textAlign === 'center') { + currentX = x + (maxWidth - line.totalWidth) / 2; + } else if (textAlign === 'justify' && line !== lines[lines.length - 1] && line.segments.length > 1) { + // Para justificado, ajusta apenas linhas que não sejam a última e tenham mais de um segmento + const extraSpace = maxWidth - line.totalWidth; + const spaceBetweenSegments = extraSpace / (line.segments.length - 1); + + for (const segment of line.segments) { + const font = segment.font; + page.drawText(segment.text, { + x: currentX, + y: currentY, + size: fontSize, + font: font, + color: color + }); + currentX += segment.width + spaceBetweenSegments; + } + } else { + // Para alinhamento à esquerda ou a última linha de justificado + for (const segment of line.segments) { + const font = segment.font; + page.drawText(segment.text, { + x: currentX, + y: currentY, + size: fontSize, + font: font, + color: color + }); + currentX += segment.width; + } + } + + currentY -= lineHeight; + } + + return currentY; + } + const texto = decodeHtmlEntities( stripUnsupportedTags( this.processarTemplate(conteudo, escritorio)