Compare commits

...

9 Commits

Author SHA1 Message Date
calixteman
293506ada7
Merge pull request #19903 from Snuffleupagus/shorten-fieldObjects-getter
Shorten the `PDFDocument.prototype.fieldObjects` getter slightly
2025-05-09 15:49:51 +02:00
calixteman
ff0d9b13a7
Merge pull request #19902 from Snuffleupagus/core-document-shorten
Shorten the code in the `src/core/document.js` file
2025-05-09 15:48:49 +02:00
calixteman
a806f00ea1
Merge pull request #19755 from calixteman/reduce_canvas_size
Add a pref in order to cap the canvas area to a factor of the window one (bug 1958015)
2025-05-09 15:47:42 +02:00
Calixte Denizet
1225c1e39a Add a pref in order to cap the canvas area to a factor of the window one (bug 1958015)
This way it helps to reduce the overall canvas dimensions and make the rendering faster.
The drawback is that when scrolling, the page can be blurry in waiting for the rendering.

The default value is 200% on desktop and will be 100% for GeckoView.
2025-05-09 13:57:16 +02:00
Jonas Jenwald
1f7581b5c6 Shorten the PDFDocument.prototype.fieldObjects getter slightly
The effect is probably not even measurable, however this patch ever so slightly reduces the asynchronicity in the `fieldObjects` getter. These changes should be safe since:

 - We're inside of the `PDFDocument`-class and the `annotationGlobals`-getter, which will always return a (shadowed) Promise and won't throw `MissingDataException`s, can be accessed directly without going through the `BasePdfManager`-instance.

 - The `acroForm`-dictionary can be accessed through the `annotationGlobals`-data, removing the need to "manually" look it up and thus the need for using `Promise.all` here.

 - We can also lookup the /Fields-data, in the `acroForm`-dictionary, synchronously since the initial `formInfo.hasFields` check guarantees that it's available.
2025-05-07 17:47:09 +02:00
Jonas Jenwald
36fafbc05c Use object destructuring a bit more in the src/core/document.js file 2025-05-07 13:41:50 +02:00
Jonas Jenwald
92b065c87e Replace a number of semi-private fields with actual private ones in src/core/document.js
These are fields that can be moved out of their class constructors, and be initialized directly.
2025-05-07 13:41:44 +02:00
Jonas Jenwald
39803a9f25 Replace a number of semi-private methods with actual private ones in src/core/document.js
There's a few remaining cases that are used with either cached getters or `BasePdfManager.prototype.ensure`-methods, and those cannot be converted.
2025-05-07 13:41:36 +02:00
Jonas Jenwald
0ded85e9b3 Add a Page helper method to create a PartialEvaluator-instance
Currently we repeat the same identical code five times in the `Page`-class when creating a `PartialEvaluator`-instance, which given the number of parameters it needs seems like unnecessary duplication.
2025-05-07 13:41:29 +02:00
9 changed files with 206 additions and 136 deletions

View File

