Spaces:
Sleeping
Sleeping
| 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 = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"> | |
| <title>Chart (Fallback)</title> | |
| <desc>This is a fallback SVG using a PNG screenshot.</desc> | |
| <image href="${pngFileName}" width="${width}" height="${height}" /> | |
| </svg>`; | |
| } | |
| 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); | |