Skip to content
Open
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
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 @@ -1356,6 +1374,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 @@ -1500,6 +1519,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 @@ -1625,6 +1645,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 @@ -1730,6 +1753,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 @@ -1923,7 +1949,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 @@ -2822,6 +2848,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 @@ -3040,25 +3067,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 @@ -3067,7 +3102,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
3 changes: 3 additions & 0 deletions src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ class Catalog {

nonBlendModesSet = new RefSet();

nonCanvasFiltersSet = new RefSet();

pageDictCache = new RefSetCache();

pageIndexCache = new RefSetCache();
Expand Down Expand Up @@ -1305,6 +1307,7 @@ class Catalog {
this.pageIndexCache.clear();
this.pageDictCache.clear();
this.nonBlendModesSet.clear();
this.nonCanvasFiltersSet.clear();

for (const { dict } of await Promise.all(this.fontCache)) {
delete dict.cacheKey;
Expand Down
8 changes: 8 additions & 0 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class Page {
globalImageCache,
systemFontCache,
nonBlendModesSet,
nonCanvasFiltersSet,
xfaFactory,
}) {
this.pdfManager = pdfManager;
Expand All @@ -114,6 +115,7 @@ class Page {
this.globalImageCache = globalImageCache;
this.systemFontCache = systemFontCache;
this.nonBlendModesSet = nonBlendModesSet;
this.nonCanvasFiltersSet = nonCanvasFiltersSet;
this.evaluatorOptions = pdfManager.evaluatorOptions;
this.xfaFactory = xfaFactory;

Expand Down Expand Up @@ -565,6 +567,10 @@ class Page {
resources,
this.nonBlendModesSet
),
hasCanvasFilters: partialEvaluator.hasCanvasFilters(
resources,
this.nonCanvasFiltersSet
),
pageIndex,
cacheKey,
});
Expand Down Expand Up @@ -1720,6 +1726,7 @@ class PDFDocument {
globalImageCache: catalog.globalImageCache,
systemFontCache: catalog.systemFontCache,
nonBlendModesSet: catalog.nonBlendModesSet,
nonCanvasFiltersSet: catalog.nonCanvasFiltersSet,
xfaFactory,
})
);
Expand Down Expand Up @@ -1820,6 +1827,7 @@ class PDFDocument {
globalImageCache: catalog.globalImageCache,
systemFontCache: catalog.systemFontCache,
nonBlendModesSet: catalog.nonBlendModesSet,
nonCanvasFiltersSet: catalog.nonCanvasFiltersSet,
xfaFactory: null,
})
);
Expand Down
145 changes: 131 additions & 14 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,109 @@ class PartialEvaluator {
return false;
}

hasCanvasFilters(resources, nonCanvasFiltersSet) {
if (!(resources instanceof Dict)) {
return false;
}
if (resources.objId && nonCanvasFiltersSet.has(resources.objId)) {
return false;
}

const processed = new RefSet(nonCanvasFiltersSet);
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 {
const transferObj = graphicState.has("TR2")
? graphicState.get("TR2")
: graphicState.get("TR");
if (this._getTransferFunctions(transferObj) !== null) {
return true;
}
} catch (ex) {
info(`hasCanvasFilters - failed to inspect filter data: "${ex}".`);
return true;
}
}
}

for (const resourceType of ["XObject", "Pattern"]) {
const resourceEntries = node.get(resourceType);
if (resourceEntries instanceof Dict) {
for (let entry of resourceEntries.getRawValues()) {
if (entry instanceof Ref) {
if (processed.has(entry)) {
continue;
}
try {
entry = xref.fetch(entry);
} catch (ex) {
info(
`hasCanvasFilters - failed to fetch ${resourceType}: "${ex}".`
);
return true;
}
}
if (!(entry instanceof BaseStream)) {
continue;
}
if (entry.dict.objId) {
processed.put(entry.dict.objId);
}
const nestedResources = entry.dict.get("Resources");
if (!(nestedResources instanceof Dict)) {
continue;
}
if (nestedResources.objId && processed.has(nestedResources.objId)) {
continue;
}

nodes.push(nestedResources);
if (nestedResources.objId) {
processed.put(nestedResources.objId);
}
}
}
}
}
Comment on lines +409 to +483

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


// When no canvas filters exist, there's no need to re-fetch/re-parse any
// of the processed `Ref`s again for subsequent pages; the early
// `return true`s above skip this, since those subtrees weren't inspected.
for (const ref of processed) {
nonCanvasFiltersSet.put(ref);
}
return false;
}

async fetchBuiltInCMap(name) {
const cachedData = this.builtInCMapCache.get(name);
if (cachedData) {
Expand Down Expand Up @@ -927,7 +1030,7 @@ class PartialEvaluator {
);
}

handleTransferFunction(tr) {
_getTransferFunctions(tr) {
let transferArray;
if (Array.isArray(tr)) {
transferArray = tr;
Expand All @@ -941,21 +1044,43 @@ class PartialEvaluator {
} else {
return null; // Not a valid transfer function entry.
}
if (!(transferArray.length === 1 || transferArray.length === 4)) {
return null; // Only 1 or 4 functions are supported, by the specification.
}

const transferMaps = [];
let numFns = 0,
numEffectfulFns = 0;
const transferFns = [];
let numEffectfulFns = 0;
for (const entry of transferArray) {
const transferObj = this.xref.fetchIfRef(entry);
numFns++;

if (isName(transferObj, "Identity")) {
transferMaps.push(null);
transferFns.push(null);
continue;
} else if (!isPDFFunction(transferObj)) {
return null; // Not a valid transfer function object.
}
transferFns.push(transferObj);
numEffectfulFns++;
}

if (numEffectfulFns === 0) {
return null; // Only /Identity transfer functions found, which are no-ops.
}
return transferFns;
}

handleTransferFunction(tr) {
const transferFns = this._getTransferFunctions(tr);
if (!transferFns) {
return null;
}

const transferMaps = [];
for (const transferObj of transferFns) {
if (!transferObj) {
transferMaps.push(null); // An `/Identity` entry.
continue;
}
const transferFn = this._pdfFunctionFactory.create(transferObj);
const transferMap = new Uint8Array(256),
tmp = new Float32Array(1);
Expand All @@ -965,14 +1090,6 @@ class PartialEvaluator {
transferMap[j] = (tmp[0] * 255) | 0;
}
transferMaps.push(transferMap);
numEffectfulFns++;
}

if (!(numFns === 1 || numFns === 4)) {
return null; // Only 1 or 4 functions are supported, by the specification.
}
if (numEffectfulFns === 0) {
return null; // Only /Identity transfer functions found, which are no-ops.
}
return transferMaps;
}
Expand Down
Loading
Loading