Merge pull request #15812 from calixteman/refactor_zoom

Only redraw after zooming is finished (bug 1661253)
This commit is contained in:
calixteman 2022-12-26 19:44:03 +01:00 committed by GitHub
commit e49dd525b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.