// P103-2:用无头 Edge/Chrome 跑**真实栅格化路径**,并对每张图输出 sha256 + 尺寸;同时检查 CSS 是否作用于 svg import { readFileSync, writeFileSync, existsSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, resolve } from 'node:path' import { JSDOM, VirtualConsole } from 'jsdom' import { execFileSync } from 'node:child_process' const HERE = dirname(fileURLToPath(import.meta.url)) const dist = resolve(HERE, '../../../release/web') const GW = 'http://106.120.102.238:28084' const fleet = readFileSync(resolve(HERE, '_fleet_fixture.json'), 'utf8') const facts = '{"cards":[{"c":"WTG01","verdict":"定论","part":"主轴承"}],"contract_sha256":"abc"}' const idx = readFileSync(dist + '/index.html', 'utf8') const jsFile = (idx.match(/src="([^"]+\.js)"/) || [])[1].replace(/^.*\//, '') const cssFile = (idx.match(/href="([^"]+\.css)"/) || [])[1].replace(/^.*\//, '') const code = readFileSync(dist + '/assets/' + jsFile, 'utf8') const css = readFileSync(dist + '/assets/' + cssFile, 'utf8') // ① 页面(jsdom)取各块 SVG 原文 const vc = new VirtualConsole() const dom = new JSDOM(idx.replace('', ''), { url: GW + '/detail/v2#tab=report', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole: vc }) const w = dom.window w.fetch = async (u) => { const s = String(u); const b = /fleet/.test(s) ? fleet : (/facts/.test(s) ? facts : '{}') return { ok: true, status: 200, json: async () => JSON.parse(b), text: async () => b } } w.eval(code) for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 400)); if (w.document.querySelector('[data-preset="month"]')) break } w.document.querySelector('[data-preset="month"]').dispatchEvent(new w.MouseEvent('click', { bubbles: true })) await new Promise((r) => setTimeout(r, 2500)) const svgs = {} for (const b of w.document.querySelectorAll('.paper [data-blk]')) { const s = b.querySelector('svg') if (s) svgs[b.getAttribute('data-blk')] = s.outerHTML } console.log('PAGE_SVGS ' + JSON.stringify(Object.keys(svgs))) // ② CSS 中是否命中 svg / .dt(若命中,栅格化必须把这份样式内联进 SVG,否则 Word 里会不一样) const cssHits = [] for (const m of css.matchAll(/([^{}]*)\{([^{}]*)\}/g)) { const sel = m[1].trim() if (/(^|[\s,>])svg\b|\.dt\b/.test(sel)) cssHits.push(sel.slice(0, 90) + ' { ' + m[2].trim().slice(0, 90) + ' }') } console.log('CSS_HITS_SVG ' + cssHits.length) cssHits.slice(0, 8).forEach((h) => console.log(' ' + h)) // ③ 真实浏览器栅格化:生成测试页,用 Edge/Chrome 无头执行,回读结果 const testHtml = `
pending
` const htmlPath = resolve(HERE, '_raster_test.html') writeFileSync(htmlPath, testHtml, 'utf8') const browsers = ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Google/Chrome/Application/chrome.exe'] let out = '' for (const b of browsers) { if (!existsSync(b)) continue for (const args of [['--headless=new', '--disable-gpu', '--no-sandbox', '--virtual-time-budget=15000', '--dump-dom', 'file:///' + htmlPath.replace(/\\/g, '/')], ['--headless', '--disable-gpu', '--no-sandbox', '--virtual-time-budget=15000', '--dump-dom', 'file:///' + htmlPath.replace(/\\/g, '/')]]) { try { out = execFileSync(b, args, { timeout: 90000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }).toString('utf8') if (out.includes('RESULT ')) { console.log('BROWSER_OK ' + b.split('/').pop() + ' ' + args[0]); break } out = '' } catch (e) { out = '' } } if (out) break } if (!out) { console.log('BROWSER_RASTERIZE unavailable ✗'); process.exit(0) } const m = out.match(/RESULT (\{.*?\})<\/pre>/s) || out.match(/RESULT (\{.*\})/) if (!m) { console.log('NO_RESULT (out len ' + out.length + ')'); process.exit(0) } const res = JSON.parse(m[1]) const crypto = await import('node:crypto') for (const [k, v] of Object.entries(res)) { if (v.url) { const buf = Buffer.from(v.url.split(',')[1], 'base64') const sha = crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16) writeFileSync(resolve(HERE, `_raster_${k}.png`), buf) console.log(` ${k.padEnd(8)} ${v.w}x${v.h} png=${buf.length}B sha256=${sha} loaded=${v.ok}`) } else console.log(' ' + k + ' ERR ' + v.err) }