/* 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. */ import { createPromiseCapability, version } from "pdfjs-lib"; import { CSS_UNITS, DEFAULT_SCALE, DEFAULT_SCALE_VALUE, getVisibleElements, isPortraitOrientation, isValidRotation, isValidScrollMode, isValidSpreadMode, MAX_AUTO_SCALE, moveToEndOfArray, NullL10n, PresentationModeState, RendererType, SCROLLBAR_PADDING, scrollIntoView, ScrollMode, SpreadMode, TextLayerMode, UNKNOWN_SCALE, VERTICAL_PADDING, watchScroll, } from "./ui_utils.js"; import { PDFRenderingQueue, RenderingStates } from "./pdf_rendering_queue.js"; import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { PDFPageView } from "./pdf_page_view.js"; import { SimpleLinkService } from "./pdf_link_service.js"; import { TextLayerBuilder } from "./text_layer_builder.js"; const DEFAULT_CACHE_SIZE = 10; /** * @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 {DownloadManager} [downloadManager] - The download manager * component. * @property {PDFFindController} [findController] - The find controller * 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, and if the improved text selection * behaviour is enabled. The constants from {TextLayerMode} should be used. * The default value is `TextLayerMode.ENABLE`. * @property {string} [imageResourcesPath] - Path for image resources, mainly * mainly for annotation icons. Include trailing slash. * @property {boolean} [renderInteractiveForms] - Enables rendering of * interactive form elements. The default value is `true`. * @property {boolean} [enablePrintAutoRotate] - Enables automatic rotation of * landscape pages upon printing. The default is `false`. * @property {string} renderer - 'canvas' or 'svg'. The default is 'canvas'. * @property {boolean} [enableWebGL] - Enables WebGL accelerated rendering for * some operations. The default value 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} [enableScripting] - Enable embedded script execution. * The default value is `false`. * @property {Object} [mouseState] - The mouse button state. The default value * is `null`. */ function PDFPageViewBuffer(size) { const data = []; this.push = function (view) { const i = data.indexOf(view); if (i >= 0) { data.splice(i, 1); } data.push(view); if (data.length > size) { data.shift().destroy(); } }; /** * After calling resize, the size of the buffer will be newSize. The optional * parameter pagesToKeep is, if present, an array of pages to push to the back * of the buffer, delaying their destruction. The size of pagesToKeep has no * impact on the final size of the buffer; if pagesToKeep has length larger * than newSize, some of those pages will be destroyed anyway. */ this.resize = function (newSize, pagesToKeep) { size = newSize; if (pagesToKeep) { const pageIdsToKeep = new Set(); for (let i = 0, iMax = pagesToKeep.length; i < iMax; ++i) { pageIdsToKeep.add(pagesToKeep[i].id); } moveToEndOfArray(data, function (page) { return pageIdsToKeep.has(page.id); }); } while (data.length > size) { data.shift().destroy(); } }; this.has = function (view) { return data.includes(view); }; } function isSameScale(oldScale, newScale) { if (newScale === oldScale) { return true; } if (Math.abs(newScale - oldScale) < 1e-15) { // Prevent unnecessary re-rendering of all pages when the scale // changes only because of limited numerical precision. return true; } return false; } /** * Simple viewer control to display PDF content/pages. * @implements {IRenderableView} */ class BaseViewer { /** * @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._name = this.constructor.name; 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 (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.removePageBorders = options.removePageBorders || false; this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode : TextLayerMode.ENABLE; this.imageResourcesPath = options.imageResourcesPath || ""; this.renderInteractiveForms = typeof options.renderInteractiveForms === "boolean" ? options.renderInteractiveForms : true; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; this.renderer = options.renderer || RendererType.CANVAS; this.enableWebGL = options.enableWebGL || false; this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; this.enableScripting = options.enableScripting || false; this._mouseState = options.mouseState || 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._initializeScriptingEvents(); // Defer the dispatching of this event, to give other viewer components // time to initialize *and* register 'baseviewerinit' event listeners. Promise.resolve().then(() => { this.eventBus.dispatch("baseviewerinit", { source: this }); }); } 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 && pageView.pdfPage; }); } /** * @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( `${this._name}.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 && this._pageLabels[val - 1], 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._pageLabels[this._currentPageNumber - 1]; } /** * @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( `${this._name}.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; } if (this._pagesRotation === rotation) { return; // The rotation didn't change. } this._pagesRotation = rotation; const pageNumber = this._currentPageNumber; for (let i = 0, ii = this._pages.length; i < ii; i++) { const pageView = this._pages[i]; pageView.update(pageView.scale, 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.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; } /** * @private */ get _viewerElement() { // In most viewers, e.g. `PDFViewer`, this should return `this.viewer`. throw new Error("Not implemented: _viewerElement"); } /** * @private */ _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 viewer is hidden in the DOM, e.g. in a `display: none`