ChartPipeline / modules /chart_engine /utils /persistent_html_to_svg_renderer.cjs
Ray1ee01's picture
Add runnable ChartPipeline Space
216c0a4 verified
Raw
History Blame Contribute Delete
5.73 kB
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);