UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

333 lines 13.5 kB
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; import { MergeError } from '../core/errors.js'; import { noOpLogger } from '../config/defaults.js'; import { Timer } from '../utils/timing.js'; /** * Service for merging PDF documents */ export class MergeService { constructor(logger = noOpLogger) { this.logger = logger; } /** * Merge multiple PDF buffers into a single PDF */ async mergePdfs(chunks, options = {}) { const timer = new Timer(); const { logging = this.logger } = options; if (!chunks || chunks.length === 0) { throw new MergeError('No PDF chunks provided for merging'); } if (chunks.length === 1) { logging.debug('Only one chunk provided, validating and returning'); const chunk = chunks[0]; if (!Buffer.isBuffer(chunk) || chunk.length === 0) { throw new MergeError('Invalid chunk: not a valid buffer'); } try { await PDFDocument.load(chunk); return chunk; } catch (error) { throw new MergeError(`Invalid PDF chunk: ${error instanceof Error ? error.message : 'Unknown error'}`, 1, error); } } logging.info(`Starting merge of ${chunks.length} PDF chunks`); try { const mergedPdf = await PDFDocument.create(); let totalPages = 0; // Process chunks sequentially to reduce memory usage for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; if (!Buffer.isBuffer(chunk) || chunk.length === 0) { throw new MergeError(`Invalid chunk at index ${i}: not a valid buffer`); } timer.mark(`chunk-${i}-start`); try { logging.debug(`Processing chunk ${i + 1}/${chunks.length} (${(chunk.length / 1024).toFixed(1)}KB)`); const pdf = await PDFDocument.load(chunk); const pageCount = pdf.getPageCount(); if (pageCount === 0) { logging.warn(`Chunk ${i + 1} has no pages, skipping`); continue; } const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices()); pages.forEach(page => mergedPdf.addPage(page)); totalPages += pageCount; timer.mark(`chunk-${i}-end`); const chunkTime = timer.elapsedSince(`chunk-${i}-start`); logging.debug(`Chunk ${i + 1} processed in ${chunkTime.toFixed(1)}ms (${pageCount} pages)`); } catch (error) { throw new MergeError(`Failed to process chunk ${i + 1}`, chunks.length, error); } } if (totalPages === 0) { throw new MergeError('No valid pages found in any chunks'); } timer.mark('save-start'); const mergedBuffer = Buffer.from(await mergedPdf.save()); timer.mark('save-end'); const totalTime = timer.elapsed(); const saveTime = timer.elapsedSince('save-start'); logging.info(`PDF merge completed: ${totalPages} pages, ` + `${(mergedBuffer.length / 1024 / 1024).toFixed(2)}MB, ` + `${totalTime.toFixed(1)}ms total (${saveTime.toFixed(1)}ms save)`); return mergedBuffer; } catch (error) { const elapsed = timer.elapsed(); if (error instanceof MergeError) { throw error; } throw new MergeError(`PDF merge failed after ${elapsed.toFixed(1)}ms: ${error instanceof Error ? error.message : 'Unknown error'}`, chunks.length, error); } } /** * Get information about PDF buffers without merging */ async analyzePdfs(chunks) { const chunkInfo = []; let totalPages = 0; let totalSize = 0; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; const sizeKB = Math.round(chunk.length / 1024); totalSize += chunk.length; try { if (!Buffer.isBuffer(chunk) || chunk.length === 0) { chunkInfo.push({ index: i, pages: 0, sizeKB, valid: false, error: 'Invalid buffer', }); continue; } const pdf = await PDFDocument.load(chunk); const pages = pdf.getPageCount(); totalPages += pages; chunkInfo.push({ index: i, pages, sizeKB, valid: true, }); } catch (error) { chunkInfo.push({ index: i, pages: 0, sizeKB, valid: false, error: error instanceof Error ? error.message : 'Unknown error', }); } } return { totalChunks: chunks.length, totalPages, totalSize, chunkInfo, }; } /** * Validate PDF buffers */ async validatePdfs(chunks) { const errors = []; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; if (!Buffer.isBuffer(chunk)) { errors.push({ chunkIndex: i, error: 'Not a buffer', }); continue; } if (chunk.length === 0) { errors.push({ chunkIndex: i, error: 'Empty buffer', }); continue; } try { const pdf = await PDFDocument.load(chunk); const pageCount = pdf.getPageCount(); if (pageCount === 0) { errors.push({ chunkIndex: i, error: 'PDF has no pages', }); } } catch (error) { errors.push({ chunkIndex: i, error: `Invalid PDF: ${error instanceof Error ? error.message : 'Unknown error'}`, }); } } return { valid: errors.length === 0, errors, }; } /** * Split a PDF into individual page buffers */ async splitPdf(pdfBuffer) { try { const pdf = await PDFDocument.load(pdfBuffer); const pageCount = pdf.getPageCount(); const pageBuffers = []; this.logger.info(`Splitting PDF into ${pageCount} individual pages`); for (let i = 0; i < pageCount; i++) { const newPdf = await PDFDocument.create(); const [copiedPage] = await newPdf.copyPages(pdf, [i]); newPdf.addPage(copiedPage); const pageBuffer = Buffer.from(await newPdf.save()); pageBuffers.push(pageBuffer); } this.logger.info(`PDF split completed: ${pageBuffers.length} pages`); return pageBuffers; } catch (error) { throw new MergeError(`Failed to split PDF: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error); } } /** * Extract specific pages from a PDF */ async extractPages(pdfBuffer, pageIndices) { try { const pdf = await PDFDocument.load(pdfBuffer); const totalPages = pdf.getPageCount(); // Validate page indices const validIndices = pageIndices.filter(index => index >= 0 && index < totalPages); if (validIndices.length === 0) { throw new MergeError('No valid page indices provided'); } const newPdf = await PDFDocument.create(); const pages = await newPdf.copyPages(pdf, validIndices); pages.forEach(page => newPdf.addPage(page)); const extractedBuffer = Buffer.from(await newPdf.save()); this.logger.info(`Extracted ${validIndices.length} pages from ${totalPages}-page PDF`); return extractedBuffer; } catch (error) { throw new MergeError(`Failed to extract pages: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error); } } /** * Get PDF metadata */ async getPdfInfo(pdfBuffer) { try { const pdf = await PDFDocument.load(pdfBuffer); const pages = pdf.getPageCount(); const sizeKB = Math.round(pdfBuffer.length / 1024); // Get metadata if available const title = pdf.getTitle(); const author = pdf.getAuthor(); const creator = pdf.getCreator(); const producer = pdf.getProducer(); const creationDate = pdf.getCreationDate(); const modificationDate = pdf.getModificationDate(); return { pages, sizeKB, ...(title && { title }), ...(author && { author }), ...(creator && { creator }), ...(producer && { producer }), ...(creationDate && { creationDate }), ...(modificationDate && { modificationDate }), }; } catch (error) { throw new MergeError(`Failed to get PDF info: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error); } } /** * Add page numbers to a PDF document */ async addPageNumbers(pdfBuffer, options = {}) { try { const pdfDoc = await PDFDocument.load(pdfBuffer); const pages = pdfDoc.getPages(); const totalPages = pages.length; // Load a standard font const font = await pdfDoc.embedFont(StandardFonts.Helvetica); const fontSize = 9; const color = rgb(0.5, 0.55, 0.6); // Similar to #7f8c8d const { userInfo = {}, isRTL = false } = options; const companyName = userInfo.name || 'Company'; const userName = userInfo.name || ''; const currentYear = new Date().getFullYear(); pages.forEach((page, index) => { const { width } = page.getSize(); const currentPage = index + 1; // Calculate positions for footer elements const footerY = 15; // Distance from bottom const separatorY = footerY + 30; // Line above footer const leftX = 20; const centerX = width / 2; const rightX = width - 20; // Clear any existing footer content by drawing a white rectangle page.drawRectangle({ x: 0, y: 0, width: width, height: 50, // Clear bottom 50px color: rgb(1, 1, 1), // White }); // Draw separator line above footer page.drawLine({ start: { x: leftX, y: separatorY }, end: { x: rightX, y: separatorY }, thickness: 0.5, color: rgb(0.8, 0.8, 0.8), // Light gray line }); // Left section: Generated by information const leftText = userName ? `Generated by ${userName}` : 'Generated by User'; const leftWidth = font.widthOfTextAtSize(leftText, fontSize); page.drawText(leftText, { x: isRTL ? rightX - leftWidth : leftX, y: footerY, size: fontSize, font, color, }); // Center section: Company and Page numbers const companyText = `${companyName}${currentYear}`; const pageText = `Page ${currentPage} of ${totalPages}`; // Draw page numbers line const pageWidth = font.widthOfTextAtSize(companyText, fontSize); page.drawText(companyText, { x: centerX - pageWidth / 2, y: footerY, size: fontSize, font, color, }); // Right section: const dateWidth = font.widthOfTextAtSize(pageText, fontSize); page.drawText(pageText, { x: isRTL ? leftX : rightX - dateWidth, y: footerY, size: fontSize, font, color, }); }); return Buffer.from(await pdfDoc.save()); } catch (error) { throw new MergeError(`Failed to add page numbers: ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, error); } } } //# sourceMappingURL=MergeService.js.map