diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index cff6ab9..11ab877 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -378,6 +378,253 @@ export class PdfService { return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph}`; }).filter(Boolean); + /** + * Renderiza um parágrafo com formatação inline (negrito, itálico, sublinhado) + */ + const renderInlineFormattedParagraph = ( + page: any, + text: string, + x: number, + y: number, + normalFont: any, + boldFont: any, + italicFont: any, + boldItalicFont: any, + fontSize: number, + color: any, + align: 'left' | 'center' | 'justify' = 'left', + maxWidth: number = MAX_WIDTH, + isContinuation: boolean = false, + marginTop: number = margens.top, + marginBottom: number = margens.bottom, + pageHeight: number = PAGE_HEIGHT + ): number | null => { + // Se não houver espaço suficiente na página atual para pelo menos uma linha de texto + if (y < marginBottom + fontSize) { + return null; // Indica que precisa criar nova página + } + + const words = text.split(/\s+/); + let currentLine = ''; + let currentX = x; + let currentY = y; + const lineHeight = fontSize * 1.2; + + // Processamento de formatação inline básica + let formattedChunks: { text: string, isItalic: boolean, isBold: boolean }[] = []; + + // Pré-processamento do texto para detectar formatação + let remaining = text; + let inBold = false; + let inItalic = false; + let currentChunk = ''; + + while (remaining.length > 0) { + if (remaining.startsWith('') || remaining.startsWith('')) { + // Adiciona o texto acumulado antes da tag + if (currentChunk) { + formattedChunks.push({ text: currentChunk, isItalic, isBold }); + currentChunk = ''; + } + + inBold = true; + remaining = remaining.replace(/^<(b|strong)>/, ''); + } + else if (remaining.startsWith('') || remaining.startsWith('')) { + // Adiciona o texto acumulado em negrito + if (currentChunk) { + formattedChunks.push({ text: currentChunk, isItalic, isBold: true }); + currentChunk = ''; + } + + inBold = false; + remaining = remaining.replace(/^<\/(b|strong)>/, ''); + } + else if (remaining.startsWith('') || remaining.startsWith('')) { + // Adiciona o texto acumulado antes da tag + if (currentChunk) { + formattedChunks.push({ text: currentChunk, isItalic, isBold }); + currentChunk = ''; + } + + inItalic = true; + remaining = remaining.replace(/^<(i|em)>/, ''); + } + else if (remaining.startsWith('') || remaining.startsWith('')) { + // Adiciona o texto acumulado em itálico + if (currentChunk) { + formattedChunks.push({ text: currentChunk, isItalic: true, isBold }); + currentChunk = ''; + } + + inItalic = false; + remaining = remaining.replace(/^<\/(i|em)>/, ''); + } + else if (remaining.startsWith('
') || remaining.startsWith('
') || remaining.startsWith('
')) { + // Adiciona o texto acumulado e então adiciona uma quebra de linha + if (currentChunk) { + formattedChunks.push({ text: currentChunk, isItalic, isBold }); + currentChunk = ''; + } + + formattedChunks.push({ text: '\n', isItalic: false, isBold: false }); + remaining = remaining.replace(/^/i, ''); + } + else { + // Acumula caractere a caractere + currentChunk += remaining[0]; + remaining = remaining.substring(1); + } + } + + // Adiciona qualquer texto restante + if (currentChunk) { + formattedChunks.push({ text: currentChunk, isItalic, isBold }); + } + + // Agora temos o texto dividido em chunks com formatação + let lines: { chunks: { text: string, isItalic: boolean, isBold: boolean }[], width: number }[] = [{ chunks: [], width: 0 }]; + let currentLineIndex = 0; + + for (const chunk of formattedChunks) { + if (chunk.text === '\n') { + // Forçar quebra de linha + lines.push({ chunks: [], width: 0 }); + currentLineIndex++; + continue; + } + + const words = chunk.text.split(/\s+/); + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + if (!word) continue; + + const font = chunk.isBold && chunk.isItalic ? boldItalicFont : + chunk.isBold ? boldFont : + chunk.isItalic ? italicFont : normalFont; + + const wordWidth = font.widthOfTextAtSize(word, fontSize); + const spaceWidth = font.widthOfTextAtSize(' ', fontSize); + + // Verificar se precisamos adicionar espaço antes da palavra + let wordWithSpace = word; + let totalWidth = wordWidth; + + // Adicionar espaço se não for a primeira palavra da linha e não estivermos após uma quebra forçada + if (lines[currentLineIndex].chunks.length > 0) { + wordWithSpace = ' ' + word; + totalWidth = wordWidth + spaceWidth; + } + + // Verificar se a palavra cabe na linha atual + if (lines[currentLineIndex].width + totalWidth <= maxWidth) { + // Adicionar à linha atual + lines[currentLineIndex].chunks.push({ + text: wordWithSpace, + isItalic: chunk.isItalic, + isBold: chunk.isBold + }); + lines[currentLineIndex].width += totalWidth; + } else { + // Criar nova linha + lines.push({ + chunks: [{ text: word, isItalic: chunk.isItalic, isBold: chunk.isBold }], + width: wordWidth + }); + currentLineIndex++; + + // Verificar se temos espaço para mais uma linha + if (currentY - lineHeight < marginBottom) { + // Não há espaço suficiente para mais uma linha + return null; // Indica que precisa criar nova página + } + } + } + } + + // Desenhar as linhas com formatação + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + currentY -= lineHeight; + + // Verificar se temos espaço para esta linha + if (currentY < marginBottom) { + return null; // Indica que precisa criar nova página + } + + // Ajustar posição X baseada no alinhamento + let startX = x; + + if (align === 'center') { + startX = x + (maxWidth - line.width) / 2; + } else if (align === 'justify' && i < lines.length - 1 && line.chunks.length > 1) { + // Justificar texto (exceto última linha do parágrafo) + let wordCount = 0; + let textWidth = 0; + + // Contar palavras e determinar largura total do texto + for (const chunk of line.chunks) { + const text = chunk.text.trim(); + if (text) { + wordCount += text.split(/\s+/).length; + const font = chunk.isBold && chunk.isItalic ? boldItalicFont : + chunk.isBold ? boldFont : + chunk.isItalic ? italicFont : normalFont; + textWidth += font.widthOfTextAtSize(text, fontSize); + } + } + + // Calcular espaçamento extra entre palavras para justificar + const totalSpaces = wordCount - 1; + const spaceWidth = totalSpaces > 0 ? (maxWidth - textWidth) / totalSpaces : 0; + + // Desenhar cada chunk com espaçamento justificado + let runningX = x; + for (const chunk of line.chunks) { + const font = chunk.isBold && chunk.isItalic ? boldItalicFont : + chunk.isBold ? boldFont : + chunk.isItalic ? italicFont : normalFont; + + // Desenhar texto do chunk + page.drawText(chunk.text, { + x: runningX, + y: currentY, + size: fontSize, + font: font, + color: color + }); + + // Avançar posição X + runningX += font.widthOfTextAtSize(chunk.text, fontSize) + spaceWidth; + } + + // Pular para a próxima linha + continue; + } + + // Desenhar cada chunk + let runningX = startX; + for (const chunk of line.chunks) { + const font = chunk.isBold && chunk.isItalic ? boldItalicFont : + chunk.isBold ? boldFont : + chunk.isItalic ? italicFont : normalFont; + + page.drawText(chunk.text, { + x: runningX, + y: currentY, + size: fontSize, + font: font, + color: color + }); + + runningX += font.widthOfTextAtSize(chunk.text, fontSize); + } + } + + return currentY; // Retorna a posição Y final + }; + let timbradoPdf: PDFDocument | null = null; let timbradoPageCount = 0; let timbradoPages: any[] = [];