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)
This commit is contained in:
calixteman 2025-05-09 15:47:42 +02:00 committed by GitHub
commit a806f00ea1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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,