UNPKG

@iqx-limited/quick-pdf

Version:

Converting PDFs to images (📃 to 📸)

473 lines (465 loc) • 15.2 kB
import { satisfies } from 'semver'; import { pathToFileURL } from 'node:url'; import { join, resolve } from 'node:path'; import { access, writeFile, unlink } from 'node:fs/promises'; import puppeteer from 'puppeteer'; import { imageSize } from 'image-size'; import PDFDocument from 'pdfkit'; import { existsSync, readFileSync } from 'node:fs'; import { HtmlValidate } from 'html-validate/node'; let firefox = null; const BROWSER_PATHS = { firefox: { linux: "/usr/bin/firefox", mac: "/Applications/Firefox.app/Contents/MacOS/firefox", windows: join("C:", "Program Files", "Mozilla Firefox", "firefox.exe") } }; const getOS = () => { if (process.platform === "win32") return "windows"; if (process.platform === "darwin") return "mac"; return "linux"; }; const isBrowserInstalled = async (browser) => { const os = getOS(); const browserPath = BROWSER_PATHS[browser][os]; try { await access(browserPath); return true; } catch { return false; } }; async function launchBrowser(browserType) { return new Promise(async (resolve) => { if (firefox) return resolve(firefox); if (!firefox) { const os = getOS(); const executablePath = BROWSER_PATHS[browserType][os]; if (!(await isBrowserInstalled(browserType))) { console.error(`${browserType.toUpperCase()} is not installed.`); process.exit(1); } const browser = await puppeteer.launch({ browser: browserType, headless: "shell", executablePath, args: ["--no-sandbox", "--disable-setuid-sandbox"] }); { firefox = browser; } return resolve(browser); } throw new Error(`Browser type ${browserType} is not supported`); }); } async function closeBrowsers() { try { if (firefox) { await (await firefox).close(); firefox = null; } } catch (err) { console.error("Error closing browsers in @iqx-limited/quick-form:", err); } } process.on("exit", async () => { await closeBrowsers(); }); process.on("SIGINT", async () => { console.log("SIGINT received. Closing browsers..."); await closeBrowsers(); }); process.on("SIGTERM", async () => { console.log("SIGTERM received. Closing browsers..."); await closeBrowsers(); }); const pagePoolSize$1 = 5; const RESOURCE_LIMIT$1 = 100; let resourceCount$1 = 0; let pagePool$1 = []; let browser$1 = null; async function launchPages$1() { if (pagePool$1.length > 0) { return pagePool$1; } pagePool$1 = await Promise.all(Array.from({ length: pagePoolSize$1 }, async () => { if (browser$1) { const page = await browser$1.newPage(); await page.setRequestInterception(true); await page.setDefaultNavigationTimeout(10000); await page.goto("about:blank"); page.on("request", request => { resourceCount$1++; if (resourceCount$1 > RESOURCE_LIMIT$1) { page.reload(); resourceCount$1 = 0; } else { request.continue(); } }); return page; } else { throw new Error("Browser not available"); } })); return pagePool$1; } const pdf2img = async (input, options = {}) => { browser$1 = await launchBrowser("firefox"); if (!browser$1?.connected) { throw new Error("Browser not available"); } const pagePool = await launchPages$1(); let page = pagePool.pop(); let tempPage = false; if (!page) { tempPage = true; page = await browser$1.newPage(); } let path = ""; let address = ""; let tempFile = false; if (Buffer.isBuffer(input)) { path = resolve(process.cwd(), "temp.pdf"); address = pathToFileURL(path).toString(); tempFile = true; await writeFile(path, input); } else { if (typeof input === "string" && input.startsWith("http")) { path = input; address = path; } else { path = resolve(input.toString()); address = pathToFileURL(path).toString(); } } try { await page.goto(address); if (options.password) { try { await page.waitForSelector('input[type="password"]', { visible: true, timeout: 5000 }); console.log("Password prompt detected, entering password..."); await page.type('input[type="password"]', options.password || ""); await page.keyboard.press("Enter"); } catch { } } await page.waitForSelector("canvas", { timeout: 5000 }); await page.waitForSelector(".loadingIcon", { timeout: 5000, hidden: true }); const imageBuffers = []; const pageCount = await page.evaluate(() => { if (window.PDFViewerApplication) { return window.PDFViewerApplication.pagesCount; } return 0; }); const metadata = await page.evaluate(() => { const app = window.PDFViewerApplication; if (app && app.pdfDocument) { return app.documentInfo ?? {}; } return {}; }); const pdfDimensions = await page.evaluate(() => { const canvas = document.querySelector("canvas"); const { width, height } = canvas.getBoundingClientRect(); return { width, height }; }); await page.setViewport({ width: pdfDimensions.width, height: pdfDimensions.height, deviceScaleFactor: 1 }); for (let i = 1; i <= pageCount; i++) { await page.evaluate((pageNum) => { if (window.PDFViewerApplication) { window.PDFViewerApplication.page = pageNum; } }, i); const pageBoundingBox = await page.evaluate(() => { const pageElement = document.querySelector("canvas"); const { x, y, width, height } = pageElement.getBoundingClientRect(); return { x, y, width, height }; }); const screenshotOptions = { fullPage: false, type: options.type ?? "png", clip: { x: pageBoundingBox.x, y: pageBoundingBox.y, width: pageBoundingBox.width, height: pageBoundingBox.height } }; if (options.type && options.type !== "png") { screenshotOptions.quality = options.quality ?? 100; } const uint8array = await page.screenshot(screenshotOptions); imageBuffers.push(Buffer.from(uint8array)); } if (tempFile) { await unlink(path); } if (tempPage) { page.close(); } else { await page.setViewport({ width: 800, height: 600, deviceScaleFactor: 1 }); pagePool.push(page); } return { length: pageCount, metadata: metadata.info, pages: imageBuffers }; } catch (error) { if (tempFile) { await unlink(path); } if (tempPage) { page.close(); } else { await page.setViewport({ width: 800, height: 600, deviceScaleFactor: 1 }); pagePool.push(page); } throw error; } }; async function getBuffer(input) { if (input instanceof Buffer) { return input; } return fetch(input.toString()) .then(res => { if (res.ok) { return res.arrayBuffer(); } else { throw new Error("Failed to Fetch the File"); } }) .then(array => Buffer.from(array)) .catch(() => { if (existsSync(input.toString())) { return readFileSync(input.toString()); } throw new Error("Failed to Fetch the File"); }); } const fetchHtmlFromUrl = async (url) => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch content from URL: ${url}`); } return await response.text(); }; const readHtmlFromFilePath = async (filePath) => { return readFileSync(filePath, "utf-8"); }; const img2pdf = async (input, options = {}) => { const { fileTypeFromBuffer } = await import('file-type'); return new Promise((resolve, reject) => { getBuffer(input).then(async (buf) => { const type = await fileTypeFromBuffer(buf); if (type?.mime !== "image/jpeg" && type?.mime !== "image/png") { throw new Error("Provided File is not a JPEG or a PNG."); } const pdfBuffers = []; const imgSize = imageSize(buf); const landscape = imgSize.width && imgSize.height ? imgSize.width > imgSize.height : false; const doc = new PDFDocument({ size: "a4", layout: landscape ? "landscape" : "portrait", margins: { top: 0, bottom: 0, left: 0, right: 0 } }); doc.on("data", (data) => { pdfBuffers.push(data); }); doc.on("end", () => { resolve(Buffer.concat(pdfBuffers)); }); doc.fontSize(options.fontSize ?? 10); const topMargin = options.header ? 20 : 0; const bottomMargin = options.footer ? 20 : 0; const sidePadding = 20; const imageHeight = doc.page.height - topMargin - bottomMargin; if (options.header) { doc.text(options.header, sidePadding, topMargin / 2 - 6, { align: "center", baseline: "top", width: doc.page.width - 2 * sidePadding, height: topMargin - 5, ellipsis: true }).moveDown(0.5); } doc.image(buf, 0, topMargin, { width: doc.page.width, height: imageHeight }); if (options.footer) { doc.text(options.footer, sidePadding, doc.page.height - bottomMargin / 2 - 6, { align: "center", width: doc.page.width - 2 * sidePadding, height: bottomMargin - 5, ellipsis: true }); } doc.end(); }).catch((e) => { reject(e); }); }); }; const pagePoolSize = 5; const RESOURCE_LIMIT = 100; let resourceCount = 0; let pagePool = []; let browser = null; async function launchPages() { if (pagePool.length > 0) { return pagePool; } pagePool = await Promise.all(Array.from({ length: pagePoolSize }, async () => { if (browser && browser.connected) { const page = await browser.newPage(); await page.setRequestInterception(true); await page.setDefaultNavigationTimeout(10000); await page.goto("about:blank"); page.on("request", request => { resourceCount++; if (resourceCount > RESOURCE_LIMIT) { page.reload(); resourceCount = 0; } else { request.continue(); } }); return page; } else { throw new Error("Browser not available"); } })); return pagePool; } const html2pdf = async (input, options = {}) => { const validator = new HtmlValidate(options.rules ?? { extends: ["html-validate:standard"], rules: { "no-trailing-whitespace": "off" } }); let htmlContent = input.toString(); if (htmlContent.startsWith("http://") || htmlContent.startsWith("https://")) { htmlContent = await fetchHtmlFromUrl(htmlContent); } else if (existsSync(input)) { htmlContent = await readHtmlFromFilePath(htmlContent); } browser = await launchBrowser("firefox"); if (!browser?.connected) { throw new Error("Browser not available"); } const pagePool = await launchPages(); let page = pagePool.pop(); let tempPage = false; if (!page) { tempPage = true; page = await browser.newPage(); } const validation = (options.validation ?? true); try { const res = validation ? await validator.validateString(htmlContent) : { valid: true }; if (res.valid) { await page.setContent(htmlContent, { waitUntil: "load" }); const pdf = await page.pdf({ format: "A4", printBackground: true }); const pdfBuffer = Buffer.from(pdf); if (options.base64 ?? false) { return pdfBuffer.toString("base64"); } return pdfBuffer; } else { throw { valid: false, count: { errors: res.errorCount, warnings: res.warningCount }, validation: res.results.map(res => { return { file: res.filePath, count: { errors: res.errorCount, warnings: res.warningCount }, messages: res.messages.map(msg => { return { message: msg.message, line: msg.line, column: msg.column, ruleId: msg.ruleId }; }) }; }) }; } } finally { if (tempPage) { page.close(); } else { await page.setViewport({ width: 800, height: 600, deviceScaleFactor: 1 }); pagePool.push(page); } } }; const requiredVersion = ">=20.0.0"; if (!satisfies(process.version, requiredVersion)) { console.error(`\nError: Node.js version ${requiredVersion} is required. You are using ${process.version}.\n`); process.exit(1); } export { html2pdf, img2pdf, pdf2img }; //# sourceMappingURL=index.mjs.map