@ -172,6 +172,10 @@
"enum": [-1, 0, 3, 15],
"default": 0
},
"capCanvasAreaFactor": {
"type": "integer",
"default": 200
},
"enablePermissions": {
"type": "boolean",
"default": false

View File

@ -79,6 +79,8 @@ import { XRef } from "./xref.js";
const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
class Page {
#resourcesPromise = null;
constructor({
pdfManager,
xref,
@ -108,7 +110,6 @@ class Page {
this.systemFontCache = systemFontCache;
this.nonBlendModesSet = nonBlendModesSet;
this.evaluatorOptions = pdfManager.evaluatorOptions;
this.resourcesPromise = null;
this.xfaFactory = xfaFactory;
const idCounters = {
@ -125,10 +126,23 @@ class Page {
};
}
/**
* @private
*/
_getInheritableProperty(key, getArray = false) {
#createPartialEvaluator(handler) {
return new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
}
#getInheritableProperty(key, getArray = false) {
const value = getInheritableProperty({
dict: this.pageDict,
key,
@ -152,7 +166,7 @@ class Page {
// For robustness: The spec states that a \Resources entry has to be
// present, but can be empty. Some documents still omit it; in this case
// we return an empty dictionary.
const resources = this._getInheritableProperty("Resources");
const resources = this.#getInheritableProperty("Resources");
return shadow(
this,
@ -161,12 +175,12 @@ class Page {
);
}
_getBoundingBox(name) {
#getBoundingBox(name) {
if (this.xfaData) {
return this.xfaData.bbox;
}
const box = lookupNormalRect(
this._getInheritableProperty(name, /* getArray = */ true),
this.#getInheritableProperty(name, /* getArray = */ true),
null
);
@ -184,7 +198,7 @@ class Page {
return shadow(
this,
"mediaBox",
this._getBoundingBox("MediaBox") || LETTER_SIZE_MEDIABOX
this.#getBoundingBox("MediaBox") || LETTER_SIZE_MEDIABOX
);
}
@ -193,7 +207,7 @@ class Page {
return shadow(
this,
"cropBox",
this._getBoundingBox("CropBox") || this.mediaBox
this.#getBoundingBox("CropBox") || this.mediaBox
);
}
@ -224,7 +238,7 @@ class Page {
}
get rotate() {
let rotate = this._getInheritableProperty("Rotate") || 0;
let rotate = this.#getInheritableProperty("Rotate") || 0;
// Normalize rotation so it's a multiple of 90 and between 0 and 270.
if (rotate % 90 !== 0) {
@ -239,10 +253,7 @@ class Page {
return shadow(this, "rotate", rotate);
}
/**
* @private
*/
_onSubStreamError(reason, objId) {
#onSubStreamError(reason, objId) {
if (this.evaluatorOptions.ignoreErrors) {
warn(`getContentStream - ignoring sub-stream (${objId}): "${reason}".`);
return;
@ -262,7 +273,7 @@ class Page {
if (Array.isArray(content)) {
return new StreamsSequenceStream(
content,
this._onSubStreamError.bind(this)
this.#onSubStreamError.bind(this)
);
}
// Replace non-existent page content with empty content.
@ -322,20 +333,7 @@ class Page {
if (this.xfaFactory) {
throw new Error("XFA: Cannot save new annotations.");
}
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
const partialEvaluator = this.#createPartialEvaluator(handler);
const deletedAnnotations = new RefSetCache();
const existingAnnotations = new RefSet();
@ -378,19 +376,7 @@ class Page {
}
async save(handler, task, annotationStorage, changes) {
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
const partialEvaluator = this.#createPartialEvaluator(handler);
// Fetch the page's annotations and save the content
// in case of interactive form fields.
@ -414,8 +400,11 @@ class Page {
}
async loadResources(keys) {
// TODO: add async `_getInheritableProperty` and remove this.
await (this.resourcesPromise ??= this.pdfManager.ensure(this, "resources"));
// TODO: add async `#getInheritableProperty` and remove this.
await (this.#resourcesPromise ??= this.pdfManager.ensure(
this,
"resources"
));
await ObjectLoader.load(this.resources, keys, this.xref);
}
@ -450,19 +439,7 @@ class Page {
const contentStreamPromise = this.getContentStream();
const resourcesPromise = this.loadResources(RESOURCES_KEYS_OPERATOR_LIST);
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
const partialEvaluator = this.#createPartialEvaluator(handler);
const newAnnotsByPage = !this.xfaFactory
? getNewAnnotationsMap(annotationStorage)
@ -670,19 +647,7 @@ class Page {
RESOURCES_KEYS_TEXT_CONTENT
);
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
const partialEvaluator = this.#createPartialEvaluator(handler);
return partialEvaluator.getTextContent({
stream: contentStream,
@ -751,19 +716,7 @@ class Page {
}
if (annotation.hasTextContent && isVisible) {
partialEvaluator ||= new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
standardFontDataCache: this.standardFontDataCache,
globalColorSpaceCache: this.globalColorSpaceCache,
globalImageCache: this.globalImageCache,
systemFontCache: this.systemFontCache,
options: this.evaluatorOptions,
});
partialEvaluator ??= this.#createPartialEvaluator(handler);
textContentPromises.push(
annotation
@ -787,7 +740,7 @@ class Page {
}
get annotations() {
const annots = this._getInheritableProperty("Annots");
const annots = this.#getInheritableProperty("Annots");
return shadow(this, "annotations", Array.isArray(annots) ? annots : []);
}
@ -927,6 +880,10 @@ function find(stream, signature, limit = 1024, backwards = false) {
* The `PDFDocument` class holds all the (worker-thread) data of the PDF file.
*/
class PDFDocument {
#pagePromises = new Map();
#version = null;
constructor(pdfManager, stream) {
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
@ -943,8 +900,6 @@ class PDFDocument {
this.pdfManager = pdfManager;
this.stream = stream;
this.xref = new XRef(stream, pdfManager);
this._pagePromises = new Map();
this._version = null;
const idCounters = {
font: 0,
@ -1065,7 +1020,7 @@ class PDFDocument {
}
if (PDF_VERSION_REGEXP.test(version)) {
this._version = version;
this.#version = version;
} else {
warn(`Invalid PDF header version: ${version}`);
}
@ -1090,10 +1045,7 @@ class PDFDocument {
return shadow(this, "numPages", num);
}
/**
* @private
*/
_hasOnlyDocumentSignatures(fields, recursionDepth = 0) {
#hasOnlyDocumentSignatures(fields, recursionDepth = 0) {
const RECURSION_LIMIT = 10;
if (!Array.isArray(fields)) {
@ -1106,10 +1058,10 @@ class PDFDocument {
}
if (field.has("Kids")) {
if (++recursionDepth > RECURSION_LIMIT) {
warn("_hasOnlyDocumentSignatures: maximum recursion depth reached");
warn("#hasOnlyDocumentSignatures: maximum recursion depth reached");
return false;
}
return this._hasOnlyDocumentSignatures(
return this.#hasOnlyDocumentSignatures(
field.get("Kids"),
recursionDepth
);
@ -1401,7 +1353,7 @@ class PDFDocument {
* the catalog, if present, should overwrite the version from the header.
*/
get version() {
return this.catalog.version || this._version;
return this.catalog.version || this.#version;
}
get formInfo() {
@ -1411,7 +1363,7 @@ class PDFDocument {
hasXfa: false,
hasSignatures: false,
};
const acroForm = this.catalog.acroForm;
const { acroForm } = this.catalog;
if (!acroForm) {
return shadow(this, "formInfo", formInfo);
}
@ -1438,7 +1390,7 @@ class PDFDocument {
const sigFlags = acroForm.get("SigFlags");
const hasSignatures = !!(sigFlags & 0x1);
const hasOnlyDocumentSignatures =
hasSignatures && this._hasOnlyDocumentSignatures(fields);
hasSignatures && this.#hasOnlyDocumentSignatures(fields);
formInfo.hasAcroForm = hasFields && !hasOnlyDocumentSignatures;
formInfo.hasSignatures = hasSignatures;
} catch (ex) {
@ -1451,22 +1403,22 @@ class PDFDocument {
}
get documentInfo() {
const { catalog, formInfo, xref } = this;
const docInfo = {
PDFFormatVersion: this.version,
Language: this.catalog.lang,
EncryptFilterName: this.xref.encrypt
? this.xref.encrypt.filterName
: null,
Language: catalog.lang,
EncryptFilterName: xref.encrypt?.filterName ?? null,
IsLinearized: !!this.linearization,
IsAcroFormPresent: this.formInfo.hasAcroForm,
IsXFAPresent: this.formInfo.hasXfa,
IsCollectionPresent: !!this.catalog.collection,
IsSignaturesPresent: this.formInfo.hasSignatures,
IsAcroFormPresent: formInfo.hasAcroForm,
IsXFAPresent: formInfo.hasXfa,
IsCollectionPresent: !!catalog.collection,
IsSignaturesPresent: formInfo.hasSignatures,
};
let infoDict;
try {
infoDict = this.xref.trailer.get("Info");
infoDict = xref.trailer.get("Info");
} catch (err) {
if (err instanceof MissingDataException) {
throw err;
@ -1565,7 +1517,7 @@ class PDFDocument {
]);
}
async _getLinearizationPage(pageIndex) {
async #getLinearizationPage(pageIndex) {
const { catalog, linearization, xref } = this;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
assert(
@ -1608,7 +1560,7 @@ class PDFDocument {
}
getPage(pageIndex) {
const cachedPromise = this._pagePromises.get(pageIndex);
const cachedPromise = this.#pagePromises.get(pageIndex);
if (cachedPromise) {
return cachedPromise;
}
@ -1618,7 +1570,7 @@ class PDFDocument {
if (xfaFactory) {
promise = Promise.resolve([Dict.empty, null]);
} else if (linearization?.pageFirst === pageIndex) {
promise = this._getLinearizationPage(pageIndex);
promise = this.#getLinearizationPage(pageIndex);
} else {
promise = catalog.getPageDict(pageIndex);
}
@ -1642,7 +1594,7 @@ class PDFDocument {
})
);
this._pagePromises.set(pageIndex, promise);
this.#pagePromises.set(pageIndex, promise);
return promise;
}
@ -1657,7 +1609,7 @@ class PDFDocument {
// Clear out the various caches to ensure that we haven't stored any
// inconsistent and/or incorrect state, since that could easily break
// subsequent `this.getPage` calls.
this._pagePromises.delete(0);
this.#pagePromises.delete(0);
await this.cleanup();
throw new XRefParseException();
@ -1696,7 +1648,7 @@ class PDFDocument {
// Clear out the various caches to ensure that we haven't stored any
// inconsistent and/or incorrect state, since that could easily break
// subsequent `this.getPage` calls.
this._pagePromises.delete(numPages - 1);
this.#pagePromises.delete(numPages - 1);
await this.cleanup();
if (reason instanceof XRefEntryException && !recoveryMode) {
@ -1743,7 +1695,7 @@ class PDFDocument {
);
}
this._pagePromises.set(pageIndex, promise);
this.#pagePromises.set(pageIndex, promise);
}
catalog.setActualNumPages(pagesTree.size);
}
@ -1871,20 +1823,17 @@ class PDFDocument {
if (!formInfo.hasFields) {
return null;
}
const [annotationGlobals, acroForm] = await Promise.all([
this.pdfManager.ensureDoc("annotationGlobals"),
this.pdfManager.ensureCatalog("acroForm"),
]);
const annotationGlobals = await this.annotationGlobals;
if (!annotationGlobals) {
return null;
}
const { acroForm } = annotationGlobals;
const visitedRefs = new RefSet();
const allFields = Object.create(null);
const fieldPromises = new Map();
const orphanFields = new RefSetCache();
for (const fieldRef of await acroForm.getAsync("Fields")) {
for (const fieldRef of acroForm.get("Fields")) {
await this.#collectFieldObjects(
"",
null,

View File

@ -660,11 +660,18 @@ class OutputScale {
* @returns {boolean} Returns `true` if scaling was limited,
* `false` otherwise.
*/
limitCanvas(width, height, maxPixels, maxDim) {
limitCanvas(width, height, maxPixels, maxDim, capAreaFactor = -1) {
let maxAreaScale = Infinity,
maxWidthScale = Infinity,
maxHeightScale = Infinity;
if (capAreaFactor >= 0) {
const cappedWindowArea = OutputScale.getCappedWindowArea(capAreaFactor);
maxPixels =
maxPixels > 0
? Math.min(maxPixels, cappedWindowArea)
: cappedWindowArea;
}
if (maxPixels > 0) {
maxAreaScale = Math.sqrt(maxPixels / (width * height));
}
@ -685,6 +692,23 @@ class OutputScale {
static get pixelRatio() {
return globalThis.devicePixelRatio || 1;
}
static getCappedWindowArea(capAreaFactor) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
return Math.ceil(
window.innerWidth *
window.innerHeight *
this.pixelRatio ** 2 *
(1 + capAreaFactor / 100)
);
}
return Math.ceil(
window.screen.availWidth *
window.screen.availHeight *
this.pixelRatio ** 2 *
(1 + capAreaFactor / 100)
);
}
}
// See https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types

View File

@ -389,7 +389,9 @@ describe("PDF viewer", () => {
pages = await loadAndWait(
"issue18694.pdf",
".textLayer .endOfContent",
"page-width"
"page-width",
null,
{ capCanvasAreaFactor: -1 }
);
});
@ -459,7 +461,12 @@ describe("PDF viewer", () => {
describe("Detail view on zoom", () => {
const BASE_MAX_CANVAS_PIXELS = 1e6;
function setupPages(zoom, devicePixelRatio, setups = {}) {
function setupPages(
zoom,
devicePixelRatio,
capCanvasAreaFactor,
setups = {}
) {
let pages;
beforeEach(async () => {
@ -476,7 +483,10 @@ describe("PDF viewer", () => {
}`,
...setups,
},
{ maxCanvasPixels: BASE_MAX_CANVAS_PIXELS * devicePixelRatio ** 2 },
{
maxCanvasPixels: BASE_MAX_CANVAS_PIXELS * devicePixelRatio ** 2,
capCanvasAreaFactor,
},
{ height: 600, width: 800, devicePixelRatio }
);
});
@ -503,6 +513,8 @@ describe("PDF viewer", () => {
const bottomRight = ctx.getImageData(width - 3, height - 3, 1, 1).data;
return {
size: width * height,
width,
height,
topLeft: globalThis.pdfjsLib.Util.makeHexColor(...topLeft),
bottomRight: globalThis.pdfjsLib.Util.makeHexColor(...bottomRight),
};
@ -528,7 +540,7 @@ describe("PDF viewer", () => {
for (const pixelRatio of [1, 2]) {
describe(`with pixel ratio ${pixelRatio}`, () => {
describe("setupPages()", () => {
const forEachPage = setupPages("100%", pixelRatio);
const forEachPage = setupPages("100%", pixelRatio, -1);
it("sets the proper devicePixelRatio", async () => {
await forEachPage(async (browserName, page) => {
@ -543,8 +555,63 @@ describe("PDF viewer", () => {
});
});
describe("when zooming with a cap on the canvas dimensions", () => {
const forEachPage = setupPages("10%", pixelRatio, 0);
it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer"
);
const before = await page.evaluate(extractCanvases, 1);
expect(before.length)
.withContext(`In ${browserName}, before`)
.toBe(1);
const factor = 50;
const handle = await waitForDetailRendered(page);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);
const after = await page.evaluate(extractCanvases, 1);
// The page dimensions are 595x841, so the base canvas is a scale
// version of that but the number of pixels is capped to
// 800x600 = 480000.
expect(after.length)
.withContext(`In ${browserName}, after`)
.toBe(2);
expect(after[0].width)
.withContext(`In ${browserName}`)
.toBe(582 * pixelRatio);
expect(after[0].height)
.withContext(`In ${browserName}`)
.toBe(823 * pixelRatio);
// The dimensions of the detail canvas are capped to 800x600 but
// it depends on the visible area which depends itself of the
// scrollbars dimensions, hence we just check that the canvas
// dimensions are capped.
expect(after[1].width)
.withContext(`In ${browserName}`)
.toBeLessThan(810 * pixelRatio);
expect(after[1].height)
.withContext(`In ${browserName}`)
.toBeLessThan(575 * pixelRatio);
expect(after[1].size)
.withContext(`In ${browserName}`)
.toBeLessThan(800 * 600 * pixelRatio ** 2);
});
});
});
describe("when zooming in past max canvas size", () => {
const forEachPage = setupPages("100%", pixelRatio);
const forEachPage = setupPages("100%", pixelRatio, -1);
it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => {
@ -616,7 +683,7 @@ describe("PDF viewer", () => {
});
describe("when starting already zoomed in past max canvas size", () => {
const forEachPage = setupPages("300%", pixelRatio);
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => {
@ -654,7 +721,7 @@ describe("PDF viewer", () => {
});
describe("when scrolling", () => {
const forEachPage = setupPages("300%", pixelRatio);
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must update the detail view", async () => {
await forEachPage(async (browserName, page) => {
@ -689,7 +756,7 @@ describe("PDF viewer", () => {
});
describe("when scrolling little enough that the existing detail covers the new viewport", () => {
const forEachPage = setupPages("300%", pixelRatio);
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must not re-create the detail canvas", async () => {
await forEachPage(async (browserName, page) => {
@ -732,7 +799,7 @@ describe("PDF viewer", () => {
});
describe("when scrolling to have two visible pages", () => {
const forEachPage = setupPages("300%", pixelRatio);
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must update the detail view", async () => {
await forEachPage(async (browserName, page) => {
@ -805,7 +872,7 @@ describe("PDF viewer", () => {
});
describe("pagerendered event", () => {
const forEachPage = setupPages("100%", pixelRatio, {
const forEachPage = setupPages("100%", pixelRatio, -1, {
eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = [];
@ -966,7 +1033,7 @@ describe("PDF viewer", () => {
}
describe("when immediately cancelled and re-rendered", () => {
const forEachPage = setupPages("100%", 1, {
const forEachPage = setupPages("100%", 1, -1, {
eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = [];
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {
@ -1031,7 +1098,7 @@ describe("PDF viewer", () => {
});
describe("when cancelled and re-rendered after 1 microtick", () => {
const forEachPage = setupPages("100%", 1, {
const forEachPage = setupPages("100%", 1, -1, {
eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = [];
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {

View File

@ -361,6 +361,7 @@ const PDFViewerApplication = {
// Set some specific preferences for tests.
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
Object.assign(opts, {
capCanvasAreaFactor: x => parseInt(x),
docBaseUrl: x => x,
enableAltText: x => x === "true",
enableAutoLinking: x => x === "true",
@ -485,7 +486,8 @@ const PDFViewerApplication = {
const enableHWA = AppOptions.get("enableHWA"),
maxCanvasPixels = AppOptions.get("maxCanvasPixels"),
maxCanvasDim = AppOptions.get("maxCanvasDim");
maxCanvasDim = AppOptions.get("maxCanvasDim"),
capCanvasAreaFactor = AppOptions.get("capCanvasAreaFactor");
const pdfViewer = (this.pdfViewer = new PDFViewer({
container,
viewer,
@ -515,6 +517,7 @@ const PDFViewerApplication = {
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
maxCanvasPixels,
maxCanvasDim,
capCanvasAreaFactor,
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
enablePermissions: AppOptions.get("enablePermissions"),
pageColors,

View File

@ -168,6 +168,11 @@ const defaultOptions = {
value: 2,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
capCanvasAreaFactor: {
/** @type {number} */
value: 200,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
cursorToolOnLoad: {
/** @type {number} */
value: 0,

View File

@ -140,10 +140,18 @@ class PDFPageDetailView extends BasePDFPageView {
return;
}
const { viewport, maxCanvasPixels } = this.pageView;
const { viewport, capCanvasAreaFactor } = this.pageView;
const visibleWidth = visibleArea.maxX - visibleArea.minX;
const visibleHeight = visibleArea.maxY - visibleArea.minY;
let { maxCanvasPixels } = this.pageView;
if (capCanvasAreaFactor >= 0) {
maxCanvasPixels = Math.min(
maxCanvasPixels,
OutputScale.getCappedWindowArea(capCanvasAreaFactor)
);
}
// "overflowScale" represents which percentage of the width and of the
// height the detail area extends outside of the visible area. We want to

View File

@ -81,6 +81,9 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
* @property {number} [maxCanvasDim] - The maximum supported canvas dimension,
* in either width or height. Use `-1` for no limit.
* The default value is 32767.
* @property {number} [capCanvasAreaFactor] - Cap the canvas area to the
* viewport increased by the value in percent. Use `-1` for no limit.
* The default value is 200%.
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
* pages would need a canvas that is larger than `maxCanvasPixels` or
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
@ -188,6 +191,8 @@ class PDFPageView extends BasePDFPageView {
this.maxCanvasPixels =
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
this.maxCanvasDim = options.maxCanvasDim || AppOptions.get("maxCanvasDim");
this.capCanvasAreaFactor =
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
this.#enableAutoLinking = options.enableAutoLinking !== false;
this.l10n = options.l10n;
@ -448,7 +453,6 @@ class PDFPageView extends BasePDFPageView {
if (!this.textLayer) {
return;
}
let error = null;
try {
await this.textLayer.render({
@ -780,7 +784,8 @@ class PDFPageView extends BasePDFPageView {
width,
height,
this.maxCanvasPixels,
this.maxCanvasDim
this.maxCanvasDim,
this.capCanvasAreaFactor
);
}
}

View File

@ -122,6 +122,9 @@ function isValidAnnotationEditorMode(mode) {
* @property {number} [maxCanvasDim] - The maximum supported canvas dimension,
* in either width or height. Use `-1` for no limit.
* The default value is 32767.
* @property {number} [capCanvasAreaFactor] - Cap the canvas area to the
* viewport increased by the value in percent. Use `-1` for no limit.
* The default value is 200%.
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
* pages would need a canvas that is larger than `maxCanvasPixels` or
* `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
@ -335,6 +338,7 @@ class PDFViewer {
}
this.maxCanvasPixels = options.maxCanvasPixels;
this.maxCanvasDim = options.maxCanvasDim;
this.capCanvasAreaFactor = options.capCanvasAreaFactor;
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
this.l10n = options.l10n;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -1002,6 +1006,7 @@ class PDFViewer {
imageResourcesPath: this.imageResourcesPath,
maxCanvasPixels: this.maxCanvasPixels,
maxCanvasDim: this.maxCanvasDim,
capCanvasAreaFactor: this.capCanvasAreaFactor,
enableDetailCanvas: this.enableDetailCanvas,
pageColors,
l10n: this.l10n,