709 lines
24 KiB
TypeScript
709 lines
24 KiB
TypeScript
import puppeteer from 'puppeteer';
|
|
import { prisma } from '../prismaClient';
|
|
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
|
|
/**
|
|
* Serviço para geração e manipulação de arquivos PDF
|
|
*/
|
|
export class PdfService {
|
|
/**
|
|
* Cria um documento PDF básico
|
|
* @returns Documento PDF
|
|
*/
|
|
async criarDocumento() {
|
|
return await PDFDocument.create();
|
|
}
|
|
|
|
/**
|
|
* Carrega um documento PDF existente
|
|
* @param pdfBytes - Bytes do PDF
|
|
* @returns Documento PDF
|
|
*/
|
|
async carregarDocumento(pdfBytes: Uint8Array) {
|
|
return await PDFDocument.load(pdfBytes);
|
|
}
|
|
|
|
/**
|
|
* Adiciona uma página ao documento
|
|
* @param doc - Documento PDF
|
|
* @param tamanho - Tamanho da página (padrão: A4)
|
|
* @returns Página adicionada
|
|
*/
|
|
adicionarPagina(doc: any, tamanho = [595.28, 841.89]) {
|
|
return doc.addPage(tamanho);
|
|
}
|
|
|
|
/**
|
|
* Salva o documento como buffer
|
|
* @param doc - Documento PDF
|
|
* @returns Buffer do PDF
|
|
*/
|
|
async salvarComoBuffer(doc: any): Promise<Buffer> {
|
|
const pdfBytes = await doc.save();
|
|
return Buffer.from(pdfBytes);
|
|
}
|
|
|
|
/**
|
|
* Carrega uma fonte padrão
|
|
* @param doc - Documento PDF
|
|
* @param fonte - Nome da fonte padrão
|
|
* @returns Fonte carregada
|
|
*/
|
|
async carregarFonte(doc: any, fonte: StandardFonts) {
|
|
return await doc.embedFont(fonte);
|
|
}
|
|
|
|
/**
|
|
* Desenha texto em uma página
|
|
* @param pagina - Página do PDF
|
|
* @param texto - Texto a ser desenhado
|
|
* @param x - Posição X
|
|
* @param y - Posição Y
|
|
* @param fonte - Fonte a ser usada
|
|
* @param tamanho - Tamanho da fonte
|
|
* @param cor - Cor do texto (padrão: preto)
|
|
*/
|
|
desenharTexto(
|
|
pagina: any,
|
|
texto: string,
|
|
x: number,
|
|
y: number,
|
|
fonte: any,
|
|
tamanho: number,
|
|
cor = rgb(0, 0, 0)
|
|
) {
|
|
pagina.drawText(texto, {
|
|
x,
|
|
y,
|
|
font: fonte,
|
|
size: tamanho,
|
|
color: cor,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cria um cabeçalho para o documento
|
|
* @param pagina - Página do PDF
|
|
* @param titulo - Título do cabeçalho
|
|
* @param fonte - Fonte a ser usada
|
|
* @param margemSuperior - Margem superior
|
|
* @param margem - Margem lateral
|
|
* @returns Posição Y após o cabeçalho
|
|
*/
|
|
criarCabecalho(
|
|
pagina: any,
|
|
titulo: string,
|
|
fonte: any,
|
|
margemSuperior: number,
|
|
margem: number
|
|
): number {
|
|
const yPos = pagina.getHeight() - margemSuperior;
|
|
|
|
this.desenharTexto(
|
|
pagina,
|
|
titulo,
|
|
margem,
|
|
yPos,
|
|
fonte,
|
|
18,
|
|
rgb(0, 0, 0.7)
|
|
);
|
|
|
|
// Linha separadora
|
|
pagina.drawLine({
|
|
start: { x: margem, y: yPos - 10 },
|
|
end: { x: pagina.getWidth() - margem, y: yPos - 10 },
|
|
thickness: 1,
|
|
color: rgb(0, 0, 0.5),
|
|
});
|
|
|
|
return yPos - 30; // Retorna a posição Y após o cabeçalho
|
|
}
|
|
|
|
/**
|
|
* Cria um rodapé para o documento
|
|
* @param pagina - Página do PDF
|
|
* @param texto - Texto do rodapé
|
|
* @param fonte - Fonte a ser usada
|
|
* @param margem - Margem
|
|
*/
|
|
criarRodape(pagina: any, texto: string, fonte: any, margem: number) {
|
|
const yPos = margem / 2;
|
|
|
|
this.desenharTexto(
|
|
pagina,
|
|
texto,
|
|
margem,
|
|
yPos,
|
|
fonte,
|
|
8,
|
|
rgb(0.5, 0.5, 0.5)
|
|
);
|
|
}
|
|
|
|
private async getConfiguracao() {
|
|
try {
|
|
// Buscar configuração ou usar valores padrão
|
|
const config = await prisma.configuracaoDocumento.findFirst({
|
|
orderBy: { criadoEm: 'desc' },
|
|
select: {
|
|
id: true,
|
|
timbradoUrl: true,
|
|
timbradoMarginTop: true,
|
|
timbradoMarginBottom: true,
|
|
timbradoMarginLeft: true,
|
|
timbradoMarginRight: true,
|
|
templatePadrao: true,
|
|
margensTemplate: true,
|
|
criadoEm: true
|
|
}
|
|
});
|
|
|
|
console.log('Configuração encontrada:', config); // Debug
|
|
|
|
// Retornar configuração padrão se não existir
|
|
return {
|
|
// Valores padrão
|
|
cabecalhoHtml: `
|
|
<div style="text-align: center; padding: 20px;">
|
|
<h2 style="margin:0;font-size:14pt">{{escritorio.nome}}</h2>
|
|
</div>
|
|
`,
|
|
rodapeHtml: `
|
|
<div style="text-align: center; font-size: 10pt;">
|
|
{{escritorio.endereco}}<br>
|
|
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
|
|
</div>
|
|
`,
|
|
// Campos que existem no modelo
|
|
timbradoUrl: config?.timbradoUrl || '',
|
|
timbradoMarginTop: config?.timbradoMarginTop || 100,
|
|
timbradoMarginBottom: config?.timbradoMarginBottom || 50,
|
|
timbradoMarginLeft: config?.timbradoMarginLeft || 50,
|
|
timbradoMarginRight: config?.timbradoMarginRight || 50,
|
|
templatePadrao: config?.templatePadrao || null,
|
|
margensTemplate: config?.margensTemplate || null
|
|
};
|
|
} catch (error) {
|
|
console.error('Erro ao buscar configuração:', error);
|
|
// Retornar configuração padrão para não interromper o fluxo
|
|
return {
|
|
cabecalhoHtml: `<div style="text-align: center; padding: 20px;"><h2 style="margin:0;font-size:14pt">Documento</h2></div>`,
|
|
rodapeHtml: `<div style="text-align: center; font-size: 10pt;">Documento gerado pelo sistema</div>`,
|
|
timbradoUrl: '',
|
|
timbradoMarginTop: 100,
|
|
timbradoMarginBottom: 50,
|
|
timbradoMarginLeft: 50,
|
|
timbradoMarginRight: 50,
|
|
templatePadrao: null,
|
|
margensTemplate: null
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gera um PDF a partir do conteúdo HTML simples (apenas texto) e dados do escritório,
|
|
* encaixando o texto sobre o timbrado (PDF de fundo) em todas as páginas.
|
|
* Não usa Puppeteer, apenas PDF-LIB.
|
|
*/
|
|
async gerarPDF(conteudo: string, escritorio: any = {}): Promise<Buffer> {
|
|
try {
|
|
console.log('Buscando configurações do documento...');
|
|
const config = await this.getConfiguracao();
|
|
|
|
// Verificar se temos um timbrado para usar
|
|
const temTimbrado = !!config.timbradoUrl;
|
|
let timbradoPath = '';
|
|
let timbradoExists = false;
|
|
|
|
if (temTimbrado) {
|
|
if (config.timbradoUrl.startsWith('/uploads')) {
|
|
timbradoPath = path.join(process.cwd(), 'public', config.timbradoUrl);
|
|
} else {
|
|
timbradoPath = path.join(process.cwd(), 'public', 'uploads', path.basename(config.timbradoUrl));
|
|
}
|
|
try {
|
|
await fs.access(timbradoPath);
|
|
console.log('Timbrado encontrado em:', timbradoPath);
|
|
timbradoExists = true;
|
|
} catch (e) {
|
|
console.error('Arquivo de timbrado não encontrado:', timbradoPath, e);
|
|
timbradoExists = false;
|
|
}
|
|
}
|
|
|
|
// Margens
|
|
const margens = config.margensTemplate || {
|
|
top: config.timbradoMarginTop || 100,
|
|
left: config.timbradoMarginLeft || 50,
|
|
right: config.timbradoMarginRight || 50,
|
|
bottom: config.timbradoMarginBottom || 50
|
|
};
|
|
|
|
console.log('Usando margens:', margens);
|
|
|
|
// Parâmetros de página
|
|
const PAGE_WIDTH = 595.28; // A4
|
|
const PAGE_HEIGHT = 841.89; // A4
|
|
const FONT_SIZE = 12;
|
|
const LINE_HEIGHT = FONT_SIZE * 1.5;
|
|
const MAX_WIDTH = PAGE_WIDTH - margens.left - margens.right;
|
|
|
|
// Crie o PDFDocument ANTES de tentar usar pdfDoc para embedFont!
|
|
let pdfDoc: PDFDocument = await PDFDocument.create();
|
|
|
|
// Fonte padrão do pdf-lib: TimesRoman, TimesBold, TimesItalic
|
|
const font = await pdfDoc.embedFont('Times-Roman');
|
|
const fontBold = await pdfDoc.embedFont('Times-Bold');
|
|
const fontItalic = await pdfDoc.embedFont('Times-Italic');
|
|
const fontBoldItalic = fontBold;
|
|
|
|
let timbradoPdf: PDFDocument | null = null;
|
|
let timbradoPageCount = 0;
|
|
let timbradoPages: any[] = [];
|
|
if (timbradoExists) {
|
|
try {
|
|
console.log('Carregando timbrado PDF...');
|
|
const timbradoBytes = await fs.readFile(timbradoPath);
|
|
timbradoPdf = await PDFDocument.load(timbradoBytes);
|
|
timbradoPageCount = timbradoPdf.getPageCount();
|
|
console.log(`Timbrado carregado com ${timbradoPageCount} páginas`);
|
|
|
|
// Usar a primeira página do timbrado como modelo para todas as páginas
|
|
if (timbradoPageCount > 0) {
|
|
// Carregue a primeira página do timbrado como imagem de fundo
|
|
const [firstPage] = await pdfDoc.embedPdf(timbradoPdf, [0]);
|
|
timbradoPages = [firstPage];
|
|
console.log('Timbrado incorporado com sucesso');
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar timbrado:', error);
|
|
timbradoExists = false;
|
|
}
|
|
}
|
|
|
|
function stripUnsupportedTags(html: string): string {
|
|
// Primeiro, substituir entidades e tags comuns
|
|
const cleanHtml = html
|
|
// Preserva quebras de linha HTML como marcadores especiais
|
|
.replace(/<br\s*\/?>|<\/p><p>|<\/div><div>/gi, '\n')
|
|
// Substitui parágrafos, divs e blockquote por marcadores de bloco + quebra de linha
|
|
.replace(/<p[^>]*style="[^"]*text-align\s*:\s*(center|right|justify)[^"]*"[^>]*>/gi,
|
|
(match, align) => `<p style="text-align:${align}">`)
|
|
.replace(/<div[^>]*style="[^"]*text-align\s*:\s*(center|right|justify)[^"]*"[^>]*>/gi,
|
|
(match, align) => `<div style="text-align:${align}">`)
|
|
.replace(/<p[^>]*>|<div[^>]*>|<blockquote[^>]*>/gi, '')
|
|
.replace(/<\/p>|<\/div>|<\/blockquote>/gi, '\n\n')
|
|
// Marca formatação de texto
|
|
.replace(/<b>|<strong>/gi, '<b>')
|
|
.replace(/<\/b>|<\/strong>/gi, '</b>')
|
|
.replace(/<i>|<em>/gi, '<i>')
|
|
.replace(/<\/i>|<\/em>/gi, '</i>')
|
|
// Normaliza espaços extras e quebras de linha
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
.replace(/[ \t]+/g, ' ');
|
|
|
|
return cleanHtml;
|
|
}
|
|
|
|
// Função para decodificar entidades HTML comuns
|
|
function decodeHtmlEntities(str: string): string {
|
|
if (!str) return '';
|
|
|
|
return str
|
|
.replace(/\r\n/g, '\n')
|
|
// Acentos e caracteres especiais
|
|
.replace(/Á/g, 'Á').replace(/á/g, 'á')
|
|
.replace(/É/g, 'É').replace(/é/g, 'é')
|
|
.replace(/Í/g, 'Í').replace(/í/g, 'í')
|
|
.replace(/Ó/g, 'Ó').replace(/ó/g, 'ó')
|
|
.replace(/Ú/g, 'Ú').replace(/ú/g, 'ú')
|
|
.replace(/À/g, 'À').replace(/à/g, 'à')
|
|
.replace(/È/g, 'È').replace(/è/g, 'è')
|
|
.replace(/Ì/g, 'Ì').replace(/ì/g, 'ì')
|
|
.replace(/Ò/g, 'Ò').replace(/ò/g, 'ò')
|
|
.replace(/Ù/g, 'Ù').replace(/ù/g, 'ù')
|
|
.replace(/Â/g, 'Â').replace(/â/g, 'â')
|
|
.replace(/Ê/g, 'Ê').replace(/ê/g, 'ê')
|
|
.replace(/Î/g, 'Î').replace(/î/g, 'î')
|
|
.replace(/Ô/g, 'Ô').replace(/ô/g, 'ô')
|
|
.replace(/Û/g, 'Û').replace(/û/g, 'û')
|
|
.replace(/Ã/g, 'Ã').replace(/ã/g, 'ã')
|
|
.replace(/Õ/g, 'Õ').replace(/õ/g, 'õ')
|
|
.replace(/Ç/g, 'Ç').replace(/ç/g, 'ç')
|
|
.replace(/Ñ/g, 'Ñ').replace(/ñ/g, 'ñ')
|
|
.replace(/Ü/g, 'Ü').replace(/ü/g, 'ü')
|
|
// Símbolos e caracteres especiais
|
|
.replace(/º/g, 'º')
|
|
.replace(/ª/g, 'ª')
|
|
.replace(/§/g, '§')
|
|
.replace(/°/g, '°')
|
|
.replace(/ /g, ' ')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
.replace(/</g, '<')
|
|
.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 = [];
|
|
|
|
// Processa texto com formatação - remover tags HTML e aplicar estilos
|
|
let processedText = '';
|
|
let currentFont = normalFont;
|
|
let boldActive = false;
|
|
let italicActive = false;
|
|
|
|
// Primeiro, extrair partes do texto com suas formatações
|
|
let i = 0;
|
|
while (i < text.length) {
|
|
if (text.substring(i, i+3) === '<b>') {
|
|
boldActive = true;
|
|
currentFont = italicActive ? boldItalicFont : boldFont;
|
|
i += 3;
|
|
}
|
|
else if (text.substring(i, i+4) === '</b>') {
|
|
boldActive = false;
|
|
currentFont = italicActive ? italicFont : normalFont;
|
|
i += 4;
|
|
}
|
|
else if (text.substring(i, i+3) === '<i>') {
|
|
italicActive = true;
|
|
currentFont = boldActive ? boldItalicFont : italicFont;
|
|
i += 3;
|
|
}
|
|
else if (text.substring(i, i+4) === '</i>') {
|
|
italicActive = false;
|
|
currentFont = boldActive ? boldFont : normalFont;
|
|
i += 4;
|
|
}
|
|
else {
|
|
// Encontrar o próximo início de tag
|
|
let nextTagPos = text.indexOf('<', i);
|
|
|
|
if (nextTagPos === -1) {
|
|
// Não há mais tags, pegar o restante do texto
|
|
textParts.push({
|
|
text: text.substring(i),
|
|
font: currentFont
|
|
});
|
|
break;
|
|
} else {
|
|
// Pegar texto até a próxima tag
|
|
textParts.push({
|
|
text: text.substring(i, nextTagPos),
|
|
font: currentFont
|
|
});
|
|
i = nextTagPos;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
);
|
|
const texto = decodeHtmlEntities(textoLimpo);
|
|
|
|
console.log('Texto processado para renderização:', texto.substring(0, 100) + '...');
|
|
|
|
// Corrigir o processamento de parágrafos para lidar melhor com formatações
|
|
const paragrafos = texto.split(/\n{2,}/).map(p => {
|
|
let paragraph = p.trim();
|
|
if (!paragraph) return '';
|
|
|
|
// Extrair informações de formatação
|
|
let align = 'left'; // padrão
|
|
const alignMatch = paragraph.match(/§§ALIGN=(center|right|justify)§§/);
|
|
if (alignMatch) {
|
|
align = alignMatch[1];
|
|
paragraph = paragraph.replace(/§§ALIGN=(center|right|justify)§§/, '');
|
|
}
|
|
|
|
let indent = 0;
|
|
const indentMatch = paragraph.match(/§§INDENT=(\d+)(pt|px|em)§§/);
|
|
if (indentMatch) {
|
|
indent = parseInt(indentMatch[1]);
|
|
if (indentMatch[2] === 'em') indent *= 12; // Converter em para pontos aproximadamente
|
|
paragraph = paragraph.replace(/§§INDENT=\d+(pt|px|em)§§/, '');
|
|
}
|
|
|
|
return `§§ALIGN=${align}§§§§INDENT=${indent}§§${paragraph.trim()}`;
|
|
}).filter(Boolean);
|
|
|
|
let currentPar = 0;
|
|
let pageIndex = 0;
|
|
console.log(`Gerando PDF com ${paragrafos.length} parágrafos`);
|
|
|
|
// Define um contador máximo para evitar loops infinitos
|
|
const MAX_PAGES = 100;
|
|
while (currentPar < paragrafos.length && pageIndex < MAX_PAGES) {
|
|
console.log(`Criando página ${pageIndex + 1}, processando parágrafo ${currentPar + 1} de ${paragrafos.length}`);
|
|
let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
|
|
|
|
// Adicionar timbrado à página
|
|
if (timbradoPages.length > 0) {
|
|
console.log(`Adicionando timbrado na página ${pageIndex + 1}`);
|
|
page.drawPage(timbradoPages[0], {
|
|
x: 0,
|
|
y: 0,
|
|
width: PAGE_WIDTH,
|
|
height: PAGE_HEIGHT
|
|
});
|
|
}
|
|
|
|
let y = PAGE_HEIGHT - margens.top;
|
|
let processedAnyParagraph = false; // Flag para verificar se processamos algum parágrafo
|
|
|
|
// Tenta processar parágrafos na página atual
|
|
while (currentPar < paragrafos.length) {
|
|
const paraText = paragrafos[currentPar];
|
|
|
|
// Extract alignment
|
|
let align: 'left' | 'center' | 'justify' = 'left';
|
|
const alignMatch = paraText.match(/^§§ALIGN=(center|justify|right)§§/);
|
|
if (alignMatch) {
|
|
align = alignMatch[1] as 'center' | 'justify' | 'left';
|
|
}
|
|
|
|
// Extract indentation
|
|
let indent = 0;
|
|
const indentMatch = paraText.match(/§§INDENT=([0-9.]+)§§/);
|
|
if (indentMatch) {
|
|
indent = parseFloat(indentMatch[1]);
|
|
}
|
|
|
|
// Clean text
|
|
let text = paraText
|
|
.replace(/^§§ALIGN=(center|justify|right)§§/, '')
|
|
.replace(/§§INDENT=[0-9.]+§§/, '')
|
|
.trim();
|
|
let x = margens.left + indent;
|
|
let parWidth = MAX_WIDTH - indent;
|
|
|
|
// Se o texto for muito curto, avance para o próximo parágrafo
|
|
if (text.length < 2) {
|
|
currentPar++;
|
|
processedAnyParagraph = true;
|
|
continue;
|
|
}
|
|
|
|
console.log(`Tentando renderizar parágrafo ${currentPar + 1} em y=${y}`);
|
|
let yAfter = renderInlineFormattedParagraph(
|
|
page,
|
|
text,
|
|
x,
|
|
y,
|
|
font,
|
|
fontBold,
|
|
fontItalic,
|
|
fontBoldItalic,
|
|
FONT_SIZE,
|
|
rgb(0, 0, 0),
|
|
align,
|
|
parWidth,
|
|
false,
|
|
margens.top,
|
|
margens.bottom,
|
|
PAGE_HEIGHT
|
|
);
|
|
|
|
if (yAfter === null) {
|
|
// Se não conseguimos renderizar este parágrafo, vamos para a próxima página
|
|
// Mas só se já tivermos processado algum parágrafo nesta página
|
|
if (processedAnyParagraph) {
|
|
console.log(`Página ${pageIndex + 1} está cheia, criando nova página`);
|
|
break;
|
|
} else {
|
|
// Se não conseguimos nem processar um parágrafo, algo está errado
|
|
// Vamos avançar para o próximo parágrafo para evitar um loop infinito
|
|
console.log(`Não foi possível processar o parágrafo ${currentPar + 1}, avançando`);
|
|
currentPar++;
|
|
processedAnyParagraph = true;
|
|
}
|
|
} else {
|
|
// Parágrafo foi renderizado com sucesso
|
|
y = yAfter - FONT_SIZE * 0.5;
|
|
currentPar++;
|
|
processedAnyParagraph = true;
|
|
console.log(`Parágrafo ${currentPar} processado com sucesso, nova posição y=${y}`);
|
|
|
|
// Verifica se ainda temos espaço para pelo menos uma linha
|
|
if (y < margens.bottom + FONT_SIZE * 2) {
|
|
console.log(`Espaço insuficiente para próximo parágrafo, criando nova página`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
pageIndex++;
|
|
}
|
|
|
|
if (pageIndex >= MAX_PAGES) {
|
|
console.warn(`AVISO: Número máximo de páginas (${MAX_PAGES}) atingido. Geração de PDF interrompida.`);
|
|
}
|
|
|
|
console.log(`PDF gerado com ${pageIndex} páginas`);
|
|
const pdfBytes = await pdfDoc.save();
|
|
return Buffer.from(pdfBytes);
|
|
} catch (error) {
|
|
console.error('Erro geral ao gerar PDF:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private processarTemplate(template: string, dados: any): string {
|
|
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
const keyParts = key.trim().split('.');
|
|
let value = dados;
|
|
for (const part of keyParts) {
|
|
if (value && typeof value === 'object' && part in value) {
|
|
value = value[part];
|
|
} else {
|
|
value = undefined;
|
|
break;
|
|
}
|
|
}
|
|
return value !== undefined ? value : match;
|
|
});
|
|
}
|
|
}
|