diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 6716b4cbd..88411cbc0 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -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.", diff --git a/web/app.js b/web/app.js index a30170ba1..493458bc2 100644 --- a/web/app.js +++ b/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; }, diff --git a/web/app_options.js b/web/app_options.js index de96674a2..4d0a479d1 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -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: "", diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 393e2c8ce..2286fe99c 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -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, diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index 2a6055adf..a050d4b63 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -143,6 +143,11 @@ display: none; } +.pdfViewer .page canvas[zooming] { + width: 100%; + height: 100%; +} + .pdfViewer .page .loadingIcon { position: absolute; display: block; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 4de3dd382..995b15d83 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -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(); + } } } diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index b6cb63846..20fc7f67d 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -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(); + } } /**