Skip to content

Commit

Permalink
When zooming replace the css-zoomed canvas by the new one only when r…
Browse files Browse the repository at this point in the history
…endering is finished

It fixes #18622.

It avoids to recreate a canvasWrapper element in order minimize the DOM operations.
  • Loading branch information
calixteman committed Dec 5, 2024
1 parent 9cbc5ba commit 2f04db5
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 93 deletions.
21 changes: 18 additions & 3 deletions test/integration/viewer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,20 @@ describe("PDF viewer", () => {
beforeEach(async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.currentScale = 0.5;
});
const handle = await waitForPageRendered(page);
if (
await page.evaluate(() => {
if (
window.PDFViewerApplication.pdfViewer.currentScale !== 0.5
) {
window.PDFViewerApplication.pdfViewer.currentScale = 0.5;
return true;
}
return false;
})
) {
await awaitPromise(handle);
}
})
);
});
Expand Down Expand Up @@ -317,12 +328,14 @@ describe("PDF viewer", () => {
const originalCanvasSize = await getCanvasSize(page);
const factor = 2;

const handle = await waitForPageRendered(page);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);

const canvasSize = await getCanvasSize(page);

Expand All @@ -343,12 +356,14 @@ describe("PDF viewer", () => {
const originalCanvasSize = await getCanvasSize(page);
const factor = 4;

const handle = await waitForPageRendered(page);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);

const canvasSize = await getCanvasSize(page);

Expand Down
147 changes: 66 additions & 81 deletions web/pdf_page_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ const LAYERS_ORDER = new Map([
class PDFPageView {
#annotationMode = AnnotationMode.ENABLE_FORMS;

#canvasWrapper = null;

#enableHWA = false;

#hasRestrictedScaling = false;
Expand All @@ -126,6 +128,8 @@ class PDFPageView {

#loadingId = null;

#originalViewport = null;

#previousRotation = null;

#scaleRoundX = 1;
Expand All @@ -144,8 +148,6 @@ class PDFPageView {
regularAnnotations: true,
};

#viewportMap = new WeakMap();

#layers = [null, null, null, null];

/**
Expand Down Expand Up @@ -195,7 +197,6 @@ class PDFPageView {
this.annotationLayer = null;
this.annotationEditorLayer = null;
this.textLayer = null;
this.zoomLayer = null;
this.xfaLayer = null;
this.structTreeLayer = null;
this.drawLayer = null;
Expand Down Expand Up @@ -508,33 +509,23 @@ class PDFPageView {
this._textHighlighter.enable();
}

/**
* @private
*/
_resetZoomLayer(removeFromDOM = false) {
if (!this.zoomLayer) {
#resetCanvas() {
const { canvas } = this;
if (!canvas) {
return;
}
const zoomLayerCanvas = this.zoomLayer.firstChild;
this.#viewportMap.delete(zoomLayerCanvas);
// Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption.
zoomLayerCanvas.width = 0;
zoomLayerCanvas.height = 0;

if (removeFromDOM) {
// Note: `ChildNode.remove` doesn't throw if the parent node is undefined.
this.zoomLayer.remove();
}
this.zoomLayer = null;
canvas.remove();
canvas.width = canvas.height = 0;
this.canvas = null;
this.#originalViewport = null;
}

reset({
keepZoomLayer = false,
keepAnnotationLayer = false,
keepAnnotationEditorLayer = false,
keepXfaLayer = false,
keepTextLayer = false,
keepCanvasWrapper = false,
} = {}) {
this.cancelRendering({
keepAnnotationLayer,
Expand All @@ -547,21 +538,21 @@ class PDFPageView {
const div = this.div;

const childNodes = div.childNodes,
zoomLayerNode = (keepZoomLayer && this.zoomLayer) || null,
annotationLayerNode =
(keepAnnotationLayer && this.annotationLayer?.div) || null,
annotationEditorLayerNode =
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null,
textLayerNode = (keepTextLayer && this.textLayer?.div) || null;
textLayerNode = (keepTextLayer && this.textLayer?.div) || null,
canvasWrapperNode = (keepCanvasWrapper && this.#canvasWrapper) || null;
for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i];
switch (node) {
case zoomLayerNode:
case annotationLayerNode:
case annotationEditorLayerNode:
case xfaLayerNode:
case textLayerNode:
case canvasWrapperNode:
continue;
}
node.remove();
Expand Down Expand Up @@ -590,16 +581,9 @@ class PDFPageView {
}
this.structTreeLayer?.hide();

if (!zoomLayerNode) {
if (this.canvas) {
this.#viewportMap.delete(this.canvas);
// Zeroing the width and height causes Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption.
this.canvas.width = 0;
this.canvas.height = 0;
delete this.canvas;
}
this._resetZoomLayer();
if (!keepCanvasWrapper && this.#canvasWrapper) {
this.#canvasWrapper = null;
this.#resetCanvas();
}
}

Expand All @@ -609,11 +593,11 @@ class PDFPageView {
}
this.#isEditing = isEditing;
this.reset({
keepZoomLayer: true,
keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true,
keepTextLayer: true,
keepCanvasWrapper: true,
});
}

Expand Down Expand Up @@ -697,7 +681,6 @@ class PDFPageView {
this.renderingState !== RenderingStates.FINISHED
) {
this.cancelRendering({
keepZoomLayer: true,
keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true,
Expand All @@ -715,7 +698,6 @@ class PDFPageView {
}

this.cssTransform({
target: this.canvas,
redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true,
redrawXfaLayer: true,
Expand All @@ -737,20 +719,14 @@ class PDFPageView {
});
return;
}
if (!this.zoomLayer && !this.canvas.hidden) {
this.zoomLayer = this.canvas.parentNode;
this.zoomLayer.style.position = "absolute";
}
}
if (this.zoomLayer) {
this.cssTransform({ target: this.zoomLayer.firstChild });
}
this.cssTransform({});
this.reset({
keepZoomLayer: true,
keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true,
keepTextLayer: true,
keepCanvasWrapper: true,
});
}

Expand Down Expand Up @@ -805,41 +781,33 @@ class PDFPageView {
}

cssTransform({
target,
redrawAnnotationLayer = false,
redrawAnnotationEditorLayer = false,
redrawXfaLayer = false,
redrawTextLayer = false,
hideTextLayer = false,
}) {
// Scale target (canvas), its wrapper and page container.
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
!(target instanceof HTMLCanvasElement)
) {
throw new Error("Expected `target` to be a canvas.");
}
if (!target.hasAttribute("zooming")) {
target.setAttribute("zooming", true);
const { style } = target;
style.width = style.height = "";
const { canvas } = this;
if (!canvas) {
return;
}

const originalViewport = this.#viewportMap.get(target);
const originalViewport = this.#originalViewport;
if (this.viewport !== originalViewport) {
// The canvas may have been originally rotated; rotate relative to that.
const relativeRotation =
this.viewport.rotation - originalViewport.rotation;
const absRotation = Math.abs(relativeRotation);
let scaleX = 1,
scaleY = 1;
if (absRotation === 90 || absRotation === 270) {
const { width, height } = this.viewport;
// Scale x and y because of the rotation.
scaleX = height / width;
scaleY = width / height;
const scaleX = height / width;
const scaleY = width / height;
canvas.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX},${scaleY})`;
} else {
canvas.style.transform =
absRotation === 180 ? `rotate(${relativeRotation}deg)` : "";
}
target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`;
}

if (redrawAnnotationLayer && this.annotationLayer) {
Expand Down Expand Up @@ -892,7 +860,6 @@ class PDFPageView {
this.#renderError = error;

this.renderingState = RenderingStates.FINISHED;
this._resetZoomLayer(/* removeFromDOM = */ true);

// Ensure that the thumbnails won't become partially (or fully) blank,
// for documents that contain interactive form elements.
Expand Down Expand Up @@ -927,9 +894,12 @@ class PDFPageView {

// Wrap the canvas so that if it has a CSS transform for high DPI the
// overflow will be hidden in Firefox.
const canvasWrapper = document.createElement("div");
canvasWrapper.classList.add("canvasWrapper");
this.#addLayer(canvasWrapper, "canvasWrapper");
let canvasWrapper = this.#canvasWrapper;
if (!canvasWrapper) {
canvasWrapper = this.#canvasWrapper = document.createElement("div");
canvasWrapper.classList.add("canvasWrapper");
this.#addLayer(canvasWrapper, "canvasWrapper");
}

if (
!this.textLayer &&
Expand Down Expand Up @@ -1004,22 +974,37 @@ class PDFPageView {
const canvas = document.createElement("canvas");
canvas.setAttribute("role", "presentation");

// Keep the canvas hidden until the first draw callback, or until drawing
// is complete when `!this.renderingQueue`, to prevent black flickering.
canvas.hidden = true;
const hasHCM = !!(pageColors?.background && pageColors?.foreground);
const prevCanvas = this.canvas;
const updateOnFirstShow = !prevCanvas && !hasHCM;
this.canvas = canvas;
this.#originalViewport = viewport;

let showCanvas = isLastShow => {
// In HCM, a final filter is applied on the canvas which means that
// before it's applied we've normal colors. Consequently, to avoid to have
// a final flash we just display it once all the drawing is done.
if (!hasHCM || isLastShow) {
canvas.hidden = false;
showCanvas = null; // Only invoke the function once.
if (updateOnFirstShow) {
// Don't add the canvas until the first draw callback, or until
// drawing is complete when `!this.renderingQueue`, to prevent black
// flickering.
canvasWrapper.append(canvas);
showCanvas = null;
return;
}
if (!isLastShow) {
return;
}

if (prevCanvas) {
prevCanvas.replaceWith(canvas);
prevCanvas.width = prevCanvas.height = 0;
} else {
// In HCM, a final filter is applied on the canvas which means that
// before it's applied we've normal colors. Consequently, to avoid to
// have a final flash we just display it once all the drawing is done.
canvasWrapper.append(canvas);
}

showCanvas = null;
};
canvasWrapper.append(canvas);
this.canvas = canvas;

const ctx = canvas.getContext("2d", {
alpha: false,
Expand Down Expand Up @@ -1073,9 +1058,6 @@ class PDFPageView {
this.#scaleRoundY = sfy[1];
}

// Add the viewport so it's known what it was originally drawn with.
this.#viewportMap.set(canvas, viewport);

// Rendering area
const transform = outputScale.scaled
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
Expand Down Expand Up @@ -1141,6 +1123,9 @@ class PDFPageView {
// callback had been invoked at least once.
if (!(error instanceof RenderingCancelledException)) {
showCanvas?.(true);
} else {
prevCanvas?.remove();
this.#resetCanvas();
}
return this.#finishRenderTask(renderTask, error);
}
Expand Down
13 changes: 4 additions & 9 deletions web/pdf_viewer.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,14 @@
height: 100%;

canvas {
position: absolute;
top: 0;
left: 0;
margin: 0;
display: block;
width: 100%;
height: 100%;

&[hidden] {
display: none;
}

&[zooming] {
width: 100%;
height: 100%;
}
contain: content;

.structTree {
contain: strict;
Expand Down

0 comments on commit 2f04db5

Please sign in to comment.