Merge pull request #15812 from calixteman/refactor_zoom
Only redraw after zooming is finished (bug 1661253)
This commit is contained in:
commit
e49dd525b0
@ -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.",
|
||||
|
12
web/app.js
12
web/app.js
@ -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;
|
||||
},
|
||||
|
@ -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: "",
|
||||
|
@ -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,
|
||||
|
@ -143,6 +143,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pdfViewer .page canvas[zooming] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pdfViewer .page .loadingIcon {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user