#!/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+<') .trim() .replace(/\n\s*\n/g, '\n') .replace(/^\s+|\s+$/gm, '') .replace(/\n{3,}/g, '\n\n') .replace(/\s+<\//g, '\s+/g, '>') .replace(/<(\w+)[^>]*>\s*<\/\1>/g, '') .replace(//g, ''); // Inject minimal CSS to reduce whitespace while preserving layout const improvedHTML = ` ${cleanedHTML.replace(/]*>|<\/body>/gi, '')} `; // 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;