diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 31fd39023..d8694e154 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -1103,8 +1103,12 @@ function setLayerDimensions( const w = `var(--scale-factor) * ${pageWidth}px`, h = `var(--scale-factor) * ${pageHeight}px`; - const widthStr = useRound ? `round(${w}, 1px)` : `calc(${w})`, - heightStr = useRound ? `round(${h}, 1px)` : `calc(${h})`; + const widthStr = useRound + ? `round(down, ${w}, var(--scale-round-x, 1px))` + : `calc(${w})`, + heightStr = useRound + ? `round(down, ${h}, var(--scale-round-y, 1px))` + : `calc(${h})`; if (!mustFlip || viewport.rotation % 180 === 0) { style.width = widthStr; diff --git a/test/integration/viewer_spec.mjs b/test/integration/viewer_spec.mjs index 61b4f071d..4d14649d2 100644 --- a/test/integration/viewer_spec.mjs +++ b/test/integration/viewer_spec.mjs @@ -19,7 +19,10 @@ import { createPromise, getSpanRectFromText, loadAndWait, + scrollIntoView, + waitForPageRendered, } from "./test_utils.mjs"; +import { PNG } from "pngjs"; describe("PDF viewer", () => { describe("Zoom origin", () => { @@ -365,4 +368,75 @@ describe("PDF viewer", () => { }); }); }); + + describe("Canvas fits the page", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "issue18694.pdf", + ".textLayer .endOfContent", + "page-width" + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that canvas perfectly fits the page whatever the zoom level is", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const debug = false; + + // The pdf has a single page with a red background. + // We set the viewer background to red, because when screenshoting + // some part of the viewer background can be visible. + // But here we don't care about the viewer background: we only + // care about the page background and the canvas default color. + + await page.evaluate(() => { + document.body.style.background = "#ff0000"; + const toolbar = document.querySelector(".toolbar"); + toolbar.style.display = "none"; + }); + await page.waitForSelector(".toolbar", { visible: false }); + await page.evaluate(() => { + const p = document.querySelector(`.page[data-page-number="1"]`); + p.style.border = "none"; + }); + + for (let i = 0; ; i++) { + const handle = await waitForPageRendered(page); + await page.evaluate(() => window.PDFViewerApplication.zoomOut()); + await awaitPromise(handle); + await scrollIntoView(page, `.page[data-page-number="1"]`); + + const element = await page.$(`.page[data-page-number="1"]`); + const png = await element.screenshot({ + type: "png", + path: debug ? `foo${i}.png` : "", + }); + const pageImage = PNG.sync.read(Buffer.from(png)); + let buffer = new Uint32Array(pageImage.data.buffer); + + // Search for the first red pixel. + const j = buffer.indexOf(0xff0000ff); + buffer = buffer.slice(j); + + expect(buffer.every(x => x === 0xff0000ff)) + .withContext(`In ${browserName}, in the ${i}th zoom in`) + .toBe(true); + + const currentScale = await page.evaluate( + () => window.PDFViewerApplication.pdfViewer.currentScale + ); + if (currentScale <= 0.1) { + break; + } + } + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 0e3f5cf3c..2cacef512 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -665,3 +665,4 @@ !highlights.pdf !highlight.pdf !bug1708040.pdf +!issue18694.pdf diff --git a/test/pdfs/issue18694.pdf b/test/pdfs/issue18694.pdf new file mode 100755 index 000000000..8ed563810 Binary files /dev/null and b/test/pdfs/issue18694.pdf differ diff --git a/test/unit/ui_utils_spec.js b/test/unit/ui_utils_spec.js index 4935489f7..0ead19043 100644 --- a/test/unit/ui_utils_spec.js +++ b/test/unit/ui_utils_spec.js @@ -16,6 +16,7 @@ import { backtrackBeforeAllVisibleElements, binarySearchFirstItem, + calcRound, getPageSizeInches, getVisibleElements, isPortraitOrientation, @@ -627,4 +628,17 @@ describe("ui_utils", function () { }); }); }); + + describe("calcRound", function () { + it("should handle different browsers/environments correctly", function () { + if ( + typeof window !== "undefined" && + window.navigator?.userAgent?.includes("Firefox") + ) { + expect(calcRound(1.6)).not.toEqual(1.6); + } else { + expect(calcRound(1.6)).toEqual(1.6); + } + }); + }); }); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index e3c154b84..6304586cd 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -33,6 +33,7 @@ import { } from "pdfjs-lib"; import { approximateFraction, + calcRound, DEFAULT_SCALE, floorToDivide, OutputScale, @@ -127,6 +128,10 @@ class PDFPageView { #previousRotation = null; + #scaleRoundX = 1; + + #scaleRoundY = 1; + #renderError = null; #renderingState = RenderingStates.INITIAL; @@ -1039,11 +1044,27 @@ class PDFPageView { const sfx = approximateFraction(outputScale.sx); const sfy = approximateFraction(outputScale.sy); - canvas.width = floorToDivide(width * outputScale.sx, sfx[0]); - canvas.height = floorToDivide(height * outputScale.sy, sfy[0]); - const { style } = canvas; - style.width = floorToDivide(width, sfx[1]) + "px"; - style.height = floorToDivide(height, sfy[1]) + "px"; + const canvasWidth = (canvas.width = floorToDivide( + calcRound(width * outputScale.sx), + sfx[0] + )); + const canvasHeight = (canvas.height = floorToDivide( + calcRound(height * outputScale.sy), + sfy[0] + )); + const pageWidth = floorToDivide(calcRound(width), sfx[1]); + const pageHeight = floorToDivide(calcRound(height), sfy[1]); + outputScale.sx = canvasWidth / pageWidth; + outputScale.sy = canvasHeight / pageHeight; + + if (this.#scaleRoundX !== sfx[1]) { + div.style.setProperty("--scale-round-x", `${sfx[1]}px`); + this.#scaleRoundX = sfx[1]; + } + if (this.#scaleRoundY !== sfy[1]) { + div.style.setProperty("--scale-round-y", `${sfy[1]}px`); + this.#scaleRoundY = sfy[1]; + } // Add the viewport so it's known what it was originally drawn with. this.#viewportMap.set(canvas, viewport); diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index d65173b81..9082cda37 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -83,6 +83,8 @@ canvas { margin: 0; display: block; + width: 100%; + height: 100%; &[hidden] { display: none; @@ -101,6 +103,9 @@ } .pdfViewer .page { + --scale-round-x: 1px; + --scale-round-y: 1px; + direction: ltr; width: 816px; height: 1056px; diff --git a/web/ui_utils.js b/web/ui_utils.js index 08455659c..12c74f1fe 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -862,6 +862,25 @@ function toggleExpandedBtn(button, toggle, view = null) { view?.classList.toggle("hidden", !toggle); } +// In Firefox, the css calc function uses f32 precision but the Chrome or Safari +// are using f64 one. So in order to have the same rendering in all browsers, we +// need to use the right precision in order to have correct dimensions. +const calcRound = + typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL") + ? Math.fround + : (function () { + if ( + typeof PDFJSDev !== "undefined" && + PDFJSDev.test("LIB") && + typeof document === "undefined" + ) { + return x => x; + } + const e = document.createElement("div"); + e.style.width = "round(down, calc(1.6666666666666665 * 792px), 1px)"; + return e.style.width === "calc(1320px)" ? Math.fround : x => x; + })(); + export { animationStarted, apiPageLayoutToViewerModes, @@ -870,6 +889,7 @@ export { AutoPrintRegExp, backtrackBeforeAllVisibleElements, // only exported for testing binarySearchFirstItem, + calcRound, CursorTool, DEFAULT_SCALE, DEFAULT_SCALE_DELTA,