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,