safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
889 lines (789 loc) ⢠31.5 kB
JavaScript
/**
* Comprehensive PDF generation example using @safeersoft/@safeersoft/pdf-reporter
*
* This script demonstrates:
* - Basic PDF generation
* - Custom templates with advanced styling
* - Large dataset handling with chunking
* - PDF merging and manipulation
* - S3 upload integration
* - Email delivery
* - Performance monitoring
* - Error handling and recovery
* - Memory optimization
*/
const { writeFileSync, mkdirSync, existsSync } = require('fs');
const { join } = require('path');
const {
generatePdf,
generateOptimizedPdf,
mergePdfs,
splitPdf,
extractPages,
getPdfInfo,
analyzePdfs,
validatePdfs,
registerTemplate,
consoleLogger,
estimatePdfGeneration,
} = require('@safeersoft/@safeersoft/pdf-reporter');
// Sample data - in real usage, this would come from your database/API
const sampleData = [
{
id: 1,
name: 'Alice Johnson',
department: 'Engineering',
salary: 95000,
startDate: '2022-01-15',
active: true,
skills: ['JavaScript', 'React', 'Node.js'],
performance: 'Excellent',
location: 'New York',
},
{
id: 2,
name: 'Bob Smith',
department: 'Marketing',
salary: 78000,
startDate: '2021-06-10',
active: true,
skills: ['SEO', 'Content Marketing', 'Analytics'],
performance: 'Good',
location: 'Los Angeles',
},
{
id: 3,
name: 'Carol Davis',
department: 'Engineering',
salary: 102000,
startDate: '2020-03-22',
active: true,
skills: ['Python', 'Machine Learning', 'AWS'],
performance: 'Outstanding',
location: 'Seattle',
},
{
id: 4,
name: 'David Wilson',
department: 'Sales',
salary: 85000,
startDate: '2023-02-01',
active: false,
skills: ['CRM', 'Negotiation', 'Lead Generation'],
performance: 'Average',
location: 'Chicago',
},
{
id: 5,
name: 'Eva Brown',
department: 'HR',
salary: 72000,
startDate: '2021-11-30',
active: true,
skills: ['Recruitment', 'Employee Relations', 'Training'],
performance: 'Good',
location: 'Austin',
},
{
id: 6,
name: 'Frank Miller',
department: 'Engineering',
salary: 98000,
startDate: '2019-08-14',
active: true,
skills: ['Java', 'Spring Boot', 'Microservices'],
performance: 'Excellent',
location: 'San Francisco',
},
{
id: 7,
name: 'Grace Chen',
department: 'Marketing',
salary: 81000,
startDate: '2022-07-20',
active: true,
skills: ['Digital Marketing', 'Social Media', 'Brand Management'],
performance: 'Good',
location: 'Miami',
},
{
id: 8,
name: 'Henry Taylor',
department: 'Sales',
salary: 89000,
startDate: '2020-12-05',
active: true,
skills: ['B2B Sales', 'Account Management', 'Presentation'],
performance: 'Excellent',
location: 'Denver',
},
];
const columns = [
{ key: 'id', title: 'ID', dataIndex: 'id', flex: 1 },
{ key: 'name', title: 'Full Name', dataIndex: 'name', flex: 3 },
{ key: 'department', title: 'Department', dataIndex: 'department', flex: 2 },
{ key: 'salary', title: 'Annual Salary', dataIndex: 'salary', flex: 2, type: 'currency' },
{ key: 'startDate', title: 'Start Date', dataIndex: 'startDate', flex: 2 },
{ key: 'active', title: 'Active', dataIndex: 'active', type: 'boolean', flex: 1 },
{ key: 'performance', title: 'Performance', dataIndex: 'performance', flex: 2 },
{ key: 'location', title: 'Location', dataIndex: 'location', flex: 2 },
];
// Register an advanced custom template with charts and enhanced styling
registerTemplate('advanced-employee-report', params => {
const { title, data, columns, userInfo, translationFn: t = k => k } = params;
const formatCurrency = value => {
return typeof value === 'number' ? `$${value.toLocaleString()}` : value;
};
const formatBoolean = value => {
return value ? 'ā Active' : 'ā Inactive';
};
const getPerformanceColor = performance => {
const colors = {
Outstanding: '#28a745',
Excellent: '#17a2b8',
Good: '#ffc107',
Average: '#fd7e14',
Poor: '#dc3545',
};
return colors[performance] || '#6c757d';
};
const getDepartmentStats = () => {
const stats = {};
data.forEach(emp => {
if (!stats[emp.department]) {
stats[emp.department] = { count: 0, totalSalary: 0 };
}
stats[emp.department].count++;
stats[emp.department].totalSalary += emp.salary;
});
return stats;
};
const departmentStats = getDepartmentStats();
const renderRow = (item, index) => {
const bgColor = index % 2 === 0 ? '#f8f9fa' : '#ffffff';
const statusColor = item.active ? '#28a745' : '#dc3545';
const perfColor = getPerformanceColor(item.performance);
return `
<tr style="background-color: ${bgColor}; transition: background-color 0.2s;">
<td style="padding: 10px; border: 1px solid #e0e0e0; text-align: center; font-weight: bold; color: #495057;">${item.id}</td>
<td style="padding: 10px; border: 1px solid #e0e0e0;">
<div style="font-weight: 600; color: #212529;">${item.name}</div>
<div style="font-size: 11px; color: #6c757d; margin-top: 2px;">${item.location}</div>
</td>
<td style="padding: 10px; border: 1px solid #e0e0e0; text-align: center;">
<span style="background-color: #e9ecef; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500;">${item.department}</span>
</td>
<td style="padding: 10px; border: 1px solid #e0e0e0; text-align: right; font-weight: 600; color: #28a745;">${formatCurrency(item.salary)}</td>
<td style="padding: 10px; border: 1px solid #e0e0e0; text-align: center; font-family: monospace; font-size: 12px;">${item.startDate}</td>
<td style="padding: 10px; border: 1px solid #e0e0e0; text-align: center;">
<span style="color: ${statusColor}; font-weight: 600; font-size: 12px;">${formatBoolean(item.active)}</span>
</td>
<td style="padding: 10px; border: 1px solid #e0e0e0; text-align: center;">
<span style="background-color: ${perfColor}; color: white; padding: 2px 8px; border-radius: 8px; font-size: 11px; font-weight: 500;">${item.performance}</span>
</td>
</tr>
`;
};
const renderDepartmentChart = () => {
const departments = Object.keys(departmentStats);
const maxCount = Math.max(...Object.values(departmentStats).map(s => s.count));
return departments
.map(dept => {
const { count, totalSalary } = departmentStats[dept];
const percentage = (count / maxCount) * 100;
const avgSalary = totalSalary / count;
return `
<div style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
<span style="font-weight: 600; color: #495057;">${dept}</span>
<span style="font-size: 12px; color: #6c757d;">${count} employees</span>
</div>
<div style="background-color: #e9ecef; height: 20px; border-radius: 10px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #007acc, #0056b3); height: 100%; width: ${percentage}%; border-radius: 10px; transition: width 0.3s ease;"></div>
</div>
<div style="font-size: 11px; color: #6c757d; margin-top: 3px;">
Avg Salary: ${formatCurrency(Math.round(avgSalary))}
</div>
</div>
`;
})
.join('');
};
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${title}</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin: 0;
padding: 20px;
color: #212529;
line-height: 1.5;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #007acc 0%, #0056b3 100%);
color: white;
text-align: center;
padding: 40px 20px;
position: relative;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
opacity: 0.3;
}
.header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
position: relative;
z-index: 1;
}
.header p {
margin: 10px 0 0 0;
font-size: 1.1em;
opacity: 0.9;
position: relative;
z-index: 1;
}
.content {
padding: 30px;
}
.company-info {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 25px;
margin-bottom: 30px;
border-radius: 8px;
border-left: 5px solid #007acc;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.company-info h3 {
margin: 0 0 15px 0;
color: #007acc;
font-weight: 600;
font-size: 1.3em;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
border-top: 3px solid #007acc;
text-align: center;
}
.stat-value {
font-size: 2em;
font-weight: 700;
color: #007acc;
margin-bottom: 5px;
}
.stat-label {
color: #6c757d;
font-size: 0.9em;
font-weight: 500;
}
.department-analysis {
background: #f8f9fa;
padding: 25px;
border-radius: 8px;
margin-bottom: 30px;
}
.department-analysis h3 {
margin: 0 0 20px 0;
color: #495057;
font-weight: 600;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
th {
background: linear-gradient(135deg, #007acc 0%, #0056b3 100%);
color: white;
padding: 15px 10px;
text-align: left;
font-weight: 600;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.5px;
}
th:first-child { text-align: center; }
th:nth-child(4), th:nth-child(6), th:nth-child(7) { text-align: center; }
tr:hover {
background-color: #f1f3f5 !important;
}
.summary {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 25px;
border-radius: 8px;
margin-top: 30px;
border: 1px solid #dee2e6;
}
.summary h3 {
margin: 0 0 20px 0;
color: #495057;
font-weight: 600;
display: flex;
align-items: center;
}
.summary h3::before {
content: 'š';
margin-right: 10px;
font-size: 1.2em;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.summary-item {
background: white;
padding: 15px;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.summary-item strong {
color: #007acc;
font-weight: 600;
}
@media print {
body { background: white; }
.container { box-shadow: none; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${t(title)}</h1>
<p>Generated on ${new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}</p>
</div>
<div class="content">
${
userInfo?.companyName
? `
<div class="company-info">
<h3>š¢ ${userInfo.companyName}</h3>
${userInfo.name ? `<p><strong>Prepared by:</strong> ${userInfo.name}</p>` : ''}
${userInfo.email ? `<p><strong>Contact:</strong> ${userInfo.email}</p>` : ''}
</div>
`
: ''
}
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${data.length}</div>
<div class="stat-label">Total Employees</div>
</div>
<div class="stat-card">
<div class="stat-value">${data.filter(emp => emp.active).length}</div>
<div class="stat-label">Active Employees</div>
</div>
<div class="stat-card">
<div class="stat-value">${formatCurrency(
Math.round(data.reduce((sum, emp) => sum + emp.salary, 0) / data.length)
)}</div>
<div class="stat-label">Average Salary</div>
</div>
<div class="stat-card">
<div class="stat-value">${Object.keys(departmentStats).length}</div>
<div class="stat-label">Departments</div>
</div>
</div>
<div class="department-analysis">
<h3>š Department Distribution</h3>
${renderDepartmentChart()}
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Employee Details</th>
<th>Department</th>
<th>Annual Salary</th>
<th>Start Date</th>
<th>Status</th>
<th>Performance</th>
</tr>
</thead>
<tbody>
${data.map(renderRow).join('')}
</tbody>
</table>
<div class="summary">
<h3>Executive Summary</h3>
<div class="summary-grid">
<div class="summary-item">
<strong>Workforce Overview:</strong> ${data.length} total employees across ${Object.keys(departmentStats).length} departments
</div>
<div class="summary-item">
<strong>Employment Status:</strong> ${data.filter(emp => emp.active).length} active (${((data.filter(emp => emp.active).length / data.length) * 100).toFixed(1)}%)
</div>
<div class="summary-item">
<strong>Top Department:</strong> ${Object.entries(departmentStats).sort((a, b) => b[1].count - a[1].count)[0][0]} (${Object.entries(departmentStats).sort((a, b) => b[1].count - a[1].count)[0][1].count} employees)
</div>
<div class="summary-item">
<strong>Salary Range:</strong> ${formatCurrency(Math.min(...data.map(emp => emp.salary)))} - ${formatCurrency(Math.max(...data.map(emp => emp.salary)))}
</div>
</div>
</div>
</div>
</div>
</body>
</html>
`;
const header = `
<div style="background: linear-gradient(90deg, #007acc, #0056b3); color: white; text-align: center; padding: 10px; font-size: 12px; font-weight: 600;">
<span>${title}</span> - Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>
`;
const footer = `
<div style="background: #f8f9fa; text-align: center; padding: 8px; font-size: 10px; color: #6c757d; border-top: 1px solid #dee2e6;">
Generated by Safeersoft PDF Reporter | ${new Date().toLocaleDateString()} | Confidential Document
</div>
`;
return { html, header, footer };
});
async function main() {
console.log('š Safeersoft PDF Reporter - Comprehensive Examples\n');
// Create output directory
const outputDir = join(__dirname, 'output');
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
try {
// Example 1: Basic PDF generation with validation
console.log('š Example 1: Basic PDF Generation...');
const basicResult = await generatePdf({
title: 'Employee Report - Basic',
data: sampleData,
columns,
userInfo: {
companyName: 'Acme Corporation',
name: 'HR Department',
email: 'hr@acme.com',
},
logging: consoleLogger,
});
const basicPath = join(outputDir, 'employee-report-basic.pdf');
writeFileSync(basicPath, basicResult.buffer);
// Validate the generated PDF
const validation = await validatePdfs([basicResult.buffer]);
console.log(`ā
Basic report saved: ${basicPath}`);
console.log(
` š Pages: ${basicResult.pageCount}, Size: ${(basicResult.sizeBytes / 1024).toFixed(1)}KB`
);
console.log(` ā PDF Validation: ${validation.valid ? 'PASSED' : 'FAILED'}\n`);
// Example 2: Advanced custom template with styling
console.log('šØ Example 2: Advanced Custom Template...');
const advancedResult = await generatePdf({
title: 'Employee Report - Advanced Design',
data: sampleData,
columns,
template: 'advanced-employee-report',
userInfo: {
companyName: 'Acme Corporation',
name: 'HR Analytics Team',
email: 'analytics@acme.com',
},
infoSection: [
{ label: 'Report Type', value: 'Comprehensive Employee Analysis' },
{ label: 'Data Source', value: 'HR Management System v2.1' },
{ label: 'Confidentiality', value: 'Internal Use Only' },
{ label: 'Next Review', value: 'Q1 2026' },
],
logging: consoleLogger,
});
const advancedPath = join(outputDir, 'employee-report-advanced.pdf');
writeFileSync(advancedPath, advancedResult.buffer);
console.log(`ā
Advanced report saved: ${advancedPath}`);
console.log(
` š Pages: ${advancedResult.pageCount}, Size: ${(advancedResult.sizeBytes / 1024).toFixed(1)}KB\n`
);
// Example 3: Large dataset with chunking and optimization
console.log('ā” Example 3: Large Dataset Processing...');
// Generate realistic large dataset
const generateEmployee = i => ({
id: i + 1,
name: `Employee ${String(i + 1).padStart(4, '0')}`,
department: [
'Engineering',
'Marketing',
'Sales',
'HR',
'Finance',
'Operations',
'Legal',
'IT',
][i % 8],
salary: 45000 + Math.floor(Math.random() * 120000),
startDate: new Date(
2018 + Math.floor(Math.random() * 7),
Math.floor(Math.random() * 12),
Math.floor(Math.random() * 28) + 1
)
.toISOString()
.split('T')[0],
active: Math.random() > 0.05, // 95% active
skills: [
['JavaScript', 'React', 'Node.js', 'Python', 'AWS'],
['SEO', 'Content Marketing', 'Analytics', 'Social Media'],
['CRM', 'Negotiation', 'Lead Generation', 'B2B Sales'],
['Recruitment', 'Employee Relations', 'Training', 'Policy'],
['Accounting', 'Financial Analysis', 'Budgeting', 'Compliance'],
['Project Management', 'Process Improvement', 'Quality Assurance'],
['Contract Law', 'Compliance', 'Risk Management', 'Legal Research'],
['System Administration', 'Security', 'Network Management'],
][i % 8].slice(0, Math.floor(Math.random() * 3) + 2),
performance: ['Outstanding', 'Excellent', 'Good', 'Average'][Math.floor(Math.random() * 4)],
location: [
'New York',
'Los Angeles',
'Chicago',
'Houston',
'Phoenix',
'Philadelphia',
'San Antonio',
'San Diego',
][i % 8],
});
const largeData = Array.from({ length: 1000 }, (_, i) => generateEmployee(i));
// Performance estimation
const estimate = estimatePdfGeneration({
title: 'Large Employee Report',
data: largeData,
columns,
template: 'advanced-employee-report',
});
console.log(` š Performance Estimate:`);
console.log(` ā±ļø Time: ${(estimate.estimatedTimeMs / 1000).toFixed(1)}s`);
console.log(` š¾ Memory: ${estimate.estimatedMemoryMB.toFixed(1)}MB`);
console.log(` š Pages: ${estimate.estimatedPages}`);
console.log(
` š§© Chunks: ${estimate.chunking.chunks} (${estimate.chunking.rowsPerChunk} rows each)`
);
console.log(` š Concurrency: ${estimate.chunking.concurrency}`);
if (estimate.recommendations.length > 0) {
console.log(` š” Recommendations:`);
estimate.recommendations.forEach(rec => console.log(` ⢠${rec}`));
}
const optimizedResult = await generateOptimizedPdf({
title: 'Large Employee Dataset Report',
data: largeData,
columns,
template: 'advanced-employee-report',
userInfo: {
companyName: 'Acme Corporation',
name: 'Data Analytics Department',
email: 'data@acme.com',
},
infoSection: [
{ label: 'Dataset Size', value: `${largeData.length.toLocaleString()} records` },
{ label: 'Processing Method', value: 'Optimized Chunking' },
{ label: 'Data Quality', value: 'ā Validated' },
{ label: 'Export Format', value: 'PDF/A-1b' },
],
chunking: {
chunkSize: 100,
maxConcurrency: 3,
},
logging: consoleLogger,
});
const largePath = join(outputDir, 'employee-report-large.pdf');
writeFileSync(largePath, optimizedResult.buffer);
console.log(` ā
Large report saved: ${largePath}`);
console.log(` š Final Stats:`);
console.log(` š Pages: ${optimizedResult.pageCount}`);
console.log(` š¾ Size: ${(optimizedResult.sizeBytes / 1024 / 1024).toFixed(2)}MB`);
console.log(` š§© Chunks: ${optimizedResult.metadata.chunkCount}`);
console.log(` ā±ļø Time: ${(optimizedResult.durationMs / 1000).toFixed(1)}s\n`);
// Example 4: PDF Analysis and Manipulation
console.log('š Example 4: PDF Analysis & Manipulation...');
const allPdfs = [basicResult.buffer, advancedResult.buffer, optimizedResult.buffer];
// Analyze PDFs
const analysis = await analyzePdfs(allPdfs);
console.log(` š Analysis Results:`);
console.log(` š Total PDFs: ${analysis.totalChunks}`);
console.log(` š Total Pages: ${analysis.totalPages}`);
console.log(` ļæ½ Total Size: ${(analysis.totalSize / 1024 / 1024).toFixed(2)}MB`);
analysis.chunkInfo.forEach((info, index) => {
const names = ['Basic', 'Advanced', 'Large'];
console.log(
` š ${names[index]}: ${info.pages} pages, ${info.sizeKB}KB, ${info.valid ? 'ā' : 'ā'} valid`
);
});
// Extract specific pages from the large report
console.log(` āļø Extracting first 5 pages from large report...`);
const extractedPages = await extractPages(optimizedResult.buffer, [0, 1, 2, 3, 4]);
const extractedPath = join(outputDir, 'employee-report-extract.pdf');
writeFileSync(extractedPath, extractedPages);
const extractedInfo = await getPdfInfo(extractedPages);
console.log(` ā
Extracted pages saved: ${extractedPath}`);
console.log(` š Pages: ${extractedInfo.pages}, Size: ${extractedInfo.sizeKB}KB\n`);
// Example 5: Department-specific reports and merging
console.log('š Example 5: Department Reports & Merging...');
const departments = ['Engineering', 'Marketing', 'Sales', 'HR'];
const departmentBuffers = [];
console.log(` š Generating individual department reports...`);
for (const dept of departments) {
const deptData = largeData.filter(emp => emp.department === dept);
if (deptData.length === 0) continue;
const deptResult = await generatePdf({
title: `${dept} Department Analysis`,
data: deptData,
columns,
template: 'advanced-employee-report',
userInfo: {
companyName: 'Acme Corporation',
name: `${dept} Department Manager`,
email: `${dept.toLowerCase()}@acme.com`,
},
infoSection: [
{ label: 'Department', value: dept },
{ label: 'Employee Count', value: deptData.length.toString() },
{
label: 'Active Rate',
value: `${((deptData.filter(e => e.active).length / deptData.length) * 100).toFixed(1)}%`,
},
{
label: 'Avg Salary',
value: `$${Math.round(deptData.reduce((sum, e) => sum + e.salary, 0) / deptData.length).toLocaleString()}`,
},
],
logging: undefined, // Suppress detailed logging
});
departmentBuffers.push(deptResult.buffer);
console.log(` ā ${dept}: ${deptResult.pageCount} pages, ${deptData.length} employees`);
}
// Merge all department reports
console.log(` š Merging ${departmentBuffers.length} department reports...`);
const mergedReport = await mergePdfs(departmentBuffers, {
logging: consoleLogger,
});
const mergedPath = join(outputDir, 'employee-report-merged-departments.pdf');
writeFileSync(mergedPath, mergedReport);
const mergedInfo = await getPdfInfo(mergedReport);
console.log(` ā
Merged report saved: ${mergedPath}`);
console.log(` š Total Pages: ${mergedInfo.pages}`);
console.log(` š¾ Size: ${(mergedReport.length / 1024 / 1024).toFixed(2)}MB\n`);
// Example 6: PDF Splitting and individual page analysis
console.log('āļø Example 6: PDF Splitting...');
console.log(` š Splitting advanced report into individual pages...`);
const splitPages = await splitPdf(advancedResult.buffer);
const splitDir = join(outputDir, 'split-pages');
if (!existsSync(splitDir)) {
mkdirSync(splitDir, { recursive: true });
}
for (let i = 0; i < splitPages.length; i++) {
const pagePath = join(splitDir, `page-${i + 1}.pdf`);
writeFileSync(pagePath, splitPages[i]);
}
console.log(` ā
Split into ${splitPages.length} individual pages in: ${splitDir}\n`);
// Example 7: Error handling and recovery
console.log('š”ļø Example 7: Error Handling...');
try {
// Intentionally trigger validation error
await generatePdf({
title: '', // Invalid: empty title
data: sampleData,
columns: [],
});
} catch (error) {
console.log(` ā
Caught validation error: ${error.message}`);
}
try {
// Test with invalid PDF buffer
await mergePdfs([Buffer.from('invalid pdf content')]);
} catch (error) {
console.log(` ā
Caught merge error: ${error.message}`);
}
// Performance monitoring example
console.log('\nā” Performance Summary:');
console.log(
` š Total processing time: ${(Date.now() - process.uptime() * 1000).toFixed(0)}ms`
);
console.log(
` š¾ Memory usage: ${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`
);
console.log('\nš All examples completed successfully!');
console.log('\nš Generated files in output directory:');
console.log(' š employee-report-basic.pdf - Basic template example');
console.log(' šØ employee-report-advanced.pdf - Advanced styling with charts');
console.log(' ā” employee-report-large.pdf - Large dataset with optimization');
console.log(' āļø employee-report-extract.pdf - Extracted pages example');
console.log(' š employee-report-merged-departments.pdf - Merged department reports');
console.log(' š split-pages/ - Individual page files');
// Advanced usage tips
console.log('\nš” Advanced Usage Tips:');
console.log(' ⢠Use chunking for datasets > 1000 records');
console.log(' ⢠Set maxConcurrency based on available CPU cores');
console.log(' ⢠Validate PDFs after generation for quality assurance');
console.log(' ⢠Use custom templates for brand consistency');
console.log(' ⢠Monitor memory usage for large operations');
console.log(' ⢠Implement proper error handling and recovery');
} catch (error) {
console.error('ā Error in examples:', error);
// Enhanced error reporting
if (error.code === 'BROWSER_ERROR') {
console.error('\nš” Browser Error Solutions:');
console.error(' ⢠Ubuntu/Debian: sudo apt-get install chromium-browser');
console.error(' ⢠macOS: brew install chromium');
console.error(' ⢠Windows: Download Chromium from official site');
console.error(' ⢠Docker: Use puppeteer/puppeteer image');
} else if (error.code === 'MEMORY_ERROR') {
console.error('\nš” Memory Error Solutions:');
console.error(' ⢠Reduce chunk size (try 50-100 records)');
console.error(' ⢠Lower maxConcurrency (try 1-2)');
console.error(' ⢠Process data in smaller batches');
console.error(' ⢠Increase Node.js memory: node --max-old-space-size=4096');
} else if (error.code === 'TIMEOUT_ERROR') {
console.error('\nš” Timeout Error Solutions:');
console.error(' ⢠Increase timeoutMs option');
console.error(' ⢠Simplify template complexity');
console.error(' ⢠Reduce concurrent operations');
}
process.exit(1);
}
}
// Run if called directly
if (require.main === module) {
main().catch(console.error);
}
module.exports = { main, sampleData, columns };