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');
}
}
}