_check_rasterize.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. // P103-2:用无头 Edge/Chrome 跑**真实栅格化路径**,并对每张图输出 sha256 + 尺寸;同时检查 CSS 是否作用于 svg
  2. import { readFileSync, writeFileSync, existsSync } from 'node:fs'
  3. import { fileURLToPath } from 'node:url'
  4. import { dirname, resolve } from 'node:path'
  5. import { JSDOM, VirtualConsole } from 'jsdom'
  6. import { execFileSync } from 'node:child_process'
  7. const HERE = dirname(fileURLToPath(import.meta.url))
  8. const dist = resolve(HERE, '../../../release/web')
  9. const GW = 'http://106.120.102.238:28084'
  10. const fleet = readFileSync(resolve(HERE, '_fleet_fixture.json'), 'utf8')
  11. const facts = '{"cards":[{"c":"WTG01","verdict":"定论","part":"主轴承"}],"contract_sha256":"abc"}'
  12. const idx = readFileSync(dist + '/index.html', 'utf8')
  13. const jsFile = (idx.match(/src="([^"]+\.js)"/) || [])[1].replace(/^.*\//, '')
  14. const cssFile = (idx.match(/href="([^"]+\.css)"/) || [])[1].replace(/^.*\//, '')
  15. const code = readFileSync(dist + '/assets/' + jsFile, 'utf8')
  16. const css = readFileSync(dist + '/assets/' + cssFile, 'utf8')
  17. // ① 页面(jsdom)取各块 SVG 原文
  18. const vc = new VirtualConsole()
  19. const dom = new JSDOM(idx.replace('</head>', '<style>' + css + '</style></head>'),
  20. { url: GW + '/detail/v2#tab=report', runScripts: 'outside-only', pretendToBeVisual: true, virtualConsole: vc })
  21. const w = dom.window
  22. w.fetch = async (u) => { const s = String(u); const b = /fleet/.test(s) ? fleet : (/facts/.test(s) ? facts : '{}')
  23. return { ok: true, status: 200, json: async () => JSON.parse(b), text: async () => b } }
  24. w.eval(code)
  25. for (let i = 0; i < 24; i++) { await new Promise((r) => setTimeout(r, 400)); if (w.document.querySelector('[data-preset="month"]')) break }
  26. w.document.querySelector('[data-preset="month"]').dispatchEvent(new w.MouseEvent('click', { bubbles: true }))
  27. await new Promise((r) => setTimeout(r, 2500))
  28. const svgs = {}
  29. for (const b of w.document.querySelectorAll('.paper [data-blk]')) {
  30. const s = b.querySelector('svg')
  31. if (s) svgs[b.getAttribute('data-blk')] = s.outerHTML
  32. }
  33. console.log('PAGE_SVGS ' + JSON.stringify(Object.keys(svgs)))
  34. // ② CSS 中是否命中 svg / .dt(若命中,栅格化必须把这份样式内联进 SVG,否则 Word 里会不一样)
  35. const cssHits = []
  36. for (const m of css.matchAll(/([^{}]*)\{([^{}]*)\}/g)) {
  37. const sel = m[1].trim()
  38. if (/(^|[\s,>])svg\b|\.dt\b/.test(sel)) cssHits.push(sel.slice(0, 90) + ' { ' + m[2].trim().slice(0, 90) + ' }')
  39. }
  40. console.log('CSS_HITS_SVG ' + cssHits.length)
  41. cssHits.slice(0, 8).forEach((h) => console.log(' ' + h))
  42. // ③ 真实浏览器栅格化:生成测试页,用 Edge/Chrome 无头执行,回读结果
  43. const testHtml = `<!doctype html><meta charset="utf-8"><body style="margin:0;background:#fff">
  44. <style>${css}</style>
  45. <div id="host"></div><pre id="out">pending</pre>
  46. <script>
  47. const svgs = ${JSON.stringify(svgs)};
  48. const keys = Object.keys(svgs);
  49. (async () => {
  50. const res = {};
  51. for (const k of keys) {
  52. const holder = document.createElement('div');
  53. holder.innerHTML = svgs[k];
  54. document.getElementById('host').appendChild(holder);
  55. const svg = holder.querySelector('svg');
  56. try {
  57. const xml = new XMLSerializer().serializeToString(svg);
  58. const vb = (svg.getAttribute('viewBox') || '').split(/[\\s,]+/).map(Number);
  59. const W = (vb[2] || 720) * 2, H = (vb[3] || 300) * 2;
  60. const img = new Image();
  61. await new Promise((r) => { img.onload = r; img.onerror = r; img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(xml); });
  62. const cv = document.createElement('canvas'); cv.width = W; cv.height = H;
  63. const ctx = cv.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, W, H);
  64. ctx.drawImage(img, 0, 0, W, H);
  65. res[k] = { url: cv.toDataURL('image/png'), w: W, h: H, ok: img.complete && img.naturalWidth > 0 };
  66. } catch (e) { res[k] = { err: String(e) }; }
  67. }
  68. document.getElementById('out').textContent = 'RESULT ' + JSON.stringify(res);
  69. document.title = 'DONE';
  70. })();
  71. </script></body>`
  72. const htmlPath = resolve(HERE, '_raster_test.html')
  73. writeFileSync(htmlPath, testHtml, 'utf8')
  74. const browsers = ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Google/Chrome/Application/chrome.exe']
  75. let out = ''
  76. for (const b of browsers) {
  77. if (!existsSync(b)) continue
  78. for (const args of [['--headless=new', '--disable-gpu', '--no-sandbox', '--virtual-time-budget=15000', '--dump-dom', 'file:///' + htmlPath.replace(/\\/g, '/')],
  79. ['--headless', '--disable-gpu', '--no-sandbox', '--virtual-time-budget=15000', '--dump-dom', 'file:///' + htmlPath.replace(/\\/g, '/')]]) {
  80. try {
  81. out = execFileSync(b, args, { timeout: 90000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }).toString('utf8')
  82. if (out.includes('RESULT ')) { console.log('BROWSER_OK ' + b.split('/').pop() + ' ' + args[0]); break }
  83. out = ''
  84. } catch (e) { out = '' }
  85. }
  86. if (out) break
  87. }
  88. if (!out) { console.log('BROWSER_RASTERIZE unavailable ✗'); process.exit(0) }
  89. const m = out.match(/RESULT (\{.*?\})<\/pre>/s) || out.match(/RESULT (\{.*\})/)
  90. if (!m) { console.log('NO_RESULT (out len ' + out.length + ')'); process.exit(0) }
  91. const res = JSON.parse(m[1])
  92. const crypto = await import('node:crypto')
  93. for (const [k, v] of Object.entries(res)) {
  94. if (v.url) {
  95. const buf = Buffer.from(v.url.split(',')[1], 'base64')
  96. const sha = crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16)
  97. writeFileSync(resolve(HERE, `_raster_${k}.png`), buf)
  98. console.log(` ${k.padEnd(8)} ${v.w}x${v.h} png=${buf.length}B sha256=${sha} loaded=${v.ok}`)
  99. } else console.log(' ' + k + ' ERR ' + v.err)
  100. }