first commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Niolla
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
// app/utils/pdfGenerator.js
|
||||
|
||||
const path = require("path");
|
||||
const ejs = require("ejs");
|
||||
const puppeteer = require("puppeteer");
|
||||
const fs = require("fs");
|
||||
const { imageToBase64 } = require("./imageToBase64.utill");
|
||||
const { log, runningEnvironment } = require("./consoleLog.utill");
|
||||
|
||||
let browser;
|
||||
|
||||
let executablePath = null;
|
||||
|
||||
const isProd = runningEnvironment();
|
||||
|
||||
if (isProd) {
|
||||
executablePath = "/usr/bin/chromium";
|
||||
} else {
|
||||
executablePath = puppeteer.executablePath();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Launch or reuse Puppeteer browser safely
|
||||
*/
|
||||
async function getBrowser() {
|
||||
if (!browser || !browser.isConnected?.()) {
|
||||
console.log("🚀 Launching Chromium from Docker system path...");
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
headless: "new",
|
||||
executablePath: executablePath,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--no-zygote"
|
||||
],
|
||||
});
|
||||
|
||||
console.log("✅ Browser launched");
|
||||
}
|
||||
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close browser safely
|
||||
*/
|
||||
async function closeBrowser() {
|
||||
if (browser) {
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (e) {}
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core PDF renderer
|
||||
*/
|
||||
async function renderPdf(templateFile, data = {}, opts = {}) {
|
||||
const templatePath = path.join(__dirname, "../templates", templateFile);
|
||||
|
||||
console.log("📝 Rendering:", templateFile);
|
||||
|
||||
// Convert image to base64 if needed
|
||||
if (data.logoUrl && !data.logoBase64) {
|
||||
try {
|
||||
data.logoBase64 = await imageToBase64(data.logoUrl);
|
||||
} catch (err) {
|
||||
console.error("❌ Logo conversion failed:", err.message);
|
||||
data.logoBase64 = null;
|
||||
}
|
||||
}
|
||||
|
||||
const html = await ejs.renderFile(templatePath, data);
|
||||
|
||||
const browserInstance = await getBrowser();
|
||||
const page = await browserInstance.newPage();
|
||||
|
||||
try {
|
||||
// Safety timeouts
|
||||
await page.setDefaultTimeout(60000);
|
||||
await page.setDefaultNavigationTimeout(60000);
|
||||
|
||||
await page.setViewport({ width: 1200, height: 800 });
|
||||
|
||||
console.log("📄 Loading HTML...");
|
||||
await page.setContent(html, { waitUntil: "networkidle0" });
|
||||
|
||||
await page.emulateMediaType("print");
|
||||
|
||||
// Wait for fonts
|
||||
await page.evaluate(() => document.fonts?.ready || Promise.resolve());
|
||||
|
||||
const margin = {
|
||||
top: 20,
|
||||
bottom: 20,
|
||||
left: 10,
|
||||
right: 10,
|
||||
...opts.margin,
|
||||
};
|
||||
|
||||
// Optional: measure content (kept for logging only)
|
||||
const contentHeightPx = await page.evaluate(() => {
|
||||
const body = document.body;
|
||||
const htmlEl = document.documentElement;
|
||||
|
||||
return Math.max(
|
||||
body.scrollHeight,
|
||||
body.offsetHeight,
|
||||
htmlEl.scrollHeight,
|
||||
htmlEl.offsetHeight
|
||||
);
|
||||
});
|
||||
|
||||
console.log("📏 Content height:", contentHeightPx, "px");
|
||||
|
||||
// ❗ REMOVED scaling logic (causes PDF distortion in production)
|
||||
|
||||
const showPageNumbers = opts.showPageNumbers === true;
|
||||
|
||||
console.log("⚙️ Generating PDF...");
|
||||
|
||||
const pdfBuffer = await page.pdf({
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
margin: {
|
||||
top: `${margin.top}mm`,
|
||||
bottom: `${margin.bottom}mm`,
|
||||
left: `${margin.left}mm`,
|
||||
right: `${margin.right}mm`,
|
||||
},
|
||||
displayHeaderFooter: showPageNumbers,
|
||||
headerTemplate: `<div></div>`,
|
||||
footerTemplate: showPageNumbers
|
||||
? `
|
||||
<div style="font-size:10px;width:100%;text-align:center;">
|
||||
Page <span class="pageNumber"></span> / <span class="totalPages"></span>
|
||||
</div>`
|
||||
: `<div></div>`,
|
||||
});
|
||||
|
||||
console.log("✅ PDF generated");
|
||||
|
||||
return pdfBuffer;
|
||||
} catch (err) {
|
||||
console.error("❌ PDF generation error:", err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
const generatePdfFromPreCost = (data) =>
|
||||
renderPdf("documents/precost/precost.ejs", data);
|
||||
|
||||
const generatePdfFromInvoice = (data) =>
|
||||
renderPdf("documents/invoice/invoice.ejs", data);
|
||||
|
||||
const generatePdfFromDispatchNote = (data) =>
|
||||
renderPdf("documents/dispatchNote/dispatchNote.ejs", data);
|
||||
|
||||
const generatePdfFromDeliveryNote = (data) =>
|
||||
renderPdf("documents/deliveryNote/deliveryNote.ejs", data);
|
||||
|
||||
const generatePdfFromPO = (data) =>
|
||||
renderPdf("documents/po/po.ejs", data);
|
||||
|
||||
const generatePdfFromCustomDocument = (data) =>
|
||||
renderPdf("documents/customDocument/customDocument.ejs", data);
|
||||
|
||||
const generatePdfFromCreditNote = (data) =>
|
||||
renderPdf("documents/creditNote/creditNote.ejs", data);
|
||||
|
||||
module.exports = {
|
||||
generatePdfFromPreCost,
|
||||
generatePdfFromInvoice,
|
||||
generatePdfFromDispatchNote,
|
||||
generatePdfFromDeliveryNote,
|
||||
generatePdfFromPO,
|
||||
generatePdfFromCustomDocument,
|
||||
generatePdfFromCreditNote,
|
||||
renderPdf,
|
||||
closeBrowser,
|
||||
};
|
||||
Reference in New Issue
Block a user