Skip to content

Commit

Permalink
Merge pull request #18218 from nicolo-ribaudo/test-maxCanvasPixels
Browse files Browse the repository at this point in the history
Respect `maxCanvasPixels` when computing canvas dimensions
  • Loading branch information
timvandermeij authored Jun 19, 2024
2 parents 94cbe9e + de23bb9 commit 3e1d779
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 62 deletions.
7 changes: 6 additions & 1 deletion test/integration/test_utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ function loadAndWait(filename, selector, zoom, pageSetup, options) {

let app_options = "";
if (options) {
const optionsObject =
typeof options === "function"
? await options(page, session.name)
: options;

// 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)}`;
}
}
Expand Down
220 changes: 168 additions & 52 deletions test/integration/viewer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -174,24 +174,6 @@ describe("PDF viewer", () => {
});

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) {
return createPromise(page, (resolve, reject) => {
const controller = new AbortController();
Expand All @@ -209,50 +191,184 @@ describe("PDF viewer", () => {
});
}

it("respects drawing delay when zooming out", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const promise = await createPromiseForFirstPageRendered(page);
describe("forced (maxCanvasPixels: 0)", () => {
let pages;

beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
{ maxCanvasPixels: 0 }
);
});

const start = await page.evaluate(() => {
const startTime = performance.now();
window.PDFViewerApplication.pdfViewer.decreaseScale({
drawingDelay: 100,
scaleFactor: 0.9,
afterAll(async () => {
await closePages(pages);
});

it("respects drawing delay when zooming out", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const promise = await createPromiseForFirstPageRendered(page);

const start = await page.evaluate(() => {
const startTime = performance.now();
window.PDFViewerApplication.pdfViewer.decreaseScale({
drawingDelay: 100,
scaleFactor: 0.9,
});
return startTime;
});
return startTime;
});

const end = await awaitPromise(promise);
const end = await awaitPromise(promise);

expect(end - start)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(100);
})
);
expect(end - start)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(100);
})
);
});

it("respects drawing delay when zooming in", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const promise = await createPromiseForFirstPageRendered(page);

const start = await page.evaluate(() => {
const startTime = performance.now();
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 100,
scaleFactor: 1.1,
});
return startTime;
});

const end = await awaitPromise(promise);

expect(end - start)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(100);
})
);
});
});

it("respects drawing delay when zooming in", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const promise = await createPromiseForFirstPageRendered(page);
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 };
}
);
});

const start = await page.evaluate(() => {
const startTime = performance.now();
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 100,
scaleFactor: 1.1,
beforeEach(async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.currentScale = 0.5;
});
return startTime;
});
})
);
});

const end = await awaitPromise(promise);
afterAll(async () => {
await closePages(pages);
});

expect(end - start)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(100);
})
);
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);
})
);
});
});
});
});
10 changes: 5 additions & 5 deletions web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import {
import {
approximateFraction,
DEFAULT_SCALE,
floorToDivide,
OutputScale,
RenderingStates,
roundToDivide,
TextLayerMode,
} from "./ui_utils.js";
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
Expand Down Expand Up @@ -1016,11 +1016,11 @@ class PDFPageView {
const sfx = approximateFraction(outputScale.sx);
const sfy = approximateFraction(outputScale.sy);

canvas.width = roundToDivide(width * outputScale.sx, sfx[0]);
canvas.height = roundToDivide(height * outputScale.sy, sfy[0]);
canvas.width = floorToDivide(width * outputScale.sx, sfx[0]);
canvas.height = floorToDivide(height * outputScale.sy, sfy[0]);
const { style } = canvas;
style.width = roundToDivide(width, sfx[1]) + "px";
style.height = roundToDivide(height, sfy[1]) + "px";
style.width = floorToDivide(width, sfx[1]) + "px";
style.height = floorToDivide(height, sfy[1]) + "px";

// Add the viewport so it's known what it was originally drawn with.
this.#viewportMap.set(canvas, viewport);
Expand Down
12 changes: 8 additions & 4 deletions web/ui_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ function binarySearchFirstItem(items, condition, start = 0) {
* @param {number} x - Positive float number.
* @returns {Array} Estimated fraction: the first array item is a numerator,
* the second one is a denominator.
* They are both natural numbers.
*/
function approximateFraction(x) {
// Fast paths for int numbers or their inversions.
Expand Down Expand Up @@ -309,9 +310,12 @@ function approximateFraction(x) {
return result;
}

function roundToDivide(x, div) {
const r = x % div;
return r === 0 ? x : Math.round(x - r + div);
/**
* @param {number} x - A positive number to round to a multiple of `div`.
* @param {number} div - A natural number.
*/
function floorToDivide(x, div) {
return x - (x % div);
}

/**
Expand Down Expand Up @@ -866,6 +870,7 @@ export {
DEFAULT_SCALE_DELTA,
DEFAULT_SCALE_VALUE,
docStyle,
floorToDivide,
getActiveOrFocusedElement,
getPageSizeInches,
getVisibleElements,
Expand All @@ -884,7 +889,6 @@ export {
ProgressBar,
removeNullCharacters,
RenderingStates,
roundToDivide,
SCROLLBAR_PADDING,
scrollIntoView,
ScrollMode,
Expand Down

0 comments on commit 3e1d779

Please sign in to comment.