Files
readest/apps/readest-app/src/__tests__/document/pdfjs-wasm-assets.test.ts
T
Huang Xin 9b4db44490 fix(pdf): ship jbig2.wasm so scanned PDFs render in packaged builds (#4382)
pdfjs-dist 5.7.x (bumped 5.4.530 -> 5.7.284 in #4143) moved the JBIG2
image decoder -- the codec used by virtually every black-and-white
*scanned* PDF -- from pure JS into a WebAssembly module the worker
fetches at runtime from `wasmUrl` (/vendor/pdfjs/). The `copy-pdfjs-wasm`
script only copied an explicit allow-list ({openjpeg.wasm,qcms_bg.wasm})
and silently dropped jbig2.wasm. cpx does not error on an empty glob, so
the missing decoder went unnoticed: scanned PDFs rendered blank (pages
still turned) in CI builds, while local `tauri build` reused a stale
pre-bump public/vendor copy and worked. Regression between 0.11.1 and
0.11.2.

Copy the whole wasm/ dir (mirrors the {cmaps,standard_fonts}/* fonts
pattern) so future decoders moving to wasm can't be dropped again, and
ship the *_nowasm_fallback.js files for graceful degradation.

Adds a regression test asserting every .wasm the bundled pdf.js
references is covered by copy-pdfjs-wasm.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 08:52:10 +02:00

91 lines
3.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { existsSync, readdirSync, readFileSync } from 'fs';
import { isAbsolute, resolve } from 'path';
/**
* Regression guard for a build-packaging bug (scanned PDFs rendered blank in
* CI builds of 0.11.2, but fine locally).
*
* pdfjs-dist 5.7.x moved several image decoders (notably JBIG2 — the codec used
* by virtually every black-and-white *scanned* PDF) from pure JS into
* WebAssembly modules that the worker fetches at runtime from `wasmUrl`
* (`/vendor/pdfjs/` — see packages/foliate-js/pdf.js). The `copy-pdfjs-wasm`
* npm script only copied an explicit allow-list (`openjpeg.wasm`, `qcms_bg.wasm`)
* and silently dropped `jbig2.wasm`. `cpx` does not error when a glob matches
* nothing, so the missing decoder went unnoticed: the worker loaded, pages
* turned, but JBIG2 image decoding failed → blank pages.
*
* Invariant: every `.wasm` file the *bundled* pdf.js (main + worker) references
* and that actually exists in pdfjs-dist's `wasm/` directory must be copied into
* the public vendor folder by `copy-pdfjs-wasm`.
*/
const appRoot = process.cwd();
/** Extract the first double-quoted argument from a cpx-based npm script. */
const sourceGlobOf = (script: string): string => {
const match = script.match(/"([^"]+)"/);
if (!match) throw new Error(`No quoted source glob in script: ${script}`);
return match[1]!;
};
/**
* Resolve which files a cpx source glob of the form `<dir>/<token>` copies,
* where `<token>` is `*`, a `{a,b,c}` brace list, or a single filename.
* Returns the absolute directory and the concrete set of copied basenames.
*/
const resolveCpxGlob = (glob: string): { dir: string; files: Set<string> } => {
const slash = glob.lastIndexOf('/');
const dirPart = glob.slice(0, slash);
const token = glob.slice(slash + 1);
const dir = isAbsolute(dirPart) ? dirPart : resolve(appRoot, dirPart);
let names: string[];
if (token.includes('*')) {
names = readdirSync(dir);
} else if (token.startsWith('{') && token.endsWith('}')) {
names = token.slice(1, -1).split(',');
} else {
names = [token];
}
return { dir, files: new Set(names) };
};
describe('pdfjs vendor wasm assets', () => {
const pkg = JSON.parse(readFileSync(resolve(appRoot, 'package.json'), 'utf8')) as {
scripts: Record<string, string>;
};
it('copies every wasm decoder the bundled pdf.js references', () => {
// Files the worker/main bundle are copied from (source of truth for CI).
const jsGlob = sourceGlobOf(pkg.scripts['copy-pdfjs-js']!);
const { dir: jsDir, files: jsFiles } = resolveCpxGlob(jsGlob);
// The wasm modules available in pdfjs-dist and what the copy script ships.
const wasmGlob = sourceGlobOf(pkg.scripts['copy-pdfjs-wasm']!);
const { dir: wasmDir, files: copiedWasm } = resolveCpxGlob(wasmGlob);
const availableWasm = new Set(readdirSync(wasmDir).filter((f) => f.endsWith('.wasm')));
// Scan the bundled JS for `*.wasm` references, keeping only ones that map to
// a real file (drops minified false positives like `e.wasm`/`t.wasm`).
const referenced = new Set<string>();
for (const file of jsFiles) {
const full = resolve(jsDir, file);
if (!existsSync(full)) continue;
const text = readFileSync(full, 'utf8');
for (const m of text.matchAll(/[A-Za-z0-9_-]+\.wasm/g)) {
if (availableWasm.has(m[0])) referenced.add(m[0]);
}
}
// Sanity: the bump that caused the bug must still ship jbig2 as wasm.
expect(referenced.has('jbig2.wasm'), 'expected pdf.js to reference jbig2.wasm').toBe(true);
const missing = [...referenced].filter((w) => !copiedWasm.has(w));
expect(missing, `copy-pdfjs-wasm must copy these referenced wasm files: ${missing}`).toEqual(
[],
);
});
});