diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 1755c691d..99cca1b41 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -9,7 +9,6 @@ "annotation_spec.js", "annotation_storage_spec.js", "api_spec.js", - "base_viewer_spec.js", "bidi_spec.js", "cff_parser_spec.js", "cmap_spec.js", @@ -35,6 +34,7 @@ "pdf_find_controller_spec.js", "pdf_find_utils_spec.js", "pdf_history_spec.js", + "pdf_viewer_spec.js", "primitives_spec.js", "stream_spec.js", "struct_tree_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 89d8d474a..0997edf51 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -54,7 +54,6 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/api_spec.js", - "pdfjs-test/unit/base_viewer_spec.js", "pdfjs-test/unit/bidi_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js", @@ -81,6 +80,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/pdf_find_controller_spec.js", "pdfjs-test/unit/pdf_find_utils_spec.js", "pdfjs-test/unit/pdf_history_spec.js", + "pdfjs-test/unit/pdf_viewer_spec.js", "pdfjs-test/unit/primitives_spec.js", "pdfjs-test/unit/scripting_spec.js", "pdfjs-test/unit/stream_spec.js", diff --git a/test/unit/base_viewer_spec.js b/test/unit/pdf_viewer_spec.js similarity index 97% rename from test/unit/base_viewer_spec.js rename to test/unit/pdf_viewer_spec.js index f59ef75cd..b8890c109 100644 --- a/test/unit/base_viewer_spec.js +++ b/test/unit/pdf_viewer_spec.js @@ -13,9 +13,9 @@ * limitations under the License. */ -import { PDFPageViewBuffer } from "../../web/base_viewer.js"; +import { PDFPageViewBuffer } from "../../web/pdf_viewer.js"; -describe("BaseViewer", function () { +describe("PDFViewer", function () { describe("PDFPageViewBuffer", function () { function createViewsMap(startId, endId) { const map = new Map(); diff --git a/web/base_viewer.js b/web/base_viewer.js deleted file mode 100644 index f27bc20d7..000000000 --- a/web/base_viewer.js +++ /dev/null @@ -1,2252 +0,0 @@ -/* Copyright 2014 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. - */ - -/** @typedef {import("../src/display/api").PDFDocumentProxy} PDFDocumentProxy */ -/** @typedef {import("../src/display/api").PDFPageProxy} PDFPageProxy */ -// 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").IDownloadManager} IDownloadManager */ -/** @typedef {import("./interfaces").IL10n} IL10n */ -// eslint-disable-next-line max-len -/** @typedef {import("./interfaces").IPDFAnnotationLayerFactory} IPDFAnnotationLayerFactory */ -// eslint-disable-next-line max-len -/** @typedef {import("./interfaces").IPDFAnnotationEditorLayerFactory} IPDFAnnotationEditorLayerFactory */ -/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */ -// eslint-disable-next-line max-len -/** @typedef {import("./interfaces").IPDFStructTreeLayerFactory} IPDFStructTreeLayerFactory */ -// eslint-disable-next-line max-len -/** @typedef {import("./interfaces").IPDFTextLayerFactory} IPDFTextLayerFactory */ -/** @typedef {import("./interfaces").IPDFXfaLayerFactory} IPDFXfaLayerFactory */ -// eslint-disable-next-line max-len -/** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */ - -import { - AnnotationEditorType, - AnnotationEditorUIManager, - AnnotationMode, - createPromiseCapability, - PermissionFlag, - PixelsPerInch, - version, -} from "pdfjs-lib"; -import { - DEFAULT_SCALE, - DEFAULT_SCALE_DELTA, - DEFAULT_SCALE_VALUE, - docStyle, - getVisibleElements, - isPortraitOrientation, - isValidRotation, - isValidScrollMode, - isValidSpreadMode, - MAX_AUTO_SCALE, - MAX_SCALE, - MIN_SCALE, - PresentationModeState, - RendererType, - RenderingStates, - SCROLLBAR_PADDING, - scrollIntoView, - ScrollMode, - SpreadMode, - TextLayerMode, - UNKNOWN_SCALE, - VERTICAL_PADDING, - watchScroll, -} from "./ui_utils.js"; -import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder.js"; -import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; -import { NullL10n } from "./l10n_utils.js"; -import { PDFPageView } from "./pdf_page_view.js"; -import { PDFRenderingQueue } from "./pdf_rendering_queue.js"; -import { SimpleLinkService } from "./pdf_link_service.js"; -import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js"; -import { TextHighlighter } from "./text_highlighter.js"; -import { TextLayerBuilder } from "./text_layer_builder.js"; -import { XfaLayerBuilder } from "./xfa_layer_builder.js"; - -const DEFAULT_CACHE_SIZE = 10; -const ENABLE_PERMISSIONS_CLASS = "enablePermissions"; - -const PagesCountLimit = { - FORCE_SCROLL_MODE_PAGE: 15000, - FORCE_LAZY_PAGE_INIT: 7500, - PAUSE_EAGER_PAGE_INIT: 250, -}; - -function isValidAnnotationEditorMode(mode) { - return ( - Object.values(AnnotationEditorType).includes(mode) && - mode !== AnnotationEditorType.DISABLE - ); -} - -/** - * @typedef {Object} PDFViewerOptions - * @property {HTMLDivElement} container - The container for the viewer element. - * @property {HTMLDivElement} [viewer] - The viewer element. - * @property {EventBus} eventBus - The application event bus. - * @property {IPDFLinkService} linkService - The navigation/linking service. - * @property {IDownloadManager} [downloadManager] - The download manager - * component. - * @property {PDFFindController} [findController] - The find controller - * component. - * @property {PDFScriptingManager} [scriptingManager] - The scripting manager - * component. - * @property {PDFRenderingQueue} [renderingQueue] - The rendering queue object. - * @property {boolean} [removePageBorders] - Removes the border shadow around - * the pages. The default value is `false`. - * @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 {number} [annotationEditorMode] - Enables the creation and editing - * of new Annotations. The constants from {@link AnnotationEditorType} should - * be used. The default value is `AnnotationEditorType.DISABLE`. - * @property {string} [imageResourcesPath] - Path for image resources, mainly - * mainly for annotation icons. Include trailing slash. - * @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of - * landscape pages upon printing. The default is `false`. - * @property {boolean} [useOnlyCssZoom] - Enables CSS only zooming. The default - * value is `false`. - * @property {number} [maxCanvasPixels] - The maximum supported canvas size in - * total pixels, i.e. width * height. Use -1 for no limit. The default value - * is 4096 * 4096 (16 mega-pixels). - * @property {IL10n} l10n - Localization service. - * @property {boolean} [enablePermissions] - Enables PDF document permissions, - * when they exist. The default value is `false`. - * @property {Object} [pageColors] - Overwrites background and foreground colors - * with user defined ones in order to improve readability in high contrast - * mode. - */ - -class PDFPageViewBuffer { - // Here we rely on the fact that `Set`s preserve the insertion order. - #buf = new Set(); - - #size = 0; - - constructor(size) { - this.#size = size; - } - - push(view) { - const buf = this.#buf; - if (buf.has(view)) { - buf.delete(view); // Move the view to the "end" of the buffer. - } - buf.add(view); - - if (buf.size > this.#size) { - this.#destroyFirstView(); - } - } - - /** - * After calling resize, the size of the buffer will be `newSize`. - * The optional parameter `idsToKeep` is, if present, a Set of page-ids to - * push to the back of the buffer, delaying their destruction. The size of - * `idsToKeep` has no impact on the final size of the buffer; if `idsToKeep` - * is larger than `newSize`, some of those pages will be destroyed anyway. - */ - resize(newSize, idsToKeep = null) { - this.#size = newSize; - - const buf = this.#buf; - if (idsToKeep) { - const ii = buf.size; - let i = 1; - for (const view of buf) { - if (idsToKeep.has(view.id)) { - buf.delete(view); // Move the view to the "end" of the buffer. - buf.add(view); - } - if (++i > ii) { - break; - } - } - } - - while (buf.size > this.#size) { - this.#destroyFirstView(); - } - } - - has(view) { - return this.#buf.has(view); - } - - [Symbol.iterator]() { - return this.#buf.keys(); - } - - #destroyFirstView() { - const firstView = this.#buf.keys().next().value; - - firstView?.destroy(); - this.#buf.delete(firstView); - } -} - -/** - * Simple viewer control to display PDF content/pages. - * - * @implements {IPDFAnnotationLayerFactory} - * @implements {IPDFAnnotationEditorLayerFactory} - * @implements {IPDFStructTreeLayerFactory} - * @implements {IPDFTextLayerFactory} - * @implements {IPDFXfaLayerFactory} - */ -class BaseViewer { - #buffer = null; - - #annotationEditorMode = AnnotationEditorType.DISABLE; - - #annotationEditorUIManager = null; - - #annotationMode = AnnotationMode.ENABLE_FORMS; - - #enablePermissions = false; - - #previousContainerHeight = 0; - - #scrollModePageState = null; - - #onVisibilityChange = null; - - /** - * @param {PDFViewerOptions} options - */ - constructor(options) { - if (this.constructor === BaseViewer) { - throw new Error("Cannot initialize BaseViewer."); - } - const viewerVersion = - typeof PDFJSDev !== "undefined" ? PDFJSDev.eval("BUNDLE_VERSION") : null; - if (version !== viewerVersion) { - throw new Error( - `The API version "${version}" does not match the Viewer version "${viewerVersion}".` - ); - } - this.container = options.container; - this.viewer = options.viewer || options.container.firstElementChild; - - if ( - typeof PDFJSDev === "undefined" || - PDFJSDev.test("!PRODUCTION || GENERIC") - ) { - if ( - !( - this.container?.tagName.toUpperCase() === "DIV" && - this.viewer?.tagName.toUpperCase() === "DIV" - ) - ) { - throw new Error("Invalid `container` and/or `viewer` option."); - } - - if ( - this.container.offsetParent && - getComputedStyle(this.container).position !== "absolute" - ) { - throw new Error("The `container` must be absolutely positioned."); - } - } - this.eventBus = options.eventBus; - this.linkService = options.linkService || new SimpleLinkService(); - this.downloadManager = options.downloadManager || null; - this.findController = options.findController || null; - this._scriptingManager = options.scriptingManager || null; - this.removePageBorders = options.removePageBorders || false; - this.textLayerMode = options.textLayerMode ?? TextLayerMode.ENABLE; - this.#annotationMode = - options.annotationMode ?? AnnotationMode.ENABLE_FORMS; - this.#annotationEditorMode = - options.annotationEditorMode ?? AnnotationEditorType.DISABLE; - this.imageResourcesPath = options.imageResourcesPath || ""; - this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; - if ( - typeof PDFJSDev === "undefined" || - PDFJSDev.test("!PRODUCTION || GENERIC") - ) { - this.renderer = options.renderer || RendererType.CANVAS; - } - this.useOnlyCssZoom = options.useOnlyCssZoom || false; - this.maxCanvasPixels = options.maxCanvasPixels; - this.l10n = options.l10n || NullL10n; - this.#enablePermissions = options.enablePermissions || false; - this.pageColors = options.pageColors || null; - - if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { - if ( - this.pageColors && - !( - CSS.supports("color", this.pageColors.background) && - CSS.supports("color", this.pageColors.foreground) - ) - ) { - if (this.pageColors.background || this.pageColors.foreground) { - console.warn( - "BaseViewer: Ignoring `pageColors`-option, since the browser doesn't support the values used." - ); - } - this.pageColors = null; - } - } - - this.defaultRenderingQueue = !options.renderingQueue; - if (this.defaultRenderingQueue) { - // Custom rendering queue is not specified, using default one - this.renderingQueue = new PDFRenderingQueue(); - this.renderingQueue.setViewer(this); - } else { - this.renderingQueue = options.renderingQueue; - } - - this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this)); - this.presentationModeState = PresentationModeState.UNKNOWN; - this._onBeforeDraw = this._onAfterDraw = null; - this._resetView(); - - if (this.removePageBorders) { - this.viewer.classList.add("removePageBorders"); - } - this.updateContainerHeightCss(); - } - - get pagesCount() { - return this._pages.length; - } - - getPageView(index) { - return this._pages[index]; - } - - /** - * @type {boolean} - True if all {PDFPageView} objects are initialized. - */ - get pageViewsReady() { - if (!this._pagesCapability.settled) { - return false; - } - // Prevent printing errors when 'disableAutoFetch' is set, by ensuring - // that *all* pages have in fact been completely loaded. - return this._pages.every(function (pageView) { - return pageView?.pdfPage; - }); - } - - /** - * @type {boolean} - */ - get renderForms() { - return this.#annotationMode === AnnotationMode.ENABLE_FORMS; - } - - /** - * @type {boolean} - */ - get enableScripting() { - return !!this._scriptingManager; - } - - /** - * @type {number} - */ - get currentPageNumber() { - return this._currentPageNumber; - } - - /** - * @param {number} val - The page number. - */ - set currentPageNumber(val) { - if (!Number.isInteger(val)) { - throw new Error("Invalid page number."); - } - if (!this.pdfDocument) { - return; - } - // The intent can be to just reset a scroll position and/or scale. - if (!this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true)) { - console.error(`currentPageNumber: "${val}" is not a valid page.`); - } - } - - /** - * @returns {boolean} Whether the pageNumber is valid (within bounds). - * @private - */ - _setCurrentPageNumber(val, resetCurrentPageView = false) { - if (this._currentPageNumber === val) { - if (resetCurrentPageView) { - this.#resetCurrentPageView(); - } - return true; - } - - if (!(0 < val && val <= this.pagesCount)) { - return false; - } - const previous = this._currentPageNumber; - this._currentPageNumber = val; - - this.eventBus.dispatch("pagechanging", { - source: this, - pageNumber: val, - pageLabel: this._pageLabels?.[val - 1] ?? null, - previous, - }); - - if (resetCurrentPageView) { - this.#resetCurrentPageView(); - } - return true; - } - - /** - * @type {string|null} Returns the current page label, or `null` if no page - * labels exist. - */ - get currentPageLabel() { - return this._pageLabels?.[this._currentPageNumber - 1] ?? null; - } - - /** - * @param {string} val - The page label. - */ - set currentPageLabel(val) { - if (!this.pdfDocument) { - return; - } - let page = val | 0; // Fallback page number. - if (this._pageLabels) { - const i = this._pageLabels.indexOf(val); - if (i >= 0) { - page = i + 1; - } - } - // The intent can be to just reset a scroll position and/or scale. - if (!this._setCurrentPageNumber(page, /* resetCurrentPageView = */ true)) { - console.error(`currentPageLabel: "${val}" is not a valid page.`); - } - } - - /** - * @type {number} - */ - get currentScale() { - return this._currentScale !== UNKNOWN_SCALE - ? this._currentScale - : DEFAULT_SCALE; - } - - /** - * @param {number} val - Scale of the pages in percents. - */ - set currentScale(val) { - if (isNaN(val)) { - throw new Error("Invalid numeric scale."); - } - if (!this.pdfDocument) { - return; - } - this._setScale(val, false); - } - - /** - * @type {string} - */ - get currentScaleValue() { - return this._currentScaleValue; - } - - /** - * @param val - The scale of the pages (in percent or predefined value). - */ - set currentScaleValue(val) { - if (!this.pdfDocument) { - return; - } - this._setScale(val, false); - } - - /** - * @type {number} - */ - get pagesRotation() { - return this._pagesRotation; - } - - /** - * @param {number} rotation - The rotation of the pages (0, 90, 180, 270). - */ - set pagesRotation(rotation) { - if (!isValidRotation(rotation)) { - throw new Error("Invalid pages rotation angle."); - } - if (!this.pdfDocument) { - return; - } - // Normalize the rotation, by clamping it to the [0, 360) range. - rotation %= 360; - if (rotation < 0) { - rotation += 360; - } - if (this._pagesRotation === rotation) { - return; // The rotation didn't change. - } - this._pagesRotation = rotation; - - const pageNumber = this._currentPageNumber; - - const updateArgs = { rotation }; - for (const pageView of this._pages) { - pageView.update(updateArgs); - } - // 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.eventBus.dispatch("rotationchanging", { - source: this, - pagesRotation: rotation, - pageNumber, - }); - - if (this.defaultRenderingQueue) { - this.update(); - } - } - - get firstPagePromise() { - return this.pdfDocument ? this._firstPageCapability.promise : null; - } - - get onePageRendered() { - return this.pdfDocument ? this._onePageRenderedCapability.promise : null; - } - - get pagesPromise() { - return this.pdfDocument ? this._pagesCapability.promise : null; - } - - /** - * Currently only *some* permissions are supported. - * @returns {Object} - */ - #initializePermissions(permissions) { - const params = { - annotationEditorMode: this.#annotationEditorMode, - annotationMode: this.#annotationMode, - textLayerMode: this.textLayerMode, - }; - if (!permissions) { - return params; - } - - if (!permissions.includes(PermissionFlag.COPY)) { - this.viewer.classList.add(ENABLE_PERMISSIONS_CLASS); - } - - if (!permissions.includes(PermissionFlag.MODIFY_CONTENTS)) { - params.annotationEditorMode = AnnotationEditorType.DISABLE; - } - - if ( - !permissions.includes(PermissionFlag.MODIFY_ANNOTATIONS) && - !permissions.includes(PermissionFlag.FILL_INTERACTIVE_FORMS) && - this.#annotationMode === AnnotationMode.ENABLE_FORMS - ) { - params.annotationMode = AnnotationMode.ENABLE; - } - - return params; - } - - #onePageRenderedOrForceFetch() { - // Unless the viewer *and* its pages are visible, rendering won't start and - // `this._onePageRenderedCapability` thus won't be resolved. - // To ensure that automatic printing, on document load, still works even in - // those cases we force-allow fetching of all pages when: - // - The current window/tab is inactive, which will prevent rendering since - // `requestAnimationFrame` is being used; fixes bug 1746213. - // - The viewer is hidden in the DOM, e.g. in a `display: none`