Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .yarnrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# pdfjs-dist 4.x and fast-xml-parser 5.x declare newer Node engines than the
# Node 14 toolchain CI still runs on. Neither runs under Node here (pdf.js is
# served to the discovery browser), so the engine field is a toolchain gate
# only. Remove this once CI moves to Node 20.
--ignore-engines true
2 changes: 1 addition & 1 deletion packages/cli-pdf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
},
"dependencies": {
"@percy/logger": "1.32.12-beta.2",
"pdfjs-dist": "^2.16.105"
"pdfjs-dist": "^4.10.38"
}
}
7 changes: 5 additions & 2 deletions packages/cli-pdf/src/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ const cjsRequire = createRequire(import.meta.url);
export function pdfjsAssets() {
let root = path.dirname(cjsRequire.resolve('pdfjs-dist/package.json'));

// pdf.js 4+ is published only as ES modules. The page imports `libFile` from
// the loopback asset server that serves `buildDir` (see loadLibrary), and
// pdf.js then spawns `workerFile` as a module worker from the same directory.
return {
root,
buildDir: path.join(root, 'legacy/build'),
standardFontsDir: path.join(root, 'standard_fonts'),
cmapsDir: path.join(root, 'cmaps'),
libPath: path.join(root, 'legacy/build/pdf.js'),
workerFile: 'pdf.worker.js'
libFile: 'pdf.mjs',
workerFile: 'pdf.worker.mjs'
};
}
18 changes: 15 additions & 3 deletions packages/cli-pdf/src/browser-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,26 @@ export function assertRasterDimensions(pageNumber, width, height) {
}
}

