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.
This commit is contained in:
Calixte Denizet 2025-05-07 19:07:21 +02:00
parent 6f052312d6
commit 1225c1e39a
8 changed files with 138 additions and 17 deletions

View File

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

View File

@ -660,11 +660,18 @@ class OutputScale {
* @returns {boolean} Returns `true` if scaling was limited, * @returns {boolean} Returns `true` if scaling was limited,
* `false` otherwise. * `false` otherwise.
*/ */
limitCanvas(width, height, maxPixels, maxDim) { limitCanvas(width, height, maxPixels, maxDim, capAreaFactor = -1) {
let maxAreaScale = Infinity, let maxAreaScale = Infinity,
maxWidthScale = Infinity, maxWidthScale = Infinity,
maxHeightScale = Infinity; maxHeightScale = Infinity;
if (capAreaFactor >= 0) {
const cappedWindowArea = OutputScale.getCappedWindowArea(capAreaFactor);
maxPixels =
maxPixels > 0
? Math.min(maxPixels, cappedWindowArea)
: cappedWindowArea;
}
if (maxPixels > 0) { if (maxPixels > 0) {
maxAreaScale = Math.sqrt(maxPixels / (width * height)); maxAreaScale = Math.sqrt(maxPixels / (width * height));
} }
@ -685,6 +692,23 @@ class OutputScale {
static get pixelRatio() { static get pixelRatio() {
return globalThis.devicePixelRatio || 1; 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 // 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( pages = await loadAndWait(
"issue18694.pdf", "issue18694.pdf",
".textLayer .endOfContent", ".textLayer .endOfContent",
"page-width" "page-width",
null,
{ capCanvasAreaFactor: -1 }
); );
}); });
@ -459,7 +461,12 @@ describe("PDF viewer", () => {
describe("Detail view on zoom", () => { describe("Detail view on zoom", () => {
const BASE_MAX_CANVAS_PIXELS = 1e6; const BASE_MAX_CANVAS_PIXELS = 1e6;
function setupPages(zoom, devicePixelRatio, setups = {}) { function setupPages(
zoom,
devicePixelRatio,
capCanvasAreaFactor,
setups = {}
) {
let pages; let pages;
beforeEach(async () => { beforeEach(async () => {
@ -476,7 +483,10 @@ describe("PDF viewer", () => {
}`, }`,
...setups, ...setups,
}, },
{ maxCanvasPixels: BASE_MAX_CANVAS_PIXELS * devicePixelRatio ** 2 }, {
maxCanvasPixels: BASE_MAX_CANVAS_PIXELS * devicePixelRatio ** 2,
capCanvasAreaFactor,
},
{ height: 600, width: 800, devicePixelRatio } { height: 600, width: 800, devicePixelRatio }
); );
}); });
@ -503,6 +513,8 @@ describe("PDF viewer", () => {
const bottomRight = ctx.getImageData(width - 3, height - 3, 1, 1).data; const bottomRight = ctx.getImageData(width - 3, height - 3, 1, 1).data;
return { return {
size: width * height, size: width * height,
width,
height,
topLeft: globalThis.pdfjsLib.Util.makeHexColor(...topLeft), topLeft: globalThis.pdfjsLib.Util.makeHexColor(...topLeft),
bottomRight: globalThis.pdfjsLib.Util.makeHexColor(...bottomRight), bottomRight: globalThis.pdfjsLib.Util.makeHexColor(...bottomRight),
}; };
@ -528,7 +540,7 @@ describe("PDF viewer", () => {
for (const pixelRatio of [1, 2]) { for (const pixelRatio of [1, 2]) {
describe(`with pixel ratio ${pixelRatio}`, () => { describe(`with pixel ratio ${pixelRatio}`, () => {
describe("setupPages()", () => { describe("setupPages()", () => {
const forEachPage = setupPages("100%", pixelRatio); const forEachPage = setupPages("100%", pixelRatio, -1);
it("sets the proper devicePixelRatio", async () => { it("sets the proper devicePixelRatio", async () => {
await forEachPage(async (browserName, page) => { 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", () => { 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 () => { it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => { await forEachPage(async (browserName, page) => {
@ -616,7 +683,7 @@ describe("PDF viewer", () => {
}); });
describe("when starting already zoomed in past max canvas size", () => { 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 () => { it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => { await forEachPage(async (browserName, page) => {
@ -654,7 +721,7 @@ describe("PDF viewer", () => {
}); });
describe("when scrolling", () => { describe("when scrolling", () => {
const forEachPage = setupPages("300%", pixelRatio); const forEachPage = setupPages("300%", pixelRatio, -1);
it("must update the detail view", async () => { it("must update the detail view", async () => {
await forEachPage(async (browserName, page) => { 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", () => { 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 () => { it("must not re-create the detail canvas", async () => {
await forEachPage(async (browserName, page) => { await forEachPage(async (browserName, page) => {
@ -732,7 +799,7 @@ describe("PDF viewer", () => {
}); });
describe("when scrolling to have two visible pages", () => { 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 () => { it("must update the detail view", async () => {
await forEachPage(async (browserName, page) => { await forEachPage(async (browserName, page) => {
@ -805,7 +872,7 @@ describe("PDF viewer", () => {
}); });
describe("pagerendered event", () => { describe("pagerendered event", () => {
const forEachPage = setupPages("100%", pixelRatio, { const forEachPage = setupPages("100%", pixelRatio, -1, {
eventBusSetup: eventBus => { eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = []; globalThis.__pageRenderedEvents = [];
@ -966,7 +1033,7 @@ describe("PDF viewer", () => {
} }
describe("when immediately cancelled and re-rendered", () => { describe("when immediately cancelled and re-rendered", () => {
const forEachPage = setupPages("100%", 1, { const forEachPage = setupPages("100%", 1, -1, {
eventBusSetup: eventBus => { eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = []; globalThis.__pageRenderedEvents = [];
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => { eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {
@ -1031,7 +1098,7 @@ describe("PDF viewer", () => {
}); });
describe("when cancelled and re-rendered after 1 microtick", () => { describe("when cancelled and re-rendered after 1 microtick", () => {
const forEachPage = setupPages("100%", 1, { const forEachPage = setupPages("100%", 1, -1, {
eventBusSetup: eventBus => { eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = []; globalThis.__pageRenderedEvents = [];
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => { eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {

View File

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

View File

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

View File

@ -140,10 +140,18 @@ class PDFPageDetailView extends BasePDFPageView {
return; return;
} }
const { viewport, maxCanvasPixels } = this.pageView; const { viewport, capCanvasAreaFactor } = this.pageView;
const visibleWidth = visibleArea.maxX - visibleArea.minX; const visibleWidth = visibleArea.maxX - visibleArea.minX;
const visibleHeight = visibleArea.maxY - visibleArea.minY; 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 // "overflowScale" represents which percentage of the width and of the
// height the detail area extends outside of the visible area. We want to // 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, * @property {number} [maxCanvasDim] - The maximum supported canvas dimension,
* in either width or height. Use `-1` for no limit. * in either width or height. Use `-1` for no limit.
* The default value is 32767. * 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 * @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
* pages would need a canvas that is larger than `maxCanvasPixels` or * 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, * `maxCanvasDim`, it will draw a second canvas on top of the CSS-zoomed one,
@ -188,6 +191,8 @@ class PDFPageView extends BasePDFPageView {
this.maxCanvasPixels = this.maxCanvasPixels =
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels"); options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
this.maxCanvasDim = options.maxCanvasDim || AppOptions.get("maxCanvasDim"); this.maxCanvasDim = options.maxCanvasDim || AppOptions.get("maxCanvasDim");
this.capCanvasAreaFactor =
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
this.#enableAutoLinking = options.enableAutoLinking !== false; this.#enableAutoLinking = options.enableAutoLinking !== false;
this.l10n = options.l10n; this.l10n = options.l10n;
@ -448,7 +453,6 @@ class PDFPageView extends BasePDFPageView {
if (!this.textLayer) { if (!this.textLayer) {
return; return;
} }
let error = null; let error = null;
try { try {
await this.textLayer.render({ await this.textLayer.render({
@ -780,7 +784,8 @@ class PDFPageView extends BasePDFPageView {
width, width,
height, height,
this.maxCanvasPixels, 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, * @property {number} [maxCanvasDim] - The maximum supported canvas dimension,
* in either width or height. Use `-1` for no limit. * in either width or height. Use `-1` for no limit.
* The default value is 32767. * 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 * @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
* pages would need a canvas that is larger than `maxCanvasPixels` or * 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, * `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.maxCanvasPixels = options.maxCanvasPixels;
this.maxCanvasDim = options.maxCanvasDim; this.maxCanvasDim = options.maxCanvasDim;
this.capCanvasAreaFactor = options.capCanvasAreaFactor;
this.enableDetailCanvas = options.enableDetailCanvas ?? true; this.enableDetailCanvas = options.enableDetailCanvas ?? true;
this.l10n = options.l10n; this.l10n = options.l10n;
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
@ -1002,6 +1006,7 @@ class PDFViewer {
imageResourcesPath: this.imageResourcesPath, imageResourcesPath: this.imageResourcesPath,
maxCanvasPixels: this.maxCanvasPixels, maxCanvasPixels: this.maxCanvasPixels,
maxCanvasDim: this.maxCanvasDim, maxCanvasDim: this.maxCanvasDim,
capCanvasAreaFactor: this.capCanvasAreaFactor,
enableDetailCanvas: this.enableDetailCanvas, enableDetailCanvas: this.enableDetailCanvas,
pageColors, pageColors,
l10n: this.l10n, l10n: this.l10n,