Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
61eb6f6
Extract ObjectHandler from WorkerTransport
Aditi-1400 Apr 12, 2026
7cd8e24
Adds RendererWorker class for offloading canvas
Aditi-1400 Apr 13, 2026
3ae950b
Add TR-based canvas filter detection in core layer
Aditi-1400 Apr 12, 2026
089354d
Add object forwarding between main thread and renderer worker
Aditi-1400 Apr 13, 2026
2db7631
Add graphics initialization and operator list execution in renderer w…
Aditi-1400 Apr 13, 2026
9b65c48
Adapt viewer and tests for OffscreenCanvas renderer worker
Aditi-1400 Apr 13, 2026
bfa56ac
Enable WebGPU in renderer worker
Aditi-1400 Apr 13, 2026
2b4aecd
Wait for GPU rendering
Aditi-1400 May 4, 2026
767b5f1
Add support for images and dependency tracker
Aditi-1400 May 16, 2026
73eefc2
Make error handling for forwarding objects better
Aditi-1400 Jun 2, 2026
fc86d76
Couple the renderer-worker handler lifecycle to its consumers
Aditi-1400 Jun 12, 2026
a2a486b
Allow PDFs checkboxes while rendering in worker
Aditi-1400 Jun 29, 2026
4df8779
Address review comments
Aditi-1400 Jun 29, 2026
01f8333
fixup! Add object forwarding between main thread and renderer worker
Aditi-1400 Jul 14, 2026
a3d2879
fixup! Add graphics initialization and operator list execution in ren…
Aditi-1400 Jul 14, 2026
14b136b
fixup! Add graphics initialization and operator list execution in ren…
Aditi-1400 Jul 14, 2026
d44b269
fixup! Add graphics initialization and operator list execution in ren…
Aditi-1400 Jul 14, 2026
5531f28
fixup! Adapt viewer and tests for OffscreenCanvas renderer worker
Aditi-1400 Jul 14, 2026
c6a0631
fixup! Adapt viewer and tests for OffscreenCanvas renderer worker
Aditi-1400 Jul 14, 2026
d8776a2
fixup! Address review comments
Aditi-1400 Jul 14, 2026
cf48e41
fixup! Add graphics initialization and operator list execution in ren…
Aditi-1400 Jul 14, 2026
eb2f3c0
fixup! Address review comments
Aditi-1400 Jul 14, 2026
fc247fc
fixup! Address review comments
Aditi-1400 Jul 14, 2026
7cbb9fe
fixup! Wait for GPU rendering
Aditi-1400 Jul 14, 2026
4557e3e
fixup! Adds RendererWorker class for offloading canvas
Aditi-1400 Jul 14, 2026
afdfee3
Update the maximum number of prefs
Aditi-1400 Jul 20, 2026
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
4 changes: 4 additions & 0 deletions external/dist/webpack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ if (typeof window !== "undefined" && "Worker" in window) {
new URL("./build/pdf.worker.mjs", import.meta.url),
{ type: "module" }
);
GlobalWorkerOptions.rendererSrc = new URL(
"./build/pdf.renderer.mjs",
import.meta.url
).href;
}

export * from "./build/pdf.mjs";
54 changes: 45 additions & 9 deletions gulpfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,24 @@ function createWorkerBundle(defines) {
.pipe(webpack2Stream(workerFileConfig));
}

function createRendererWorkerBundle(defines) {
const rendererWorkerDefines = {
...defines,
WORKER_THREAD: true,
};
const rendererWorkerFileConfig = createWebpackConfig(rendererWorkerDefines, {
filename: rendererWorkerDefines.MINIFIED
? "pdf.renderer.min.mjs"
: "pdf.renderer.mjs",
library: {
type: "module",
},
});
return gulp
.src("./src/pdf.renderer.js", { encoding: false })
.pipe(webpack2Stream(rendererWorkerFileConfig));
}

