| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- // 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('</head>', '<style>' + css + '</style></head>'),
- { 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 = `<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">
- <style>${css}</style>
- <div id="host"></div><pre id="out">pending</pre>
- <script>
- const svgs = ${JSON.stringify(svgs)};
- const keys = Object.keys(svgs);
- (async () => {
- const res = {};
- for (const k of keys) {
- const holder = document.createElement('div');
- holder.innerHTML = svgs[k];
- document.getElementById('host').appendChild(holder);
- const svg = holder.querySelector('svg');
- try {
- const xml = new XMLSerializer().serializeToString(svg);
- const vb = (svg.getAttribute('viewBox') || '').split(/[\\s,]+/).map(Number);
- const W = (vb[2] || 720) * 2, H = (vb[3] || 300) * 2;
- const img = new Image();
- await new Promise((r) => { img.onload = r; img.onerror = r; img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(xml); });
- const cv = document.createElement('canvas'); cv.width = W; cv.height = H;
- const ctx = cv.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H);
- ctx.drawImage(img, 0, 0, W, H);
- res[k] = { url: cv.toDataURL('image/png'), w: W, h: H, ok: img.complete && img.naturalWidth > 0 };
- } catch (e) { res[k] = { err: String(e) }; }
- }
- document.getElementById('out').textContent = 'RESULT ' + JSON.stringify(res);
- document.title = 'DONE';
- })();
- </script></body>`
- 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)
- }
|