const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); const adapterFile = path.join(__dirname, '..', 'ui', 'reader', 'pdf-adapter.mjs'); const src = fs.readFileSync(adapterFile, 'utf8'); // pdf-adapter.mjs 直接 import vendor 的 pdf.min.mjs,在 Node 里加载会因缺 DOMMatrix 而抛错, // 所以只把不依赖 DOM 的钳制段落切出来求值,断言的仍是生产源码本身。 function loadPureBlock() { const start = src.indexOf('const MAX_CANVAS_SIDE'); const end = src.indexOf('const CSS = `'); assert.ok(start >= 0 && end > start, '找不到画布上限段落,钳制代码可能被移动或删除'); const block = src.slice(start, end).replace(/^export /gm, ''); assert.doesNotMatch( block, /\b(window|document|navigator)\b/, '钳制逻辑必须是纯函数,一旦依赖 DOM 就无法在单测里做真实数值断言' ); return new Function(`${block}\nreturn { clampCanvasSize, clampRenderQuality };`)(); } const { clampCanvasSize, clampRenderQuality } = loadPureBlock(); const MAX_SIDE = 16384; const MAX_AREA = 268435456; function assertWithinLimits(out, label) { assert.ok( out.width <= MAX_SIDE && out.height <= MAX_SIDE, `${label} 单边 ${out.width}x${out.height} 超过 ${MAX_SIDE},浏览器会静默给出不可用画布,整页空白且不报错` ); assert.ok( out.width * out.height <= MAX_AREA, `${label} 面积 ${out.width * out.height} 超过 ${MAX_AREA},同样会静默失败` ); assert.ok( out.width >= 1 && out.height >= 1, `${label} 钳制后出现 ${out.width}x${out.height},0 尺寸画布会让 getContext 之后的绘制全部丢弃` ); } test('A4 常规缩放与超采样不触发钳制', () => { // 612x792pt 的 A4 @ scale1.2,倍率 2:1468x1900,远在上限内 const out = clampCanvasSize(612 * 1.2, 792 * 1.2, 2); assert.strictEqual(out.clamped, false, '常规页被误钳会白白牺牲清晰度'); assert.strictEqual(out.quality, 2, '未触发上限时必须原样保留名义倍率'); assert.strictEqual(out.width, 1468, `期望 1468 实际 ${out.width}`); assert.strictEqual(out.height, 1900, `期望 1900 实际 ${out.height}`); assertWithinLimits(out, 'A4@1.2x2'); }); test('A4 最高缩放叠加 dpr3 仍不触发钳制', () => { // 实测 8929x12628(113M)是可用的,钳制不该把这一档也压下去 const out = clampCanvasSize(612 * 5, 792 * 5, 3); assert.strictEqual(out.clamped, false, '这一档实测可用,钳制过度会让高缩放反而更糊'); assert.strictEqual(out.width, 9180, `期望 9180 实际 ${out.width}`); assert.strictEqual(out.height, 11880, `期望 11880 实际 ${out.height}`); assertWithinLimits(out, 'A4@5x3'); }); test('2000pt 大幅面 @ scale5 dpr2 被钳回上限内', () => { // 期望 backing 20000x14000(280M),实测不可用 const out = clampCanvasSize(2000 * 5, 1400 * 5, 2); assert.strictEqual(out.clamped, true, '280M 画布必须被识别为超限,否则用户只看到空白页且没有任何提示'); assertWithinLimits(out, '2000pt@5x2'); assert.strictEqual(out.width, MAX_SIDE, `长边应正好压到上限以保留最大清晰度,实际 ${out.width}`); assert.ok(out.quality < 2, `生效倍率 ${out.quality} 应低于名义倍率 2`); }); test('4000pt 图纸 @ scale3 dpr2 被钳回上限内', () => { // 期望 backing 24000x18000(432M),实测不可用 const out = clampCanvasSize(4000 * 3, 3000 * 3, 2); assert.strictEqual(out.clamped, true, '432M 画布必须被识别为超限'); assertWithinLimits(out, '4000pt@3x2'); assert.strictEqual(out.width, MAX_SIDE, `长边应正好压到上限,实际 ${out.width}`); assert.strictEqual(out.height, 12288, `期望 12288 实际 ${out.height}`); }); test('钳制后的实际比例如实报告,供 render 的 transform 使用', () => { const cssWidth = 4000 * 3; const cssHeight = 3000 * 3; const out = clampCanvasSize(cssWidth, cssHeight, 2); // transform 用名义倍率而不是取整后的实际比例,画面会错位或只画出一角 assert.strictEqual(out.scaleX, out.width / cssWidth, 'scaleX 必须等于 backing 宽除以 CSS 宽'); assert.strictEqual(out.scaleY, out.height / cssHeight, 'scaleY 必须等于 backing 高除以 CSS 高'); assert.ok( Math.abs(out.scaleX - out.scaleY) < 1e-6, `等比钳制下 x/y 比例应基本一致,实际 ${out.scaleX} / ${out.scaleY}` ); }); test('短边的取整损失也要反映到比例上,不能直接沿用生效倍率', () => { // 长边正好压到上限时它的比例恰等于生效倍率,只有短边能暴露 floor 带来的偏差 const out = clampCanvasSize(16000, 17000, 1); assert.strictEqual(out.scaleX, out.width / 16000, 'scaleX 必须按 floor 之后的实际宽算'); assert.notStrictEqual( out.scaleX, out.quality, `短边 ${out.width}px 的实际比例应当略小于生效倍率 ${out.quality},直接沿用会让内容画出画布边界` ); assert.strictEqual(out.scaleY, out.height / 17000, 'scaleY 必须按 floor 之后的实际高算'); }); test('取整误差不会让某一边掉到 0 像素', () => { const out = clampCanvasSize(100000, 1, 1); assertWithinLimits(out, '极端长条页'); assert.strictEqual(out.height, 1, `期望至少 1px,实际 ${out.height}`); assert.strictEqual(out.scaleY, 1, 'scaleY 必须按补足到 1px 后的实际比例报告,否则内容会被压到画布外'); }); test('长边超限而面积未超限的页也被钳', () => { // 20000x2000 = 40M,面积远未超限,只有单边超限 const out = clampCanvasSize(20000, 2000, 1); assert.strictEqual(out.clamped, true, '只超单边同样不可用,不能只看面积'); assertWithinLimits(out, '超长单边'); assert.strictEqual(out.width, MAX_SIDE, `期望 ${MAX_SIDE} 实际 ${out.width}`); }); test('接近正方形的超大页同时压住面积与单边', () => { // 16000x17000 = 272M,面积超限且短边已经贴着上限,是最容易只压一头的形状 const out = clampCanvasSize(16000, 17000, 1); assert.strictEqual(out.clamped, true, '272M 面积必须被识别为超限'); assertWithinLimits(out, '16000x17000'); assert.strictEqual(out.height, MAX_SIDE, `长边应压到上限,实际 ${out.height}`); }); test('面积约束是真在起作用,而不是被单边约束顺带盖住', () => { // 当前常量下 MAX_AREA 恰好等于 MAX_SIDE 的平方,压住长边就顺带压住了面积, // 面积检查的价值只有在上限常量按别的浏览器口径调整时才显现。 // 这里把单边上限换成 65535 重新求值,确认删掉面积项会立刻放出 600M 的画布。 const start = src.indexOf('const MAX_CANVAS_SIDE'); const end = src.indexOf('const CSS = `'); const block = src.slice(start, end) .replace(/^export /gm, '') .replace('const MAX_CANVAS_SIDE = 16384;', 'const MAX_CANVAS_SIDE = 65535;'); const wide = new Function(`${block}\nreturn clampCanvasSize;`)(); const out = wide(30000, 20000, 1); assert.ok( out.width * out.height <= MAX_AREA, `面积 ${out.width * out.height} 超过 ${MAX_AREA}:面积约束没有独立生效,只靠单边约束挡不住扁平的大幅面页` ); assert.strictEqual(out.clamped, true, '面积超限也必须如实上报,否则外壳无法解释清晰度为何被降'); }); test('大范围尺寸与倍率组合下上限恒成立', () => { const sides = [1, 200, 612, 792, 1190, 2384, 5000, 10000, 20000, 40000]; const qualities = [1, 1.25, 1.5, 2, 2.5, 3, 4]; const scales = [0.25, 1, 1.2, 2, 3, 5]; for (const w of sides) { for (const h of sides) { for (const q of qualities) { for (const s of scales) { const out = clampCanvasSize(w * s, h * s, q); const label = `${w}x${h} @scale${s} @${q}x`; assertWithinLimits(out, label); assert.ok( out.quality <= q + 1e-9, `${label}:生效倍率 ${out.quality} 不该超过名义倍率 ${q}` ); assert.strictEqual(out.scaleX, out.width / (w * s), `${label}:scaleX 与实际 backing 宽不符`); assert.strictEqual(out.scaleY, out.height / (h * s), `${label}:scaleY 与实际 backing 高不符`); } } } } }); test('非法尺寸与倍率退回安全值而不是抛错或产出 NaN', () => { for (const bad of [undefined, null, NaN, 0, -5, 'abc', Infinity]) { const out = clampCanvasSize(bad, bad, bad); assert.ok( Number.isInteger(out.width) && Number.isInteger(out.height), `尺寸 ${String(bad)} 产出了非整数 ${out.width}x${out.height},canvas.width 赋 NaN 会静默变 0` ); assertWithinLimits(out, `非法输入 ${String(bad)}`); } }); test('renderQuality 只接受 1 到 4,非法值回退到 1', () => { assert.strictEqual(clampRenderQuality(1), 1); assert.strictEqual(clampRenderQuality(2), 2); assert.strictEqual(clampRenderQuality(4), 4); assert.strictEqual(clampRenderQuality(1.5), 1.5, '允许非整数档位,1.5x 实测已能显著降低误差'); assert.strictEqual(clampRenderQuality(8), 4, '超过 4 倍换不来可感知的清晰度,只会成倍吃显存'); assert.strictEqual(clampRenderQuality(0.5), 1, '低于 1 会比现状更糊'); for (const bad of [undefined, null, NaN, 'abc', {}, Infinity, -Infinity]) { assert.strictEqual( clampRenderQuality(bad), 1, `非法值 ${String(bad)} 必须回退到 1,回退到 NaN 会让整块 backing 计算失效` ); } }); test('源码锁死画布上限常量与两条约束', () => { assert.match(src, /const MAX_CANVAS_SIDE = 16384;/, 'Chromium 单边上限,改动前必须先实测'); assert.match(src, /const MAX_CANVAS_AREA = 268435456;/, 'Chromium 总面积上限'); assert.match( src, /Math\.min\(want, bySide, byArea\)/, '单边与面积两条约束都要参与,只留一条在上限常量变动后就会漏放超限画布' ); assert.match(src, /MAX_CANVAS_SIDE \/ Math\.max\(cssWidth, cssHeight\)/, '单边约束按长边算'); assert.match(src, /Math\.sqrt\(MAX_CANVAS_AREA \/ \(cssWidth \* cssHeight\)\)/, '面积约束按开方算'); assert.match(src, /Math\.max\(1, Math\.floor\(cssWidth \* applied\)\)/, '钳制后至少保留 1px'); assert.match(src, /Math\.max\(1, Math\.floor\(cssHeight \* applied\)\)/, '钳制后至少保留 1px'); }); test('源码锁死超采样倍率的取值与生效方式', () => { assert.match( src, /Math\.max\(1, Math\.min\(4, n\)\)/, 'renderQuality 必须被 clamp 到 1..4' ); // 一次锁死整条链路:dpr 取大而不是叠乘、期望倍率必须过钳制、画布尺寸只能来自钳制结果 assert.match( src, /const dpr = window\.devicePixelRatio \|\| 1;\s*\n\s*const wanted = Math\.max\(dpr, clampRenderQuality\(renderQuality\)\);\s*\n\s*const fit = clampCanvasSize\(vp\.width, vp\.height, wanted\);\s*\n\s*p\.canvas\.width = fit\.width;\s*\n\s*p\.canvas\.height = fit\.height;/, '有效倍率必须是 max(dpr, renderQuality) 且画布尺寸只能取钳制结果,绕过任一步都会重新引入静默空白页' ); assert.strictEqual( [...src.matchAll(/Math\.max\(dpr, clampRenderQuality\(renderQuality\)\)/g)].length, 2, 'renderPage 与 renderStats 必须用同一个有效倍率公式,否则界面报告的清晰度和实际渲染的不一致' ); assert.match( src, /transform: identity \? null : \[fit\.scaleX, 0, 0, fit\.scaleY, 0, 0\]/, 'transform 必须用钳制后的实际比例,用名义倍率会画错位或只画出一角' ); assert.doesNotMatch( src, /transform: dpr === 1 \? null : \[dpr, 0, 0, dpr, 0, 0\]/, '旧的 dpr 直接当 transform 的写法在钳制生效时会画错' ); }); test('源码锁死倍率变化触发全页重建', () => { assert.match( src, /if \(nextScale !== scale \|\| nextQuality !== renderQuality\) \{\s*\n\s*epoch\+\+;/, '倍率变化不走 epoch++ 重建,同屏会残留旧清晰度的页' ); assert.match( src, /nextQuality = opts && opts\.renderQuality !== undefined\s*\n?\s*\? clampRenderQuality\(opts\.renderQuality\)\s*\n?\s*: renderQuality/, '缺省 renderQuality 时必须沿用当前值,否则每次渲染都会把用户设置重置成 1' ); assert.match(src, /renderStats\(\)/, 'renderStats 是外壳读取当前清晰度状态的唯一正当出口'); assert.doesNotMatch(src, /window\.__test/, '不允许为测试往生产代码加全局钩子'); }); test('AI 截图路径同样受画布上限保护', () => { assert.match( src, /const fit = clampCanvasSize\(area\.width, area\.height, wantScale\);/, '大幅面页在 renderScale 下限 1 时画布等于页面点尺寸,MediaBox 异常的文件会顶到上限' ); assert.match(src, /const renderScale = fit\.quality;/, '截图的 viewport 与 transform 必须用钳制后的倍率'); assert.match(src, /canvas\.width = fit\.width;/, '截图画布尺寸直接取钳制结果'); // 图像只保留一条 JPEG 编码路径,钳制不该顺手引入格式回退 assert.deepStrictEqual( [...src.matchAll(/toDataURL\('([^']+)'/g)].map((m) => m[1]), [], '编码仍应集中在 visual-context.mjs,适配器里不该出现新的编码路径' ); });