Only redraw after zooming is finished (bug 1661253)

Right now, the visible pages are redrawn for each scale change.
Consequently, zooming with mouse wheel or in pinching can be pretty janky
(even on a desktop machine but with a hdpi screen).
So the main idea in this patch is to draw the visible pages only once zooming
is finished.
This commit is contained in:
Calixte Denizet 2022-12-12 14:24:27 +01:00
parent 31da2fad51
commit 663007a5c7
7 changed files with 190 additions and 78 deletions

View File

@ -28,6 +28,12 @@
], ],
"default": 0 "default": 0
}, },
"defaultZoomDelay": {
"title": "Default zoom delay",
"description": "Delay (in ms) to wait before redrawing the canvas.",
"type": "integer",
"default": 400
},
"defaultZoomValue": { "defaultZoomValue": {
"title": "Default zoom level", "title": "Default zoom level",
"description": "Default zoom level of the viewer. Accepted values: 'auto', 'page-actual', 'page-width', 'page-height', 'page-fit', or a zoom level in percents.", "description": "Default zoom level of the viewer. Accepted values: 'auto', 'page-actual', 'page-width', 'page-height', 'page-fit', or a zoom level in percents.",

View File

@ -655,14 +655,18 @@ const PDFViewerApplication = {
if (this.pdfViewer.isInPresentationMode) { if (this.pdfViewer.isInPresentationMode) {
return; return;
} }
this.pdfViewer.increaseScale(steps); this.pdfViewer.increaseScale(steps, {
delay: AppOptions.get("defaultZoomDelay"),
});
}, },
zoomOut(steps) { zoomOut(steps) {
if (this.pdfViewer.isInPresentationMode) { if (this.pdfViewer.isInPresentationMode) {
return; return;
} }
this.pdfViewer.decreaseScale(steps); this.pdfViewer.decreaseScale(steps, {
delay: AppOptions.get("defaultZoomDelay"),
});
}, },
zoomReset() { zoomReset() {
@ -2019,9 +2023,7 @@ const PDFViewerApplication = {
this._wheelUnusedTicks = 0; this._wheelUnusedTicks = 0;
} }
this._wheelUnusedTicks += ticks; this._wheelUnusedTicks += ticks;
const wholeTicks = const wholeTicks = Math.trunc(this._wheelUnusedTicks);
Math.sign(this._wheelUnusedTicks) *
Math.floor(Math.abs(this._wheelUnusedTicks));
this._wheelUnusedTicks -= wholeTicks; this._wheelUnusedTicks -= wholeTicks;
return wholeTicks; return wholeTicks;
}, },

View File

