const fs = require('fs'); const path = require('path'); const readline = require('readline'); const puppeteer = require('puppeteer'); const os = require('os'); let browserPromise = null; let userDataDir = null; function chromeTmpRoot() { return process.env.CHROMIUM_TMP || (fs.existsSync('/dev/shm') ? '/dev/shm/chartpipeline_chromium' : path.join(os.tmpdir(), 'chartpipeline_chromium')); } function getBrowser() { if (!browserPromise) { const baseTmp = chromeTmpRoot(); fs.mkdirSync(baseTmp, { recursive: true }); const cacheDir = path.join(baseTmp, 'cache'); const mediaCacheDir = path.join(baseTmp, 'media_cache'); const crashDir = path.join(baseTmp, 'crash'); fs.mkdirSync(cacheDir, { recursive: true }); fs.mkdirSync(mediaCacheDir, { recursive: true }); fs.mkdirSync(crashDir, { recursive: true }); userDataDir = fs.mkdtempSync(path.join(baseTmp, 'puppeteer_profile_')); const launchOpts = { headless: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', `--user-data-dir=${userDataDir}`, `--disk-cache-dir=${cacheDir}`, `--media-cache-dir=${mediaCacheDir}`, `--crash-dumps-dir=${crashDir}` ] }; if (process.env.PUPPETEER_EXECUTABLE_PATH) { launchOpts.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH; } browserPromise = puppeteer.launch(launchOpts); } return browserPromise; } async function waitSvgReady(page) { const svgReady = () => page.evaluate(() => { const hasReadySvg = (svg) => { if (!svg || svg.childNodes.length === 0) return false; return svg.querySelectorAll('path, circle, rect, g, text').length > 0; }; const chartSvg = document.querySelector('#chart-container svg'); if (hasReadySvg(chartSvg)) return true; for (const selector of ['#svg-output', '#manual-svg-output']) { const output = document.querySelector(selector); if (output && output.innerHTML.trim() && output.querySelector('svg')) return true; } return false; }); const waitStart = Date.now(); while (Date.now() - waitStart < 3000 && !(await svgReady())) { await new Promise(resolve => setTimeout(resolve, 100)); } } async function extractSvgContent(page, outputSvg, width, height) { const isECharts = await page.evaluate(() => typeof echarts !== 'undefined'); let svgContent = null; if (isECharts) { svgContent = await page.evaluate(() => { const svgOutput = document.querySelector('#svg-output'); return svgOutput ? svgOutput.innerHTML : null; }); } if (!svgContent) { svgContent = await page.evaluate(() => { const container = document.querySelector('#chart-container'); if (!container) return null; const hasGraphicElements = (svg) => ( svg && svg.childNodes.length > 0 && svg.querySelectorAll('path, circle, rect, g, text, line, polygon, polyline, ellipse, image').length > 0 ); const svgElement = Array.from(container.querySelectorAll('svg')).find(hasGraphicElements); if (!svgElement || svgElement.childNodes.length === 0) return null; if (typeof echarts !== 'undefined') { const chart = echarts.getInstanceByDom(container); if (chart) chart.setOption({ animation: false }); } const svgClone = svgElement.cloneNode(true); if (!svgClone.hasAttribute('width')) { svgClone.setAttribute('width', container.clientWidth); } if (!svgClone.hasAttribute('height')) { svgClone.setAttribute('height', container.clientHeight); } if (!svgClone.hasAttribute('viewBox')) { svgClone.setAttribute('viewBox', `0 0 ${container.clientWidth} ${container.clientHeight}`); } return svgClone.outerHTML; }); } if (!svgContent) { await new Promise(resolve => setTimeout(resolve, 500)); const pngPath = outputSvg.replace('.svg', '.png'); await page.screenshot({ path: pngPath, fullPage: true }); const pngFileName = path.basename(pngPath); svgContent = ` Chart (Fallback) This is a fallback SVG using a PNG screenshot. `; } return svgContent; } async function render(req) { const browser = await getBrowser(); const page = await browser.newPage(); try { await page.setViewport({ width: req.width, height: req.height }); await page.goto('file://' + path.resolve(req.htmlFile), { waitUntil: 'networkidle0', timeout: 60000 }); await waitSvgReady(page); const svgContent = await extractSvgContent(page, req.outputSvg, req.width, req.height); if (!svgContent) { throw new Error('SVG content is empty'); } fs.writeFileSync(req.outputSvg, svgContent); return { id: req.id, ok: true }; } finally { await page.close(); } } const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; const req = JSON.parse(line); render(req) .then(resp => process.stdout.write(JSON.stringify(resp) + '\n')) .catch(error => { process.stdout.write(JSON.stringify({ id: req.id, ok: false, error: error && error.stack ? error.stack : String(error) }) + '\n'); }); }); async function shutdown() { if (browserPromise) { const browser = await browserPromise; await browser.close(); } if (userDataDir) { fs.rmSync(userDataDir, { recursive: true, force: true }); } process.exit(0); } process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown);