Replace Puppeteer fallback with PDFKit for reliable PDF generation in Firebase Functions

This commit is contained in:
Jon
2025-08-02 15:35:32 -04:00
parent c709e8b8c4
commit 1954d9d0a6
3 changed files with 256 additions and 92 deletions

View File

@@ -12,6 +12,7 @@
"@google-cloud/documentai": "^9.3.0",
"@google-cloud/storage": "^7.16.0",
"@supabase/supabase-js": "^2.53.0",
"@types/pdfkit": "^0.17.2",
"axios": "^1.11.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
@@ -26,6 +27,7 @@
"morgan": "^1.10.0",
"openai": "^5.10.2",
"pdf-parse": "^1.1.1",
"pdfkit": "^0.17.1",
"pg": "^8.11.3",
"puppeteer": "^21.11.0",
"redis": "^4.6.10",
@@ -1097,6 +1099,15 @@
"@supabase/storage-js": "^2.10.4"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -1276,6 +1287,15 @@
"@types/node": "*"
}
},
"node_modules/@types/pdfkit": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.2.tgz",
"integrity": "sha512-a7mqP/l8lsLMVNhQ3N2blU5pA1KX0YFE8FxWp0OTqZQKEZoPk7ndAlW+kdFBAWpFmLpy6fFbMRm4a6ZELWNgOQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/pg": {
"version": "8.15.4",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz",
@@ -1984,6 +2004,15 @@
"node": ">=8"
}
},
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
@@ -2159,6 +2188,15 @@
"node": ">=12"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -2356,6 +2394,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
@@ -2437,6 +2481,12 @@
"integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==",
"license": "BSD-3-Clause"
},
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -3026,7 +3076,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-fifo": {
@@ -3337,6 +3386,23 @@
}
}
},
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
@@ -4140,6 +4206,12 @@
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/jpeg-exif": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
"license": "MIT"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4295,6 +4367,25 @@
"resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -4877,6 +4968,12 @@
"node": ">= 14"
}
},
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4991,6 +5088,19 @@
"ms": "^2.1.1"
}
},
"node_modules/pdfkit": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.1.tgz",
"integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==",
"license": "MIT",
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^2.0.4",
"jpeg-exif": "^1.1.4",
"linebreak": "^1.1.0",
"png-js": "^1.0.0"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -5105,6 +5215,11 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
@@ -5476,6 +5591,12 @@
"node": ">=4"
}
},
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@@ -6123,6 +6244,12 @@
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6397,6 +6524,26 @@
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@@ -28,6 +28,7 @@
"@google-cloud/documentai": "^9.3.0",
"@google-cloud/storage": "^7.16.0",
"@supabase/supabase-js": "^2.53.0",
"@types/pdfkit": "^0.17.2",
"axios": "^1.11.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
@@ -42,6 +43,7 @@
"morgan": "^1.10.0",
"openai": "^5.10.2",
"pdf-parse": "^1.1.1",
"pdfkit": "^0.17.1",
"pg": "^8.11.3",
"puppeteer": "^21.11.0",
"redis": "^4.6.10",

View File

