diff --git a/src/display/optional_content_config.js b/src/display/optional_content_config.js index 4f70893a4..baa4aeb5c 100644 --- a/src/display/optional_content_config.js +++ b/src/display/optional_content_config.js @@ -12,52 +12,85 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { objectFromMap, warn } from "../shared/util.js"; + +import { objectFromMap, unreachable, warn } from "../shared/util.js"; + +const INTERNAL = Symbol("INTERNAL"); class OptionalContentGroup { + #visible = true; + constructor(name, intent) { - this.visible = true; this.name = name; this.intent = intent; } + + /** + * @type {boolean} + */ + get visible() { + return this.#visible; + } + + /** + * @ignore + */ + _setVisible(internal, visible) { + if (internal !== INTERNAL) { + unreachable("Internal method `_setVisible` called."); + } + this.#visible = visible; + } } class OptionalContentConfig { + #cachedHasInitialVisibility = true; + + #groups = new Map(); + + #initialVisibility = null; + + #order = null; + constructor(data) { this.name = null; this.creator = null; - this._order = null; - this._groups = new Map(); if (data === null) { return; } this.name = data.name; this.creator = data.creator; - this._order = data.order; + this.#order = data.order; for (const group of data.groups) { - this._groups.set( + this.#groups.set( group.id, new OptionalContentGroup(group.name, group.intent) ); } if (data.baseState === "OFF") { - for (const group of this._groups) { - group.visible = false; + for (const group of this.#groups.values()) { + group._setVisible(INTERNAL, false); } } for (const on of data.on) { - this._groups.get(on).visible = true; + this.#groups.get(on)._setVisible(INTERNAL, true); } for (const off of data.off) { - this._groups.get(off).visible = false; + this.#groups.get(off)._setVisible(INTERNAL, false); + } + + // The following code must always run *last* in the constructor. + this.#initialVisibility = new Map(); + for (const [id, group] of this.#groups) { + this.#initialVisibility.set(id, group.visible); } } - _evaluateVisibilityExpression(array) { + #evaluateVisibilityExpression(array) { const length = array.length; if (length < 2) { return true; @@ -67,9 +100,9 @@ class OptionalContentConfig { const element = array[i]; let state; if (Array.isArray(element)) { - state = this._evaluateVisibilityExpression(element); - } else if (this._groups.has(element)) { - state = this._groups.get(element).visible; + state = this.#evaluateVisibilityExpression(element); + } else if (this.#groups.has(element)) { + state = this.#groups.get(element).visible; } else { warn(`Optional content group not found: ${element}`); return true; @@ -95,7 +128,7 @@ class OptionalContentConfig { } isVisible(group) { - if (this._groups.size === 0) { + if (this.#groups.size === 0) { return true; } if (!group) { @@ -103,57 +136,57 @@ class OptionalContentConfig { return true; } if (group.type === "OCG") { - if (!this._groups.has(group.id)) { + if (!this.#groups.has(group.id)) { warn(`Optional content group not found: ${group.id}`); return true; } - return this._groups.get(group.id).visible; + return this.#groups.get(group.id).visible; } else if (group.type === "OCMD") { // Per the spec, the expression should be preferred if available. if (group.expression) { - return this._evaluateVisibilityExpression(group.expression); + return this.#evaluateVisibilityExpression(group.expression); } if (!group.policy || group.policy === "AnyOn") { // Default for (const id of group.ids) { - if (!this._groups.has(id)) { + if (!this.#groups.has(id)) { warn(`Optional content group not found: ${id}`); return true; } - if (this._groups.get(id).visible) { + if (this.#groups.get(id).visible) { return true; } } return false; } else if (group.policy === "AllOn") { for (const id of group.ids) { - if (!this._groups.has(id)) { + if (!this.#groups.has(id)) { warn(`Optional content group not found: ${id}`); return true; } - if (!this._groups.get(id).visible) { + if (!this.#groups.get(id).visible) { return false; } } return true; } else if (group.policy === "AnyOff") { for (const id of group.ids) { - if (!this._groups.has(id)) { + if (!this.#groups.has(id)) { warn(`Optional content group not found: ${id}`); return true; } - if (!this._groups.get(id).visible) { + if (!this.#groups.get(id).visible) { return true; } } return false; } else if (group.policy === "AllOff") { for (const id of group.ids) { - if (!this._groups.has(id)) { + if (!this.#groups.has(id)) { warn(`Optional content group not found: ${id}`); return true; } - if (this._groups.get(id).visible) { + if (this.#groups.get(id).visible) { return false; } } @@ -167,29 +200,44 @@ class OptionalContentConfig { } setVisibility(id, visible = true) { - if (!this._groups.has(id)) { + if (!this.#groups.has(id)) { warn(`Optional content group not found: ${id}`); return; } - this._groups.get(id).visible = !!visible; + this.#groups.get(id)._setVisible(INTERNAL, !!visible); + + this.#cachedHasInitialVisibility = null; + } + + get hasInitialVisibility() { + if (this.#cachedHasInitialVisibility !== null) { + return this.#cachedHasInitialVisibility; + } + for (const [id, group] of this.#groups) { + const visible = this.#initialVisibility.get(id); + if (group.visible !== visible) { + return (this.#cachedHasInitialVisibility = false); + } + } + return (this.#cachedHasInitialVisibility = true); } getOrder() { - if (!this._groups.size) { + if (!this.#groups.size) { return null; } - if (this._order) { - return this._order.slice(); + if (this.#order) { + return this.#order.slice(); } - return Array.from(this._groups.keys()); + return [...this.#groups.keys()]; } getGroups() { - return this._groups.size > 0 ? objectFromMap(this._groups) : null; + return this.#groups.size > 0 ? objectFromMap(this.#groups) : null; } getGroup(id) { - return this._groups.get(id) || null; + return this.#groups.get(id) || null; } } diff --git a/web/base_viewer.js b/web/base_viewer.js index c03318a88..a8faa2d48 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -1856,6 +1856,7 @@ class BaseViewer { return Promise.resolve(null); } if (!this._optionalContentConfigPromise) { + console.error("optionalContentConfigPromise: Not initialized yet."); // Prevent issues if the getter is accessed *before* the `onePageRendered` // promise has resolved; won't (normally) happen in the default viewer. return this.pdfDocument.getOptionalContentConfig(); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 388391762..df8dd2fc3 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -99,6 +99,8 @@ const MAX_CANVAS_PIXELS = compatibilityParams.maxCanvasPixels || 16777216; class PDFPageView { #annotationMode = AnnotationMode.ENABLE_FORMS; + #useThumbnailCanvas = true; + /** * @param {PDFPageViewOptions} options */ @@ -151,7 +153,12 @@ class PDFPageView { this.renderingState = RenderingStates.INITIAL; this.resume = null; this._renderError = null; - this._isStandalone = !this.renderingQueue?.hasViewer(); + if ( + typeof PDFJSDev === "undefined" || + PDFJSDev.test("!PRODUCTION || GENERIC") + ) { + this._isStandalone = !this.renderingQueue?.hasViewer(); + } this._annotationCanvasMap = null; @@ -174,6 +181,26 @@ class PDFPageView { this.div = div; container?.append(div); + + if ( + (typeof PDFJSDev === "undefined" || + PDFJSDev.test("!PRODUCTION || GENERIC")) && + this._isStandalone + ) { + const { optionalContentConfigPromise } = options; + if (optionalContentConfigPromise) { + // Ensure that the thumbnails always display the *initial* document + // state. + optionalContentConfigPromise.then(optionalContentConfig => { + if ( + optionalContentConfigPromise !== this._optionalContentConfigPromise + ) { + return; + } + this.#useThumbnailCanvas = optionalContentConfig.hasInitialVisibility; + }); + } + } } setPdfPage(pdfPage) { @@ -359,7 +386,11 @@ class PDFPageView { this.loadingIconDiv = document.createElement("div"); this.loadingIconDiv.className = "loadingIcon notVisible"; - if (this._isStandalone) { + if ( + (typeof PDFJSDev === "undefined" || + PDFJSDev.test("!PRODUCTION || GENERIC")) && + this._isStandalone + ) { this.toggleLoadingIconSpinner(/* viewVisible = */ true); } this.loadingIconDiv.setAttribute("role", "img"); @@ -376,6 +407,16 @@ class PDFPageView { } if (optionalContentConfigPromise instanceof Promise) { this._optionalContentConfigPromise = optionalContentConfigPromise; + + // Ensure that the thumbnails always display the *initial* document state. + optionalContentConfigPromise.then(optionalContentConfig => { + if ( + optionalContentConfigPromise !== this._optionalContentConfigPromise + ) { + return; + } + this.#useThumbnailCanvas = optionalContentConfig.hasInitialVisibility; + }); } const totalRotation = (this.rotation + this.pdfPageRotate) % 360; @@ -384,7 +425,11 @@ class PDFPageView { rotation: totalRotation, }); - if (this._isStandalone) { + if ( + (typeof PDFJSDev === "undefined" || + PDFJSDev.test("!PRODUCTION || GENERIC")) && + this._isStandalone + ) { docStyle.setProperty("--scale-factor", this.viewport.scale); } @@ -999,6 +1044,14 @@ class PDFPageView { this.div.removeAttribute("data-page-label"); } } + + /** + * For use by the `PDFThumbnailView.setImage`-method. + * @ignore + */ + get thumbnailCanvas() { + return this.#useThumbnailCanvas ? this.canvas : null; + } } export { PDFPageView }; diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js index e45c61f7d..2cf3f6c5c 100644 --- a/web/pdf_thumbnail_view.js +++ b/web/pdf_thumbnail_view.js @@ -37,7 +37,6 @@ const THUMBNAIL_WIDTH = 98; // px * The default value is `null`. * @property {IPDFLinkService} linkService - The navigation/linking service. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. - * @property {function} checkSetImageDisabled * @property {IL10n} l10n - Localization service. * @property {Object} [pageColors] - Overwrites background and foreground colors * with user defined ones in order to improve readability in high contrast @@ -88,7 +87,6 @@ class PDFThumbnailView { optionalContentConfigPromise, linkService, renderingQueue, - checkSetImageDisabled, l10n, pageColors, }) { @@ -109,11 +107,6 @@ class PDFThumbnailView { this.renderTask = null; this.renderingState = RenderingStates.INITIAL; this.resume = null; - this._checkSetImageDisabled = - checkSetImageDisabled || - function () { - return false; - }; const pageWidth = this.viewport.width, pageHeight = this.viewport.height, @@ -356,13 +349,10 @@ class PDFThumbnailView { } setImage(pageView) { - if (this._checkSetImageDisabled()) { - return; - } if (this.renderingState !== RenderingStates.INITIAL) { return; } - const { canvas, pdfPage } = pageView; + const { thumbnailCanvas: canvas, pdfPage } = pageView; if (!canvas) { return; } diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index 582448253..d54cb043c 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -85,12 +85,6 @@ class PDFThumbnailViewer { this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this)); this._resetView(); - - eventBus._on("optionalcontentconfigchanged", () => { - // Ensure that the thumbnails always render with the *default* optional - // content configuration. - this._setImageDisabled = true; - }); } /** @@ -195,8 +189,6 @@ class PDFThumbnailViewer { this._currentPageNumber = 1; this._pageLabels = null; this._pagesRotation = 0; - this._optionalContentConfigPromise = null; - this._setImageDisabled = false; // Remove the thumbnails from the DOM. this.container.textContent = ""; @@ -220,13 +212,8 @@ class PDFThumbnailViewer { firstPagePromise .then(firstPdfPage => { - this._optionalContentConfigPromise = optionalContentConfigPromise; - const pagesCount = pdfDocument.numPages; const viewport = firstPdfPage.getViewport({ scale: 1 }); - const checkSetImageDisabled = () => { - return this._setImageDisabled; - }; for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { const thumbnail = new PDFThumbnailView({ @@ -236,7 +223,6 @@ class PDFThumbnailViewer { optionalContentConfigPromise, linkService: this.linkService, renderingQueue: this.renderingQueue, - checkSetImageDisabled, l10n: this.l10n, pageColors: this.pageColors, });