salesforce_node_pdf_api/index.js
2025-09-03 23:52:13 +05:30

497 lines
18 KiB
JavaScript

#!/usr/bin/env node
/**
* Property Brochure PDF Generator API - A4 Scale
* Uses consistent A4 format with proper scaling
*
* NEW FLOW: Supports both download links and direct file responses
* - return_download_link: true -> Generate PDF, save on server, return download link
* - return_download_link: false -> Generate PDF, return base64 content (old behavior)
*/
const express = require('express');
const puppeteer = require('puppeteer');
const bodyParser = require('body-parser');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 8000;
// Configure temp folder
const TEMP_FOLDER = 'temp';
if (!fs.existsSync(TEMP_FOLDER)) {
fs.mkdirSync(TEMP_FOLDER, { recursive: true });
}
// Middleware
app.use(cors());
app.use(bodyParser.json({ limit: '100mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '100mb' }));
// Serve static files from temp folder
app.use('/download', express.static(TEMP_FOLDER));
/**
* Generate PDF with format options (A3 or A4) and minimal whitespace
*/
async function generatePDF(htmlContent, outputPDF, format = 'A4') {
let browser = null;
try {
console.log("🚀 Starting PDF generation...");
// Launch browser
browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu'
]
});
const page = await browser.newPage();
// Set viewport based on format
const viewportDimensions = format === 'A3'
? { width: 1123, height: 1587 } // A3: 297mm x 420mm
: { width: 794, height: 1123 }; // A4: 210mm x 297mm
await page.setViewport(viewportDimensions);
page.setDefaultTimeout(0); // Infinite timeout
// Clean HTML content to remove excessive whitespace while preserving layout
const cleanedHTML = htmlContent
.replace(/\s+/g, ' ')
.replace(/>\s+</g, '><')
.trim()
.replace(/\n\s*\n/g, '\n')
.replace(/^\s+|\s+$/gm, '')
.replace(/\n{3,}/g, '\n\n')
.replace(/\s+<\//g, '</')
.replace(/>\s+/g, '>')
.replace(/<(\w+)[^>]*>\s*<\/\1>/g, '')
.replace(/<!--[\s\S]*?-->/g, '');
// Inject minimal CSS to reduce whitespace while preserving layout
const improvedHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* Minimal reset to reduce whitespace */
* {
box-sizing: border-box !important;
}
body {
font-family: Arial, sans-serif !important;
line-height: 1.2 !important;
color: #000 !important;
background: #fff !important;
margin: 0 !important;
padding: 0 !important;
font-size: 12px !important;
overflow: visible !important;
}
/* Page setup with minimal margins */
@page {
size: ${format} !important;
margin: 0.2in !important;
}
/* Reduce excessive spacing */
h1, h2, h3, h4, h5, h6 {
margin: 0 0 10px 0 !important;
padding: 0 !important;
font-weight: bold !important;
page-break-after: avoid !important;
page-break-inside: avoid !important;
}
p {
margin: 0 0 8px 0 !important;
padding: 0 !important;
page-break-inside: avoid !important;
orphans: 1 !important;
widows: 1 !important;
}
/* Tables with minimal spacing */
table {
border-collapse: collapse !important;
border-spacing: 0 !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
page-break-inside: avoid !important;
}
td, th {
margin: 0 !important;
padding: 4px !important;
border: none !important;
vertical-align: top !important;
}
/* Images with minimal spacing */
img {
max-width: 100% !important;
height: auto !important;
display: block !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
page-break-inside: avoid !important;
}
/* Page breaks */
.page-break, .pagebreak, .new-page {
page-break-before: always !important;
margin: 0 !important;
padding: 0 !important;
}
.no-break {
page-break-inside: avoid !important;
}
/* Hide elements that might cause spacing */
.hidden, .invisible, [style*="display: none"], [style*="visibility: hidden"] {
display: none !important;
}
</style>
</head>
<body>
${cleanedHTML.replace(/<body[^>]*>|<\/body>/gi, '')}
</body>
</html>
`;
// Set content with improved HTML
await page.setContent(improvedHTML, {
waitUntil: 'networkidle0',
timeout: 0 // Infinite timeout
});
// Wait for content to load completely
await page.waitForTimeout(0);
// Wait for all resources to load
try {
await page.waitForNetworkIdle({ timeout: 0 });
} catch (error) {
// Ignore timeout errors for networkidle
}
// Additional CSS injection to reduce spacing
await page.addStyleTag({
content: `
/* Final spacing reduction */
* {
-webkit-print-color-adjust: exact !important;
color-adjust: exact !important;
}
/* Reduce remaining spacing */
body, html {
overflow: visible !important;
}
/* Ensure proper page breaks */
.page-break {
page-break-before: always !important;
}
`
});
console.log(`📄 Generating ${format} PDF with minimal whitespace...`);
const pdfOptions = {
path: outputPDF,
printBackground: true,
margin: {
top: '0.2in', // Minimal margins
right: '0.2in',
bottom: '0.2in',
left: '0.2in'
},
scale: 1.0,
format: format, // Dynamic format (A3 or A4)
preferCSSPageSize: false, // Use our CSS settings
displayHeaderFooter: false, // No headers/footers to avoid spacing
omitBackground: false
};
// Generate PDF
await page.pdf(pdfOptions);
await browser.close();
// Get PDF file size for logging (no size restriction for download links)
const pdfStats = fs.statSync(outputPDF);
const pdfSizeMB = pdfStats.size / (1024 * 1024);
console.log(`✅ MINIMAL-WHITESPACE ${format} PDF generated: ${outputPDF}`);
console.log(`📐 Format: ${format} (${format === 'A3' ? '297mm x 420mm' : '210mm x 297mm'})`);
console.log(`📏 Margins: 0.2in (minimal margins)`);
console.log(`🔍 Scale: 1.0`);
console.log(`📊 File size: ${pdfSizeMB.toFixed(2)} MB`);
console.log(`🚫 White spaces: REDUCED while preserving layout`);
} catch (error) {
if (error.name === 'TimeoutError') {
throw new Error("Timeout: Page took too long to load.");
}
console.error(`❌ PDF generation error: ${error.message}`);
throw error;
} finally {
// Cleanup
if (browser) {
try {
await browser.close();
} catch (error) {
console.error('Error closing browser:', error);
}
}
}
}
/**
* Main PDF generation endpoint - supports both download link and base64 response
*/
app.post('/generate-pdf', async (req, res) => {
try {
// Get request data
let inputContent = null;
let outputName = null;
let returnDownloadLink = false; // Default to old behavior
let format = 'A4'; // Default format
if (req.is('application/json')) {
try {
const data = req.body;
if (data) {
// Handle different request formats
if (data.input) {
// Direct HTML input
inputContent = data.input;
outputName = data.output || null;
returnDownloadLink = data.return_download_link || false;
format = data.format || 'A4';
} else if (data.html) {
// Alternative HTML field
inputContent = data.html;
outputName = data.filename || null;
returnDownloadLink = data.return_download_link || false;
format = data.format || 'A4';
} else if (data.content) {
// Content field
inputContent = data.content;
outputName = data.name || null;
returnDownloadLink = data.return_download_link || false;
format = data.format || 'A4';
}
}
} catch (parseError) {
console.error('JSON parse error:', parseError);
return res.status(400).json({ error: 'Invalid JSON in request body' });
}
} else if (req.is('application/x-www-form-urlencoded')) {
// Handle form data
inputContent = req.body.input || req.body.html || req.body.content;
outputName = req.body.output || req.body.filename || req.body.name;
returnDownloadLink = req.body.return_download_link === 'true' || req.body.return_download_link === true;
format = req.body.format || 'A4';
} else {
// Try to get from raw body
try {
const rawData = JSON.parse(req.body);
inputContent = rawData.input || rawData.html || rawData.content;
outputName = rawData.output || rawData.filename || rawData.name;
returnDownloadLink = rawData.return_download_link || false;
format = rawData.format || 'A4';
} catch (error) {
return res.status(400).json({ error: 'Invalid request format' });
}
}
// Validate input
if (!inputContent) {
return res.status(400).json({ error: 'No HTML content provided' });
}
// Validate format
if (!['A3', 'A4'].includes(format.toUpperCase())) {
return res.status(400).json({ error: 'Invalid format. Supported formats: A3, A4' });
}
format = format.toUpperCase();
console.log(`📥 Received request - Format: ${format}, Download Link: ${returnDownloadLink}`);
// Generate unique filename
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const randomId = crypto.randomBytes(4).toString('hex');
const filename = outputName ?
`${outputName.replace(/[^a-zA-Z0-9-_]/g, '_')}_${format}_${randomId}.pdf` :
`property_brochure_${format}_${randomId}.pdf`;
const outputPDF = path.join(TEMP_FOLDER, filename);
// Generate PDF
await generatePDF(inputContent, outputPDF, format);
if (returnDownloadLink) {
// NEW FLOW: Return download link
const downloadUrl = `https://salesforce.tech4biz.io/download/${filename}`;
const pdfStats = fs.statSync(outputPDF);
const pdfSizeMB = pdfStats.size / (1024 * 1024);
const result = {
success: true,
message: `MINIMAL-WHITESPACE ${format} PDF generated successfully`,
download_url: downloadUrl,
filename: filename,
format: format,
file_size_mb: pdfSizeMB.toFixed(2),
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
generated_at: new Date().toISOString(),
pdf_id: randomId,
status: "download_ready",
features: ["reduced_whitespace", "preserved_layout", "minimal_margins"]
};
console.log(`🔗 PDF ready for download: ${downloadUrl}`);
console.log(`📊 File size: ${pdfSizeMB.toFixed(2)} MB`);
res.json(result);
} else {
// OLD FLOW: Return base64 content
const pdfBuffer = fs.readFileSync(outputPDF);
const base64Content = pdfBuffer.toString('base64');
// Clean up the file
if (fs.existsSync(outputPDF)) {
fs.unlinkSync(outputPDF);
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(Buffer.from(base64Content, 'base64'));
}
} catch (error) {
console.error(`❌ API Error: ${error.message}`);
res.status(500).json({ error: error.message });
}
});
/**
* Download endpoint for PDF files
*/
app.get('/download/:filename', (req, res) => {
try {
const filename = req.params.filename;
const filePath = path.join(TEMP_FOLDER, filename);
// Security check - ensure filename is safe
if (!filename.match(/^[a-zA-Z0-9._-]+\.pdf$/)) {
return res.status(400).json({ error: 'Invalid filename' });
}
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'PDF file not found or expired' });
}
// Send file response
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(filePath)}"`);
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
// Keep the generated PDF file (don't delete it)
fileStream.on('end', () => {
console.log(`📥 PDF downloaded: ${filePath}`);
});
} catch (error) {
console.error(`❌ Download Error: ${error.message}`);
res.status(500).json({ error: error.message });
}
});
/**
* Health check endpoint
*/
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: '9.1.0-minimal-whitespace-a3-a4',
supported_formats: ['A3', 'A4'],
features: [
'reduced_white_spaces',
'minimal_margins',
'html_preprocessing',
'preserved_layout',
'no_extra_pages',
'a3_a4_formats',
'clean_pdf_output',
'download_links',
'base64_response',
'infinite_timeouts',
'format_validation'
]
});
});
/**
* Clean up temporary HTML files older than 1 hour (keep PDF files)
*/
function cleanupTempFiles() {
try {
const currentTime = Date.now();
const files = fs.readdirSync(TEMP_FOLDER);
files.forEach(filename => {
const filepath = path.join(TEMP_FOLDER, filename);
const stats = fs.statSync(filepath);
// Only clean up HTML files, keep PDF files
if (stats.isFile() && filename.endsWith('.html') && (currentTime - stats.mtime.getTime()) > 3600000) { // 1 hour
try {
fs.unlinkSync(filepath);
console.log(`🗑️ Cleaned up old HTML file: ${filename}`);
} catch (error) {
console.error(`Error deleting file ${filename}:`, error);
}
}
});
} catch (error) {
console.error('Error during cleanup:', error);
}
}
// Clean up old files every hour
setInterval(cleanupTempFiles, 3600000);
// Start server
app.listen(PORT, () => {
console.log(`🚀 MINIMAL-WHITESPACE PDF Generator API running on port ${PORT}`);
console.log(`📁 Temp folder: ${TEMP_FOLDER}`);
console.log(`🔗 Download endpoint: http://localhost:${PORT}/download/:filename`);
console.log(`📊 Health check: http://localhost:${PORT}/health`);
console.log(`🚫 WHITESPACE: REDUCED - Minimal spacing, preserved layout, clean output`);
console.log(`📐 FORMATS: A3 (297mm x 420mm) and A4 (210mm x 297mm)`);
console.log(`✨ Features: Reduced white spaces, HTML preprocessing, Preserved layout, Format options`);
});
module.exports = app;