From 1225c1e39ae5ba07dfe59f781c73d6db0477af79 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 7 May 2025 19:07:21 +0200 Subject: [PATCH] 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. --- extensions/chromium/preferences_schema.json | 4 + src/display/display_utils.js | 26 +++++- test/integration/viewer_spec.mjs | 91 ++++++++++++++++++--- web/app.js | 5 +- web/app_options.js | 5 ++ web/pdf_page_detail_view.js | 10 ++- web/pdf_page_view.js | 9 +- web/pdf_viewer.js | 5 ++ 8 files changed, 138 insertions(+), 17 deletions(-) diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 9fb054666..2bfa8ba15 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -172,6 +172,10 @@ "enum": [-1, 0, 3, 15], "default": 0 }, + "capCanvasAreaFactor": { + "type": "integer", + "default": 200 + }, "enablePermissions": { "type": "boolean", "default": false diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 34c259247..a229f2d42 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -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 diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 52b89c90f..20a53759b 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -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 }) => { diff --git a/web/app.js b/web/app.js index 39f3a0071..e2da40c78 100644 --- a/web/app.js +++ b/web/app.js @@ -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, diff --git a/web/app_options.js b/web/app_options.js index fc1a612e5..461336dc1 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -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, diff --git a/web/pdf_page_detail_view.js b/web/pdf_page_detail_view.js index 113a97dff..3ac4a4e52 100644 --- a/web/pdf_page_detail_view.js +++ b/web/pdf_page_detail_view.js @@ -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 diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index b5fdfb5f7..e742e4652 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -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 ); } } diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 751decf5c..3d1ed09a1 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -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,