Skip to main content
Glama
render.js6.75 kB
import puppeteer from 'puppeteer'; import { pathToFileURL } from 'url'; import path from 'path'; import os from 'os'; import fs from 'fs'; const CHROME_VERSION = '131.0.6778.204'; function getPlatformPath() { const platform = process.platform; const arch = os.arch(); if (platform === 'darwin') { return arch === 'arm64' ? `mac_arm-${CHROME_VERSION}/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing` : `mac-${CHROME_VERSION}/chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`; } if (platform === 'linux') { return `linux-${CHROME_VERSION}/chrome-linux/chrome`; } if (platform === 'win32') { return `win64-${CHROME_VERSION}/chrome-win/chrome.exe`; } throw new Error(`Unsupported platform: ${platform}`); } async function renderPDF({ htmlPath, pdfPath, runningsPath, cssPath, highlightCssPath, paperFormat, paperOrientation, paperBorder, watermarkScope, showPageNumbers, renderDelay, loadTimeout }) { let browser; const verbose = process.env.M2P_VERBOSE === 'true'; if (verbose) { console.error(`[markdown2pdf] Starting PDF rendering`); console.error(`[markdown2pdf] Timeouts - load: ${loadTimeout}ms, render: ${renderDelay}ms`); } try { // Try with our specific Chrome version first const chromePath = path.join( os.homedir(), '.cache', 'puppeteer', 'chrome', getPlatformPath() ); if (!fs.existsSync(chromePath)) { if (verbose) { console.error(`[markdown2pdf] Chrome not found at: ${chromePath}, using fallback`); } throw new Error(`Chrome executable not found at: ${chromePath}`); } browser = await puppeteer.launch({ headless: true, executablePath: chromePath, product: 'chrome', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', // Prevent shared memory issues '--disable-accelerated-2d-canvas', '--disable-gpu', '--max-old-space-size=4096' // Increase memory limit to 4GB ] }); } catch (err) { // Fall back to default Puppeteer-installed Chrome if (verbose) { console.error('[markdown2pdf] Falling back to default Chrome installation'); } browser = await puppeteer.launch({ headless: true, product: 'chrome', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--disable-gpu', '--max-old-space-size=4096' ] }); } try { const page = await browser.newPage(); // Monitor browser crashes browser.on('disconnected', () => { throw new Error('Browser disconnected unexpectedly. This may indicate an out-of-memory issue or browser crash. Try reducing content size or increasing system resources.'); }); page.on('error', err => { throw new Error(`Page crashed: ${err.message}`); }); page.on('pageerror', err => { if (verbose) { console.error(`[markdown2pdf] Page error:`, err); } }); // Set viewport await page.setViewport({ width: 1200, height: 1600 }); if (verbose) { console.error(`[markdown2pdf] Loading HTML from: ${htmlPath}`); } // Load the HTML file with timeout const htmlFileUrl = pathToFileURL(htmlPath).href; await page.goto(htmlFileUrl, { waitUntil: 'networkidle0', timeout: loadTimeout }).catch(err => { if (err.message.includes('timeout')) { throw new Error(`Failed to load HTML content within ${loadTimeout/1000}s timeout. The content may be too large or complex. Error: ${err.message}`); } throw new Error(`Failed to load HTML content: ${err.message}`); }); if (verbose) { console.error(`[markdown2pdf] HTML loaded successfully`); } // Import runnings (header/footer) const runningsUrl = pathToFileURL(runningsPath).href; const runningsModule = await import(runningsUrl).catch(err => { throw new Error(`Failed to import runnings.js: ${err.message}`); }); // Add CSS if provided if (cssPath && fs.existsSync(cssPath)) { await page.addStyleTag({ path: cssPath }).catch(err => { throw new Error(`Failed to add CSS: ${err.message}`); }); } if (highlightCssPath && fs.existsSync(highlightCssPath)) { await page.addStyleTag({ path: highlightCssPath }).catch(err => { throw new Error(`Failed to add highlight CSS: ${err.message}`); }); } // Wait for specified delay await new Promise(resolve => setTimeout(resolve, renderDelay)); // Check for mermaid errors const mermaidError = await page.evaluate(() => { const errorDiv = document.getElementById('mermaid-error'); return errorDiv ? errorDiv.innerText : null; }); if (mermaidError) { throw new Error(`Mermaid diagram rendering failed: ${mermaidError}`); } // Force repaint to ensure proper rendering await page.evaluate(() => { document.body.style.transform = 'scale(1)'; return document.body.offsetHeight; }); // Get watermark text if present const watermarkText = await page.evaluate(() => { const watermark = document.querySelector('.watermark'); return watermark ? watermark.textContent : ''; }); const templatesFactory = runningsModule?.default; if (typeof templatesFactory !== 'function') { throw new Error('Invalid runnings export: expected default function'); } const templates = templatesFactory({ watermarkText, watermarkScope, showPageNumbers }); const shouldDisplayHeaderFooter = Boolean( showPageNumbers || (watermarkText && watermarkScope === 'all-pages') ); await page.pdf({ path: pdfPath, format: paperFormat, landscape: paperOrientation === 'landscape', margin: { top: paperBorder, right: paperBorder, bottom: paperBorder, left: paperBorder }, printBackground: true, displayHeaderFooter: shouldDisplayHeaderFooter, headerTemplate: shouldDisplayHeaderFooter ? templates.header : '', footerTemplate: shouldDisplayHeaderFooter ? templates.footer : '', preferCSSPageSize: true }); return pdfPath; } finally { if (browser) { await browser.close(); } } } export default renderPDF;

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LAMENTIS1/mcp_pdf'

If you have feedback or need assistance with the MCP directory API, please join our Discord server