mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-18 14:18:23 +02:00
It's already enabled by default in Firefox, and since there's no open issues regarding auto-linking I suppose that we can attempt to enable it unconditionally.
1150 lines
34 KiB
JavaScript
1150 lines
34 KiB
JavaScript
/* 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.
|
|
*/
|
|
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
|
/** @typedef {import("./interfaces").IL10n} IL10n */
|
|
/** @typedef {import("./interfaces").IRenderableView} IRenderableView */
|
|
// eslint-disable-next-line max-len
|
|
/** @typedef {import("./pdf_rendering_queue").PDFRenderingQueue} PDFRenderingQueue */
|
|
|
|
import {
|
|
AbortException,
|
|
AnnotationMode,
|
|
OutputScale,
|
|
PixelsPerInch,
|
|
setLayerDimensions,
|
|
shadow,
|
|
} from "pdfjs-lib";
|
|
import {
|
|
approximateFraction,
|
|
calcRound,
|
|
DEFAULT_SCALE,
|
|
floorToDivide,
|
|
RenderingStates,
|
|
TextLayerMode,
|
|
} from "./ui_utils.js";
|
|
import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js";
|
|
import { AnnotationLayerBuilder } from "./annotation_layer_builder.js";
|
|
import { AppOptions } from "./app_options.js";
|
|
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";
|
|
import { TextHighlighter } from "./text_highlighter.js";
|
|
import { TextLayerBuilder } from "./text_layer_builder.js";
|
|
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
|
|
|
/**
|
|
* @typedef {Object} PDFPageViewOptions
|
|
* @property {HTMLDivElement} [container] - The viewer element.
|
|
* @property {EventBus} eventBus - The application event bus.
|
|
* @property {number} id - The page unique ID (normally its number).
|
|
* @property {number} [scale] - The page scale display.
|
|
* @property {PageViewport} defaultViewport - The page viewport.
|
|
* @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise] -
|
|
* A promise that is resolved with an {@link OptionalContentConfig} instance.
|
|
* The default value is `null`.
|
|
* @property {PDFRenderingQueue} [renderingQueue] - The rendering queue object.
|
|
* @property {number} [textLayerMode] - Controls if the text layer used for
|
|
* selection and searching is created. The constants from {TextLayerMode}
|
|
* should be used. The default value is `TextLayerMode.ENABLE`.
|
|
* @property {number} [annotationMode] - Controls if the annotation layer is
|
|
* created, and if interactive form elements or `AnnotationStorage`-data are
|
|
* being rendered. The constants from {@link AnnotationMode} should be used;
|
|
* see also {@link RenderParameters} and {@link GetOperatorListParameters}.
|
|
* The default value is `AnnotationMode.ENABLE_FORMS`.
|
|
* @property {string} [imageResourcesPath] - Path for image resources, mainly
|
|
* for annotation icons. Include trailing slash.
|
|
* @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 {number} [maxCanvasDim] - The maximum supported canvas dimension,
|
|
* in either width or height. Use `-1` for no limit.
|
|
* The default value is 32767.
|
|
* @property {boolean} [enableDetailCanvas] - When enabled, if the rendered
|
|
* pages would need a canvas that is larger than `maxCanvasPixels` or
|
|
* `maxCanvasDim`, 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.
|
|
* @property {IL10n} [l10n] - Localization service.
|
|
* @property {Object} [layerProperties] - The object that is used to lookup
|
|
* the necessary layer-properties.
|
|
* @property {boolean} [enableHWA] - Enables hardware acceleration for
|
|
* rendering. The default value is `false`.
|
|
* @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from
|
|
* text that look like URLs. The default value is `true`.
|
|
*/
|
|
|
|
const DEFAULT_LAYER_PROPERTIES =
|
|
typeof PDFJSDev === "undefined" || !PDFJSDev.test("COMPONENTS")
|
|
? null
|
|
: {
|
|
annotationEditorUIManager: null,
|
|
annotationStorage: null,
|
|
downloadManager: null,
|
|
enableScripting: false,
|
|
fieldObjectsPromise: null,
|
|
findController: null,
|
|
hasJSActionsPromise: null,
|
|
get linkService() {
|
|
return new SimpleLinkService();
|
|
},
|
|
};
|
|
|
|
const LAYERS_ORDER = new Map([
|
|
["canvasWrapper", 0],
|
|
["textLayer", 1],
|
|
["annotationLayer", 2],
|
|
["annotationEditorLayer", 3],
|
|
["xfaLayer", 3],
|
|
]);
|
|
|
|
/**
|
|
* @implements {IRenderableView}
|
|
*/
|
|
class PDFPageView extends BasePDFPageView {
|
|
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
|
|
|
#canvasWrapper = null;
|
|
|
|
#enableAutoLinking = true;
|
|
|
|
#hasRestrictedScaling = false;
|
|
|
|
#isEditing = false;
|
|
|
|
#layerProperties = null;
|
|
|
|
#needsRestrictedScaling = false;
|
|
|
|
#originalViewport = null;
|
|
|
|
#previousRotation = null;
|
|
|
|
#scaleRoundX = 1;
|
|
|
|
#scaleRoundY = 1;
|
|
|
|
#textLayerMode = TextLayerMode.ENABLE;
|
|
|
|
#userUnit = 1;
|
|
|
|
#useThumbnailCanvas = {
|
|
directDrawing: true,
|
|
initialOptionalContent: true,
|
|
regularAnnotations: true,
|
|
};
|
|
|
|
#layers = [null, null, null, null];
|
|
|
|
/**
|
|
* @param {PDFPageViewOptions} options
|
|
*/
|
|
constructor(options) {
|
|
super(options);
|
|
|
|
const container = options.container;
|
|
const defaultViewport = options.defaultViewport;
|
|
|
|
this.renderingId = "page" + this.id;
|
|
this.#layerProperties = options.layerProperties || DEFAULT_LAYER_PROPERTIES;
|
|
|
|
this.pdfPage = null;
|
|
this.pageLabel = null;
|
|
this.rotation = 0;
|
|
this.scale = options.scale || DEFAULT_SCALE;
|
|
this.viewport = defaultViewport;
|
|
this.pdfPageRotate = defaultViewport.rotation;
|
|
this._optionalContentConfigPromise =
|
|
options.optionalContentConfigPromise || null;
|
|
this.#textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE;
|
|
this.#annotationMode =
|
|
options.annotationMode ?? AnnotationMode.ENABLE_FORMS;
|
|
this.imageResourcesPath = options.imageResourcesPath || "";
|
|
this.enableDetailCanvas = options.enableDetailCanvas ?? true;
|
|
this.maxCanvasPixels =
|
|
options.maxCanvasPixels ?? AppOptions.get("maxCanvasPixels");
|
|
this.maxCanvasDim = options.maxCanvasDim || AppOptions.get("maxCanvasDim");
|
|
this.#enableAutoLinking = options.enableAutoLinking !== false;
|
|
|
|
this.l10n = options.l10n;
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
this.l10n ||= new GenericL10n();
|
|
}
|
|
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
|
this._isStandalone = !this.renderingQueue?.hasViewer();
|
|
this._container = container;
|
|
}
|
|
|
|
this._annotationCanvasMap = null;
|
|
|
|
this.annotationLayer = null;
|
|
this.annotationEditorLayer = null;
|
|
this.textLayer = null;
|
|
this.xfaLayer = null;
|
|
this.structTreeLayer = null;
|
|
this.drawLayer = null;
|
|
|
|
this.detailView = null;
|
|
|
|
const div = document.createElement("div");
|
|
div.className = "page";
|
|
div.setAttribute("data-page-number", this.id);
|
|
div.setAttribute("role", "region");
|
|
div.setAttribute("data-l10n-id", "pdfjs-page-landmark");
|
|
div.setAttribute("data-l10n-args", JSON.stringify({ page: this.id }));
|
|
this.div = div;
|
|
|
|
this.#setDimensions();
|
|
container?.append(div);
|
|
|
|
if (
|
|
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
|
this._isStandalone
|
|
) {
|
|
// Ensure that the various layers always get the correct initial size,
|
|
// see issue 15795.
|
|
container?.style.setProperty(
|
|
"--scale-factor",
|
|
this.scale * PixelsPerInch.PDF_TO_CSS_UNITS
|
|
);
|
|
|
|
if (this.pageColors?.background) {
|
|
container?.style.setProperty(
|
|
"--page-bg-color",
|
|
this.pageColors.background
|
|
);
|
|
}
|
|
|
|
const { optionalContentConfigPromise } = options;
|
|
if (optionalContentConfigPromise) {
|
|
// Ensure that the thumbnails always display the *initial* document
|
|
// state, for documents with optional content.
|
|
optionalContentConfigPromise.then(optionalContentConfig => {
|
|
if (
|
|
optionalContentConfigPromise !== this._optionalContentConfigPromise
|
|
) {
|
|
return;
|
|
}
|
|
this.#useThumbnailCanvas.initialOptionalContent =
|
|
optionalContentConfig.hasInitialVisibility;
|
|
});
|
|
}
|
|
|
|
// Ensure that Fluent is connected in e.g. the COMPONENTS build.
|
|
if (!options.l10n) {
|
|
this.l10n.translate(this.div);
|
|
}
|
|
}
|
|
}
|
|
|
|
#addLayer(div, name) {
|
|
const pos = LAYERS_ORDER.get(name);
|
|
const oldDiv = this.#layers[pos];
|
|
this.#layers[pos] = div;
|
|
if (oldDiv) {
|
|
oldDiv.replaceWith(div);
|
|
return;
|
|
}
|
|
for (let i = pos - 1; i >= 0; i--) {
|
|
const layer = this.#layers[i];
|
|
if (layer) {
|
|
layer.after(div);
|
|
return;
|
|
}
|
|
}
|
|
this.div.prepend(div);
|
|
}
|
|
|
|
#setDimensions() {
|
|
const { div, viewport } = this;
|
|
|
|
if (viewport.userUnit !== this.#userUnit) {
|
|
if (viewport.userUnit !== 1) {
|
|
div.style.setProperty("--user-unit", viewport.userUnit);
|
|
} else {
|
|
div.style.removeProperty("--user-unit");
|
|
}
|
|
this.#userUnit = viewport.userUnit;
|
|
}
|
|
if (this.pdfPage) {
|
|
if (this.#previousRotation === viewport.rotation) {
|
|
return;
|
|
}
|
|
this.#previousRotation = viewport.rotation;
|
|
}
|
|
|
|
setLayerDimensions(
|
|
div,
|
|
viewport,
|
|
/* mustFlip = */ true,
|
|
/* mustRotate = */ false
|
|
);
|
|
}
|
|
|
|
setPdfPage(pdfPage) {
|
|
if (
|
|
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
|
this._isStandalone &&
|
|
(this.pageColors?.foreground === "CanvasText" ||
|
|
this.pageColors?.background === "Canvas")
|
|
) {
|
|
this._container?.style.setProperty(
|
|
"--hcm-highlight-filter",
|
|
pdfPage.filterFactory.addHighlightHCMFilter(
|
|
"highlight",
|
|
"CanvasText",
|
|
"Canvas",
|
|
"HighlightText",
|
|
"Highlight"
|
|
)
|
|
);
|
|
this._container?.style.setProperty(
|
|
"--hcm-highlight-selected-filter",
|
|
pdfPage.filterFactory.addHighlightHCMFilter(
|
|
"highlight_selected",
|
|
"CanvasText",
|
|
"Canvas",
|
|
"HighlightText",
|
|
"Highlight"
|
|
)
|
|
);
|
|
}
|
|
this.pdfPage = pdfPage;
|
|
this.pdfPageRotate = pdfPage.rotate;
|
|
|
|
const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
|
|
this.viewport = pdfPage.getViewport({
|
|
scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
|
|
rotation: totalRotation,
|
|
});
|
|
this.#setDimensions();
|
|
this.reset();
|
|
}
|
|
|
|
destroy() {
|
|
this.reset();
|
|
this.pdfPage?.cleanup();
|
|
}
|
|
|
|
hasEditableAnnotations() {
|
|
return !!this.annotationLayer?.hasEditableAnnotations();
|
|
}
|
|
|
|
get _textHighlighter() {
|
|
return shadow(
|
|
this,
|
|
"_textHighlighter",
|
|
new TextHighlighter({
|
|
pageIndex: this.id - 1,
|
|
eventBus: this.eventBus,
|
|
findController: this.#layerProperties.findController,
|
|
})
|
|
);
|
|
}
|
|
|
|
#dispatchLayerRendered(name, error) {
|
|
this.eventBus.dispatch(name, {
|
|
source: this,
|
|
pageNumber: this.id,
|
|
error,
|
|
});
|
|
}
|
|
|
|
async #renderAnnotationLayer() {
|
|
let error = null;
|
|
try {
|
|
await this.annotationLayer.render({
|
|
viewport: this.viewport,
|
|
intent: "display",
|
|
structTreeLayer: this.structTreeLayer,
|
|
});
|
|
} catch (ex) {
|
|
console.error("#renderAnnotationLayer:", ex);
|
|
error = ex;
|
|
} finally {
|
|
this.#dispatchLayerRendered("annotationlayerrendered", error);
|
|
}
|
|
}
|
|
|
|
async #renderAnnotationEditorLayer() {
|
|
let error = null;
|
|
try {
|
|
await this.annotationEditorLayer.render({
|
|
viewport: this.viewport,
|
|
intent: "display",
|
|
});
|
|
} catch (ex) {
|
|
console.error("#renderAnnotationEditorLayer:", ex);
|
|
error = ex;
|
|
} finally {
|
|
this.#dispatchLayerRendered("annotationeditorlayerrendered", error);
|
|
}
|
|
}
|
|
|
|
async #renderDrawLayer() {
|
|
try {
|
|
await this.drawLayer.render({
|
|
intent: "display",
|
|
});
|
|
} catch (ex) {
|
|
console.error("#renderDrawLayer:", ex);
|
|
}
|
|
}
|
|
|
|
async #renderXfaLayer() {
|
|
let error = null;
|
|
try {
|
|
const result = await this.xfaLayer.render({
|
|
viewport: this.viewport,
|
|
intent: "display",
|
|
});
|
|
if (result?.textDivs && this._textHighlighter) {
|
|
// Given that the following method fetches the text asynchronously we
|
|
// can invoke it *before* appending the xfaLayer to the DOM (below),
|
|
// since a pending search-highlight/scroll operation thus won't run
|
|
// until after the xfaLayer is available in the viewer.
|
|
this.#buildXfaTextContentItems(result.textDivs);
|
|
}
|
|
} catch (ex) {
|
|
console.error("#renderXfaLayer:", ex);
|
|
error = ex;
|
|
} finally {
|
|
if (this.xfaLayer?.div) {
|
|
// Pause translation when inserting the xfaLayer in the DOM.
|
|
this.l10n.pause();
|
|
this.#addLayer(this.xfaLayer.div, "xfaLayer");
|
|
this.l10n.resume();
|
|
}
|
|
this.#dispatchLayerRendered("xfalayerrendered", error);
|
|
}
|
|
}
|
|
|
|
async #renderTextLayer() {
|
|
if (!this.textLayer) {
|
|
return;
|
|
}
|
|
|
|
let error = null;
|
|
try {
|
|
await this.textLayer.render({
|
|
viewport: this.viewport,
|
|
});
|
|
} catch (ex) {
|
|
if (ex instanceof AbortException) {
|
|
return;
|
|
}
|
|
console.error("#renderTextLayer:", ex);
|
|
error = ex;
|
|
}
|
|
this.#dispatchLayerRendered("textlayerrendered", error);
|
|
|
|
this.#renderStructTreeLayer();
|
|
}
|
|
|
|
/**
|
|
* The structure tree is currently only supported when the text layer is
|
|
* enabled and a canvas is used for rendering.
|
|
*
|
|
* The structure tree must be generated after the text layer for the
|
|
* aria-owns to work.
|
|
*/
|
|
async #renderStructTreeLayer() {
|
|
if (!this.textLayer) {
|
|
return;
|
|
}
|
|
|
|
const treeDom = await this.structTreeLayer?.render();
|
|
if (treeDom) {
|
|
this.l10n.pause();
|
|
this.structTreeLayer?.addElementsToTextLayer();
|
|
if (this.canvas && treeDom.parentNode !== this.canvas) {
|
|
// Pause translation when inserting the structTree in the DOM.
|
|
this.canvas.append(treeDom);
|
|
}
|
|
this.l10n.resume();
|
|
}
|
|
this.structTreeLayer?.show();
|
|
}
|
|
|
|
async #buildXfaTextContentItems(textDivs) {
|
|
const text = await this.pdfPage.getTextContent();
|
|
const items = [];
|
|
for (const item of text.items) {
|
|
items.push(item.str);
|
|
}
|
|
this._textHighlighter.setTextMapping(textDivs, items);
|
|
this._textHighlighter.enable();
|
|
}
|
|
|
|
async #injectLinkAnnotations(textLayerPromise) {
|
|
let error = null;
|
|
try {
|
|
await textLayerPromise;
|
|
|
|
if (!this.annotationLayer) {
|
|
return; // Rendering was cancelled while the textLayerPromise resolved.
|
|
}
|
|
await this.annotationLayer.injectLinkAnnotations({
|
|
inferredLinks: Autolinker.processLinks(this),
|
|
viewport: this.viewport,
|
|
structTreeLayer: this.structTreeLayer,
|
|
});
|
|
} catch (ex) {
|
|
console.error("#injectLinkAnnotations:", ex);
|
|
error = ex;
|
|
}
|
|
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
|
this.#dispatchLayerRendered("linkannotationsadded", error);
|
|
}
|
|
}
|
|
|
|
_resetCanvas() {
|
|
super._resetCanvas();
|
|
this.#originalViewport = null;
|
|
}
|
|
|
|
reset({
|
|
keepAnnotationLayer = false,
|
|
keepAnnotationEditorLayer = false,
|
|
keepXfaLayer = false,
|
|
keepTextLayer = false,
|
|
keepCanvasWrapper = false,
|
|
preserveDetailViewState = false,
|
|
} = {}) {
|
|
this.cancelRendering({
|
|
keepAnnotationLayer,
|
|
keepAnnotationEditorLayer,
|
|
keepXfaLayer,
|
|
keepTextLayer,
|
|
});
|
|
this.renderingState = RenderingStates.INITIAL;
|
|
|
|
const div = this.div;
|
|
|
|
const childNodes = div.childNodes,
|
|
annotationLayerNode =
|
|
(keepAnnotationLayer && this.annotationLayer?.div) || null,
|
|
annotationEditorLayerNode =
|
|
(keepAnnotationEditorLayer && this.annotationEditorLayer?.div) || null,
|
|
xfaLayerNode = (keepXfaLayer && this.xfaLayer?.div) || null,
|
|
textLayerNode = (keepTextLayer && this.textLayer?.div) || null,
|
|
canvasWrapperNode = (keepCanvasWrapper && this.#canvasWrapper) || null;
|
|
for (let i = childNodes.length - 1; i >= 0; i--) {
|
|
const node = childNodes[i];
|
|
switch (node) {
|
|
case annotationLayerNode:
|
|
case annotationEditorLayerNode:
|
|
case xfaLayerNode:
|
|
case textLayerNode:
|
|
case canvasWrapperNode:
|
|
continue;
|
|
}
|
|
node.remove();
|
|
const layerIndex = this.#layers.indexOf(node);
|
|
if (layerIndex >= 0) {
|
|
this.#layers[layerIndex] = null;
|
|
}
|
|
}
|
|
div.removeAttribute("data-loaded");
|
|
|
|
if (annotationLayerNode) {
|
|
// Hide the annotation layer until all elements are resized
|
|
// so they are not displayed on the already resized page.
|
|
this.annotationLayer.hide();
|
|
}
|
|
if (annotationEditorLayerNode) {
|
|
this.annotationEditorLayer.hide();
|
|
}
|
|
if (xfaLayerNode) {
|
|
// Hide the XFA layer until all elements are resized
|
|
// so they are not displayed on the already resized page.
|
|
this.xfaLayer.hide();
|
|
}
|
|
if (textLayerNode) {
|
|
this.textLayer.hide();
|
|
}
|
|
this.structTreeLayer?.hide();
|
|
|
|
if (!keepCanvasWrapper && this.#canvasWrapper) {
|
|
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) {
|
|
// The page can be invisible, consequently there's no annotation layer and
|
|
// we can't know if there are editable annotations.
|
|
// So to avoid any issue when the page is rendered the #isEditing flag must
|
|
// be set.
|
|
this.#isEditing = isEditing;
|
|
if (!this.hasEditableAnnotations()) {
|
|
return;
|
|
}
|
|
this.reset({
|
|
keepAnnotationLayer: true,
|
|
keepAnnotationEditorLayer: true,
|
|
keepXfaLayer: true,
|
|
keepTextLayer: true,
|
|
keepCanvasWrapper: true,
|
|
});
|
|
}
|
|
|
|
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.
|
|
* @property {number} [rotation] The new rotation, if specified.
|
|
* @property {Promise<OptionalContentConfig>} [optionalContentConfigPromise]
|
|
* A promise that is resolved with an {@link OptionalContentConfig}
|
|
* instance. The default value is `null`.
|
|
* @property {number} [drawingDelay]
|
|
*/
|
|
|
|
/**
|
|
* Update e.g. the scale and/or rotation of the page.
|
|
* @param {PDFPageViewUpdateParameters} params
|
|
*/
|
|
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.
|
|
}
|
|
if (optionalContentConfigPromise instanceof Promise) {
|
|
this._optionalContentConfigPromise = optionalContentConfigPromise;
|
|
|
|
// Ensure that the thumbnails always display the *initial* document state,
|
|
// for documents with optional content.
|
|
optionalContentConfigPromise.then(optionalContentConfig => {
|
|
if (
|
|
optionalContentConfigPromise !== this._optionalContentConfigPromise
|
|
) {
|
|
return;
|
|
}
|
|
this.#useThumbnailCanvas.initialOptionalContent =
|
|
optionalContentConfig.hasInitialVisibility;
|
|
});
|
|
}
|
|
this.#useThumbnailCanvas.directDrawing = true;
|
|
|
|
const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
|
|
this.viewport = this.viewport.clone({
|
|
scale: this.scale * PixelsPerInch.PDF_TO_CSS_UNITS,
|
|
rotation: totalRotation,
|
|
});
|
|
this.#setDimensions();
|
|
|
|
if (
|
|
(typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
|
|
this._isStandalone
|
|
) {
|
|
this._container?.style.setProperty("--scale-factor", this.viewport.scale);
|
|
}
|
|
|
|
this.#computeScale();
|
|
|
|
if (this.canvas) {
|
|
const onlyCssZoom =
|
|
this.#hasRestrictedScaling && this.#needsRestrictedScaling;
|
|
const postponeDrawing = drawingDelay >= 0 && drawingDelay < 1000;
|
|
|
|
if (postponeDrawing || onlyCssZoom) {
|
|
if (
|
|
postponeDrawing &&
|
|
!onlyCssZoom &&
|
|
this.renderingState !== RenderingStates.FINISHED
|
|
) {
|
|
this.cancelRendering({
|
|
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;
|
|
// Ensure that the thumbnails won't become partially (or fully) blank,
|
|
// if the sidebar is opened before the actual rendering is done.
|
|
this.#useThumbnailCanvas.directDrawing = false;
|
|
}
|
|
|
|
this.cssTransform({
|
|
redrawAnnotationLayer: true,
|
|
redrawAnnotationEditorLayer: true,
|
|
redrawXfaLayer: true,
|
|
redrawTextLayer: !postponeDrawing,
|
|
hideTextLayer: postponeDrawing,
|
|
});
|
|
|
|
// The "pagerendered"-event will be dispatched once the actual
|
|
// rendering is done, hence don't dispatch it here as well.
|
|
if (!postponeDrawing) {
|
|
this.detailView?.update({ underlyingViewUpdated: true });
|
|
|
|
this.dispatchPageRendered(
|
|
/* cssTransform */ true,
|
|
/* isDetailView */ false
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
this.cssTransform({});
|
|
this.reset({
|
|
keepAnnotationLayer: true,
|
|
keepAnnotationEditorLayer: true,
|
|
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 {
|
|
this.#needsRestrictedScaling = outputScale.limitCanvas(
|
|
width,
|
|
height,
|
|
this.maxCanvasPixels,
|
|
this.maxCanvasDim
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* PLEASE NOTE: Most likely you want to use the `this.reset()` method,
|
|
* rather than calling this one directly.
|
|
*/
|
|
cancelRendering({
|
|
keepAnnotationLayer = false,
|
|
keepAnnotationEditorLayer = false,
|
|
keepXfaLayer = false,
|
|
keepTextLayer = false,
|
|
cancelExtraDelay = 0,
|
|
} = {}) {
|
|
super.cancelRendering({ cancelExtraDelay });
|
|
|
|
if (this.textLayer && (!keepTextLayer || !this.textLayer.div)) {
|
|
this.textLayer.cancel();
|
|
this.textLayer = null;
|
|
}
|
|
if (
|
|
this.annotationLayer &&
|
|
(!keepAnnotationLayer || !this.annotationLayer.div)
|
|
) {
|
|
this.annotationLayer.cancel();
|
|
this.annotationLayer = null;
|
|
this._annotationCanvasMap = null;
|
|
}
|
|
if (this.structTreeLayer && !this.textLayer) {
|
|
this.structTreeLayer = null;
|
|
}
|
|
if (
|
|
this.annotationEditorLayer &&
|
|
(!keepAnnotationEditorLayer || !this.annotationEditorLayer.div)
|
|
) {
|
|
if (this.drawLayer) {
|
|
this.drawLayer.cancel();
|
|
this.drawLayer = null;
|
|
}
|
|
this.annotationEditorLayer.cancel();
|
|
this.annotationEditorLayer = null;
|
|
}
|
|
if (this.xfaLayer && (!keepXfaLayer || !this.xfaLayer.div)) {
|
|
this.xfaLayer.cancel();
|
|
this.xfaLayer = null;
|
|
this._textHighlighter?.disable();
|
|
}
|
|
}
|
|
|
|
cssTransform({
|
|
redrawAnnotationLayer = false,
|
|
redrawAnnotationEditorLayer = false,
|
|
redrawXfaLayer = false,
|
|
redrawTextLayer = false,
|
|
hideTextLayer = false,
|
|
}) {
|
|
const { canvas } = this;
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const originalViewport = this.#originalViewport;
|
|
if (this.viewport !== originalViewport) {
|
|
// The canvas may have been originally rotated; rotate relative to that.
|
|
const relativeRotation =
|
|
(360 + this.viewport.rotation - originalViewport.rotation) % 360;
|
|
if (relativeRotation === 90 || relativeRotation === 270) {
|
|
const { width, height } = this.viewport;
|
|
// Scale x and y because of the rotation.
|
|
const scaleX = height / width;
|
|
const scaleY = width / height;
|
|
canvas.style.transform = `rotate(${relativeRotation}deg) scale(${scaleX},${scaleY})`;
|
|
} else {
|
|
canvas.style.transform =
|
|
relativeRotation === 0 ? "" : `rotate(${relativeRotation}deg)`;
|
|
}
|
|
}
|
|
|
|
if (redrawAnnotationLayer && this.annotationLayer) {
|
|
this.#renderAnnotationLayer();
|
|
}
|
|
if (redrawAnnotationEditorLayer && this.annotationEditorLayer) {
|
|
if (this.drawLayer) {
|
|
this.#renderDrawLayer();
|
|
}
|
|
this.#renderAnnotationEditorLayer();
|
|
}
|
|
if (redrawXfaLayer && this.xfaLayer) {
|
|
this.#renderXfaLayer();
|
|
}
|
|
|
|
if (this.textLayer) {
|
|
if (hideTextLayer) {
|
|
this.textLayer.hide();
|
|
this.structTreeLayer?.hide();
|
|
} else if (redrawTextLayer) {
|
|
this.#renderTextLayer();
|
|
}
|
|
}
|
|
}
|
|
|
|
get width() {
|
|
return this.viewport.width;
|
|
}
|
|
|
|
get height() {
|
|
return this.viewport.height;
|
|
}
|
|
|
|
getPagePoint(x, y) {
|
|
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, pdfPage, viewport } = this;
|
|
|
|
if (!pdfPage) {
|
|
this.renderingState = RenderingStates.FINISHED;
|
|
throw new Error("pdfPage is not loaded");
|
|
}
|
|
|
|
this.renderingState = RenderingStates.RUNNING;
|
|
|
|
const canvasWrapper = this._ensureCanvasWrapper();
|
|
|
|
if (
|
|
!this.textLayer &&
|
|
this.#textLayerMode !== TextLayerMode.DISABLE &&
|
|
!pdfPage.isPureXfa
|
|
) {
|
|
this._accessibilityManager ||= new TextAccessibilityManager();
|
|
|
|
this.textLayer = new TextLayerBuilder({
|
|
pdfPage,
|
|
highlighter: this._textHighlighter,
|
|
accessibilityManager: this._accessibilityManager,
|
|
enablePermissions:
|
|
this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS,
|
|
onAppend: textLayerDiv => {
|
|
// Pause translation when inserting the textLayer in the DOM.
|
|
this.l10n.pause();
|
|
this.#addLayer(textLayerDiv, "textLayer");
|
|
this.l10n.resume();
|
|
},
|
|
});
|
|
}
|
|
|
|
if (
|
|
!this.annotationLayer &&
|
|
this.#annotationMode !== AnnotationMode.DISABLE
|
|
) {
|
|
const {
|
|
annotationStorage,
|
|
annotationEditorUIManager,
|
|
downloadManager,
|
|
enableScripting,
|
|
fieldObjectsPromise,
|
|
hasJSActionsPromise,
|
|
linkService,
|
|
} = this.#layerProperties;
|
|
|
|
this._annotationCanvasMap ||= new Map();
|
|
this.annotationLayer = new AnnotationLayerBuilder({
|
|
pdfPage,
|
|
annotationStorage,
|
|
imageResourcesPath: this.imageResourcesPath,
|
|
renderForms: this.#annotationMode === AnnotationMode.ENABLE_FORMS,
|
|
linkService,
|
|
downloadManager,
|
|
enableScripting,
|
|
hasJSActionsPromise,
|
|
fieldObjectsPromise,
|
|
annotationCanvasMap: this._annotationCanvasMap,
|
|
accessibilityManager: this._accessibilityManager,
|
|
annotationEditorUIManager,
|
|
onAppend: annotationLayerDiv => {
|
|
this.#addLayer(annotationLayerDiv, "annotationLayer");
|
|
},
|
|
});
|
|
}
|
|
|
|
const { width, height } = viewport;
|
|
this.#originalViewport = viewport;
|
|
|
|
const { canvas, prevCanvas, ctx } = this._createCanvas(newCanvas => {
|
|
// Always inject the canvas as the first element in the wrapper.
|
|
canvasWrapper.prepend(newCanvas);
|
|
});
|
|
canvas.setAttribute("role", "presentation");
|
|
|
|
if (!this.outputScale) {
|
|
this.#computeScale();
|
|
}
|
|
const { outputScale } = this;
|
|
this.#hasRestrictedScaling = this.#needsRestrictedScaling;
|
|
|
|
const sfx = approximateFraction(outputScale.sx);
|
|
const sfy = approximateFraction(outputScale.sy);
|
|
|
|
const canvasWidth = (canvas.width = floorToDivide(
|
|
calcRound(width * outputScale.sx),
|
|
sfx[0]
|
|
));
|
|
const canvasHeight = (canvas.height = floorToDivide(
|
|
calcRound(height * outputScale.sy),
|
|
sfy[0]
|
|
));
|
|
const pageWidth = floorToDivide(calcRound(width), sfx[1]);
|
|
const pageHeight = floorToDivide(calcRound(height), sfy[1]);
|
|
outputScale.sx = canvasWidth / pageWidth;
|
|
outputScale.sy = canvasHeight / pageHeight;
|
|
|
|
if (this.#scaleRoundX !== sfx[1]) {
|
|
div.style.setProperty("--scale-round-x", `${sfx[1]}px`);
|
|
this.#scaleRoundX = sfx[1];
|
|
}
|
|
if (this.#scaleRoundY !== sfy[1]) {
|
|
div.style.setProperty("--scale-round-y", `${sfy[1]}px`);
|
|
this.#scaleRoundY = sfy[1];
|
|
}
|
|
|
|
// Rendering area
|
|
const transform = outputScale.scaled
|
|
? [outputScale.sx, 0, 0, outputScale.sy, 0, 0]
|
|
: null;
|
|
const resultPromise = this._drawCanvas(
|
|
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(
|
|
/* cssTransform */ false,
|
|
/* isDetailView */ false
|
|
);
|
|
}
|
|
).then(async () => {
|
|
this.structTreeLayer ||= new StructTreeLayerBuilder(
|
|
pdfPage,
|
|
viewport.rawDims
|
|
);
|
|
|
|
const textLayerPromise = this.#renderTextLayer();
|
|
|
|
if (this.annotationLayer) {
|
|
await this.#renderAnnotationLayer();
|
|
|
|
if (this.#enableAutoLinking && this.annotationLayer && this.textLayer) {
|
|
await this.#injectLinkAnnotations(textLayerPromise);
|
|
}
|
|
}
|
|
|
|
const { annotationEditorUIManager } = this.#layerProperties;
|
|
|
|
if (!annotationEditorUIManager) {
|
|
return;
|
|
}
|
|
this.drawLayer ||= new DrawLayerBuilder({
|
|
pageIndex: this.id,
|
|
});
|
|
await this.#renderDrawLayer();
|
|
this.drawLayer.setParent(canvasWrapper);
|
|
|
|
this.annotationEditorLayer ||= new AnnotationEditorLayerBuilder({
|
|
uiManager: annotationEditorUIManager,
|
|
pdfPage,
|
|
l10n,
|
|
structTreeLayer: this.structTreeLayer,
|
|
accessibilityManager: this._accessibilityManager,
|
|
annotationLayer: this.annotationLayer?.annotationLayer,
|
|
textLayer: this.textLayer,
|
|
drawLayer: this.drawLayer.getDrawLayer(),
|
|
onAppend: annotationEditorLayerDiv => {
|
|
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
|
},
|
|
});
|
|
this.#renderAnnotationEditorLayer();
|
|
});
|
|
|
|
if (pdfPage.isPureXfa) {
|
|
if (!this.xfaLayer) {
|
|
const { annotationStorage, linkService } = this.#layerProperties;
|
|
|
|
this.xfaLayer = new XfaLayerBuilder({
|
|
pdfPage,
|
|
annotationStorage,
|
|
linkService,
|
|
});
|
|
}
|
|
this.#renderXfaLayer();
|
|
}
|
|
|
|
div.setAttribute("data-loaded", true);
|
|
|
|
this.dispatchPageRender();
|
|
|
|
return resultPromise;
|
|
}
|
|
|
|
/**
|
|
* @param {string|null} label
|
|
*/
|
|
setPageLabel(label) {
|
|
this.pageLabel = typeof label === "string" ? label : null;
|
|
|
|
this.div.setAttribute(
|
|
"data-l10n-args",
|
|
JSON.stringify({ page: this.pageLabel ?? this.id })
|
|
);
|
|
|
|
if (this.pageLabel !== null) {
|
|
this.div.setAttribute("data-page-label", this.pageLabel);
|
|
} else {
|
|
this.div.removeAttribute("data-page-label");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For use by the `PDFThumbnailView.setImage`-method.
|
|
* @ignore
|
|
*/
|
|
get thumbnailCanvas() {
|
|
const { directDrawing, initialOptionalContent, regularAnnotations } =
|
|
this.#useThumbnailCanvas;
|
|
return directDrawing && initialOptionalContent && regularAnnotations
|
|
? this.canvas
|
|
: null;
|
|
}
|
|
}
|
|
|
|
export { PDFPageView };
|