function createWebBundle(defines, options) {
const viewerFileConfig = createWebpackConfig(defines, {
filename: "viewer.mjs",
Expand Down Expand Up @@ -1233,6 +1251,7 @@ function buildGeneric(defines, dir) {
return ordered([
createMainBundle(defines).pipe(gulp.dest(dir + "build")),
createWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createRendererWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createSandboxBundle(defines).pipe(gulp.dest(dir + "build")),
createWebBundle(defines).pipe(gulp.dest(dir + "web")),
gulp
Expand Down Expand Up @@ -1377,6 +1396,7 @@ function buildMinified(defines, dir) {
return ordered([
createMainBundle(defines).pipe(gulp.dest(dir + "build")),
createWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createRendererWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createSandboxBundle(defines).pipe(gulp.dest(dir + "build")),
createImageDecodersBundle({ ...defines, IMAGE_DECODERS: true }).pipe(
gulp.dest(dir + "image_decoders")
Expand Down Expand Up @@ -1502,6 +1522,9 @@ gulp.task(
createWorkerBundle(defines).pipe(
gulp.dest(MOZCENTRAL_CONTENT_DIR + "build")
),
createRendererWorkerBundle(defines).pipe(
gulp.dest(MOZCENTRAL_CONTENT_DIR + "build")
),
createWebBundle(defines).pipe(
gulp.dest(MOZCENTRAL_CONTENT_DIR + "web")
),
Expand Down Expand Up @@ -1607,6 +1630,9 @@ gulp.task(
createWorkerBundle(defines).pipe(
gulp.dest(CHROME_BUILD_CONTENT_DIR + "build")
),
createRendererWorkerBundle(defines).pipe(
gulp.dest(CHROME_BUILD_CONTENT_DIR + "build")
),
createSandboxBundle(defines).pipe(
gulp.dest(CHROME_BUILD_CONTENT_DIR + "build")
),
Expand Down Expand Up @@ -1800,7 +1826,7 @@ function buildLib(defines, dir) {
gulp.src(
[
"src/{core,display,shared}/**/*.js",
"src/{pdf,pdf.image_decoders,pdf.worker}.js",
"src/{pdf,pdf.image_decoders,pdf.worker,pdf.renderer}.js",
],
{ base: "src/", encoding: false, sourcemaps: enableSourceMaps }
),
Expand Down Expand Up @@ -2699,6 +2725,7 @@ function buildInternalViewer(defines, dir) {
return ordered([
createMainBundle(defines).pipe(gulp.dest(dir + "build")),
createWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createRendererWorkerBundle(defines).pipe(gulp.dest(dir + "build")),
createInternalViewerBundle(defines).pipe(gulp.dest(dir + "web")),
preprocessHTML("web/internal/debugger.html", defines).pipe(
gulp.dest(dir + "web")
Expand Down Expand Up @@ -2917,25 +2944,33 @@ gulp.task(
gulp
.src(
[
GENERIC_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.mjs",
GENERIC_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.mjs.map",
GENERIC_DIR +
"build/{pdf,pdf.worker,pdf.sandbox,pdf.renderer}.mjs",
GENERIC_DIR +
"build/{pdf,pdf.worker,pdf.sandbox,pdf.renderer}.mjs.map",
],
{ encoding: false }
)
.pipe(gulp.dest(DIST_DIR + "build/")),
gulp
.src(
[
GENERIC_LEGACY_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.mjs",
GENERIC_LEGACY_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.mjs.map",
GENERIC_LEGACY_DIR +
"build/{pdf,pdf.worker,pdf.sandbox,pdf.renderer}.mjs",
GENERIC_LEGACY_DIR +
"build/{pdf,pdf.worker,pdf.sandbox,pdf.renderer}.mjs.map",
],
{ encoding: false }
)
.pipe(gulp.dest(DIST_DIR + "legacy/build/")),
gulp
.src(MINIFIED_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.min.mjs", {
encoding: false,
})
.src(
MINIFIED_DIR +
"build/{pdf,pdf.worker,pdf.sandbox,pdf.renderer}.min.mjs",
{
encoding: false,
}
)
.pipe(gulp.dest(DIST_DIR + "build/")),
gulp
.src(MINIFIED_DIR + "image_decoders/pdf.image_decoders.min.mjs", {
Expand All @@ -2944,7 +2979,8 @@ gulp.task(
.pipe(gulp.dest(DIST_DIR + "image_decoders/")),
gulp
.src(
MINIFIED_LEGACY_DIR + "build/{pdf,pdf.worker,pdf.sandbox}.min.mjs",
MINIFIED_LEGACY_DIR +
"build/{pdf,pdf.worker,pdf.sandbox,pdf.renderer}.min.mjs",
{ encoding: false }
)
.pipe(gulp.dest(DIST_DIR + "legacy/build/")),
Expand Down
1 change: 1 addition & 0 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ class Page {
resources,
this.nonBlendModesSet
),
hasCanvasFilters: partialEvaluator.hasCanvasFilters(resources),
pageIndex,
cacheKey,
});
Expand Down
115 changes: 115 additions & 0 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,121 @@ class PartialEvaluator {
return false;
}

_hasTransferMaps(transferObj) {
let transferArray;
if (Array.isArray(transferObj)) {
transferArray = transferObj;
} else if (isPDFFunction(transferObj)) {
transferArray = [transferObj];
} else {
return false;
}

const numFns = transferArray.length;
if (!(numFns === 1 || numFns === 4)) {
return false;
}

let numEffectfulFns = 0;
for (const entry of transferArray) {
const transfer = this.xref.fetchIfRef(entry);
if (isName(transfer, "Identity")) {
continue;
}
if (!isPDFFunction(transfer)) {
return false;
}
numEffectfulFns++;
}
return numEffectfulFns > 0;
}

hasCanvasFilters(resources) {
if (!(resources instanceof Dict)) {
return false;
}

const processed = new RefSet();
if (resources.objId) {
processed.put(resources.objId);
}
const xref = this.xref;
const nodes = [resources];
while (nodes.length) {
const node = nodes.shift();

const graphicStates = node.get("ExtGState");
if (graphicStates instanceof Dict) {
for (let graphicState of graphicStates.getRawValues()) {
if (graphicState instanceof Ref) {
if (processed.has(graphicState)) {
continue;
}
try {
graphicState = xref.fetch(graphicState);
} catch (ex) {
info(`hasCanvasFilters - failed to fetch ExtGState: "${ex}".`);
// A fetch failure means we can't inspect the resource, so fall
// back to main-thread rendering rather than misclassify a corrupt
// PDF as filter-free.
return true;
}
}
if (!(graphicState instanceof Dict)) {
continue;
}
if (graphicState.objId) {
processed.put(graphicState.objId);
}
try {
if (this._hasTransferMaps(graphicState.get("TR"))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently added support for TR2 too

return true;
}
} catch (ex) {
info(`hasCanvasFilters - failed to inspect filter data: "${ex}".`);
return true;
}
}
}

const xObjects = node.get("XObject");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only visit XObject but a TR/TR2 can be in a tiling pattern too.

if (xObjects instanceof Dict) {
for (let xObject of xObjects.getRawValues()) {
if (xObject instanceof Ref) {
if (processed.has(xObject)) {
continue;
}
try {
xObject = xref.fetch(xObject);
} catch (ex) {
info(`hasCanvasFilters - failed to fetch XObject: "${ex}".`);
return true;
}
}
if (!(xObject instanceof BaseStream)) {
continue;
}
if (xObject.dict.objId) {
processed.put(xObject.dict.objId);
}
const xResources = xObject.dict.get("Resources");
if (!(xResources instanceof Dict)) {
continue;
}
if (xResources.objId && processed.has(xResources.objId)) {
continue;
}

nodes.push(xResources);
if (xResources.objId) {
processed.put(xResources.objId);
}
}
}
}
Comment on lines +445 to +512

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two loops look very similar, maybe we could have an helper function to avoid redundancy.

@Aditi-1400 Aditi-1400 Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try doing that, in one of the earlier versions of the patch, I think this is probably the commit which captures the idea, later though I felt it added too much complexity and reduced readability of the code without significant returns, so I isolated them, let me know if you prefer extracting the common bits though, it should be a minimal effort to go back to that approach

return false;
}

async fetchBuiltInCMap(name) {
const cachedData = this.builtInCMapCache.get(name);
if (cachedData) {
Expand Down
Loading
Loading