@@ -17,6 +17,25 @@ try {
}),
};
}
// Import PDFKit for fallback PDF generation
let PDFDocument: any;
try {
PDFDocument = require('pdfkit');
} catch (error) {
// Mock PDFKit for test environment
PDFDocument = class MockPDFDocument {
constructor() {}
pipe() { return this; }
end() { return this; }
font() { return this; }
fontSize() { return this; }
text() { return this; }
moveDown() { return this; }
addPage() { return this; }
};
}
import fs from 'fs';
import path from 'path';
import { logger } from '../utils/logger';
@@ -756,73 +775,49 @@ class PDFGenerationService {
}
/**
* Generate a simple PDF without Chrome (fallback method)
* Generate a simple PDF using PDFKit (fallback method)
*/
private async generateSimplePDF(analysisData: any): Promise<Buffer> {
try {
// Create a simple HTML document that can be converted to PDF
const simpleHTML = this.generateSimpleHTML(analysisData);
// For now, return a simple text representation
// In a production environment, you might want to use a different PDF library
const textContent = this.convertAnalysisToText(analysisData);
// Create a simple HTML document
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CIM Review Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; border-bottom: 2px solid #007bff; }
h2 { color: #555; margin-top: 30px; }
.section { margin-bottom: 20px; }
.field { margin-bottom: 10px; }
.label { font-weight: bold; color: #666; }
.value { margin-left: 10px; }
</style>
</head>
<body>
<h1>BLUEPOINT Capital Partners</h1>
<h2>CIM Review Report</h2>
<p><strong>Generated:</strong> ${new Date().toLocaleDateString()} at ${new Date().toLocaleTimeString()}</p>
<hr>
${textContent}
<hr>
<p><em>BLUEPOINT Capital Partners | CIM Document Processor | Confidential</em></p>
</body>
</html>
`;
// Try to generate PDF with Puppeteer again, but with different options
try {
const page = await this.getPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const buffer = await page.pdf({
format: 'A4',
margin: { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' },
printBackground: true,
return new Promise((resolve, reject) => {
const doc = new PDFDocument({
size: 'A4',
margins: {
top: 50,
bottom: 50,
left: 50,
right: 50
}
});
this.releasePage(page);
return buffer;
} catch (puppeteerError) {
logger.error('Puppeteer fallback also failed, returning error response', puppeteerError);
throw new Error('PDF generation is currently unavailable. Please try again later.');
}
} catch (error) {
logger.error('Simple PDF generation failed', error);
throw error;
}
}
/**
* Convert analysis data to simple text format
*/
private convertAnalysisToText(analysisData: any): string {
let text = '';
const chunks: Buffer[] = [];
doc.on('data', (chunk: Buffer) => chunks.push(chunk));
doc.on('end', () => {
const result = Buffer.concat(chunks);
resolve(result);
});
doc.on('error', (error: any) => {
reject(error);
});
// Add header
doc.fontSize(24)
.font('Helvetica-Bold')
.text('BLUEPOINT Capital Partners', { align: 'center' });
doc.moveDown(0.5);
doc.fontSize(18)
.font('Helvetica-Bold')
.text('CIM Review Report', { align: 'center' });
doc.moveDown(0.5);
doc.fontSize(10)
.font('Helvetica')
.text(`Generated: ${new Date().toLocaleDateString()} at ${new Date().toLocaleTimeString()}`, { align: 'center' });
doc.moveDown(2);
// Add content sections
const sections = [
{ title: 'Deal Overview', data: analysisData.dealOverview },
{ title: 'Business Description', data: analysisData.businessDescription },
@@ -835,27 +830,47 @@ class PDFGenerationService {
sections.forEach(section => {
if (section.data) {
text += `<div class="section"><h2>${section.title}</h2>`;
// Add section title
doc.fontSize(14)
.font('Helvetica-Bold')
.text(section.title);
doc.moveDown(0.5);
// Add section content
Object.entries(section.data).forEach(([key, value]) => {
if (value && typeof value !== 'object') {
text += `<div class="field"><span class="label">${this.formatFieldName(key)}:</span><span class="value">${value}</span></div>`;
doc.fontSize(10)
.font('Helvetica-Bold')
.text(`${this.formatFieldName(key)}:`, { continued: true });
doc.fontSize(10)
.font('Helvetica')
.text(` ${value}`);
doc.moveDown(0.3);
}
});
text += '</div>';
doc.moveDown(1);
}
});
return text;
// Add footer
doc.moveDown(2);
doc.fontSize(8)
.font('Helvetica')
.text('BLUEPOINT Capital Partners | CIM Document Processor | Confidential', { align: 'center' });
doc.end();
});
} catch (error) {
logger.error('PDFKit PDF generation failed', error);
throw error;
}
}
/**
* Generate simple HTML for fallback PDF generation
*/
private generateSimpleHTML(analysisData: any): string {
return this.convertAnalysisToText(analysisData);
}
/**
* Generate HTML from CIM Review analysis data