@ -68,6 +68,12 @@ const defaultOptions = {
value: 0, value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE, kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
}, },
defaultZoomDelay: {
/** @type {number} */
value:
typeof PDFJSDev === "undefined" || !PDFJSDev.test("GENERIC") ? 400 : -1,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
defaultZoomValue: { defaultZoomValue: {
/** @type {string} */ /** @type {string} */
value: "", value: "",

View File

@ -118,6 +118,8 @@ class PDFPageView {
#layerProperties = null; #layerProperties = null;
#previousRotation = null;
#useThumbnailCanvas = { #useThumbnailCanvas = {
initialOptionalContent: true, initialOptionalContent: true,
regularAnnotations: true, regularAnnotations: true,
@ -227,9 +229,14 @@ class PDFPageView {
} }
#setDimensions() { #setDimensions() {
const { div, viewport } = this; const { viewport } = this;
if (this.#previousRotation === viewport.rotation) {
return;
}
this.#previousRotation = viewport.rotation;
setLayerDimensions( setLayerDimensions(
div, this.div,
viewport, viewport,
/* mustFlip = */ true, /* mustFlip = */ true,
/* mustRotate = */ false /* mustRotate = */ false
@ -245,6 +252,7 @@ class PDFPageView {
scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
rotation: totalRotation, rotation: totalRotation,
}); });
this.#setDimensions();
this.reset(); this.reset();
} }
@ -417,7 +425,6 @@ class PDFPageView {
}); });
this.renderingState = RenderingStates.INITIAL; this.renderingState = RenderingStates.INITIAL;
this.#setDimensions();
const div = this.div; const div = this.div;
const childNodes = div.childNodes, const childNodes = div.childNodes,
@ -502,7 +509,12 @@ class PDFPageView {
} }
} }
update({ scale = 0, rotation = null, optionalContentConfigPromise = null }) { update({
scale = 0,
rotation = null,
optionalContentConfigPromise = null,
drawingDelay = -1,
}) {
this.scale = scale || this.scale; this.scale = scale || this.scale;
if (typeof rotation === "number") { if (typeof rotation === "number") {
this.rotation = rotation; // The rotation may be zero. this.rotation = rotation; // The rotation may be zero.
@ -528,6 +540,7 @@ class PDFPageView {
scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS, scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
rotation: totalRotation, rotation: totalRotation,
}); });
this.#setDimensions();
if ( if (
(typeof PDFJSDev === "undefined" || (typeof PDFJSDev === "undefined" ||
@ -572,17 +585,40 @@ class PDFPageView {
} }
} }
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
if (this.canvas) { if (this.canvas) {
if ( if (
postponeDrawing ||
this.useOnlyCssZoom || this.useOnlyCssZoom ||
(this.hasRestrictedScaling && isScalingRestricted) (this.hasRestrictedScaling && isScalingRestricted)
) { ) {
if (
postponeDrawing &&
this.renderingState !== RenderingStates.FINISHED
) {
this.cancelRendering({
keepZoomLayer: true,
keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true,
keepTextLayer: true,
cancelExtraDelay: drawingDelay,
});
// It isn't really finished, but once we have finished
// to postpone, we'll call this.reset(...) which will set
// the rendering state to INITIAL, hence the next call to
// PDFViewer.update() will trigger a redraw (if it's mandatory).
this.renderingState = RenderingStates.FINISHED;
}
this.cssTransform({ this.cssTransform({
target: this.canvas, target: this.canvas,
redrawAnnotationLayer: true, redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true, redrawAnnotationEditorLayer: true,
redrawXfaLayer: true, redrawXfaLayer: true,
redrawTextLayer: true, redrawTextLayer: !postponeDrawing,
hideTextLayer: postponeDrawing,
}); });
this.eventBus.dispatch("pagerendered", { this.eventBus.dispatch("pagerendered", {
@ -620,9 +656,10 @@ class PDFPageView {
keepAnnotationEditorLayer = false, keepAnnotationEditorLayer = false,
keepXfaLayer = false, keepXfaLayer = false,
keepTextLayer = false, keepTextLayer = false,
cancelExtraDelay = 0,
} = {}) { } = {}) {
if (this.paintTask) { if (this.paintTask) {
this.paintTask.cancel(); this.paintTask.cancel(cancelExtraDelay);
this.paintTask = null; this.paintTask = null;
} }
this.resume = null; this.resume = null;
@ -662,11 +699,20 @@ class PDFPageView {
redrawAnnotationEditorLayer = false, redrawAnnotationEditorLayer = false,
redrawXfaLayer = false, redrawXfaLayer = false,
redrawTextLayer = false, redrawTextLayer = false,
hideTextLayer = false,
}) { }) {
// Scale target (canvas or svg), its wrapper and page container. // Scale target (canvas or svg), its wrapper and page container.
const width = this.viewport.width;
const height = this.viewport.height; if (target instanceof HTMLCanvasElement) {
if (!target.hasAttribute("zooming")) {
target.setAttribute("zooming", true);
const { style } = target;
style.width = style.height = "";
}
} else {
const div = this.div; const div = this.div;
const { width, height } = this.viewport;
target.style.width = target.style.width =
target.parentNode.style.width = target.parentNode.style.width =
div.style.width = div.style.width =
@ -675,18 +721,27 @@ class PDFPageView {
target.parentNode.style.height = target.parentNode.style.height =
div.style.height = div.style.height =
Math.floor(height) + "px"; Math.floor(height) + "px";
}
const originalViewport = this.paintedViewportMap.get(target);
if (this.viewport !== originalViewport) {
// The canvas may have been originally rotated; rotate relative to that. // The canvas may have been originally rotated; rotate relative to that.
const relativeRotation = const relativeRotation =
this.viewport.rotation - this.paintedViewportMap.get(target).rotation; this.viewport.rotation - originalViewport.rotation;
const absRotation = Math.abs(relativeRotation); const absRotation = Math.abs(relativeRotation);
let scaleX = 1, let scaleX = 1,
scaleY = 1; scaleY = 1;
if (absRotation === 90 || absRotation === 270) { if (absRotation === 90 || absRotation === 270) {
const { width, height } = this.viewport;
// Scale x and y because of the rotation. // Scale x and y because of the rotation.
scaleX = height / width; scaleX = height / width;
scaleY = width / height; scaleY = width / height;
} }
if (absRotation !== 0) {
target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`; target.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX}, ${scaleY})`;
}
}
if (redrawAnnotationLayer && this.annotationLayer) { if (redrawAnnotationLayer && this.annotationLayer) {
this.#renderAnnotationLayer(); this.#renderAnnotationLayer();
@ -697,10 +752,15 @@ class PDFPageView {
if (redrawXfaLayer && this.xfaLayer) { if (redrawXfaLayer && this.xfaLayer) {
this.#renderXfaLayer(); this.#renderXfaLayer();
} }
if (redrawTextLayer && this.textLayer) {
if (this.textLayer) {
if (hideTextLayer) {
this.textLayer.hide();
} else if (redrawTextLayer) {
this.#renderTextLayer(); this.#renderTextLayer();
} }
} }
}
get width() { get width() {
return this.viewport.width; return this.viewport.width;
@ -933,8 +993,8 @@ class PDFPageView {
onRenderContinue(cont) { onRenderContinue(cont) {
cont(); cont();
}, },
cancel() { cancel(extraDelay = 0) {
renderTask.cancel(); renderTask.cancel(extraDelay);
}, },
get separateAnnots() { get separateAnnots() {
return renderTask.separateAnnots; return renderTask.separateAnnots;
@ -942,6 +1002,7 @@ class PDFPageView {
}; };
const viewport = this.viewport; const viewport = this.viewport;
const { width, height } = viewport;
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.setAttribute("role", "presentation"); canvas.setAttribute("role", "presentation");
@ -968,12 +1029,12 @@ class PDFPageView {
}); });
// Use a scale that makes the canvas have the originally intended size // Use a scale that makes the canvas have the originally intended size
// of the page. // of the page.
outputScale.sx *= actualSizeViewport.width / viewport.width; outputScale.sx *= actualSizeViewport.width / width;
outputScale.sy *= actualSizeViewport.height / viewport.height; outputScale.sy *= actualSizeViewport.height / height;
} }
if (this.maxCanvasPixels > 0) { if (this.maxCanvasPixels > 0) {
const pixelsInViewport = viewport.width * viewport.height; const pixelsInViewport = width * height;
const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport); const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
if (outputScale.sx > maxScale || outputScale.sy > maxScale) { if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
outputScale.sx = maxScale; outputScale.sx = maxScale;
@ -1003,7 +1064,7 @@ class PDFPageView {
const renderContext = { const renderContext = {
canvasContext: ctx, canvasContext: ctx,
transform, transform,
viewport: this.viewport, viewport,
annotationMode: this.#annotationMode, annotationMode: this.#annotationMode,
optionalContentConfigPromise: this._optionalContentConfigPromise, optionalContentConfigPromise: this._optionalContentConfigPromise,
annotationCanvasMap: this._annotationCanvasMap, annotationCanvasMap: this._annotationCanvasMap,

View File

@ -143,6 +143,11 @@
display: none; display: none;
} }
.pdfViewer .page canvas[zooming] {
width: 100%;
height: 100%;
}
.pdfViewer .page .loadingIcon { .pdfViewer .page .loadingIcon {
position: absolute; position: absolute;
display: block; display: block;

View File

@ -216,6 +216,8 @@ class PDFViewer {
#onVisibilityChange = null; #onVisibilityChange = null;
#scaleTimeoutId = null;
/** /**
* @param {PDFViewerOptions} options * @param {PDFViewerOptions} options
*/ */
@ -454,7 +456,7 @@ class PDFViewer {
if (!this.pdfDocument) { if (!this.pdfDocument) {
return; return;
} }
this._setScale(val, false); this._setScale(val, { noScroll: false });
} }
/** /**
@ -471,7 +473,7 @@ class PDFViewer {
if (!this.pdfDocument) { if (!this.pdfDocument) {
return; return;
} }
this._setScale(val, false); this._setScale(val, { noScroll: false });
} }
/** /**
@ -503,14 +505,12 @@ class PDFViewer {
const pageNumber = this._currentPageNumber; const pageNumber = this._currentPageNumber;
const updateArgs = { rotation }; this.refresh(true, { rotation });
for (const pageView of this._pages) {
pageView.update(updateArgs);
}
// Prevent errors in case the rotation changes *before* the scale has been // Prevent errors in case the rotation changes *before* the scale has been
// set to a non-default value. // set to a non-default value.
if (this._currentScaleValue) { if (this._currentScaleValue) {
this._setScale(this._currentScaleValue, true); this._setScale(this._currentScaleValue, { noScroll: true });
} }
this.eventBus.dispatch("rotationchanging", { this.eventBus.dispatch("rotationchanging", {
@ -1080,7 +1080,11 @@ class PDFViewer {
); );
} }
_setScaleUpdatePages(newScale, newValue, noScroll = false, preset = false) { _setScaleUpdatePages(
newScale,
newValue,
{ noScroll = false, preset = false, delay: drawingDelay = -1 }
) {
this._currentScaleValue = newValue.toString(); this._currentScaleValue = newValue.toString();
if (this.#isSameScale(newScale)) { if (this.#isSameScale(newScale)) {
@ -1099,10 +1103,22 @@ class PDFViewer {
newScale * PixelsPerInch.PDF_TO_CSS_UNITS newScale * PixelsPerInch.PDF_TO_CSS_UNITS
); );
const updateArgs = { scale: newScale }; const mustPostponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
for (const pageView of this._pages) { const updateArgs = {
pageView.update(updateArgs); scale: newScale,
};
if (mustPostponeDrawing) {
updateArgs.drawingDelay = drawingDelay;
} }
this.refresh(true, updateArgs);
if (mustPostponeDrawing) {
this.#scaleTimeoutId = setTimeout(() => {
this.#scaleTimeoutId = null;
this.refresh();
}, drawingDelay);
}
this._currentScale = newScale; this._currentScale = newScale;
if (!noScroll) { if (!noScroll) {
@ -1152,11 +1168,12 @@ class PDFViewer {
return 1; return 1;
} }
_setScale(value, noScroll = false) { _setScale(value, options) {
let scale = parseFloat(value); let scale = parseFloat(value);
if (scale > 0) { if (scale > 0) {
this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ false); options.preset = false;
this._setScaleUpdatePages(scale, value, options);
} else { } else {
const currentPage = this._pages[this._currentPageNumber - 1]; const currentPage = this._pages[this._currentPageNumber - 1];
if (!currentPage) { if (!currentPage) {
@ -1211,7 +1228,8 @@ class PDFViewer {
console.error(`_setScale: "${value}" is an unknown zoom value.`); console.error(`_setScale: "${value}" is an unknown zoom value.`);
return; return;
} }
this._setScaleUpdatePages(scale, value, noScroll, /* preset = */ true); options.preset = true;
this._setScaleUpdatePages(scale, value, options);
} }
} }
@ -1223,7 +1241,7 @@ class PDFViewer {
if (this.isInPresentationMode) { if (this.isInPresentationMode) {
// Fixes the case when PDF has different page sizes. // Fixes the case when PDF has different page sizes.
this._setScale(this._currentScaleValue, true); this._setScale(this._currentScaleValue, { noScroll: true });
} }
this.#scrollIntoView(pageView); this.#scrollIntoView(pageView);
} }
@ -1725,11 +1743,7 @@ class PDFViewer {
} }
this._optionalContentConfigPromise = promise; this._optionalContentConfigPromise = promise;
const updateArgs = { optionalContentConfigPromise: promise }; this.refresh(false, { optionalContentConfigPromise: promise });
for (const pageView of this._pages) {
pageView.update(updateArgs);
}
this.update();
this.eventBus.dispatch("optionalcontentconfigchanged", { this.eventBus.dispatch("optionalcontentconfigchanged", {
source: this, source: this,
@ -1792,7 +1806,7 @@ class PDFViewer {
// Call this before re-scrolling to the current page, to ensure that any // Call this before re-scrolling to the current page, to ensure that any
// changes in scale don't move the current page. // changes in scale don't move the current page.
if (this._currentScaleValue && isNaN(this._currentScaleValue)) { if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
this._setScale(this._currentScaleValue, true); this._setScale(this._currentScaleValue, { noScroll: true });
} }
this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true); this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true);
this.update(); this.update();
@ -1864,7 +1878,7 @@ class PDFViewer {
// Call this before re-scrolling to the current page, to ensure that any // Call this before re-scrolling to the current page, to ensure that any
// changes in scale don't move the current page. // changes in scale don't move the current page.
if (this._currentScaleValue && isNaN(this._currentScaleValue)) { if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
this._setScale(this._currentScaleValue, true); this._setScale(this._currentScaleValue, { noScroll: true });
} }
this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true); this._setCurrentPageNumber(pageNumber, /* resetCurrentPageView = */ true);
this.update(); this.update();
@ -2005,29 +2019,37 @@ class PDFViewer {
/** /**
* Increase the current zoom level one, or more, times. * Increase the current zoom level one, or more, times.
* @param {number} [steps] - Defaults to zooming once. * @param {number} [steps] - Defaults to zooming once.
* @param {Object|null} [options]
*/ */
increaseScale(steps = 1) { increaseScale(steps = 1, options = null) {
let newScale = this._currentScale; let newScale = this._currentScale;
do { do {
newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2); newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.ceil(newScale * 10) / 10; newScale = Math.ceil(newScale * 10) / 10;
newScale = Math.min(MAX_SCALE, newScale); newScale = Math.min(MAX_SCALE, newScale);
} while (--steps > 0 && newScale < MAX_SCALE); } while (--steps > 0 && newScale < MAX_SCALE);
this.currentScaleValue = newScale;
options ||= Object.create(null);
options.noScroll = false;
this._setScale(newScale, options);
} }
/** /**
* Decrease the current zoom level one, or more, times. * Decrease the current zoom level one, or more, times.
* @param {number} [steps] - Defaults to zooming once. * @param {number} [steps] - Defaults to zooming once.
* @param {Object|null} [options]
*/ */
decreaseScale(steps = 1) { decreaseScale(steps = 1, options = null) {
let newScale = this._currentScale; let newScale = this._currentScale;
do { do {
newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2); newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2);
newScale = Math.floor(newScale * 10) / 10; newScale = Math.floor(newScale * 10) / 10;
newScale = Math.max(MIN_SCALE, newScale); newScale = Math.max(MIN_SCALE, newScale);
} while (--steps > 0 && newScale > MIN_SCALE); } while (--steps > 0 && newScale > MIN_SCALE);
this.currentScaleValue = newScale;
options ||= Object.create(null);
options.noScroll = false;
this._setScale(newScale, options);
} }
#updateContainerHeightCss(height = this.container.clientHeight) { #updateContainerHeightCss(height = this.container.clientHeight) {
@ -2098,16 +2120,21 @@ class PDFViewer {
this.#annotationEditorUIManager.updateParams(type, value); this.#annotationEditorUIManager.updateParams(type, value);
} }
refresh() { refresh(noUpdate = false, updateArgs = Object.create(null)) {
if (!this.pdfDocument) { if (!this.pdfDocument) {
return; return;
} }
const updateArgs = {};
for (const pageView of this._pages) { for (const pageView of this._pages) {
pageView.update(updateArgs); pageView.update(updateArgs);
} }
if (this.#scaleTimeoutId !== null) {
clearTimeout(this.#scaleTimeoutId);
this.#scaleTimeoutId = null;
}
if (!noUpdate) {
this.update(); this.update();
} }
} }
}
export { PagesCountLimit, PDFPageViewBuffer, PDFViewer }; export { PagesCountLimit, PDFPageViewBuffer, PDFViewer };

View File

@ -86,8 +86,8 @@ class TextLayerBuilder {
} }
const scale = viewport.scale * (globalThis.devicePixelRatio || 1); const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
if (this.renderingDone) {
const { rotation } = viewport; const { rotation } = viewport;
if (this.renderingDone) {
const mustRotate = rotation !== this.#rotation; const mustRotate = rotation !== this.#rotation;
const mustRescale = scale !== this.#scale; const mustRescale = scale !== this.#scale;
if (mustRotate || mustRescale) { if (mustRotate || mustRescale) {
@ -101,10 +101,10 @@ class TextLayerBuilder {
mustRescale, mustRescale,
mustRotate, mustRotate,
}); });
this.show();
this.#scale = scale; this.#scale = scale;
this.#rotation = rotation; this.#rotation = rotation;
} }
this.show();
return; return;
} }
@ -125,21 +125,26 @@ class TextLayerBuilder {
await this.textLayerRenderTask.promise; await this.textLayerRenderTask.promise;
this.#finishRendering(); this.#finishRendering();
this.#scale = scale; this.#scale = scale;
this.#rotation = rotation;
this.show(); this.show();
this.accessibilityManager?.enable(); this.accessibilityManager?.enable();
} }
hide() { hide() {
if (!this.div.hidden) {
// We turn off the highlighter in order to avoid to scroll into view an // We turn off the highlighter in order to avoid to scroll into view an
// element of the text layer which could be hidden. // element of the text layer which could be hidden.
this.highlighter?.disable(); this.highlighter?.disable();
this.div.hidden = true; this.div.hidden = true;
} }
}
show() { show() {
if (this.div.hidden && this.renderingDone) {
this.div.hidden = false; this.div.hidden = false;
this.highlighter?.enable(); this.highlighter?.enable();
} }
}
/** /**
* Cancel rendering of the text layer. * Cancel rendering of the text layer.