mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 22:58:07 +02:00
[api-minor] Render high-res partial page views when falling back to CSS zoom (bug 1492303)
When rendering big PDF pages at high zoom levels, we currently fall back to CSS zoom to avoid rendering canvases with too many pixels. This causes zoomed in PDF to look blurry, and the text to be potentially unreadable. This commit adds support for rendering _part_ of a page (called `PDFPageDetailView` in the code), so that we can render portion of a page in a smaller canvas without hiting the maximun canvas size limit. Specifically, we render an area of that page that is slightly larger than the area that is visible on the screen (100% larger in each direction, unless we have to limit it due to the maximum canvas size). As the user scrolls around the page, we re-render a new area centered around what is currently visible.
This commit is contained in:
parent
06257f782e
commit
458b2ee402
14 changed files with 1142 additions and 116 deletions
|
@ -507,6 +507,7 @@ const PDFViewerApplication = {
|
|||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||
enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
|
||||
enablePermissions: AppOptions.get("enablePermissions"),
|
||||
pageColors,
|
||||
mlManager: this.mlManager,
|
||||
|
@ -2331,7 +2332,7 @@ function onPageRender({ pageNumber }) {
|
|||
}
|
||||
}
|
||||
|
||||
function onPageRendered({ pageNumber, error }) {
|
||||
function onPageRendered({ pageNumber, isDetailView, error }) {
|
||||
// If the page is still visible when it has finished rendering,
|
||||
// ensure that the page number input loading indicator is hidden.
|
||||
if (pageNumber === this.page) {
|
||||
|
@ -2339,7 +2340,7 @@ function onPageRendered({ pageNumber, error }) {
|
|||
}
|
||||
|
||||
// Use the rendered page to set the corresponding thumbnail image.
|
||||
if (this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
|
||||
if (!isDetailView && this.pdfSidebar?.visibleView === SidebarView.THUMBS) {
|
||||
const pageView = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
|
||||
const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
|
||||
/* index = */ pageNumber - 1
|
||||
|
|
|
@ -200,6 +200,11 @@ const defaultOptions = {
|
|||
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("MOZCENTRAL"),
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
|
||||
},
|
||||
enableDetailCanvas: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER,
|
||||
},
|
||||
enableGuessAltText: {
|
||||
/** @type {boolean} */
|
||||
value: true,
|
||||
|
|
|
@ -90,7 +90,7 @@ class BasePDFPageView {
|
|||
}
|
||||
}
|
||||
|
||||
_createCanvas(onShow) {
|
||||
_createCanvas(onShow, hideUntilComplete = false) {
|
||||
const { pageColors } = this;
|
||||
const hasHCM = !!(pageColors?.background && pageColors?.foreground);
|
||||
const prevCanvas = this.canvas;
|
||||
|
@ -98,7 +98,7 @@ class BasePDFPageView {
|
|||
// In HCM, a final filter is applied on the canvas which means that
|
||||
// before it's applied we've normal colors. Consequently, to avoid to
|
||||
// have a final flash we just display it once all the drawing is done.
|
||||
const updateOnFirstShow = !prevCanvas && !hasHCM;
|
||||
const updateOnFirstShow = !prevCanvas && !hasHCM && !hideUntilComplete;
|
||||
|
||||
const canvas = (this.canvas = document.createElement("canvas"));
|
||||
|
||||
|
@ -154,39 +154,37 @@ class BasePDFPageView {
|
|||
this.canvas = null;
|
||||
}
|
||||
|
||||
async _drawCanvas(options, prevCanvas, onFinish) {
|
||||
async _drawCanvas(options, onCancel, onFinish) {
|
||||
const renderTask = (this.renderTask = this.pdfPage.render(options));
|
||||
renderTask.onContinue = this.#renderContinueCallback;
|
||||
renderTask.onError = error => {
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
onCancel();
|
||||
this.#renderError = null;
|
||||
}
|
||||
};
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await renderTask.promise;
|
||||
this.#showCanvas?.(true);
|
||||
this.#finishRenderTask(renderTask, null, onFinish);
|
||||
} catch (error) {
|
||||
} catch (e) {
|
||||
error = e;
|
||||
// When zooming with a `drawingDelay` set, avoid temporarily showing
|
||||
// a black canvas if rendering was cancelled before the `onContinue`-
|
||||
// callback had been invoked at least once.
|
||||
if (!(error instanceof RenderingCancelledException)) {
|
||||
this.#showCanvas?.(true);
|
||||
} else {
|
||||
prevCanvas?.remove();
|
||||
this._resetCanvas();
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
return;
|
||||
}
|
||||
this.#finishRenderTask(renderTask, error, onFinish);
|
||||
}
|
||||
}
|
||||
|
||||
async #finishRenderTask(renderTask, error, onFinish) {
|
||||
// The renderTask may have been replaced by a new one, so only remove
|
||||
// the reference to the renderTask if it matches the one that is
|
||||
// triggering this callback.
|
||||
if (renderTask === this.renderTask) {
|
||||
this.renderTask = null;
|
||||
}
|
||||
|
||||
if (error instanceof RenderingCancelledException) {
|
||||
this.#renderError = null;
|
||||
return;
|
||||
this.#showCanvas?.(true);
|
||||
} finally {
|
||||
// The renderTask may have been replaced by a new one, so only remove
|
||||
// the reference to the renderTask if it matches the one that is
|
||||
// triggering this callback.
|
||||
if (renderTask === this.renderTask) {
|
||||
this.renderTask = null;
|
||||
}
|
||||
}
|
||||
this.#renderError = error;
|
||||
|
||||
|
@ -214,11 +212,12 @@ class BasePDFPageView {
|
|||
});
|
||||
}
|
||||
|
||||
dispatchPageRendered(cssTransform) {
|
||||
dispatchPageRendered(cssTransform, isDetailView) {
|
||||
this.eventBus.dispatch("pagerendered", {
|
||||
source: this,
|
||||
pageNumber: this.id,
|
||||
cssTransform,
|
||||
isDetailView,
|
||||
timestamp: performance.now(),
|
||||
error: this.#renderError,
|
||||
});
|
||||
|
|
250
web/pdf_page_detail_view.js
Normal file
250
web/pdf_page_detail_view.js
Normal file
|
@ -0,0 +1,250 @@
|
|||
/* Copyright 2012 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BasePDFPageView } from "./base_pdf_page_view.js";
|
||||
import { RenderingStates } from "./ui_utils.js";
|
||||
|
||||
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
|
||||
|
||||
/**
|
||||
* @implements {IRenderableView}
|
||||
*/
|
||||
class PDFPageDetailView extends BasePDFPageView {
|
||||
#detailArea = null;
|
||||
|
||||
constructor({ pageView }) {
|
||||
super(pageView);
|
||||
|
||||
this.pageView = pageView;
|
||||
this.renderingId = "detail" + this.id;
|
||||
|
||||
this.div = pageView.div;
|
||||
}
|
||||
|
||||
setPdfPage(pdfPage) {
|
||||
this.pageView.setPdfPage(pdfPage);
|
||||
}
|
||||
|
||||
get pdfPage() {
|
||||
return this.pageView.pdfPage;
|
||||
}
|
||||
|
||||
reset({ keepCanvas = false } = {}) {
|
||||
this.cancelRendering();
|
||||
this.renderingState = RenderingStates.INITIAL;
|
||||
|
||||
if (!keepCanvas) {
|
||||
this._resetCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
#shouldRenderDifferentArea(visibleArea) {
|
||||
if (!this.#detailArea) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const minDetailX = this.#detailArea.minX;
|
||||
const minDetailY = this.#detailArea.minY;
|
||||
const maxDetailX = this.#detailArea.width + minDetailX;
|
||||
const maxDetailY = this.#detailArea.height + minDetailY;
|
||||
|
||||
if (
|
||||
visibleArea.minX < minDetailX ||
|
||||
visibleArea.minY < minDetailY ||
|
||||
visibleArea.maxX > maxDetailX ||
|
||||
visibleArea.maxY > maxDetailY
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const {
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
scale,
|
||||
} = this.pageView.viewport;
|
||||
|
||||
if (this.#detailArea.scale !== scale) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const paddingLeftSize = visibleArea.minX - minDetailX;
|
||||
const paddingRightSize = maxDetailX - visibleArea.maxX;
|
||||
const paddingTopSize = visibleArea.minY - minDetailY;
|
||||
const paddingBottomSize = maxDetailY - visibleArea.maxY;
|
||||
|
||||
// If the user is moving in any direction such that the remaining area
|
||||
// rendered outside of the screen is less than MOVEMENT_THRESHOLD of the
|
||||
// padding we render on each side, trigger a re-render. This is so that if
|
||||
// the user then keeps scrolling in that direction, we have a chance of
|
||||
// finishing rendering the new detail before they get past the rendered
|
||||
// area.
|
||||
|
||||
const MOVEMENT_THRESHOLD = 0.5;
|
||||
const ratio = (1 + MOVEMENT_THRESHOLD) / MOVEMENT_THRESHOLD;
|
||||
|
||||
if (
|
||||
(minDetailX > 0 && paddingRightSize / paddingLeftSize > ratio) ||
|
||||
(maxDetailX < maxWidth && paddingLeftSize / paddingRightSize > ratio) ||
|
||||
(minDetailY > 0 && paddingBottomSize / paddingTopSize > ratio) ||
|
||||
(maxDetailY < maxHeight && paddingTopSize / paddingBottomSize > ratio)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
update({ visibleArea = null, underlyingViewUpdated = false } = {}) {
|
||||
if (underlyingViewUpdated) {
|
||||
this.cancelRendering();
|
||||
this.renderingState = RenderingStates.INITIAL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.#shouldRenderDifferentArea(visibleArea)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewport, maxCanvasPixels } = this.pageView;
|
||||
|
||||
const visibleWidth = visibleArea.maxX - visibleArea.minX;
|
||||
const visibleHeight = visibleArea.maxY - visibleArea.minY;
|
||||
|
||||
// "overflowScale" represents which percentage of the width and of the
|
||||
// height the detail area extends outside of the visible area. We want to
|
||||
// draw a larger area so that we don't have to constantly re-draw while
|
||||
// scrolling. The detail area's dimensions thus become
|
||||
// visibleLength * (2 * overflowScale + 1).
|
||||
// We default to adding a whole height/length of detail area on each side,
|
||||
// but we can reduce it to make sure that we stay within the maxCanvasPixels
|
||||
// limit.
|
||||
const visiblePixels =
|
||||
visibleWidth * visibleHeight * (window.devicePixelRatio || 1) ** 2;
|
||||
const maxDetailToVisibleLinearRatio = Math.sqrt(
|
||||
maxCanvasPixels / visiblePixels
|
||||
);
|
||||
const maxOverflowScale = (maxDetailToVisibleLinearRatio - 1) / 2;
|
||||
let overflowScale = Math.min(1, maxOverflowScale);
|
||||
if (overflowScale < 0) {
|
||||
overflowScale = 0;
|
||||
// In this case, we render a detail view that is exactly as big as the
|
||||
// visible area, but we ignore the .maxCanvasPixels limit.
|
||||
// TODO: We should probably instead give up and not render the detail view
|
||||
// in this case. It's quite rare to hit it though, because usually
|
||||
// .maxCanvasPixels will at least have enough pixels to cover the visible
|
||||
// screen.
|
||||
}
|
||||
|
||||
const overflowWidth = visibleWidth * overflowScale;
|
||||
const overflowHeight = visibleHeight * overflowScale;
|
||||
|
||||
const minX = Math.max(0, visibleArea.minX - overflowWidth);
|
||||
const maxX = Math.min(viewport.width, visibleArea.maxX + overflowWidth);
|
||||
const minY = Math.max(0, visibleArea.minY - overflowHeight);
|
||||
const maxY = Math.min(viewport.height, visibleArea.maxY + overflowHeight);
|
||||
const width = maxX - minX;
|
||||
const height = maxY - minY;
|
||||
|
||||
this.#detailArea = { minX, minY, width, height, scale: viewport.scale };
|
||||
|
||||
this.reset({ keepCanvas: true });
|
||||
}
|
||||
|
||||
async draw() {
|
||||
// The PDFPageView might have already dropped this PDFPageDetailView. In
|
||||
// that case, simply do nothing.
|
||||
if (this.pageView.detailView !== this) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If there is already the lower resolution canvas behind,
|
||||
// we don't show the new one until when it's fully ready.
|
||||
const hideUntilComplete =
|
||||
this.pageView.renderingState === RenderingStates.FINISHED ||
|
||||
this.renderingState === RenderingStates.FINISHED;
|
||||
|
||||
if (this.renderingState !== RenderingStates.INITIAL) {
|
||||
console.error("Must be in new state before drawing");
|
||||
this.reset(); // Ensure that we reset all state to prevent issues.
|
||||
}
|
||||
const { div, pdfPage, viewport } = this.pageView;
|
||||
|
||||
if (!pdfPage) {
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
throw new Error("pdfPage is not loaded");
|
||||
}
|
||||
|
||||
this.renderingState = RenderingStates.RUNNING;
|
||||
|
||||
const canvasWrapper = this.pageView._ensureCanvasWrapper();
|
||||
|
||||
const { canvas, prevCanvas, ctx } = this._createCanvas(newCanvas => {
|
||||
// If there is already the background canvas, inject this new canvas
|
||||
// after it. We cannot simply use .append because all canvases must
|
||||
// be before the SVG elements used for drawings.
|
||||
if (canvasWrapper.firstElementChild?.tagName === "CANVAS") {
|
||||
canvasWrapper.firstElementChild.after(newCanvas);
|
||||
} else {
|
||||
canvasWrapper.prepend(newCanvas);
|
||||
}
|
||||
}, hideUntilComplete);
|
||||
canvas.setAttribute("aria-hidden", "true");
|
||||
|
||||
const { width, height } = viewport;
|
||||
|
||||
const area = this.#detailArea;
|
||||
|
||||
const { devicePixelRatio = 1 } = window;
|
||||
const transform = [
|
||||
devicePixelRatio,
|
||||
0,
|
||||
0,
|
||||
devicePixelRatio,
|
||||
-area.minX * devicePixelRatio,
|
||||
-area.minY * devicePixelRatio,
|
||||
];
|
||||
|
||||
canvas.width = area.width * devicePixelRatio;
|
||||
canvas.height = area.height * devicePixelRatio;
|
||||
const { style } = canvas;
|
||||
style.width = `${(area.width * 100) / width}%`;
|
||||
style.height = `${(area.height * 100) / height}%`;
|
||||
style.top = `${(area.minY * 100) / height}%`;
|
||||
style.left = `${(area.minX * 100) / width}%`;
|
||||
|
||||
const renderingPromise = this._drawCanvas(
|
||||
this.pageView._getRenderingContext(ctx, transform),
|
||||
() => {
|
||||
// If the rendering is cancelled, keep the old canvas visible.
|
||||
this.canvas?.remove();
|
||||
this.canvas = prevCanvas;
|
||||
},
|
||||
() => {
|
||||
this.dispatchPageRendered(
|
||||
/* cssTransform */ false,
|
||||
/* isDetailView */ true
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
div.setAttribute("data-loaded", true);
|
||||
|
||||
this.dispatchPageRender();
|
||||
|
||||
return renderingPromise;
|
||||
}
|
||||
}
|
||||
|
||||
export { PDFPageDetailView };
|
|
@ -46,6 +46,7 @@ import { Autolinker } from "./autolinker.js";
|
|||
import { BasePDFPageView } from "./base_pdf_page_view.js";
|
||||
import { DrawLayerBuilder } from "./draw_layer_builder.js";
|
||||
import { GenericL10n } from "web-null_l10n";
|
||||
import { PDFPageDetailView } from "./pdf_page_detail_view.js";
|
||||
import { SimpleLinkService } from "./pdf_link_service.js";
|
||||
import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js";
|
||||
import { TextAccessibilityManager } from "./text_accessibility.js";
|
||||
|
@ -77,6 +78,12 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
||||
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
|
||||
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
|
||||
* pages would need a canvas that is larger than `maxCanvasPixels`, it will
|
||||
* draw a second canvas on top of the CSS-zoomed one, that only renders the
|
||||
* part of the page that is close to the viewport. The default value is
|
||||
* `true`.
|
||||
|
||||
* @property {Object} [pageColors] - Overwrites background and foreground colors
|
||||
* with user defined ones in order to improve readability in high contrast
|
||||
* mode.
|
||||
|
@ -129,6 +136,8 @@ class PDFPageView extends BasePDFPageView {
|
|||
|
||||
#layerProperties = null;
|
||||
|
||||
#needsRestrictedScaling = false;
|
||||
|
||||
#originalViewport = null;
|
||||
|
||||
#previousRotation = null;
|
||||
|
@ -173,6 +182,7 @@ class PDFPageView extends BasePDFPageView {
|
|||
this.#annotationMode =
|
||||
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||
this.maxCanvasPixels =
|
||||
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
||||
this.#enableAutoLinking = options.enableAutoLinking || false;
|
||||
|
@ -196,6 +206,8 @@ class PDFPageView extends BasePDFPageView {
|
|||
this.structTreeLayer = null;
|
||||
this.drawLayer = null;
|
||||
|
||||
this.detailView = null;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.className = "page";
|
||||
div.setAttribute("data-page-number", this.id);
|
||||
|
@ -519,6 +531,7 @@ class PDFPageView extends BasePDFPageView {
|
|||
keepXfaLayer = false,
|
||||
keepTextLayer = false,
|
||||
keepCanvasWrapper = false,
|
||||
preserveDetailViewState = false,
|
||||
} = {}) {
|
||||
this.cancelRendering({
|
||||
keepAnnotationLayer,
|
||||
|
@ -578,6 +591,17 @@ class PDFPageView extends BasePDFPageView {
|
|||
this.#canvasWrapper = null;
|
||||
this._resetCanvas();
|
||||
}
|
||||
|
||||
if (!preserveDetailViewState) {
|
||||
this.detailView?.reset({ keepCanvas: keepCanvasWrapper });
|
||||
|
||||
// If we are keeping the canvas around we must also keep the `detailView`
|
||||
// object, so that next time we need a detail view we'll update the
|
||||
// existing canvas rather than creating a new one.
|
||||
if (!keepCanvasWrapper) {
|
||||
this.detailView = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleEditingMode(isEditing) {
|
||||
|
@ -598,6 +622,22 @@ class PDFPageView extends BasePDFPageView {
|
|||
});
|
||||
}
|
||||
|
||||
updateVisibleArea(visibleArea) {
|
||||
if (this.enableDetailCanvas) {
|
||||
if (
|
||||
this.#needsRestrictedScaling &&
|
||||
this.maxCanvasPixels > 0 &&
|
||||
visibleArea
|
||||
) {
|
||||
this.detailView ??= new PDFPageDetailView({ pageView: this });
|
||||
this.detailView.update({ visibleArea });
|
||||
} else if (this.detailView) {
|
||||
this.detailView.reset();
|
||||
this.detailView = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFPageViewUpdateParameters
|
||||
* @property {number} [scale] The new scale, if specified.
|
||||
|
@ -653,22 +693,11 @@ class PDFPageView extends BasePDFPageView {
|
|||
this._container?.style.setProperty("--scale-factor", this.viewport.scale);
|
||||
}
|
||||
|
||||
this.#computeScale();
|
||||
|
||||
if (this.canvas) {
|
||||
let onlyCssZoom = false;
|
||||
if (this.#hasRestrictedScaling) {
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
||||
this.maxCanvasPixels === 0
|
||||
) {
|
||||
onlyCssZoom = true;
|
||||
} else if (this.maxCanvasPixels > 0) {
|
||||
const { width, height } = this.viewport;
|
||||
const { sx, sy } = this.outputScale;
|
||||
onlyCssZoom =
|
||||
((Math.floor(width) * sx) | 0) * ((Math.floor(height) * sy) | 0) >
|
||||
this.maxCanvasPixels;
|
||||
}
|
||||
}
|
||||
const onlyCssZoom =
|
||||
this.#hasRestrictedScaling && this.#needsRestrictedScaling;
|
||||
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
|
||||
|
||||
if (postponeDrawing || onlyCssZoom) {
|
||||
|
@ -705,7 +734,12 @@ class PDFPageView extends BasePDFPageView {
|
|||
// The "pagerendered"-event will be dispatched once the actual
|
||||
// rendering is done, hence don't dispatch it here as well.
|
||||
if (!postponeDrawing) {
|
||||
this.dispatchPageRendered(true);
|
||||
this.detailView?.update({ underlyingViewUpdated: true });
|
||||
|
||||
this.dispatchPageRendered(
|
||||
/* cssTransform */ true,
|
||||
/* isDetailView */ false
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -717,7 +751,38 @@ class PDFPageView extends BasePDFPageView {
|
|||
keepXfaLayer: true,
|
||||
keepTextLayer: true,
|
||||
keepCanvasWrapper: true,
|
||||
// It will be reset by the .update call below
|
||||
preserveDetailViewState: true,
|
||||
});
|
||||
|
||||
this.detailView?.update({ underlyingViewUpdated: true });
|
||||
}
|
||||
|
||||
#computeScale() {
|
||||
const { width, height } = this.viewport;
|
||||
const outputScale = (this.outputScale = new OutputScale());
|
||||
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
||||
this.maxCanvasPixels === 0
|
||||
) {
|
||||
const invScale = 1 / this.scale;
|
||||
// Use a scale that makes the canvas have the originally intended size
|
||||
// of the page.
|
||||
outputScale.sx *= invScale;
|
||||
outputScale.sy *= invScale;
|
||||
this.#needsRestrictedScaling = true;
|
||||
} else if (this.maxCanvasPixels > 0) {
|
||||
const pixelsInViewport = width * height;
|
||||
const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
|
||||
if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
|
||||
outputScale.sx = maxScale;
|
||||
outputScale.sy = maxScale;
|
||||
this.#needsRestrictedScaling = true;
|
||||
} else {
|
||||
this.#needsRestrictedScaling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -830,12 +895,37 @@ class PDFPageView extends BasePDFPageView {
|
|||
return this.viewport.convertToPdfPoint(x, y);
|
||||
}
|
||||
|
||||
// Wrap the canvas so that if it has a CSS transform for high DPI the
|
||||
// overflow will be hidden in Firefox.
|
||||
_ensureCanvasWrapper() {
|
||||
let canvasWrapper = this.#canvasWrapper;
|
||||
if (!canvasWrapper) {
|
||||
canvasWrapper = this.#canvasWrapper = document.createElement("div");
|
||||
canvasWrapper.classList.add("canvasWrapper");
|
||||
this.#addLayer(canvasWrapper, "canvasWrapper");
|
||||
}
|
||||
return canvasWrapper;
|
||||
}
|
||||
|
||||
_getRenderingContext(canvasContext, transform) {
|
||||
return {
|
||||
canvasContext,
|
||||
transform,
|
||||
viewport: this.viewport,
|
||||
annotationMode: this.#annotationMode,
|
||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
pageColors: this.pageColors,
|
||||
isEditing: this.#isEditing,
|
||||
};
|
||||
}
|
||||
|
||||
async draw() {
|
||||
if (this.renderingState !== RenderingStates.INITIAL) {
|
||||
console.error("Must be in new state before drawing");
|
||||
this.reset(); // Ensure that we reset all state to prevent issues.
|
||||
}
|
||||
const { div, l10n, pageColors, pdfPage, viewport } = this;
|
||||
const { div, l10n, pdfPage, viewport } = this;
|
||||
|
||||
if (!pdfPage) {
|
||||
this.renderingState = RenderingStates.FINISHED;
|
||||
|
@ -844,14 +934,7 @@ class PDFPageView extends BasePDFPageView {
|
|||
|
||||
this.renderingState = RenderingStates.RUNNING;
|
||||
|
||||
// Wrap the canvas so that if it has a CSS transform for high DPI the
|
||||
// overflow will be hidden in Firefox.
|
||||
let canvasWrapper = this.#canvasWrapper;
|
||||
if (!canvasWrapper) {
|
||||
canvasWrapper = this.#canvasWrapper = document.createElement("div");
|
||||
canvasWrapper.classList.add("canvasWrapper");
|
||||
this.#addLayer(canvasWrapper, "canvasWrapper");
|
||||
}
|
||||
const canvasWrapper = this._ensureCanvasWrapper();
|
||||
|
||||
if (
|
||||
!this.textLayer &&
|
||||
|
@ -918,29 +1001,12 @@ class PDFPageView extends BasePDFPageView {
|
|||
});
|
||||
canvas.setAttribute("role", "presentation");
|
||||
|
||||
const outputScale = (this.outputScale = new OutputScale());
|
||||
|
||||
if (
|
||||
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
||||
this.maxCanvasPixels === 0
|
||||
) {
|
||||
const invScale = 1 / this.scale;
|
||||
// Use a scale that makes the canvas have the originally intended size
|
||||
// of the page.
|
||||
outputScale.sx *= invScale;
|
||||
outputScale.sy *= invScale;
|
||||
this.#hasRestrictedScaling = true;
|
||||
} else if (this.maxCanvasPixels > 0) {
|
||||
const pixelsInViewport = width * height;
|
||||
const maxScale = Math.sqrt(this.maxCanvasPixels / pixelsInViewport);
|
||||
if (outputScale.sx > maxScale || outputScale.sy > maxScale) {
|
||||
outputScale.sx = maxScale;
|
||||
outputScale.sy = maxScale;
|
||||
this.#hasRestrictedScaling = true;
|
||||
} else {
|
||||
this.#hasRestrictedScaling = false;
|
||||
}
|
||||
if (!this.outputScale) {
|
||||
this.#computeScale();
|
||||
}
|
||||
const { outputScale } = this;
|
||||
this.#hasRestrictedScaling = this.#needsRestrictedScaling;
|
||||
|
||||
const sfx = approximateFraction(outputScale.sx);
|
||||
const sfy = approximateFraction(outputScale.sy);
|
||||
|
||||
|
@ -970,26 +1036,22 @@ class PDFPageView extends BasePDFPageView {
|
|||
const transform = outputScale.scaled
|
||||
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
||||
: null;
|
||||
const renderContext = {
|
||||
canvasContext: ctx,
|
||||
transform,
|
||||
viewport,
|
||||
annotationMode: this.#annotationMode,
|
||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
pageColors,
|
||||
isEditing: this.#isEditing,
|
||||
};
|
||||
const resultPromise = this._drawCanvas(
|
||||
renderContext,
|
||||
prevCanvas,
|
||||
this._getRenderingContext(ctx, transform),
|
||||
() => {
|
||||
prevCanvas?.remove();
|
||||
this._resetCanvas();
|
||||
},
|
||||
renderTask => {
|
||||
// Ensure that the thumbnails won't become partially (or fully) blank,
|
||||
// for documents that contain interactive form elements.
|
||||
this.#useThumbnailCanvas.regularAnnotations =
|
||||
!renderTask.separateAnnots;
|
||||
|
||||
this.dispatchPageRendered(false);
|
||||
this.dispatchPageRendered(
|
||||
/* cssTransform */ false,
|
||||
/* isDetailView */ false
|
||||
);
|
||||
}
|
||||
).then(async () => {
|
||||
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
||||
|
|
|
@ -110,7 +110,8 @@ class PDFRenderingQueue {
|
|||
*
|
||||
* Priority:
|
||||
* 1. visible pages
|
||||
* 2. if last scrolled down, the page after the visible pages, or
|
||||
* 2. zoomed-in partial views of visible pages
|
||||
* 3. if last scrolled down, the page after the visible pages, or
|
||||
* if last scrolled up, the page before the visible pages
|
||||
*/
|
||||
const visibleViews = visible.views,
|
||||
|
@ -125,6 +126,14 @@ class PDFRenderingQueue {
|
|||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < numVisible; i++) {
|
||||
const { detailView } = visibleViews[i].view;
|
||||
if (detailView && !this.isViewFinished(detailView)) {
|
||||
return detailView;
|
||||
}
|
||||
}
|
||||
|
||||
const firstId = visible.first.id,
|
||||
lastId = visible.last.id;
|
||||
|
||||
|
|
|
@ -118,6 +118,11 @@ function isValidAnnotationEditorMode(mode) {
|
|||
* @property {number} [maxCanvasPixels] - The maximum supported canvas size in
|
||||
* total pixels, i.e. width * height. Use `-1` for no limit, or `0` for
|
||||
* CSS-only zooming. The default value is 4096 * 8192 (32 mega-pixels).
|
||||
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
|
||||
* pages would need a canvas that is larger than `maxCanvasPixels`, it will
|
||||
* draw a second canvas on top of the CSS-zoomed one, that only renders the
|
||||
* part of the page that is close to the viewport. The default value is
|
||||
* `true`.
|
||||
* @property {IL10n} [l10n] - Localization service.
|
||||
* @property {boolean} [enablePermissions] - Enables PDF document permissions,
|
||||
* when they exist. The default value is `false`.
|
||||
|
@ -319,6 +324,7 @@ class PDFViewer {
|
|||
this.removePageBorders = options.removePageBorders || false;
|
||||
}
|
||||
this.maxCanvasPixels = options.maxCanvasPixels;
|
||||
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
this.l10n ||= new GenericL10n();
|
||||
|
@ -873,7 +879,7 @@ class PDFViewer {
|
|||
eventBus._on("pagerender", onBeforeDraw, { signal });
|
||||
|
||||
const onAfterDraw = evt => {
|
||||
if (evt.cssTransform) {
|
||||
if (evt.cssTransform || evt.isDetailView) {
|
||||
return;
|
||||
}
|
||||
this._onePageRenderedCapability.resolve({ timestamp: evt.timestamp });
|
||||
|
@ -993,6 +999,7 @@ class PDFViewer {
|
|||
annotationMode,
|
||||
imageResourcesPath: this.imageResourcesPath,
|
||||
maxCanvasPixels: this.maxCanvasPixels,
|
||||
enableDetailCanvas: this.enableDetailCanvas,
|
||||
pageColors,
|
||||
l10n: this.l10n,
|
||||
layerProperties: this._layerProperties,
|
||||
|
@ -1672,6 +1679,15 @@ class PDFViewer {
|
|||
const newCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * numVisiblePages + 1);
|
||||
this.#buffer.resize(newCacheSize, visible.ids);
|
||||
|
||||
for (const { view, visibleArea } of visiblePages) {
|
||||
view.updateVisibleArea(visibleArea);
|
||||
}
|
||||
for (const view of this.#buffer) {
|
||||
if (!visible.ids.has(view.id)) {
|
||||
view.updateVisibleArea(null);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderingQueue.renderHighestPriority(visible);
|
||||
|
||||
const isSimpleLayout =
|
||||
|
|
|
@ -554,10 +554,11 @@ function getVisibleElements({
|
|||
continue;
|
||||
}
|
||||
|
||||
const hiddenHeight =
|
||||
Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
|
||||
const hiddenWidth =
|
||||
Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
|
||||
const minY = Math.max(0, top - currentHeight);
|
||||
const minX = Math.max(0, left - currentWidth);
|
||||
|
||||
const hiddenHeight = minY + Math.max(0, viewBottom - bottom);
|
||||
const hiddenWidth = minX + Math.max(0, viewRight - right);
|
||||
|
||||
const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
|
||||
fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
|
||||
|
@ -567,6 +568,18 @@ function getVisibleElements({
|
|||
id: view.id,
|
||||
x: currentWidth,
|
||||
y: currentHeight,
|
||||
visibleArea:
|
||||
// We only specify which part of the page is visible when it's not
|
||||
// the full page, as there is no point in handling a partial page
|
||||
// rendering otherwise.
|
||||
percent === 100
|
||||
? null
|
||||
: {
|
||||
minX,
|
||||
minY,
|
||||
maxX: Math.min(viewRight, right) - currentWidth,
|
||||
maxY: Math.min(viewBottom, bottom) - currentHeight,
|
||||
},
|
||||
view,
|
||||
percent,
|
||||
widthPercent: (fractionWidth * 100) | 0,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue