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
},
"defaultZoomDelay": {
"title": "Default zoom delay",
"description": "Delay (in ms) to wait before redrawing the canvas.",
"type": "integer",
"default": 400
},
"defaultZoomValue": {
"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.",

View File

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

View File

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

View File

@ -118,6 +118,8 @@ class PDFPageView {
#layerProperties = null;
#previousRotation = null;
#useThumbnailCanvas = {
initialOptionalContent: true,
regularAnnotations: true,
@ -227,9 +229,14 @@ class PDFPageView {
}
#setDimensions() {
const { div, viewport } = this;
const { viewport } = this;
if (this.#previousRotation === viewport.rotation) {
return;
}
this.#previousRotation = viewport.rotation;
setLayerDimensions(
div,
this.div,
viewport,
/* mustFlip = */ true,
/* mustRotate = */ false
@ -245,6 +252,7 @@ class PDFPageView {
scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
rotation: totalRotation,
});
this.#setDimensions();
this.reset();
}
@ -417,7 +425,6 @@ class PDFPageView {
});
this.renderingState = RenderingStates.INITIAL;
this.#setDimensions();
const div = this.div;
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;
if (typeof rotation === "number") {
this.rotation = rotation; // The rotation may be zero.
@ -528,6 +540,7 @@ class PDFPageView {
scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
rotation: totalRotation,
});
this.#setDimensions();
if (
(typeof PDFJSDev === "undefined" ||
@ -572,17 +585,40 @@ class PDFPageView {
}
}
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
if (this.canvas) {
if (
postponeDrawing ||
this.useOnlyCssZoom ||
(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({
target: this.canvas,
redrawAnnotationLayer: true,
redrawAnnotationEditorLayer: true,
redrawXfaLayer: true,
redrawTextLayer: true,
redrawTextLayer: !postponeDrawing,
hideTextLayer: postponeDrawing,
});
this.eventBus.dispatch("pagerendered", {
@ -620,9 +656,10 @@ class PDFPageView {
keepAnnotationEditorLayer = false,
keepXfaLayer = false,
keepTextLayer = false,
cancelExtraDelay = 0,
} = {}) {
if (this.paintTask) {
this.paintTask.cancel();
this.paintTask.cancel(cancelExtraDelay);
this.paintTask = null;
}
this.resume = null;
@ -662,31 +699,49 @@ class PDFPageView {
redrawAnnotationEditorLayer = false,
redrawXfaLayer = false,
redrawTextLayer = false,
hideTextLayer = false,
}) {
// Scale target (canvas or svg), its wrapper and page container.
const width = this.viewport.width;
const height = this.viewport.height;
const div = this.div;
target.style.width =
target.parentNode.style.width =
div.style.width =
Math.floor(width) + "px";
target.style.height =
target.parentNode.style.height =
div.style.height =
Math.floor(height) + "px";
// The canvas may have been originally rotated; rotate relative to that.
const relativeRotation =
this.viewport.rotation - this.paintedViewportMap.get(target).rotation;
const absRotation = Math.abs(relativeRotation);
let scaleX = 1,
scaleY = 1;
if (absRotation === 90 || absRotation === 270) {
// Scale x and y because of the rotation.
scaleX = height / width;
scaleY = width / 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 { width, height } = this.viewport;
target.style.width =
target.parentNode.style.width =
div.style.width =
Math.floor(width) + "px";
target.style.height =
target.parentNode.style.height =
div.style.height =
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.
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;
}
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) {
this.#renderAnnotationLayer();
@ -697,8 +752,13 @@ class PDFPageView {
if (redrawXfaLayer && this.xfaLayer) {
this.#renderXfaLayer();
}
if (redrawTextLayer && this.textLayer) {
this.#renderTextLayer();
if (this.textLayer) {
if (hideTextLayer) {
this.textLayer.hide();
} else if (redrawTextLayer) {
this.#renderTextLayer();
}
}
}
@ -933,8 +993,8 @@ class PDFPageView {
onRenderContinue(cont) {
cont();
},
cancel() {
renderTask.cancel();
cancel(extraDelay = 0) {
renderTask.cancel(extraDelay);
},
get separateAnnots() {
return renderTask.separateAnnots;
@ -942,6 +1002,7 @@ class PDFPageView {
};
const viewport = this.viewport;
const { width, height } = viewport;
const canvas = document.createElement("canvas");
canvas.setAttribute("role", "presentation");
@ -968,12 +1029,12 @@ class PDFPageView {
});
// Use a scale that makes the canvas have the originally intended size
// of the page.
outputScale.sx *= actualSizeViewport.width / viewport.width;
outputScale.sy *= actualSizeViewport.height / viewport.height;
outputScale.sx *= actualSizeViewport.width / width;
outputScale.sy *= actualSizeViewport.height / height;
}
if (this.maxCanvasPixels > 0) {
const pixelsInViewport = viewport.width * viewport.height;
const pixelsInViewport = width * height;
const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
outputScale.sx = maxScale;
@ -1003,7 +1064,7 @@ class PDFPageView {
const renderContext = {
canvasContext: ctx,
transform,
viewport: this.viewport,
viewport,
annotationMode: this.#annotationMode,
optionalContentConfigPromise: this._optionalContentConfigPromise,
annotationCanvasMap: this._annotationCanvasMap,

View File

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

View File

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

View File

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