diff --git a/src/server/services/PdfService copy 2.ts b/src/server/services/PdfService copy 2.ts deleted file mode 100644 index cf4e73e..0000000 --- a/src/server/services/PdfService copy 2.ts +++ /dev/null @@ -1,634 +0,0 @@ -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 { - 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: ` -
-

{{escritorio.nome}}

-
- `, - rodapeHtml: ` -
- {{escritorio.endereco}}
- Tel: {{escritorio.telefone}} | Email: {{escritorio.email}} -
- `, - // 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: `

Documento

`, - rodapeHtml: `
Documento gerado pelo sistema
`, - 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 { - 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); - timbradoExists = true; - } catch (e) { - timbradoExists = false; - } - } - - // Margens - const margens = config.margensTemplate || { - top: config.timbradoMarginTop || 100, - left: config.timbradoMarginLeft || 50, - right: config.timbradoMarginRight || 50, - bottom: config.timbradoMarginBottom || 50 - }; - - // 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; - - // Função para decodificar entidades HTML comuns (acentuação, símbolos, º, etc) - function decodeHtmlEntities(str: string): string { - if (!str) return ''; - return str - // Acentos minúsculos e maiúsculos - .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 comuns - .replace(/º/gi, 'º') - .replace(/ª/gi, 'ª') - .replace(/§/gi, '§') - .replace(/°/gi, '°') - .replace(/©/gi, '©') - .replace(/®/gi, '®') - .replace(/¶/gi, '¶') - .replace(/•/gi, '•') - .replace(/–/gi, '–') - .replace(/—/gi, '—') - .replace(/«/gi, '«') - .replace(/»/gi, '»') - .replace(/‘/gi, '‘') - .replace(/’/gi, '’') - .replace(/“/gi, '“') - .replace(/”/gi, '”') - .replace(/€/gi, '€') - .replace(/¥/gi, '¥') - .replace(/£/gi, '£') - .replace(/¢/gi, '¢') - .replace(/ /gi, ' ') - .replace(/&/gi, '&') - .replace(/"/gi, '"') - .replace(/'/g, "'") - .replace(/</gi, '<') - .replace(/>/gi, '>') - // Numéricos - .replace(/&#(\d+);/g, (m, code) => String.fromCharCode(code)); - } - - // Função para remover todas as tags HTML, exceto as de formatação suportadas - function stripUnsupportedTags(html: string): string { - // Remove todas as tags, exceto , , , , ,
,

, , - // Remove atributos de exceto text-indent e text-align - return html - // Mantém apenas com text-indent ou text-align:center - .replace(/])*?>/gi, '') - // Remove atributos de , , , , ,

- .replace(/<(b|strong|i|em|u|p)[^>]*>/gi, '<$1>') - // Remove todas as outras tags e atributos - .replace(/<(?!\/?(b|strong|i|em|u|br|p|span)[ >])[^>]+>/gi, '') - // Remove spans sem text-indent ou text-align - .replace(/(.*?)<\/span>/gi, '$1') - // Remove comentários HTML - .replace(//g, ''); - } - - // Função para renderizar texto com formatação inline (negrito, itálico, sublinhado) e controle de quebra de linha e recuo - function renderInlineFormattedParagraph( - page, - text: string, - x: number, - y: number, - font, - fontBold, - fontItalic, - fontBoldItalic, - fontSize: number, - color, - align: string, - maxWidth: number, - indent: boolean, - topMargin: number, - bottomMargin: number, - pageHeight: number - ) { - let regex = /(<\/?(b|strong|i|em|u)>)/gi; - let tokens: { text: string, bold: boolean, italic: boolean, underline: boolean }[] = []; - let lastIndex = 0; - let match; - let currentStyle = { bold: false, italic: false, underline: false }; - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - tokens.push({ - text: text.substring(lastIndex, match.index), - bold: currentStyle.bold, - italic: currentStyle.italic, - underline: currentStyle.underline - }); - } - if (/^<(b|strong)>$/i.test(match[0])) currentStyle.bold = true; - if (/^<\/(b|strong)>$/i.test(match[0])) currentStyle.bold = false; - if (/^<(i|em)>$/i.test(match[0])) currentStyle.italic = true; - if (/^<\/(i|em)>$/i.test(match[0])) currentStyle.italic = false; - if (/^$/i.test(match[0])) currentStyle.underline = true; - if (/^<\/u>$/i.test(match[0])) currentStyle.underline = false; - lastIndex = regex.lastIndex; - } - if (lastIndex < text.length) { - tokens.push({ - text: text.substring(lastIndex), - bold: currentStyle.bold, - italic: currentStyle.italic, - underline: currentStyle.underline - }); - } - - let words: { text: string, font: any, underline: boolean, bold: boolean, italic: boolean }[] = []; - for (const token of tokens) { - let usedFont = font; - if (token.bold && token.italic) usedFont = fontBoldItalic; - else if (token.bold) usedFont = fontBold; - else if (token.italic) usedFont = fontItalic; - // Corrige: split por espaço, mas preserva espaços como tokens separados - const split = token.text.split(/(\s+)/); - for (const part of split) { - words.push({ text: part, font: usedFont, underline: token.underline, bold: token.bold, italic: token.italic }); - } - } - - let lines: { tokens: { text: string, font: any, underline: boolean, bold: boolean, italic: boolean }[] }[] = []; - let currentTokens: { text: string, font: any, underline: boolean, bold: boolean, italic: boolean }[] = []; - let currentWidth = 0; - - for (const [i, word] of words.entries()) { - const wordWidth = word.font.widthOfTextAtSize(word.text, fontSize); - if (currentWidth + wordWidth > maxWidth && currentTokens.length > 0) { - lines.push({ tokens: currentTokens }); - currentTokens = [word]; - currentWidth = wordWidth; - } else { - currentTokens.push(word); - currentWidth += wordWidth; - } - } - if (currentTokens.length > 0) { - lines.push({ tokens: currentTokens }); - } - - // Ajuste: Força alinhamento consistente para todas as linhas do parágrafo - // Se align === 'center', todas as linhas centralizadas - // Se align === 'justify', todas as linhas justificadas (exceto a última ou linhas curtas) - // Se align === 'left' ou outro, todas à esquerda - - for (let i = 0; i < lines.length; i++) { - if (y < bottomMargin + fontSize * 1.5) { - return null; - } - let line = lines[i]; - let drawX = x; - - // Remove espaços no início e fim da linha para não justificar esses espaços - let trimmedTokens = [...line.tokens]; - while (trimmedTokens.length && /^\s+$/.test(trimmedTokens[0].text)) trimmedTokens.shift(); - while (trimmedTokens.length && /^\s+$/.test(trimmedTokens[trimmedTokens.length - 1].text)) trimmedTokens.pop(); - - let totalWidth = trimmedTokens.reduce((acc, t) => acc + t.font.widthOfTextAtSize(t.text, fontSize), 0); - - // Corrige centralização: centraliza a linha inteira, inclusive se for a única linha do parágrafo - if (align === 'center') { - drawX = x + (maxWidth - totalWidth) / 2; - for (const t of trimmedTokens) { - page.drawText(t.text, { - x: drawX, - y, - size: fontSize, - font: t.font, - color - }); - if (t.underline && t.text.trim() !== '') { - const underlineY = y - 1.5; - page.drawLine({ - start: { x: drawX, y: underlineY }, - end: { x: drawX + t.font.widthOfTextAtSize(t.text, fontSize), y: underlineY }, - thickness: 0.75, - color - }); - } - drawX += t.font.widthOfTextAtSize(t.text, fontSize); - } - } else if (align === 'justify' && i < lines.length - 1 && trimmedTokens.length > 2) { - // Justify: distribui espaço entre palavras, exceto na última linha ou linhas curtas - const spaces = trimmedTokens.filter(t => /^\s+$/.test(t.text)); - const numSpaces = spaces.length; - const lineTextWidth = trimmedTokens.reduce((acc, t) => acc + t.font.widthOfTextAtSize(t.text, fontSize), 0); - const extraSpace = maxWidth - lineTextWidth; - let spaceWidth = 0; - // Só justifica se houver pelo menos 2 espaços e a linha não for muito curta - if (numSpaces > 0 && extraSpace > 0) { - spaceWidth = extraSpace / numSpaces; - } - let localDrawX = x; - for (const t of trimmedTokens) { - page.drawText(t.text, { - x: localDrawX, - y, - size: fontSize, - font: t.font, - color - }); - if (t.underline && t.text.trim() !== '') { - const underlineY = y - 1.5; - page.drawLine({ - start: { x: localDrawX, y: underlineY }, - end: { x: localDrawX + t.font.widthOfTextAtSize(t.text, fontSize), y: underlineY }, - thickness: 0.75, - color - }); - } - if (/^\s+$/.test(t.text) && spaceWidth > 0) { - localDrawX += t.font.widthOfTextAtSize(t.text, fontSize) + spaceWidth; - } else { - localDrawX += t.font.widthOfTextAtSize(t.text, fontSize); - } - } - } else { - // Alinhamento à esquerda (padrão) ou última linha de parágrafo justificado - for (const t of trimmedTokens) { - page.drawText(t.text, { - x: drawX, - y, - size: fontSize, - font: t.font, - color - }); - if (t.underline && t.text.trim() !== '') { - const underlineY = y - 1.5; - page.drawLine({ - start: { x: drawX, y: underlineY }, - end: { x: drawX + t.font.widthOfTextAtSize(t.text, fontSize), y: underlineY }, - thickness: 0.75, - color - }); - } - drawX += t.font.widthOfTextAtSize(t.text, fontSize); - } - } - y -= fontSize * 1.5; - } - return y; - } - - const texto = decodeHtmlEntities( - stripUnsupportedTags( - this.processarTemplate(conteudo, escritorio) - .replace(//gi, '\n') - .replace(/<\/p>/gi, '\n\n') - .replace(/]*>/gi, '') - .replace(/]*text-indent[^>]*>/gi, '§§INDENT§§') - .replace(/]*text-align\s*:\s*center[^>]*>/gi, '§§CENTER§§') - .replace(/<\/span>/gi, '') - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .replace(/\n{3,}/g, '\n\n') - .trim() - ) - ); - - const paragrafos = texto.split(/\n{2,}/).map(p => p.trim()).filter(Boolean); - - let timbradoPdf: PDFDocument | null = null; - let timbradoPageCount = 0; - let timbradoPages: any[] = []; - if (timbradoExists) { - const timbradoBytes = await fs.readFile(timbradoPath); - timbradoPdf = await PDFDocument.load(timbradoBytes); - timbradoPageCount = timbradoPdf.getPageCount(); - timbradoPages = []; - for (let i = 0; i < timbradoPageCount; i++) { - const [embedded] = await pdfDoc.embedPages([timbradoPdf.getPage(i)]); - timbradoPages.push(embedded); - } - } - - let currentPar = 0; - let pageIndex = 0; - while (currentPar < paragrafos.length) { - let page = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]); - if (timbradoPages.length > 0) { - const embedded = timbradoPages[pageIndex % timbradoPages.length]; - page.drawPage(embedded, { - x: 0, - y: 0, - width: PAGE_WIDTH, - height: PAGE_HEIGHT - }); - } - let y = PAGE_HEIGHT - margens.top; - while (currentPar < paragrafos.length) { - let indent = paragrafos[currentPar].startsWith('§§INDENT§§'); - let center = paragrafos[currentPar].startsWith('§§CENTER§§'); - let align: 'center' | 'justify' | 'left' = center ? 'center' : 'justify'; - let text = paragrafos[currentPar].replace(/^§§INDENT§§/, '').replace(/^§§CENTER§§/, '').trim(); - - // Se o parágrafo for centralizado, força alinhamento center e ignora recuo - if (center) { - indent = false; - align = 'center'; - } - - text = decodeHtmlEntities(text); - - let x = margens.left; - let parWidth = MAX_WIDTH; - - let yAfter = renderInlineFormattedParagraph( - page, - text, - x, - y, - font, - fontBold, - fontItalic, - fontBoldItalic, - FONT_SIZE, - rgb(0, 0, 0), - align, - parWidth, - indent, - margens.top, - margens.bottom, - PAGE_HEIGHT - ); - if (yAfter === null) { - pageIndex++; - break; - } - y = yAfter - FONT_SIZE * 0.5; - currentPar++; - if (y < margens.bottom + FONT_SIZE * 1.5) break; - } - pageIndex++; - } - - 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; - }); - } -} diff --git a/src/server/services/PdfService copy 3.ts b/src/server/services/PdfService copy 3.ts deleted file mode 100644 index cdec045..0000000 --- a/src/server/services/PdfService copy 3.ts +++ /dev/null @@ -1,219 +0,0 @@ -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() }); - } - // Remove última página se estiver vazia (sem conteúdo visível) - if (pdfDoc.getPageCount() > 1) { - const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1); - const textContent = lastPage.getTextContent ? await lastPage.getTextContent() : null; - // pdf-lib não tem getTextContent, então alternativa: verifica se altura do conteúdo é mínima - if (lastPage.getHeight && lastPage.getWidth && lastPage.getHeight() > 0 && lastPage.getWidth() > 0) { - // Heurística: verifica se a página tem apenas o timbrado (sem outros objetos) - if (lastPage.node.Contents().size() === 0) { - pdfDoc.removePage(pdfDoc.getPageCount() - 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'); - } - } -} \ No newline at end of file diff --git a/src/server/services/PdfService copy.ts b/src/server/services/PdfService copy.ts index 7e21f14..4f61f2d 100644 --- a/src/server/services/PdfService copy.ts +++ b/src/server/services/PdfService copy.ts @@ -1,47 +1,53 @@ 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 { - // Buscar configuração ou usar valores padrão const config = await prisma.configuracaoDocumento.findFirst({ orderBy: { criadoEm: 'desc' }, select: { id: true, - headerImageUrl: true, - footerImageUrl: true, - logoUrl: 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 + // 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 { - cabecalhoHtml: config?.cabecalhoHtml || ` -
-

{{escritorio.nome}}

-
- `, - rodapeHtml: config?.rodapeHtml || ` -
- {{escritorio.endereco}}
- Tel: {{escritorio.telefone}} | Email: {{escritorio.email}} -
- `, - logoUrl: config?.logoUrl || '', timbradoUrl: config?.timbradoUrl || '', - timbradoMarginTop: config?.timbradoMarginTop || 100, - timbradoMarginBottom: config?.timbradoMarginBottom || 50, - timbradoMarginLeft: config?.timbradoMarginLeft || 50, - timbradoMarginRight: config?.timbradoMarginRight || 50 + 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); @@ -49,13 +55,19 @@ export class PdfService { } } + // Gera HTML com timbrado e margens private gerarHTML(content: string, config: any) { - const timbradoUrl = config.timbradoUrl - ? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}` + // 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}` : ''; - - console.log('Gerando PDF com timbrado:', timbradoUrl); - + // Adiciona log para debug + if (timbradoUrl) { + console.log('[PDF] Timbrado inserido no HTML:', timbradoUrl); + } return ` @@ -63,91 +75,162 @@ export class PdfService { -

-
- ${content} -
+ ${timbradoUrl ? '
' : ''} +
+ ${cleanContent}
`; } - async gerarPDF(conteudo: string, escritorio: any) { + // Gera PDF a partir do HTML, depois sobrepõe o timbrado em cada página + async gerarPDF(conteudo: string) { try { - const config = await prisma.configuracaoDocumento.findFirst({ - orderBy: { criadoEm: 'desc' } - }); - - console.log('Configuração do PDF:', config); - - const html = this.gerarHTML(conteudo, config); - - const browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox'] - }); - + 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.setViewport({ - width: 794, // A4 width in pixels (72 dpi) - height: 1123, - deviceScaleFactor: 2 - }); - - await page.setContent(html, { + 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: { top: '0', right: '0', bottom: '0', left: '0' }, + margin, preferCSSPageSize: true }); - await browser.close(); - return Buffer.from(pdfBuffer); + // 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'); } } -} +} \ No newline at end of file diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 4f61f2d..aed0324 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -198,32 +198,27 @@ export class PdfService { 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) + let pageCountBefore = pdfDoc.getPageCount(); + // Remove páginas em branco no final do PDF (heurística baseada no tamanho do conteúdo) + while (pageCountBefore > 1) { 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) { + const contentStreams = (lastPage as any).node.Contents(); + // Se não houver conteúdo, é uma página em branco + if (!contentStreams || contentStreams.length === 0) { pdfDoc.removePage(pageCountBefore - 1); + pageCountBefore = pdfDoc.getPageCount(); + continue; + } + // Soma o tamanho dos streams de conteúdo + const totalLength = contentStreams + .map((stream: any) => stream.contents.length) + .reduce((acc: number, len: number) => acc + len, 0); + // Se o conteúdo for muito pequeno (< 30 bytes), considera em branco + if (totalLength < 30) { + pdfDoc.removePage(pageCountBefore - 1); + pageCountBefore = pdfDoc.getPageCount(); + } else { + break; } } const mergedPdf = await pdfDoc.save();