safeersoft-pdf-reporter
v1.0.5
Published
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
Downloads
74
Maintainers
Readme
safeersoft-pdf-reporter
A framework-agnostic TypeScript library for generating PDF reports with chunking, merging, S3 upload, and email delivery capabilities. Built with Puppeteer and pdf-lib for high-performance PDF generation.
Features
- 🚀 High Performance: Concurrent chunk processing with configurable limits and memory optimization
- 📊 Rich Templates: Built-in templates with RTL support, images, charts, and custom styling
- 🔧 Framework Agnostic: Works with any Node.js framework (Express, NestJS, Fastify, Koa) or standalone scripts
- ☁️ S3 Integration: Optional upload to Amazon S3 with presigned URLs and custom ACLs
- 📧 Email Delivery: Send PDFs as attachments or download links with customizable templates
- 🎨 Template System: Pluggable templates with custom compilers, CSS injection, and dynamic content
- 🔀 PDF Operations: Merge, split, extract pages, analyze, and validate existing PDFs
- 💾 Memory Efficient: Sequential processing to handle datasets of any size (tested with 100k+ records)
- 🛡️ Type Safe: Full TypeScript support with comprehensive type definitions and IntelliSense
- 🪝 Extensible: Hook system for custom processing logic, data transformation, and workflow integration
- 📈 Performance Monitoring: Built-in estimation, timing, and resource usage tracking
- 🌐 Internationalization: Multi-language support with RTL text, custom fonts, and localization helpers
- 🔒 Security: Input validation, sanitization, and secure file handling
- 🐛 Error Handling: Comprehensive error types with detailed context and recovery suggestions
Quick Start
Installation
npm install safeersoft-pdf-reporter
# Optional peer dependencies
npm install @aws-sdk/client-s3 nodemailer # For S3 and email featuresBasic Usage
import { generatePdf } from 'safeersoft-pdf-reporter';
const result = await generatePdf({
title: 'Sales Report',
data: [
{ name: 'John Doe', sales: 1200, region: 'North', commission: 0.15, lastSale: '2024-01-15' },
{ name: 'Jane Smith', sales: 1500, region: 'South', commission: 0.18, lastSale: '2024-01-20' },
{ name: 'Bob Johnson', sales: 980, region: 'East', commission: 0.12, lastSale: '2024-01-18' },
],
columns: [
{ key: 'name', title: 'Sales Rep', dataIndex: 'name', flex: 3 },
{ key: 'sales', title: 'Sales ($)', dataIndex: 'sales', flex: 2, type: 'currency' },
{ key: 'region', title: 'Region', dataIndex: 'region', flex: 2 },
{ key: 'commission', title: 'Commission', dataIndex: 'commission', flex: 2, type: 'percentage' },
{ key: 'lastSale', title: 'Last Sale', dataIndex: 'lastSale', flex: 2, type: 'date' },
],
userInfo: {
companyName: 'Acme Corporation',
companyLogo: 'https://example.com/logo.png',
name: 'Report Generator',
email: '[email protected]',
},
});
console.log(`Generated PDF: ${result.sizeBytes} bytes, ${result.pageCount} pages`);
console.log(`File saved as: ${result.fileName}`);
// result.buffer contains the PDF data - can be saved to file or sent as responseAdvanced Configuration
import { generatePdf, consoleLogger } from 'safeersoft-pdf-reporter';
const result = await generatePdf({
title: 'Comprehensive Sales Analysis',
data: salesData,
columns: salesColumns,
// Company branding and information
userInfo: {
companyName: 'Acme Corporation',
companyLogo: 'https://cdn.example.com/logo.png',
name: 'Sales Analytics Team',
email: '[email protected]',
phone: '+1-555-0123',
address: '123 Business St, Suite 100, City, State 12345',
},
// Header information sections
infoSection: [
{ label: 'Report Period', value: 'Q1 2024' },
{ label: 'Generated By', value: 'Sales Analytics System' },
{ label: 'Department', value: 'Sales & Marketing' },
{ label: 'Confidentiality', value: 'Internal Use Only' },
],
// Internationalization and localization
locale: 'en-US',
translationFn: (key) => translations[key] || key,
// Performance optimization
chunking: {
enabled: true,
chunkSize: 150,
maxConcurrency: 3,
},
// PDF formatting options
pdf: {
format: 'A4',
orientation: 'portrait',
margin: {
top: '15mm',
bottom: '15mm',
left: '10mm',
right: '10mm',
},
displayHeaderFooter: true,
printBackground: true,
},
// Advanced browser configuration
puppeteer: {
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--max-old-space-size=4096',
],
timeout: 30000,
},
// Custom logging
logging: consoleLogger,
// Overall timeout
timeoutMs: 300000, // 5 minutes
});With S3 Upload
import { generatePdf } from 'safeersoft-pdf-reporter';
const result = await generatePdf({
title: 'Quarterly Financial Report',
data: financialData,
columns: financialColumns,
s3: {
bucket: 'company-reports-bucket',
region: 'us-east-1',
folder: 'quarterly-reports/2024/Q1',
// Optional: Provide credentials (or use IAM roles/environment variables)
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
// File access control
acl: 'private', // 'public-read', 'private', 'authenticated-read'
// Custom metadata
metadata: {
department: 'finance',
quarter: 'Q1-2024',
confidentiality: 'internal',
},
// Cache control for CDN
cacheControl: 'max-age=86400', // 24 hours
// Content disposition for downloads
contentDisposition: 'attachment; filename="Q1-2024-Financial-Report.pdf"',
},
});
console.log(`PDF uploaded to S3: ${result.s3?.url}`);
console.log(`S3 Key: ${result.s3?.key}`);
console.log(`Bucket: ${result.s3?.bucket}`);
console.log(`Presigned URL (24h): ${result.s3?.presignedUrl}`);With Email Delivery
import { generatePdf } from 'safeersoft-pdf-reporter';
const result = await generatePdf({
title: 'Monthly Performance Report',
data: performanceData,
columns: performanceColumns,
email: {
to: ['[email protected]', '[email protected]'],
cc: ['[email protected]'],
bcc: ['[email protected]'],
from: '[email protected]',
replyTo: '[email protected]',
subject: 'Monthly Performance Report - {{date}}',
// Email template with dynamic content
htmlTemplate: `
<h2>Monthly Performance Report</h2>
<p>Dear {{recipientName}},</p>
<p>Please find attached the monthly performance report for {{period}}.</p>
<p><strong>Report Summary:</strong></p>
<ul>
<li>Total Records: {{recordCount}}</li>
<li>Report Pages: {{pageCount}}</li>
<li>Generated: {{generatedDate}}</li>
</ul>
<p>Best regards,<br>Analytics Team</p>
`,
// Plain text fallback
textTemplate: `
Monthly Performance Report
Dear {{recipientName}},
Please find attached the monthly performance report for {{period}}.
Report Summary:
- Total Records: {{recordCount}}
- Report Pages: {{pageCount}}
- Generated: {{generatedDate}}
Best regards,
Analytics Team
`,
// Attachment mode: 'attachment' (default) or 'link' (S3 URL)
attachmentMode: 'attachment',
// SMTP configuration
smtp: {
host: 'smtp.company.com',
port: 587,
secure: false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
// Advanced SMTP options
connectionTimeout: 10000,
greetingTimeout: 5000,
socketTimeout: 10000,
// TLS options
tls: {
rejectUnauthorized: false,
ciphers: 'SSLv3',
},
},
// Custom template variables
templateVariables: {
recipientName: 'Team',
period: 'January 2024',
generatedDate: new Date().toLocaleDateString(),
},
// Email priority
priority: 'normal', // 'high', 'normal', 'low'
// Custom headers
headers: {
'X-Report-Type': 'performance',
'X-Department': 'analytics',
},
},
});
console.log(`Email sent: ${result.email?.sent}`);
console.log(`Message ID: ${result.email?.messageId}`);
console.log(`Recipients: ${result.email?.recipients?.join(', ')}`);Advanced Usage
Large Dataset Processing
For large datasets, the library automatically chunks data for optimal performance:
import { generateOptimizedPdf, estimatePdfGeneration } from 'safeersoft-pdf-reporter';
// Get comprehensive performance estimates
const estimate = await estimatePdfGeneration({
data: largeDataset, // 10,000+ rows
columns: reportColumns,
options: {
template: 'modern-business',
chunking: {
chunkSize: 200,
maxConcurrency: 4,
},
},
});
console.log(`📊 Performance Estimation:`);
console.log(` ⏱️ Estimated time: ${estimate.estimatedTimeMs}ms (${(estimate.estimatedTimeMs / 1000).toFixed(1)}s)`);
console.log(` 💾 Estimated memory: ${estimate.estimatedMemoryMB}MB`);
console.log(` 📄 Estimated pages: ${estimate.estimatedPages}`);
console.log(` 🧩 Recommended chunks: ${estimate.chunking.chunks}`);
console.log(` ⚡ Concurrency: ${estimate.chunking.maxConcurrency}`);
console.log(` 📋 Recommendations:`, estimate.recommendations);
// Generate with intelligent optimization
const result = await generateOptimizedPdf({
title: 'Large Dataset Analysis Report',
data: largeDataset,
columns: reportColumns,
// Automatic chunking optimization
chunking: {
chunkSize: estimate.chunking.rowsPerChunk,
maxConcurrency: estimate.chunking.maxConcurrency,
enabled: true,
},
// Performance monitoring hooks
hooks: {
beforeChunkRender: async (params) => {
console.log(`🔄 Processing chunk ${params.chunkInfo?.current}/${params.chunkInfo?.total}`);
console.log(` 📊 Rows: ${params.data.length}`);
},
afterChunkRender: async (buffer, params) => {
const sizeMB = (buffer.length / 1024 / 1024).toFixed(2);
console.log(`✅ Chunk ${params.chunkIndex + 1} completed: ${sizeMB}MB`);
},
beforeMerge: async (chunks) => {
const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
console.log(`🔗 Merging ${chunks.length} chunks (${(totalSize / 1024 / 1024).toFixed(2)}MB total)`);
},
afterMerge: async (finalPdf) => {
console.log(`🎉 Merge completed: ${(finalPdf.length / 1024 / 1024).toFixed(2)}MB final size`);
},
},
});
console.log(`📈 Final Results:`);
console.log(` 📄 Pages: ${result.pageCount}`);
console.log(` 💾 Size: ${(result.sizeBytes / 1024 / 1024).toFixed(2)}MB`);
console.log(` ⏱️ Time: ${result.processingTime}ms`);Advanced Template System
Create sophisticated custom templates with dynamic content:
import { registerTemplate, generatePdf } from 'safeersoft-pdf-reporter';
// Register a comprehensive business report template
registerTemplate('executive-dashboard', (params) => {
const { title, data, columns, userInfo, translationFn: t = k => k } = params;
// Calculate summary statistics
const totalRevenue = data.reduce((sum, row) => sum + (row.revenue || 0), 0);
const avgRevenue = totalRevenue / data.length;
const topPerformer = data.sort((a, b) => (b.revenue || 0) - (a.revenue || 0))[0];
// Group data by categories
const byRegion = data.reduce((acc, row) => {
const region = row.region || 'Unknown';
acc[region] = (acc[region] || 0) + (row.revenue || 0);
return acc;
}, {});
const html = `
<!DOCTYPE html>
<html lang="${params.locale || 'en'}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 25px 50px rgba(0,0,0,0.15);
overflow: hidden;
max-width: 1200px;
margin: 0 auto;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
text-align: center;
position: relative;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image:
radial-gradient(circle at 25% 25%, rgba(255,255,255,0.1) 2px, transparent 2px),
radial-gradient(circle at 75% 75%, rgba(255,255,255,0.1) 2px, transparent 2px);
background-size: 50px 50px;
animation: float 20s infinite linear;
}
@keyframes float {
0% { transform: translate(0, 0); }
100% { transform: translate(-50px, -50px); }
}
.header h1 {
font-size: 2.8em;
font-weight: 700;
margin-bottom: 10px;
position: relative;
z-index: 1;
}
.header .subtitle {
font-size: 1.2em;
opacity: 0.9;
position: relative;
z-index: 1;
}
.company-info {
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
position: relative;
z-index: 1;
}
.company-logo {
width: 60px;
height: 60px;
margin-right: 15px;
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.3);
}
.executive-summary {
padding: 40px;
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-bottom: 1px solid #dee2e6;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 25px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
border: 1px solid #e9ecef;
text-align: center;
transition: transform 0.3s ease;
}
.summary-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
.summary-value {
font-size: 2.5em;
font-weight: 700;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.summary-label {
color: #666;
font-weight: 600;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 40px;
}
.section-title {
font-size: 1.8em;
font-weight: 600;
color: #495057;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 3px solid #667eea;
position: relative;
}
.section-title::after {
content: '';
position: absolute;
bottom: -3px;
left: 0;
width: 50px;
height: 3px;
background: #764ba2;
}
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
margin: 20px 0;
}
.data-table th {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px 15px;
text-align: left;
font-weight: 600;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.data-table td {
padding: 18px 15px;
border-bottom: 1px solid #f1f3f4;
transition: background-color 0.3s ease;
}
.data-table tr:hover td {
background-color: #f8f9fa;
}
.data-table tr:last-child td {
border-bottom: none;
}
.region-chart {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin: 20px 0;
}
.region-item {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
text-align: center;
border: 1px solid #e9ecef;
}
.region-name {
font-weight: 600;
color: #495057;
margin-bottom: 8px;
}
.region-value {
font-size: 1.4em;
font-weight: 700;
color: #667eea;
}
.footer-info {
background: #f8f9fa;
padding: 30px 40px;
border-top: 1px solid #dee2e6;
color: #666;
text-align: center;
font-size: 0.9em;
}
.currency { color: #28a745; font-weight: 600; }
.percentage { color: #007bff; font-weight: 600; }
.date { color: #6c757d; }
.highlight { background: linear-gradient(135deg, #fff3cd, #ffeaa7); padding: 2px 8px; border-radius: 4px; }
@media print {
body { background: white; padding: 0; }
.container { box-shadow: none; border-radius: 0; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="company-info">
${userInfo?.companyLogo ? `<img src="${userInfo.companyLogo}" alt="Company Logo" class="company-logo">` : ''}
<div>
<h1>${title}</h1>
<div class="subtitle">${userInfo?.companyName || 'Executive Dashboard'}</div>
</div>
</div>
</div>
<div class="executive-summary">
<h2 style="text-align: center; margin-bottom: 30px; color: #495057;">Executive Summary</h2>
<div class="summary-grid">
<div class="summary-card">
<div class="summary-value">${data.length.toLocaleString()}</div>
<div class="summary-label">${t('Total Records')}</div>
</div>
<div class="summary-card">
<div class="summary-value">$${(totalRevenue / 1000).toFixed(0)}K</div>
<div class="summary-label">${t('Total Revenue')}</div>
</div>
<div class="summary-card">
<div class="summary-value">$${(avgRevenue / 1000).toFixed(1)}K</div>
<div class="summary-label">${t('Average Revenue')}</div>
</div>
<div class="summary-card">
<div class="summary-value">${Object.keys(byRegion).length}</div>
<div class="summary-label">${t('Active Regions')}</div>
</div>
</div>
${topPerformer ? `
<div style="text-align: center; padding: 20px; background: white; border-radius: 12px; box-shadow: 0 6px 20px rgba(0,0,0,0.08);">
<h3 style="color: #495057; margin-bottom: 10px;">🏆 Top Performer</h3>
<div style="font-size: 1.3em; font-weight: 600; color: #667eea;">${topPerformer.name || 'Unknown'}</div>
<div style="color: #28a745; font-weight: 600;">$${(topPerformer.revenue || 0).toLocaleString()}</div>
</div>
` : ''}
</div>
<div class="content">
<div class="section">
<h2 class="section-title">${t('Performance by Region')}</h2>
<div class="region-chart">
${Object.entries(byRegion).map(([region, revenue]) => `
<div class="region-item">
<div class="region-name">${region}</div>
<div class="region-value">$${((revenue as number) / 1000).toFixed(0)}K</div>
</div>
`).join('')}
</div>
</div>
<div class="section">
<h2 class="section-title">${t('Detailed Performance Data')}</h2>
<table class="data-table">
<thead>
<tr>
${columns.map(col => `<th>${col.title}</th>`).join('')}
</tr>
</thead>
<tbody>
${data.slice(0, 50).map(row => `
<tr>
${columns.map(col => {
let value = row[col.dataIndex];
let className = '';
if (col.type === 'currency' && typeof value === 'number') {
value = `$${value.toLocaleString()}`;
className = 'currency';
} else if (col.type === 'percentage' && typeof value === 'number') {
value = `${(value * 100).toFixed(1)}%`;
className = 'percentage';
} else if (col.type === 'date') {
className = 'date';
}
return `<td class="${className}">${value || ''}</td>`;
}).join('')}
</tr>
`).join('')}
</tbody>
</table>
${data.length > 50 ? `
<div style="text-align: center; margin-top: 20px; color: #666; font-style: italic;">
Showing first 50 of ${data.length.toLocaleString()} total records
</div>
` : ''}
</div>
</div>
<div class="footer-info">
<div><strong>${t('Generated')}:</strong> ${new Date().toLocaleString()}</div>
<div><strong>${t('Report Type')}:</strong> Executive Dashboard</div>
<div><strong>${t('Confidentiality')}:</strong> Internal Use Only</div>
${userInfo?.name ? `<div><strong>${t('Prepared by')}:</strong> ${userInfo.name}</div>` : ''}
</div>
</div>
</body>
</html>
`;
return {
html,
header: `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 10px 20px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; font-size: 10px;">
<div><strong>${title}</strong></div>
<div>Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>
<div>${new Date().toLocaleDateString()}</div>
</div>
`,
footer: `
<div style="text-align: center; padding: 8px; font-size: 9px; color: #666; border-top: 1px solid #eee;">
Confidential - Generated by Safeersoft PDF Reporter | ${userInfo?.companyName || 'Company Name'}
</div>
`,
};
});
// Use the advanced template
const result = await generatePdf({
title: 'Q1 2024 Executive Dashboard',
data: executiveData,
columns: executiveColumns,
template: 'executive-dashboard',
locale: 'en-US',
userInfo: {
companyName: 'TechCorp Industries',
companyLogo: 'https://cdn.company.com/logo.png',
name: 'Executive Analytics Team',
email: '[email protected]',
},
translationFn: (key) => {
const translations = {
'Total Records': 'Total Records',
'Total Revenue': 'Total Revenue',
'Average Revenue': 'Average Revenue',
'Active Regions': 'Active Regions',
'Performance by Region': 'Performance by Region',
'Detailed Performance Data': 'Detailed Performance Data',
'Generated': 'Generated',
'Report Type': 'Report Type',
'Confidentiality': 'Confidentiality',
'Prepared by': 'Prepared by',
};
return translations[key] || key;
},
});Create and register custom templates:
import { registerTemplate, generatePdf } from 'safeersoft-pdf-reporter';
// Register a custom template
registerTemplate('invoice', (params) => {
const { title, data, columns, userInfo } = params;
const html = `
<html>
<body>
<h1>${title}</h1>
<div>Company: ${userInfo?.companyName}</div>
<table>
<thead>
${columns.map(col => `<th>${col.title}</th>`).join('')}
</thead>
<tbody>
${data.map(row =>
`<tr>${columns.map(col => `<td>${row[col.dataIndex] || ''}</td>`).join('')}</tr>`
).join('')}
</tbody>
</table>
</body>
</html>
`;
return {
html,
header: `<div>${title} - Page <span class="pageNumber"></span></div>`,
footer: `<div>${new Date().toLocaleDateString()}</div>`,
};
});
// Use the custom template
const result = await generatePdf({
title: 'Invoice #12345',
data: invoiceItems,
columns: invoiceColumns,
template: 'invoice',
userInfo: {
companyName: 'Acme Corp',
},
});Comprehensive PDF Operations
Advanced PDF manipulation with detailed control:
import {
mergePdfs,
splitPdf,
extractPages,
getPdfInfo,
analyzePdfs,
validatePdfs
} from 'safeersoft-pdf-reporter';
// Comprehensive PDF analysis
const pdfBuffers = [pdf1Buffer, pdf2Buffer, pdf3Buffer];
const analysisResults = await analyzePdfs(pdfBuffers);
console.log('📊 PDF Analysis Results:');
analysisResults.forEach((result, index) => {
console.log(` 📄 PDF ${index + 1}:`);
console.log(` 📋 Pages: ${result.pageCount}`);
console.log(` 💾 Size: ${(result.fileSize / 1024).toFixed(1)}KB`);
console.log(` 📐 Dimensions: ${result.dimensions.width}x${result.dimensions.height}`);
console.log(` 🔒 Encrypted: ${result.isEncrypted ? 'Yes' : 'No'}`);
console.log(` ✅ Valid: ${result.isValid ? 'Yes' : 'No'}`);
console.log(` 📅 Created: ${result.creationDate}`);
console.log(` ✏️ Modified: ${result.modificationDate}`);
console.log(` 👤 Author: ${result.author || 'Unknown'}`);
console.log(` 🏷️ Title: ${result.title || 'Untitled'}`);
});
// Validate PDF integrity
const validationResults = await validatePdfs(pdfBuffers);
console.log('\n🔍 PDF Validation:');
validationResults.forEach((result, index) => {
console.log(` 📄 PDF ${index + 1}: ${result.isValid ? '✅ Valid' : '❌ Invalid'}`);
if (!result.isValid) {
console.log(` ⚠️ Issues: ${result.errors.join(', ')}`);
}
});
// Advanced merging with options
const mergedPdf = await mergePdfs(pdfBuffers, {
// Add bookmarks for each source PDF
addBookmarks: true,
bookmarkTitles: ['Financial Report', 'Sales Analysis', 'Marketing Summary'],
// Include metadata
metadata: {
title: 'Comprehensive Business Report',
author: 'Analytics Team',
subject: 'Q1 2024 Business Analysis',
keywords: 'finance, sales, marketing, quarterly',
creator: 'PDF Reporter Library',
},
// Page numbering options
addPageNumbers: true,
pageNumberStyle: 'Page {current} of {total}',
pageNumberPosition: 'bottom-center',
// Quality settings
optimize: true,
compressionLevel: 6, // 1-9, higher = more compression
logging: consoleLogger,
});
console.log('\n🔗 Merge Results:');
console.log(` 📄 Total pages: ${mergedPdf.pageCount}`);
console.log(` 💾 Final size: ${(mergedPdf.length / 1024).toFixed(1)}KB`);
// Intelligent page extraction
const largeReport = await generatePdf({
title: 'Large Business Report',
data: largeDataset,
columns: reportColumns,
});
// Extract executive summary (first 3 pages)
const executiveSummary = await extractPages(largeReport.buffer, [0, 1, 2], {
maintainBookmarks: true,
updatePageReferences: true,
addExtractedPageInfo: true,
metadata: {
title: 'Executive Summary - Large Business Report',
subject: 'Extracted Pages 1-3',
},
});
// Extract financial data (specific pages)
const financialPages = await extractPages(largeReport.buffer, [5, 6, 7, 8], {
addWatermark: {
text: 'FINANCIAL DATA - CONFIDENTIAL',
opacity: 0.1,
fontSize: 48,
color: '#FF0000',
rotation: 45,
},
});
// Split with advanced options
const individualPages = await splitPdf(largeReport.buffer, {
// Naming pattern for split files
filenamePattern: 'report-page-{pageNumber:03d}.pdf',
// Include metadata in each page
preserveMetadata: true,
// Add page-specific metadata
addPageMetadata: true,
// Optimize each page
optimize: true,
// Progress callback
onProgress: (current, total) => {
console.log(`📄 Splitting: ${current}/${total} pages processed`);
},
});
console.log('\n✂️ Split Results:');
console.log(` 📄 Created ${individualPages.length} individual page files`);
individualPages.forEach((page, index) => {
console.log(` Page ${index + 1}: ${(page.length / 1024).toFixed(1)}KB`);
});
// Batch processing example
const batchResults = await Promise.all([
// Process multiple reports in parallel
generatePdf({ title: 'Sales Report', data: salesData, columns: salesColumns }),
generatePdf({ title: 'Finance Report', data: financeData, columns: financeColumns }),
generatePdf({ title: 'HR Report', data: hrData, columns: hrColumns }),
]);
console.log('\n📊 Batch Processing Results:');
batchResults.forEach((result, index) => {
console.log(` Report ${index + 1}: ${result.pageCount} pages, ${(result.sizeBytes / 1024).toFixed(1)}KB`);
});
// Merge all batch results
const consolidatedReport = await mergePdfs(
batchResults.map(result => result.buffer),
{
addBookmarks: true,
bookmarkTitles: ['Sales Report', 'Finance Report', 'HR Report'],
addSectionDividers: true,
metadata: {
title: 'Consolidated Business Reports',
author: 'Business Intelligence Team',
},
}
);Hooks and Customization
Add custom processing logic with hooks:
import { generatePdf } from 'safeersoft-pdf-reporter';
const result = await generatePdf({
title: 'Custom Processing Report',
data: reportData,
columns: reportColumns,
hooks: {
beforeChunkRender: async (params) => {
console.log(`Processing chunk ${params.chunkInfo?.current} of ${params.chunkInfo?.total}`);
},
afterChunkRender: async (buffer, params) => {
console.log(`Chunk ${params.chunkIndex + 1} generated: ${buffer.length} bytes`);
},
transformHtml: (html, stage) => {
if (stage === 'pre') {
// Add watermark or modify HTML before PDF generation
return html.replace('<body>', '<body><div class="watermark">CONFIDENTIAL</div>');
}
return html;
},
transformColumnValue: (value, column, row) => {
if (column.dataIndex === 'price' && typeof value === 'number') {
return `$${value.toFixed(2)}`; // Format currency
}
return value;
},
},
});Configuration Options
PdfGenerationOptions
interface PdfGenerationOptions {
// Required
title: string;
data: any[];
columns: ColumnDefinition[];
// Optional
locale?: string; // 'en', 'ar', etc. for RTL support
userInfo?: UserInfo; // Company info, logos
infoSection?: InfoSection[]; // Header information sections
filteredColumnKeys?: string[]; // Columns to exclude
translationFn?: (key: string) => string; // i18n function
// Chunking
chunking?: {
enabled?: boolean;
chunkSize?: number; // Rows per chunk (default: 100)
maxConcurrency?: number; // Parallel chunks (default: 2)
};
// Templates
template?: string | TemplateCompilerFunction;
headerTemplate?: string | Function;
footerTemplate?: string | Function;
// S3 Upload
s3?: {
bucket: string;
region: string;
accessKeyId?: string; // Optional, uses AWS credential chain
secretAccessKey?: string;
folder?: string;
acl?: string; // Default: 'public-read'
};
// Email
email?: {
to: string;
from?: string;
subject?: string;
attachmentMode?: 'attachment' | 'link'; // Default: 'attachment'
smtp?: {
host: string;
port: number;
secure?: boolean;
auth: { user: string; pass: string; };
};
transport?: any; // Custom nodemailer transporter
};
// Advanced
hooks?: HooksConfig;
timeoutMs?: number; // Overall timeout (default: 300000)
logging?: LoggingAdapter; // Custom logger
puppeteer?: PuppeteerOptions; // Puppeteer configuration
pdf?: PdfOptions; // PDF format options
}Framework Integration Examples
Express.js
import express from 'express';
import { generatePdf } from 'safeersoft-pdf-reporter';
const app = express();
app.post('/reports/pdf', async (req, res) => {
try {
const { title, data, columns } = req.body;
const result = await generatePdf({
title,
data,
columns,
userInfo: {
companyName: 'My Company',
companyLogo: 'https://example.com/logo.png',
},
});
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${result.fileName}"`);
res.send(result.buffer);
} catch (error) {
res.status(500).json({ error: error.message });
}
});NestJS
import { Injectable } from '@nestjs/common';
import { generatePdf, PdfGenerationOptions } from 'safeersoft-pdf-reporter';
@Injectable()
export class ReportsService {
async generateReport(options: PdfGenerationOptions) {
return await generatePdf({
...options,
logging: {
debug: (msg, ...args) => console.debug(`[PDF] ${msg}`, ...args),
info: (msg, ...args) => console.log(`[PDF] ${msg}`, ...args),
warn: (msg, ...args) => console.warn(`[PDF] ${msg}`, ...args),
error: (msg, ...args) => console.error(`[PDF] ${msg}`, ...args),
},
});
}
}Fastify
import Fastify from 'fastify';
import { generatePdf } from 'safeersoft-pdf-reporter';
const fastify = Fastify();
fastify.post('/pdf-report', async (request, reply) => {
const { title, data, columns } = request.body;
const result = await generatePdf({ title, data, columns });
reply
.type('application/pdf')
.header('Content-Disposition', `attachment; filename="${result.fileName}"`)
.send(result.buffer);
});Performance Guidelines
Memory Management
- Large datasets: Use chunking with
chunkSize: 200andmaxConcurrency: 2-4 - Memory constraints: Reduce concurrency and chunk size
- Sequential processing: Set
maxConcurrency: 1for memory-limited environments
Optimization Tips
// For large datasets
const options = {
chunking: {
chunkSize: 200,
maxConcurrency: 3,
},
pdf: {
margin: { top: '10mm', bottom: '10mm', left: '5mm', right: '5mm' },
},
puppeteer: {
args: ['--max-old-space-size=4096'], // Increase heap size
},
};
// For speed over memory efficiency
const options = {
chunking: {
chunkSize: 500,
maxConcurrency: 5,
},
};Error Handling
import {
generatePdf,
PdfGenerationError,
TemplateError,
S3UploadError,
EmailError,
isPdfReporterError
} from 'safeersoft-pdf-reporter';
try {
const result = await generatePdf(options);
} catch (error) {
if (isPdfReporterError(error)) {
console.error(`PDF Reporter Error [${error.code}]:`, error.message);
if (error instanceof S3UploadError) {
console.error(`S3 Upload failed for bucket: ${error.bucket}`);
} else if (error instanceof EmailError) {
console.error(`Email failed for recipient: ${error.recipient}`);
}
} else {
console.error('Unexpected error:', error);
}
}Performance Optimization
Optimize PDF generation for large datasets and high-throughput scenarios:
import {
generatePdf,
analyzePdfPerformance,
optimizePdfGeneration,
createPerformanceProfile
} from 'safeersoft-pdf-reporter';
// Create performance profile for optimization
const performanceProfile = createPerformanceProfile({
maxMemoryUsage: '2GB',
cpuCores: 4,
concurrentProcesses: 2,
expectedPageCount: 50,
expectedDataSize: 10000,
complexityLevel: 'high',
});
// Optimized configuration for large datasets
const optimizedConfig = {
title: 'Large Financial Report',
data: financialData, // 100,000+ records
columns: reportColumns,
// Performance optimizations
chunking: {
enabled: true,
chunkSize: 2000,
concurrency: 2,
chunkTimeout: 30000,
memoryThreshold: 0.8,
},
// PDF optimizations
pdfOptions: {
compress: true,
optimization: {
images: { quality: 85, maxWidth: 1200 },
fonts: { subset: true, embed: false },
content: { removeUnusedObjects: true, compressStreams: true },
},
preferCSSPageSize: true,
generateTaggedPDF: false,
},
onProgress: (chunk, totalChunks, stats) => {
console.log(`📊 Processing chunk ${chunk}/${totalChunks}`);
console.log(` 💾 Memory usage: ${stats.memoryUsage.toFixed(1)}MB`);
},
};
const result = await generatePdf(optimizedConfig);
console.log(`📈 Generated ${result.pageCount} pages in ${result.processingTime}ms`);Security & Access Control
Implement security features for sensitive documents:
import { generatePdf, encryptPdf, addWatermark } from 'safeersoft-pdf-reporter';
const secureConfig = {
title: 'Confidential Financial Report',
data: sensitiveData,
columns: reportColumns,
security: {
encryption: {
userPassword: 'view-password-123',
ownerPassword: 'admin-password-456',
permissions: {
printing: 'lowResolution',
modifyContents: false,
copy: false,
},
},
watermark: {
text: 'CONFIDENTIAL - INTERNAL USE ONLY',
opacity: 0.15,
fontSize: 72,
color: '#FF0000',
rotation: 45,
},
},
};
const secureResult = await generatePdf(secureConfig);
console.log('🔒 Secure PDF generated with encryption and watermark');Internationalization
Support multiple languages and regions:
import { generatePdf, setLocale, formatCurrency, formatDate } from 'safeersoft-pdf-reporter';
// Configure Spanish locale
setLocale('es-ES');
const internationalConfig = {
title: 'Informe Financiero Q1 2024',
data: financialData,
columns: [
{
title: 'Fecha',
dataIndex: 'date',
format: (value) => formatDate(value, 'es-ES')
},
{
title: 'Cantidad',
dataIndex: 'amount',
format: (value) => formatCurrency(value, 'EUR', 'es-ES')
},
],
localization: {
locale: 'es-ES',
currency: 'EUR',
timezone: 'Europe/Madrid',
numberFormat: { decimal: ',', thousands: '.' },
},
};
const localizedResult = await generatePdf(internationalConfig);
console.log('🌍 Localized PDF generated for Spanish market');Troubleshooting
Common Issues
Browser not found
# Install Chromium
sudo apt-get install chromium-browser
# Or set custom path
export PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browserMemory issues with large datasets
const options = {
chunking: { chunkSize: 50, maxConcurrency: 1 },
puppeteer: { args: ['--max-old-space-size=2048'] },
};S3 permissions
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::your-bucket/*"
}
]
}Debug Logging
Enable detailed logging for troubleshooting:
import { generatePdf, consoleLogger, createCustomLogger } from 'safeersoft-pdf-reporter';
// Built-in console logger
const result = await generatePdf({
title: 'Debug Report',
data: debugData,
columns: debugColumns,
logging: consoleLogger,
});
// Custom logger with file output
const customLogger = createCustomLogger({
level: 'debug',
format: 'json',
file: './logs/pdf-generation.log',
console: true,
timestamp: true,
});
const resultWithCustomLog = await generatePdf({
title: 'Production Report',
data: productionData,
columns: productionColumns,
logging: customLogger,
});Performance Monitoring
Monitor and analyze PDF generation performance:
import {
generatePdf,
createPerformanceMonitor,
analyzePerformanceMetrics
} from 'safeersoft-pdf-reporter';
// Create performance monitor
const monitor = createPerformanceMonitor({
trackMemory: true,
trackCPU: true,
trackNetworkLatency: true,
sampleInterval: 1000, // 1 second
});
const result = await generatePdf({
title: 'Performance Monitored Report',
data: largeDataset,
columns: reportColumns,
// Performance monitoring hooks
hooks: {
beforeGeneration: async () => {
monitor.start();
console.log('📊 Starting performance monitoring');
},
afterGeneration: async (result) => {
const metrics = monitor.stop();
console.log('📈 Performance Metrics:');
console.log(` ⏱️ Total time: ${metrics.totalTime}ms`);
console.log(` 💾 Peak memory: ${metrics.peakMemory}MB`);
console.log(` 🔄 CPU usage: ${metrics.avgCpuUsage}%`);
console.log(` 📊 Throughput: ${metrics.recordsPerSecond} records/sec`);
// Analyze and suggest optimizations
const suggestions = analyzePerformanceMetrics(metrics);
console.log('💡 Optimization suggestions:', suggestions);
},
onMemoryWarning: async (usage) => {
console.warn(`⚠️ High memory usage detected: ${usage}MB`);
},
},
});Advanced Error Recovery
Implement robust error handling and recovery mechanisms:
import {
generatePdf,
PdfGenerationError,
RetryableError,
createRetryPolicy,
createFallbackStrategy
} from 'safeersoft-pdf-reporter';
// Create retry policy for transient errors
const retryPolicy = createRetryPolicy({
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000,
backoffFactor: 2,
retryCondition: (error) => error instanceof RetryableError,
});
// Create fallback strategy for critical failures
const fallbackStrategy = createFallbackStrategy({
// Fallback to simplified template
template: 'simple-table',
// Reduce data complexity
dataTransform: (data) => data.slice(0, 1000),
// Use minimal options
options: {
chunking: { chunkSize: 50, maxConcurrency: 1 },
pdf: { quality: 'low' },
},
});
async function robustPdfGeneration(config) {
try {
return await generatePdf(config);
} catch (error) {
console.error('🚨 Primary generation failed:', error.message);
if (retryPolicy.shouldRetry(error)) {
console.log('🔄 Retrying with exponential backoff...');
return await retryPolicy.execute(() => generatePdf(config));
}
if (fallbackStrategy.canHandle(error)) {
console.log('🛡️ Attempting fallback strategy...');
const fallbackConfig = fallbackStrategy.transform(config);
return await generatePdf(fallbackConfig);
}
// Final fallback: generate error report
console.log('📄 Generating error report...');
return await generatePdf({
title: 'PDF Generation Error Report',
data: [{ error: error.message, timestamp: new Date() }],
columns: [
{ title: 'Error', dataIndex: 'error' },
{ title: 'Timestamp', dataIndex: 'timestamp' },
],
template: 'simple-table',
});
}
}
// Usage
const result = await robustPdfGeneration({
title: 'Critical Business Report',
data: criticalData,
columns: criticalColumns,
});Production Deployment
Docker Configuration
# Multi-stage build for optimized production image
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS production
# Install Chromium and required dependencies
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
&& rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S pdfgen -u 1001
WORKDIR /app
# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# Set Puppeteer to use installed Chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
# Create directories for temp files
RUN mkdir -p /app/temp /app/logs && \
chown -R pdfgen:nodejs /app
USER pdfgen
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD node -e "console.log('Health check passed')" || exit 1
CMD ["node", "dist/server.js"]Kubernetes Deployment
# kubernetes-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: pdf-reporter-service
labels:
app: pdf-reporter
spec:
replicas: 3
selector:
matchLabels:
app: pdf-reporter
template:
metadata:
labels:
app: pdf-reporter
spec:
containers:
- name: pdf-reporter
image: your-registry/pdf-reporter:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: PDF_TEMP_DIR
value: "/tmp/pdf-generation"
- name: LOG_LEVEL
value: "info"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "2Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: temp-storage
mountPath: /tmp/pdf-generation
volumes:
- name: temp-storage
emptyDir:
sizeLimit: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: pdf-reporter-service
spec:
selector:
app: pdf-reporter
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancerEnvironment Configuration
# .env.production
NODE_ENV=production
PORT=3000
# PDF Generation Settings
PDF_TEMP_DIR=/tmp/pdf-generation
PDF_MAX_CONCURRENT_PROCESSES=4
PDF_MEMORY_LIMIT=2048
PDF_TIMEOUT=300000
# Browser Settings
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage
# AWS S3 Configuration
AWS_REGION=us-east-1
AWS_S3_BUCKET=company-reports
AWS_S3_PREFIX=pdf-reports
# Email Configuration
SMTP_HOST=smtp.company.com
SMTP_PORT=587
SMTP_SECURE=false
[email protected]
SMTP_PASS=your-smtp-password
# Monitoring and Logging
LOG_LEVEL=info
LOG_FORMAT=json
METRICS_ENABLED=true
HEALTH_CHECK_ENABLED=true
# Security
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=900000
CORS_ORIGINS=https://app.company.com,https://dashboard.company.comAdvanced Integration Patterns
Message Queue Integration
// Bull Queue integration for background processing
import Queue from 'bull';
import { generatePdf } from 'safeersoft-pdf-reporter';
const pdfQueue = new Queue('PDF generation', {
redis: { port: 6379, host: 'localhost' },
defaultJobOptions: {
removeOnComplete: 10,
removeOnFail: 5,
attempts: 3,
backoff: 'exponential',
},
});
pdfQueue.process('generate', async (job) => {
const { title, data, columns, options } = job.data;
// Update job progress
job.progress(10);
try {
const result = await generatePdf({
title,
data,
columns,
...options,
hooks: {
beforeChunkRender: async (params) => {
const progress = 10 + (params.chunkInfo.current / params.chunkInfo.total) * 80;
job.progress(Math.round(progress));
},
},
});
job.progress(100);
return result;
} catch (error) {
throw new Error(`PDF generation failed: ${error.message}`);
}
});
// Add job to queue
export async function enqueuePdfGeneration(config) {
const job = await pdfQueue.add('generate', config, {
priority: config.priority || 0,
delay: config.delay || 0,
});
return {
jobId: job.id,
status: 'queued',
estimatedCompletion: new Date(Date.now() + 30000),
};
}Microservices Architecture
// PDF Service with gRPC
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { generatePdf } from 'safeersoft-pdf-reporter';
class PdfService {
async GeneratePdf(call, callback) {
try {
const { title, data, columns, options } = call.request;
const result = await generatePdf({
title,
data: JSON.parse(data),
columns: JSON.parse(columns),
...JSON.parse(options || '{}'),
});
callback(null, {
success: true,
pdfBuffer: result.buffer,
pageCount: result.pageCount,
sizeBytes: result.sizeBytes,
processingTime: result.processingTime,
});
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
}
}
async EstimateGeneration(call, callback) {
try {
const estimate = await estimatePdfGeneration(call.request);
callback(null, estimate);
} catch (error) {
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
}
}
}
const server = new Server();
server.addService(PdfServiceDefinition, new PdfService());
server.bindAsync('0.0.0.0:50051', ServerCredentials.createInsecure(), () => {
console.log('PDF gRPC service running on port 50051');
server.start();
});Event-Driven Architecture
// Event-driven PDF generation with EventEmitter
import { EventEmitter } from 'events';
import { generatePdf } from 'safeersoft-pdf-reporter';
class PdfGenerationOrchestrator extends EventEmitter {
constructor() {
super();
this.setupEventHandlers();
}
setupEventHandlers() {
this.on('pdf:requested', this.handlePdfRequest.bind(this));
this.on('pdf:completed', this.handlePdfCompleted.bind(this));
this.on('pdf:failed', this.handlePdfFailed.bind(this));
}
async handlePdfRequest({ requestId, config }) {
try {
this.emit('pdf:started', { requestId });
const result = await generatePdf({
...config,
hooks: {
beforeChunkRender: (params) => {
this.emit('pdf:progress', {
requestId,
progress: (params.chunkInfo.current / params.chunkInfo.total) * 100,
});
},
},
});
this.emit('pdf:completed', { requestId, result });
} catch (error) {
this.emit('pdf:failed', { requestId, error });
}
}
async handlePdfCompleted({ requestId, result }) {
// Store result, send notifications, etc.
console.log(`✅ PDF ${requestId} completed: ${result.pageCount} pages`);
// Trigger downstream events
this.emit('notification:send', {
type: 'pdf_ready',
requestId,
metadata: result,
});
}
async handlePdfFailed({ requestId, error }) {
console.error(`❌ PDF ${requestId} failed:`, error.message);
// Trigger error handling
this.emit('error:handle', {
type: 'pdf_generation_failure',
requestId,
error,
});
}
}
// Usage
const orchestrator = new PdfGenerationOrchestrator();
orchestrator.on('pdf:progress', ({ requestId, progress }) => {
console.log(`📊 PDF ${requestId}: ${progress.toFixed(1)}% complete`);
});
orchestrator.emit('pdf:requested', {
requestId: 'req-12345',
config: {
title: 'Annual Report',
data: annualData,
columns: reportColumns,
},
});Testing Strategies
Unit Testing
// test/pdf-generation.test.ts
import { generatePdf, estimatePdfGeneration } from 'safeersoft-pdf-reporter';
import { mockBrowser } from './mocks/browser-mock';
describe('PDF Generation', () => {
beforeEach(() => {
mockBrowser.reset();
});
test('should generate PDF with basic configuration', async () => {
const result = await generatePdf({
title: 'Test Report',
data: [{ name: 'John', age: 30 }],
columns: [
{ title: 'Name', dataIndex: 'name' },
{ title: 'Age', dataIndex: 'age' },
],
});
expect(result.buffer).toBeInstanceOf(Buffer);
expect(result.pageCount).toBeGreaterThan(0);
expect(result.sizeBytes).toBeGreaterThan(0);
});
test('should handle large datasets with chunking', async () => {
const largeData = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Person ${i}`,
value: Math.random() * 1000,
}));
const result = await generatePdf({
title: 'Large Dataset Test',
data: largeData,
columns: [
{ title: 'ID', dataIndex: 'id' },
{ title: 'Name', dataIndex: 'name' },
{ title: 'Value', dataIndex: 'value' },
],
chunking: {
enabled: true,
chunkSize: 100,
maxConcurrency: 2,
},
});
expect(result.pageCount).toBeGreaterThan(5);
expect(result.processingTime).toBeGreaterThan(0);
});
test('should estimate generation performance accurately', async () => {
const testData = Array.from({ length: 500 }, (_, i) => ({ id: i }));
const estimate = await estimatePdfGeneration({
data: testData,
columns: [{ title: 'ID', dataIndex: 'id' }],
});
expect(estimate.estimatedTimeMs).toBeGreaterThan(0);
expect(estimate.estimatedMemoryMB).toBeGreaterThan(0);
expect(estimate.estimatedPages).toBeGreaterThan(0);
});
});Integration Testing
// test/integration/pdf-workflows.test.ts
import { generatePdf, mergePdfs, splitPdf } from 'safeersoft-pdf-reporter';
import fs from 'fs/promises';
import path from 'path';
describe('PDF Workflow Integration', () => {
const outputDir = path.join(__dirname, 'output');
beforeAll(async () => {
await fs.mkdir(outputDir, { recursive: true });
});
afterAll(async () => {
await fs.rmdir(outputDir, { recursive: true });
});
test('complete workflow: generate, split, merge', async () => {
// Generate initial PDF
const originalPdf = await generatePdf({
title: 'Workflow Test Report',
data: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Item ${i}` })),
columns: [
{ title: 'ID', dataIndex: 'id' },
{ title: 'Name', dataIndex: 'name' },
],
});
expect(originalPdf.pageCount).toBeGreaterThan(1);
// Split PDF into individual pages
const pages = await splitPdf(originalPdf.buffer);
expect(pages.length).toBe(originalPdf.pageCount);
// Merge selected pages back together
const selectedPages = pages.slice(0, 3);
const mergedPdf = await mergePdfs(selectedPages);
expect(mergedPdf.length).toBeGreaterThan(0);
// Save results for manual inspection
await fs.writeFile(
path.join(outputDir, 'original.pdf'),
originalPdf.buffer
);
await fs.writeFile(
path.join(outputDir, 'merged.pdf'),
mergedPdf
);
});
});Load Testing
// test/load/performance.test.ts
import { generatePdf } from 'safeersoft-pdf-reporter';
import { performance } from 'perf_hooks';
describe('Performance Load Tests', () => {
test('should handle concurrent PDF generation', async () => {
const concurrentRequests = 10;
const dataSize = 500;
const testData = Array.from({ length: dataSize }, (_, i) => ({
id: i,
name: `Person ${i}`,
email: `person${i}@example.com`,
value: Math.random() * 1000,
}));
const columns = [
{ title: 'ID', dataIndex: 'id' },
{ title: 'Name', dataIndex: 'name' },
{ title: 'Email', dataIndex: 'email' },
{ title: 'Value', dataIndex: 'value', type: 'currency' },
];
const startTime = performance.now();
const promises = Array.from({ length: concurrentRequests }, (_, i) =>
generatePdf({
title: `Concurrent Report ${i}`,
data: testData,
columns,
chunking: {
enabled: true,
chunkSize: 100,
maxConcurrency: 2,
},
})
);
const results = await Promise.all(promises);
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTime = totalTime / concurrentRequests;
console.log(`📊 Load Test Results:`);
console.log(` Total time: ${totalTime.toFixed(2)}ms`);
console.log(` Average time per PDF: ${avgTime.toFixed(2)}ms`);
console.log(` Total pages generated: ${results.reduce((sum, r) => sum + r.pageCount, 0)}`);
console.log(` Total size: ${(results.reduce((sum, r) => sum + r.sizeBytes, 0) / 1024 / 1024).toFixed(2)}MB`);
expect(results).toHaveLength(concurrentRequests);
expect(avgTime).toBeLessThan(30000); // Should complete within 30 seconds
});
});Real-World Usage Scenarios
Financial Reports
import { generatePdf, registerTemplate } from 'safeersoft-pdf-reporter';
// Register financial template with charts and KPIs
registerTemplate('financial-dashboard', (params) => {
const { data, title, userInfo } = params;
// Calculate financial metrics
const revenue = data.reduce((sum, item) => sum + (item.revenue || 0), 0);
const profit = data.reduce((sum, item) => sum + (item.profit || 0), 0);
const growthRate = ((revenue - params.previousRevenue) / params.previousRevenue) * 100;
return {
html: `
<!DOCTYPE html>
<html>
<head>
<style>
.financial-kpi {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin: 30px 0;
}
.kpi-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 25px;
border-radius: 15px;
text-align: center;
}
.kpi-value { font-size: 2.5em; font-weight: 700; }
.kpi-label { font-size: 0.9em; opacity: 0.9; }
.chart-container {
background: white;
padding: 30px;
border-radius: 15px;
margin: 20px 0;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="financial-header">
<h1>${title}</h1>
<p>Period: ${params.period} | Generated: ${new Date().toLocaleDateString()}</p>
</div>
<div class="financial-kpi">
<div class="kpi-card">
<div class="kpi-value">$${(revenue / 1000000).toFixed(1)}M</div>
<div class="kpi-label">Total Revenue</div>
</div>
<div class="kpi-card">
<div class="kpi-value">$${(profit / 1000000).toFixed(1)}M</div>
<div class="kpi-label">Net Profit</div>
</div>
<div class="kpi-card">
<div class="kpi-value">${growthRate.toFixed(1)}%</div>
<div class="kpi-label">Growth Rate</div>
</div>
<div class="kpi-card">
<div class="kpi-value">${((profit / revenue) * 100).toFixed(1)}%</div>
<div class="kpi-label">Profit Margin</div>
</div>
</div>
<div class="chart-container">
<h3>Revenue Breakdown by Department</h3>
<!-- Revenue chart would go here -->
</div>
<table>
<thead>
<tr>
<th>Department</th>
<th>Revenue</th>
<th>Profit</th>
<th>Margin</th>
<th>Growth</th>
</tr>
</thead>
<tbody>
${data.map(item => `
<tr>
<td>${item.department}</td>
<td>$${item.revenue?.toLocaleString()}</td>
<td>$${item.profit?.toLocaleString()}</td>
<td>${((item.profit / item.revenue) * 100).toFixed(1)}%</td>
<td style="color: ${item.growth > 0 ? '#28a745' : '#dc3545'}">
${item.growth > 0 ? '+' : ''}${item.growth?.toFixed(1)}%
</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>
`,
};
});
// Generate financial report
const financialReport = await generatePdf({
title: 'Q1 2024 Financial Dashboard',
data: financialData,
columns: financialColumns,
template: 'financial-dashboard',
userInfo: {
companyName: 'TechCorp Financial',
companyLogo: 'https://company.com/logo.png',
name: 'CFO Office',
},
// Auto-save to S3 and email to stakeholders
s3: {
bucket: 'financial-reports',
folder: 'quarterly/2024/Q1',
acl: 'private',
},
email: {
to: ['[email protected]', '[email protected]'],
subject: 'Q1 2024 Financial Dashboard',
attachmentMode: 'link', // Send S3 link instead of attachment
},
});Employee Performance Reports
// HR Performance template with employee photos and ratings
const hrPerformanceConfig = {
title: 'Annual Performance Review - 2024',
data: employeeData.map(emp => ({
...emp,
performanceScore: calculatePerformanceScore(emp),
ratingColor: getRatingColor(emp.rating),
photoUrl: `https://hr.company.com/photos/${emp.employeeId}.jpg`,
})),
columns: [
{ title: 'Employee', dataIndex: 'name', flex: 3 },
{ title: 'Department', dataIndex: 'department', flex: 2 },
{ title: 'Performance Score', dataIndex: 'performanceScore', flex: 2, type: 'percentage' },
{ title: 'Rating', dataIndex: 'rating', flex: 2 },
{ title: 'Goals Met', dataIndex: 'goalsMet', flex: 2, type: 'boolean' },
],
template: 'employee-performance',
// Advanced PDF security for confidential HR data
security: {
encryption: {
userPassword: generateSecurePassword(),
ownerPassword: process.env.HR_MASTER_PASSWORD,
permissions: {
printing: 'lowResolution',
modifyContents: false,
copy: false,
modifyAnnotations: false,
},
},
watermark: {
text: 'CONFIDENTIAL - HR USE ONLY',
opacity: 0.1,
fontSize: 72,
color: '#FF0000',
},
},
// Advanced chunking for large employee datasets
chunking: {
enabled: true,
chunkSize: 50, // 50 employees per chunk
maxConcurrency: 2,
chunkBy: 'department', // Group by department
},
};
const hrReport = await generatePdf(hrPerformanceConfig);Multi-Language Reports
// Internationalized report with RTL support
const multiLanguageConfig = {
title: 'تقرير المبيعات الشهري', // Arabic title
data: salesData,
columns: [
{ title: 'اسم العميل', dataIndex: 'customerName', flex: 3 }, // Arabic headers
{ title: 'المبلغ', dataIndex: 'amount', flex: 2, type: 'currency' },
{ title: 'التاريخ', dataIndex: 'date', flex: 2, type: 'date' },
{ title: 'المنطقة', dataIndex: 'region', flex: 2