import puppeteer from 'puppeteer'; import { PDFDocument } from 'pdf-lib'; import fs from 'fs/promises'; import path from 'path'; import { prisma } from '../prismaClient'; // Função utilitária para converter pixels em milímetros (1px ≈ 0.264583 mm) function pxToMm(px: number | null | undefined): string { if (!px) return '0mm'; return (px * 0.264583).toFixed(2) + 'mm'; } export class PdfService { // Busca configuração do documento, usando apenas campos válidos do schema private async getConfiguracao() { try { 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 } }); // Extrai recuo de primeira linha se existir em margensTemplate let firstLineIndent = '0mm'; if (config?.margensTemplate) { try { const margens = typeof config.margensTemplate === 'string' ? JSON.parse(config.margensTemplate) : config.margensTemplate; if (margens && margens.firstLineIndent) { firstLineIndent = `${margens.firstLineIndent}mm`; } } catch (e) { // Se der erro, ignora e usa padrão } } return { timbradoUrl: config?.timbradoUrl || '', timbradoMarginTop: config?.timbradoMarginTop ?? 100, timbradoMarginBottom: config?.timbradoMarginBottom ?? 50, timbradoMarginLeft: config?.timbradoMarginLeft ?? 50, timbradoMarginRight: config?.timbradoMarginRight ?? 50, templatePadrao: config?.templatePadrao || '', firstLineIndent }; } catch (error) { console.error('Erro ao buscar configuração:', error); throw new Error('Falha ao buscar configuração'); } } // Gera HTML com timbrado e margens private gerarHTML(content: string, config: any) { // Limpa espaços, quebras de linha e tags vazias do final do conteúdo let cleanContent = content .replace(/(\s||

\s*<\/p>)+$/gi, '') .replace(/(

\s*<\/p>)+/gi, ''); const timbradoUrl = config.timbradoUrl ? `${process.env.FRONTEND_URL || 'http://localhost:5000'}${config.timbradoUrl}` : ''; // Adiciona log para debug if (timbradoUrl) { console.log('[PDF] Timbrado inserido no HTML:', timbradoUrl); } return ` ${timbradoUrl ? '

' : ''}
${cleanContent}
`; } // Gera PDF a partir do HTML, depois sobrepõe o timbrado em cada página async gerarPDF(conteudo: string) { try { const config = await this.getConfiguracao(); // Usa templatePadrao se não houver conteúdo const html = this.gerarHTML(conteudo || config.templatePadrao, config); const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] }); const page = await browser.newPage(); await page.setContent(html, { waitUntil: ['networkidle0', 'load'], timeout: 30000 }); const margin = { top: pxToMm(config.timbradoMarginTop), bottom: pxToMm(config.timbradoMarginBottom), left: pxToMm(config.timbradoMarginLeft), right: pxToMm(config.timbradoMarginRight) }; const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true, margin, preferCSSPageSize: true }); await browser.close(); // Se não houver timbrado, retorna direto if (!config.timbradoUrl) { return Buffer.from(pdfBuffer); } // Caminho absoluto do PDF de timbrado const timbradoPath = config.timbradoUrl.startsWith('/uploads') ? path.join(process.cwd(), 'public', config.timbradoUrl) : path.join(process.cwd(), 'public', 'uploads', path.basename(config.timbradoUrl)); let timbradoBytes: Buffer; try { timbradoBytes = await fs.readFile(timbradoPath); } catch (e) { console.error('Timbrado não encontrado:', timbradoPath); return Buffer.from(pdfBuffer); } // Mescla cada página do PDF gerado com o timbrado const pdfDoc = await PDFDocument.load(pdfBuffer); const timbradoDoc = await PDFDocument.load(timbradoBytes); const timbradoPage = timbradoDoc.getPage(0); const [timbradoEmbed] = await pdfDoc.embedPages([timbradoPage]); const pageCount = pdfDoc.getPageCount(); for (let i = 0; i < pageCount; i++) { const page = pdfDoc.getPage(i); page.drawPage(timbradoEmbed, { x: 0, y: 0, width: page.getWidth(), height: page.getHeight() }); } // Após mesclar o timbrado const pageCountBefore = pdfDoc.getPageCount(); if (pageCountBefore > 1) { // Heurística aprimorada: remove a última página apenas se estiver realmente vazia (sem texto, imagens ou gráficos) const lastPage = pdfDoc.getPage(pageCountBefore - 1); let isBlank = true; // Verifica se há texto const annotations = lastPage.node.Annots; if (annotations && annotations.length > 0) { isBlank = false; } // Verifica se há objetos gráficos (imagens, paths, etc.) if (isBlank && typeof lastPage.node?.Resources === 'function') { const resourcesDict = lastPage.node.Resources(); if (resourcesDict && typeof resourcesDict === 'object') { // Corrige: usa PDFName para acessar o XObject // eslint-disable-next-line @typescript-eslint/no-var-requires const { PDFName } = require('pdf-lib'); const xObject = resourcesDict.get(PDFName.of('XObject')); if (xObject && typeof xObject === 'object' && Object.keys(xObject).length > 0) { isBlank = false; } } } // Só remove se realmente não houver nada if (isBlank) { pdfDoc.removePage(pageCountBefore - 1); } } const mergedPdf = await pdfDoc.save(); return Buffer.from(mergedPdf); } catch (error) { console.error('Erro ao gerar PDF:', error); throw new Error('Falha ao gerar PDF'); } } }