Merge pull request #18218 from nicolo-ribaudo/test-maxCanvasPixels

Respect `maxCanvasPixels` when computing canvas dimensions
This commit is contained in:
Tim van der Meij 2024-06-19 16:47:09 +02:00 committed by GitHub
commit 3e1d779859
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 187 additions and 62 deletions

View file

@ -38,8 +38,13 @@ function loadAndWait(filename, selector, zoom, pageSetup, options) {
let app_options = ""; let app_options = "";
if (options) { if (options) {
const optionsObject =
typeof options === "function"
? await options(page, session.name)
: options;
// Options must be handled in app.js::_parseHashParams. // Options must be handled in app.js::_parseHashParams.
for (const [key, value] of Object.entries(options)) { for (const [key, value] of Object.entries(optionsObject)) {
app_options += `&${key}=${encodeURIComponent(value)}`; app_options += `&${key}=${encodeURIComponent(value)}`;
} }
} }

View file

@ -174,24 +174,6 @@ describe("PDF viewer", () => {
}); });
describe("CSS-only zoom", () => { describe("CSS-only zoom", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
{
maxCanvasPixels: 0,
}
);
});
afterAll(async () => {
await closePages(pages);
});
function createPromiseForFirstPageRendered(page) { function createPromiseForFirstPageRendered(page) {
return createPromise(page, (resolve, reject) => { return createPromise(page, (resolve, reject) => {
const controller = new AbortController(); const controller = new AbortController();
@ -209,6 +191,23 @@ describe("PDF viewer", () => {
}); });
} }
describe("forced (maxCanvasPixels: 0)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
{ maxCanvasPixels: 0 }
);
});
afterAll(async () => {
await closePages(pages);
});
it("respects drawing delay when zooming out", async () => { it("respects drawing delay when zooming out", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
@ -255,4 +254,121 @@ describe("PDF viewer", () => {
); );
}); });
}); });
describe("triggers when going bigger than maxCanvasPixels", () => {
let pages;
const MAX_CANVAS_PIXELS = new Map();
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
async (page, browserName) => {
const ratio = await page.evaluate(() => window.devicePixelRatio);
const maxCanvasPixels = 1_000_000 * ratio ** 2;
MAX_CANVAS_PIXELS.set(browserName, maxCanvasPixels);
return { maxCanvasPixels };
}
);
});
beforeEach(async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.currentScale = 0.5;
});
})
);
});
afterAll(async () => {
await closePages(pages);
});
function getCanvasSize(page) {
return page.evaluate(() => {
const canvas = window.document.querySelector(".canvasWrapper canvas");
return canvas.width * canvas.height;
});
}
// MAX_CANVAS_PIXELS must be big enough that the originally rendered
// canvas still has enough space to grow before triggering CSS-only zoom
it("test correctly configured", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const canvasSize = await getCanvasSize(page);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBeLessThan(MAX_CANVAS_PIXELS.get(browserName) / 4);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBeGreaterThan(MAX_CANVAS_PIXELS.get(browserName) / 16);
})
);
});
it("does not trigger CSS-only zoom below maxCanvasPixels", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const originalCanvasSize = await getCanvasSize(page);
const factor = 2;
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
const canvasSize = await getCanvasSize(page);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBe(originalCanvasSize * factor ** 2);
expect(canvasSize)
.withContext(`In ${browserName}, MAX_CANVAS_PIXELS`)
.toBeLessThan(MAX_CANVAS_PIXELS.get(browserName));
})
);
});
it("triggers CSS-only zoom above maxCanvasPixels", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const originalCanvasSize = await getCanvasSize(page);
const factor = 4;
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
const canvasSize = await getCanvasSize(page);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBeLessThan(originalCanvasSize * factor ** 2);
expect(canvasSize)
.withContext(`In ${browserName}, <= MAX_CANVAS_PIXELS`)
.toBeLessThanOrEqual(MAX_CANVAS_PIXELS.get(browserName));
expect(canvasSize)
.withContext(`In ${browserName}, > MAX_CANVAS_PIXELS * 0.99`)
.toBeGreaterThan(MAX_CANVAS_PIXELS.get(browserName) * 0.99);
})
);
});
});
});
}); });

View file

@ -34,9 +34,9 @@ import {
import { import {
approximateFraction, approximateFraction,
DEFAULT_SCALE, DEFAULT_SCALE,
floorToDivide,
OutputScale, OutputScale,
RenderingStates, RenderingStates,
roundToDivide,
TextLayerMode, TextLayerMode,
} from "./ui_utils.js"; } from "./ui_utils.js";
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js"; import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
@ -1016,11 +1016,11 @@ class PDFPageView {
const sfx = approximateFraction(outputScale.sx); const sfx = approximateFraction(outputScale.sx);
const sfy = approximateFraction(outputScale.sy); const sfy = approximateFraction(outputScale.sy);
canvas.width = roundToDivide(width * outputScale.sx, sfx[0]); canvas.width = floorToDivide(width * outputScale.sx, sfx[0]);
canvas.height = roundToDivide(height * outputScale.sy, sfy[0]); canvas.height = floorToDivide(height * outputScale.sy, sfy[0]);
const { style } = canvas; const { style } = canvas;
style.width = roundToDivide(width, sfx[1]) + "px"; style.width = floorToDivide(width, sfx[1]) + "px";
style.height = roundToDivide(height, sfy[1]) + "px"; style.height = floorToDivide(height, sfy[1]) + "px";
// Add the viewport so it's known what it was originally drawn with. // Add the viewport so it's known what it was originally drawn with.
this.#viewportMap.set(canvas, viewport); this.#viewportMap.set(canvas, viewport);

View file

@ -263,6 +263,7 @@ function binarySearchFirstItem(items, condition, start = 0) {
* @param {number} x - Positive float number. * @param {number} x - Positive float number.
* @returns {Array} Estimated fraction: the first array item is a numerator, * @returns {Array} Estimated fraction: the first array item is a numerator,
* the second one is a denominator. * the second one is a denominator.
* They are both natural numbers.
*/ */
function approximateFraction(x) { function approximateFraction(x) {
// Fast paths for int numbers or their inversions. // Fast paths for int numbers or their inversions.
@ -309,9 +310,12 @@ function approximateFraction(x) {
return result; return result;
} }
function roundToDivide(x, div) { /**
const r = x % div; * @param {number} x - A positive number to round to a multiple of `div`.
return r === 0 ? x : Math.round(x - r + div); * @param {number} div - A natural number.
*/
function floorToDivide(x, div) {
return x - (x % div);
} }
/** /**
@ -866,6 +870,7 @@ export {
DEFAULT_SCALE_DELTA, DEFAULT_SCALE_DELTA,
DEFAULT_SCALE_VALUE, DEFAULT_SCALE_VALUE,
docStyle, docStyle,
floorToDivide,
getActiveOrFocusedElement, getActiveOrFocusedElement,
getPageSizeInches, getPageSizeInches,
getVisibleElements, getVisibleElements,
@ -884,7 +889,6 @@ export {
ProgressBar, ProgressBar,
removeNullCharacters, removeNullCharacters,
RenderingStates, RenderingStates,
roundToDivide,
SCROLLBAR_PADDING, SCROLLBAR_PADDING,
scrollIntoView, scrollIntoView,
ScrollMode, ScrollMode,