export async function openDocument(_, { origin }) {
let lib = window['pdfjs-dist/build/pdf'] || window.pdfjsLib;
// pdf.js 4+ ships only as ES modules, so it can no longer be injected as a
// classic script body the way the 2.x UMD build was. The page imports it from
// the loopback asset server instead. The legacy build assigns
// `globalThis.pdfjsLib` itself, so a repeat call finds it and does nothing.
export async function loadLibrary(_, { origin, libFile }) {
if (!window.pdfjsLib) {
window.pdfjsLib = await import(`${origin}/pdfjs/${libFile}`);
}

return true;
}

export async function openDocument(_, { origin, workerFile }) {
let lib = window.pdfjsLib;

if (!lib) {
throw new Error('pdf.js did not initialise in the page');
}

lib.GlobalWorkerOptions.workerSrc = `${origin}/pdfjs/pdf.worker.js`;
lib.GlobalWorkerOptions.workerSrc = `${origin}/pdfjs/${workerFile}`;

let doc = await lib.getDocument({
url: `${origin}/doc.pdf`,
Expand Down
1 change: 1 addition & 0 deletions packages/cli-pdf/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { pdfjsAssets } from './assets.js';
export { resolvePages, MAX_PAGES } from './pages.js';
export {
loadLibrary,
openDocument,
measurePages,
renderPage,
Expand Down
14 changes: 10 additions & 4 deletions packages/cli-pdf/test/assets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('@percy/cli-pdf assets', () => {

it('points at the legacy build directory', () => {
expect(fs.existsSync(assets.buildDir)).toBe(true);
expect(fs.existsSync(path.join(assets.buildDir, 'pdf.js'))).toBe(true);
expect(fs.existsSync(path.join(assets.buildDir, assets.libFile))).toBe(true);
expect(fs.existsSync(path.join(assets.buildDir, assets.workerFile))).toBe(true);
});

Expand All @@ -22,8 +22,14 @@ describe('@percy/cli-pdf assets', () => {
expect(fs.readdirSync(assets.cmapsDir).length).toBeGreaterThan(0);
});

it('exposes the injectable pdf.js library file', () => {
expect(fs.existsSync(assets.libPath)).toBe(true);
expect(fs.readFileSync(assets.libPath, 'utf-8')).toContain('getDocument');
it('names the ES module entry and worker the page loads', () => {
// pdf.js 4+ only ships ES modules; the page imports the library and pdf.js
// spawns the worker as a module worker, so both must be .mjs files
expect(assets.libFile).toMatch(/\.mjs$/);
expect(assets.workerFile).toMatch(/\.mjs$/);

let lib = fs.readFileSync(path.join(assets.buildDir, assets.libFile), 'utf-8');
expect(lib).toContain('getDocument');
expect(lib).toContain('globalThis.pdfjsLib');
});
});
46 changes: 31 additions & 15 deletions packages/cli-pdf/test/browser-scripts.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import url from 'url';
import path from 'path';
import {
fitScale, assertRasterDimensions,
MIN_DIMENSION, MAX_DIMENSION, DEFAULT_SCALE, MAX_SCALE,
openDocument, measurePages, renderPage, destroyDocument
loadLibrary, openDocument, measurePages, renderPage, destroyDocument
} from '../src/browser-scripts.js';

describe('@percy/cli-pdf browser scripts', () => {
Expand Down Expand Up @@ -73,7 +75,7 @@ describe('@percy/cli-pdf browser scripts', () => {
destroy: async () => { doc.destroyed = true; }
};

global.window = lib === null ? {} : { 'pdfjs-dist/build/pdf': lib || stubLib() };
global.window = lib === null ? {} : { pdfjsLib: lib || stubLib() };
global.document = {
createElement: (tag) => {
let canvas = {
Expand Down Expand Up @@ -110,16 +112,40 @@ describe('@percy/cli-pdf browser scripts', () => {
delete global.document;
});

describe('loadLibrary', () => {
it('imports the served module and exposes it as window.pdfjsLib', async () => {
global.window = {};
// a file: URL stands in for the asset server origin; the script appends
// `/pdfjs/<libFile>`, which lands on the fixture module
let origin = url.pathToFileURL(path.resolve('test/fixtures')).href;

await expectAsync(loadLibrary(null, { origin, libFile: 'pdf.mjs' })).toBeResolvedTo(true);

expect(global.window.pdfjsLib.getDocument()).toBe('served');
});

it('leaves an already initialised pdf.js alone', async () => {
// an unguarded import of this origin would reject in Node
let lib = stubLib();
global.window = { pdfjsLib: lib };

await expectAsync(loadLibrary(null, { origin: 'http://localhost:9999', libFile: 'pdf.mjs' }))
.toBeResolvedTo(true);

expect(global.window.pdfjsLib).toBe(lib);
});
});

describe('openDocument', () => {
it('points pdf.js at the served worker, document, fonts and cmaps', async () => {
stubPageGlobals({ numPages: 4 });
let lib = global.window['pdfjs-dist/build/pdf'];
let lib = global.window.pdfjsLib;

await expectAsync(openDocument(null, { origin: 'http://localhost:9999' }))
await expectAsync(openDocument(null, { origin: 'http://localhost:9999', workerFile: 'pdf.worker.mjs' }))
.toBeResolvedTo({ pageCount: 4 });

expect(lib.GlobalWorkerOptions.workerSrc)
.toBe('http://localhost:9999/pdfjs/pdf.worker.js');
.toBe('http://localhost:9999/pdfjs/pdf.worker.mjs');

let [options] = lib.getDocumentCalls;
expect(options.url).toBe('http://localhost:9999/doc.pdf');
Expand All @@ -137,16 +163,6 @@ describe('@percy/cli-pdf browser scripts', () => {
expect(global.window.__percyPdf.doc).toBe(doc);
});

it('falls back to the window.pdfjsLib global', async () => {
stubPageGlobals({ lib: null });
let lib = stubLib();
global.window.pdfjsLib = lib;

await expectAsync(openDocument(null, { origin: 'http://localhost:2' }))
.toBeResolvedTo({ pageCount: 3 });
expect(lib.getDocumentCalls.length).toBe(1);
});

it('throws when pdf.js did not initialise', async () => {
stubPageGlobals({ lib: null });

Expand Down
4 changes: 4 additions & 0 deletions packages/cli-pdf/test/fixtures/pdfjs/pdf.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Stands in for pdf.js's legacy/build/pdf.mjs when the loadLibrary browser
// script is exercised in Node: the asset server origin is a file: URL pointing
// at this directory's parent, so `${origin}/pdfjs/pdf.mjs` resolves here.
export const getDocument = () => 'served';
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"content-disposition": "^0.5.4",
"cross-spawn": "^7.0.3",
"fast-glob": "^3.2.11",
"fast-xml-parser": "^4.4.1",
"fast-xml-parser": "^5.11.2",
"micromatch": "^4.0.8",
"mime-types": "^2.1.34",
"pako": "^2.1.0",
Expand Down
12 changes: 4 additions & 8 deletions packages/core/src/pdf-rasterize.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fs from 'fs';
import logger from '@percy/logger';
import { Server } from './server.js';

Expand Down Expand Up @@ -58,7 +57,7 @@ export async function rasterizePdf(percy, pdfBuffer, options) {

let {
pdfjsAssets, resolvePages, fitScale, assertRasterDimensions,
openDocument, measurePages, renderPage, destroyDocument,
loadLibrary, openDocument, measurePages, renderPage, destroyDocument,
DEFAULT_SCALE, MAX_SCALE, PAGE_RENDER_TIMEOUT
} = await import('@percy/cli-pdf');

Expand All @@ -80,15 +79,12 @@ export async function rasterizePdf(percy, pdfBuffer, options) {
page = await percy.browser.page({ meta: { snapshot: { name: 'pdf' } } });
await page.goto(`${origin}/`);

let pdfjsSource = await fs.promises.readFile(assets.libPath, 'utf-8');

await withTimeout(
/* eslint-disable-next-line no-new-func */
page.eval(new Function(pdfjsSource)),
PAGE_RENDER_TIMEOUT, 'injecting pdf.js');
page.eval(loadLibrary, { origin, libFile: assets.libFile }),
PAGE_RENDER_TIMEOUT, 'loading pdf.js');

let { pageCount } = await withTimeout(
page.eval(openDocument, { origin }),
page.eval(openDocument, { origin, workerFile: assets.workerFile }),
PAGE_RENDER_TIMEOUT, 'opening the PDF');

let selected;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 11 additions & 11 deletions test/regression/assets/pdfs/expected/darwin-arm64/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generated": "2026-09-11T05:57:29.845Z",
"generated": "2026-10-15T11:47:16.205Z",
"platform": "darwin-arm64",
"browser": "Chrome/143.0.7499.0",
"pdfs": {
Expand All @@ -12,8 +12,8 @@
"page": 1,
"width": 1191,
"height": 1684,
"bytes": 470788,
"sha256": "4d7a640bc93744620eb8f6bb61b211a0656ffda73ef7693ef2a59e191b1e78d7"
"bytes": 470336,
"sha256": "552e90c8b1581fccaa12ca5137cc478ea73671c1f3254e4256e1292c895bb0ee"
}
]
},
Expand All @@ -26,22 +26,22 @@
"page": 1,
"width": 1224,
"height": 1584,
"bytes": 482333,
"sha256": "13375a4c2a74fc707e1b3747e964f433b98f25cebacb52bea995942cbbf262fe"
"bytes": 482141,
"sha256": "8be8f1aec14f7c194ba3bc823e256430b5ccb80e43d3b1dea792315562809160"
},
{
"page": 2,
"width": 1224,
"height": 1584,
"bytes": 528854,
"sha256": "a21a60255b97e79dc3e408a2d90717e6eb51cc0c4ca9d196ae0b11de0696d751"
"bytes": 528837,
"sha256": "d3385075c955a164b917eae98fc4ccfefb8253f8efc118f48b58dd4ed5f504e5"
},
{
"page": 3,
"width": 1224,
"height": 1584,
"bytes": 402184,
"sha256": "b42084cd9dfca9ff61c4c3518808ec0fdb55cf767a998d532d26647c05dba0fc"
"bytes": 402244,
"sha256": "440991358f7f88114e9abdecc351e3d46431ca7318242345c91b25815da528ec"
}
]
},
Expand All @@ -54,8 +54,8 @@
"page": 1,
"width": 1224,
"height": 1584,
"bytes": 411537,
"sha256": "08783e272c4880f8e63e42500fd65b80c596813024cb315bebc3cb81d5746a38"
"bytes": 411452,
"sha256": "1fb7654c3dbd9dba8c05c80681deb7c716dba846da418118f7b726cee601b7e9"
}
]
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions test/regression/assets/pdfs/expected/linux-x64/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"generated": "2026-09-11T06:18:42.036Z",
"generated": "2026-10-15T11:45:18.818Z",
"platform": "linux-x64",
"browser": "Chrome/143.0.7499.0",
"pdfs": {
Expand All @@ -12,8 +12,8 @@
"page": 1,
"width": 1191,
"height": 1684,
"bytes": 461354,
"sha256": "0fb1414f82961173acbcb4acffd5b64da856ebb86ba3c4393aa7c7a63995841a"
"bytes": 461279,
"sha256": "d6307ee3b6aa26c7b0d51bbcc34ac4ec3172d83eed7e0064fe8e7bd375735762"
}
]
},
Expand Down Expand Up @@ -54,8 +54,8 @@
"page": 1,
"width": 1224,
"height": 1584,
"bytes": 379684,
"sha256": "73de82e2d54dddca2e9219c8fe282ac372d4bd96347be5bd214902ac0109a7fd"
"bytes": 379630,
"sha256": "af6209757505407b8413b02124bcfdcdaf336575d9f2f9f9631be4a1144f80d2"
}
]
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading