From 1d19b74e0dfd91b16787b63bb843c57511a4441c Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Tue, 4 Aug 2020 21:18:20 +0200 Subject: [PATCH] Add support for toggling of Optional Content in the viewer (issue 12096) --- l10n/en-US/viewer.properties | 5 +- l10n/sv-SE/viewer.properties | 5 +- src/display/optional_content_config.js | 22 +++ src/pdf.js | 3 + web/app.js | 25 ++- web/base_viewer.js | 56 +++++- web/firefox_print_service.js | 47 ++++- web/images/toolbarButton-viewLayers.png | Bin 0 -> 238 bytes web/images/toolbarButton-viewLayers@2x.png | Bin 0 -> 317 bytes web/pdf_layer_viewer.js | 215 +++++++++++++++++++++ web/pdf_outline_viewer.js | 1 + web/pdf_page_view.js | 11 +- web/pdf_print_service.js | 36 +++- web/pdf_sidebar.js | 39 +++- web/viewer.css | 21 +- web/viewer.html | 5 + web/viewer.js | 2 + 17 files changed, 475 insertions(+), 18 deletions(-) create mode 100644 web/images/toolbarButton-viewLayers.png create mode 100644 web/images/toolbarButton-viewLayers@2x.png create mode 100644 web/pdf_layer_viewer.js diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index 6f7598e3dac62f..d31103c008f26b 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -137,17 +137,20 @@ print_progress_close=Cancel # (the _label strings are alt text for the buttons, the .title strings are # tooltips) toggle_sidebar.title=Toggle Sidebar -toggle_sidebar_notification.title=Toggle Sidebar (document contains outline/attachments) +toggle_sidebar_notification2.title=Toggle Sidebar (document contains outline/attachments/layers) toggle_sidebar_label=Toggle Sidebar document_outline.title=Show Document Outline (double-click to expand/collapse all items) document_outline_label=Document Outline attachments.title=Show Attachments attachments_label=Attachments +layers.title=Show Layers (double-click to reset all layers to the default state) +layers_label=Layers thumbs.title=Show Thumbnails thumbs_label=Thumbnails findbar.title=Find in Document findbar_label=Find +additional_layers=Additional Layers # LOCALIZATION NOTE (page_canvas): "{{page}}" will be replaced by the page number. page_canvas=Page {{page}} # Thumbnails panel item (tooltip and alt text for images) diff --git a/l10n/sv-SE/viewer.properties b/l10n/sv-SE/viewer.properties index 3d2f0cf438c23e..3ca7688c99cac7 100644 --- a/l10n/sv-SE/viewer.properties +++ b/l10n/sv-SE/viewer.properties @@ -137,17 +137,20 @@ print_progress_close=Avbryt # (the _label strings are alt text for the buttons, the .title strings are # tooltips) toggle_sidebar.title=Visa/dölj sidofält -toggle_sidebar_notification.title=Visa/dölj sidofält (dokument innehåller översikt/bilagor) +toggle_sidebar_notification2.title=Visa/dölj sidofält (dokument innehåller översikt/bilagor/lager) toggle_sidebar_label=Visa/dölj sidofält document_outline.title=Visa dokumentdisposition (dubbelklicka för att expandera/komprimera alla objekt) document_outline_label=Dokumentöversikt attachments.title=Visa Bilagor attachments_label=Bilagor +layers.title=Visa lager (dubbelklicka för att återställa alla lager till ursrungligt läge) +layers_label=Lager thumbs.title=Visa miniatyrer thumbs_label=Miniatyrer findbar.title=Sök i dokument findbar_label=Sök +additional_layers=Ytterliggare lager # LOCALIZATION NOTE (page_canvas): "{{page}}" will be replaced by the page number. page_canvas=Sida {{page}} # Thumbnails panel item (tooltip and alt text for images) diff --git a/src/display/optional_content_config.js b/src/display/optional_content_config.js index 9bc53d6aefd637..e09fc797f7f5fa 100644 --- a/src/display/optional_content_config.js +++ b/src/display/optional_content_config.js @@ -122,6 +122,28 @@ class OptionalContentConfig { warn(`Unknown group type ${group.type}.`); return true; } + + toggleVisibility(id, visible = true) { + if (!this.groups.has(id)) { + warn(`Optional content group not found: ${id}`); + return; + } + this.groups.get(id).visible = !!visible; + } + + getGroups() { + if (!this.groups.size) { + return null; + } + if (this.order) { + return this.order.slice(); + } + return Array.from(this.groups.keys()); + } + + getGroup(id) { + return this.groups.get(id); + } } export { OptionalContentConfig }; diff --git a/src/pdf.js b/src/pdf.js index c6a6d13382665c..714797251008ed 100644 --- a/src/pdf.js +++ b/src/pdf.js @@ -52,6 +52,7 @@ import { import { AnnotationLayer } from "./display/annotation_layer.js"; import { apiCompatibilityParams } from "./display/api_compatibility.js"; import { GlobalWorkerOptions } from "./display/worker_options.js"; +import { OptionalContentConfig } from "./display/optional_content_config.js"; import { renderTextLayer } from "./display/text_layer.js"; import { SVGGraphics } from "./display/svg.js"; @@ -160,6 +161,8 @@ export { apiCompatibilityParams, // From "./display/worker_options.js": GlobalWorkerOptions, + // From "./display/optional_content_config.js": + OptionalContentConfig, // From "./display/text_layer.js": renderTextLayer, // From "./display/svg.js": diff --git a/web/app.js b/web/app.js index e84a2af22b78a4..f1dfd2845324ba 100644 --- a/web/app.js +++ b/web/app.js @@ -64,6 +64,7 @@ import { PDFDocumentProperties } from "./pdf_document_properties.js"; import { PDFFindBar } from "./pdf_find_bar.js"; import { PDFFindController } from "./pdf_find_controller.js"; import { PDFHistory } from "./pdf_history.js"; +import { PDFLayerViewer } from "./pdf_layer_viewer.js"; import { PDFLinkService } from "./pdf_link_service.js"; import { PDFOutlineViewer } from "./pdf_outline_viewer.js"; import { PDFPresentationMode } from "./pdf_presentation_mode.js"; @@ -208,6 +209,8 @@ const PDFViewerApplication = { pdfOutlineViewer: null, /** @type {PDFAttachmentViewer} */ pdfAttachmentViewer: null, + /** @type {PDFLayerViewer} */ + pdfLayerViewer: null, /** @type {PDFCursorTools} */ pdfCursorTools: null, /** @type {ViewHistory} */ @@ -508,6 +511,12 @@ const PDFViewerApplication = { downloadManager, }); + this.pdfLayerViewer = new PDFLayerViewer({ + container: appConfig.sidebar.layersView, + eventBus, + l10n: this.l10n, + }); + this.pdfSidebar = new PDFSidebar({ elements: appConfig.sidebar, pdfViewer: this.pdfViewer, @@ -735,6 +744,7 @@ const PDFViewerApplication = { this.pdfSidebar.reset(); this.pdfOutlineViewer.reset(); this.pdfAttachmentViewer.reset(); + this.pdfLayerViewer.reset(); if (this.pdfHistory) { this.pdfHistory.reset(); @@ -1269,6 +1279,11 @@ const PDFViewerApplication = { pdfDocument.getAttachments().then(attachments => { this.pdfAttachmentViewer.render({ attachments }); }); + // Ensure that the layers accurately reflects the current state in the + // viewer itself, rather than the default state provided by the API. + pdfViewer.getOptionalContentConfig().then(optionalContentConfig => { + this.pdfLayerViewer.render({ optionalContentConfig, pdfDocument }); + }); }); this._initializePageLabels(pdfDocument); @@ -1609,10 +1624,12 @@ const PDFViewerApplication = { const pagesOverview = this.pdfViewer.getPagesOverview(); const printContainer = this.appConfig.printContainer; + const optionalContentConfigPromise = this.pdfViewer.getOptionalContentConfig(); const printService = PDFPrintServiceFactory.instance.createPrintService( this.pdfDocument, pagesOverview, printContainer, + optionalContentConfigPromise, this.l10n ); this.printService = printService; @@ -1683,6 +1700,7 @@ const PDFViewerApplication = { eventBus._on("scalechanged", webViewerScaleChanged); eventBus._on("rotatecw", webViewerRotateCw); eventBus._on("rotateccw", webViewerRotateCcw); + eventBus._on("optionalcontentconfig", webViewerOptionalContentConfig); eventBus._on("switchscrollmode", webViewerSwitchScrollMode); eventBus._on("scrollmodechanged", webViewerScrollModeChanged); eventBus._on("switchspreadmode", webViewerSwitchSpreadMode); @@ -1758,6 +1776,7 @@ const PDFViewerApplication = { eventBus._off("scalechanged", webViewerScaleChanged); eventBus._off("rotatecw", webViewerRotateCw); eventBus._off("rotateccw", webViewerRotateCcw); + eventBus._off("optionalcontentconfig", webViewerOptionalContentConfig); eventBus._off("switchscrollmode", webViewerSwitchScrollMode); eventBus._off("scrollmodechanged", webViewerScrollModeChanged); eventBus._off("switchspreadmode", webViewerSwitchSpreadMode); @@ -2319,6 +2338,10 @@ function webViewerRotateCw() { function webViewerRotateCcw() { PDFViewerApplication.rotatePages(-90); } +function webViewerOptionalContentConfig(evt) { + PDFViewerApplication.pdfViewer.optionalContentConfig = + evt.optionalContentConfig; +} function webViewerSwitchScrollMode(evt) { PDFViewerApplication.pdfViewer.scrollMode = evt.mode; } @@ -2865,7 +2888,7 @@ function apiPageModeToSidebarView(mode) { case "UseAttachments": return SidebarView.ATTACHMENTS; case "UseOC": - // Not implemented, since we don't support Optional Content Groups yet. + return SidebarView.LAYERS; } return SidebarView.NONE; // Default value. } diff --git a/web/base_viewer.js b/web/base_viewer.js index ede70ee5f72346..ef33745c4c09ca 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -13,6 +13,7 @@ * limitations under the License. */ +import { createPromiseCapability, OptionalContentConfig } from "pdfjs-lib"; import { CSS_UNITS, DEFAULT_SCALE, @@ -38,7 +39,6 @@ import { } from "./ui_utils.js"; import { PDFRenderingQueue, RenderingStates } from "./pdf_rendering_queue.js"; import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; -import { createPromiseCapability } from "pdfjs-lib"; import { PDFPageView } from "./pdf_page_view.js"; import { SimpleLinkService } from "./pdf_link_service.js"; import { TextLayerBuilder } from "./text_layer_builder.js"; @@ -434,6 +434,11 @@ class BaseViewer { } const pagesCount = pdfDocument.numPages; const firstPagePromise = pdfDocument.getPage(1); + const optionalContentConfigPromise = pdfDocument + .getOptionalContentConfig() + .then(optionalContentConfig => { + return (this._optionalContentConfig = optionalContentConfig); + }); this._pagesCapability.promise.then(() => { this.eventBus.dispatch("pagesloaded", { @@ -482,6 +487,7 @@ class BaseViewer { id: pageNum, scale, defaultViewport: viewport.clone(), + optionalContentConfigPromise, renderingQueue: this.renderingQueue, textLayerFactory, textLayerMode: this.textLayerMode, @@ -599,6 +605,7 @@ class BaseViewer { this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); this._location = null; this._pagesRotation = 0; + this._optionalContentConfig = null; this._pagesRequests = new WeakMap(); this._firstPageCapability = createPromiseCapability(); this._onePageRenderedCapability = createPromiseCapability(); @@ -1215,6 +1222,53 @@ class BaseViewer { }); } + /** + * @type {OptionalContentConfig} + */ + get optionalContentConfig() { + return this._optionalContentConfig; + } + + /** + * @param {OptionalContentConfig} val - An {OptionalContentConfig} instance. + */ + set optionalContentConfig(val) { + if (!(val instanceof OptionalContentConfig)) { + throw new Error(`Invalid optionalContentConfig: ${val}`); + } + if (!this.pdfDocument) { + return; + } + if (!this._optionalContentConfig) { + // Ignore the setter *before* the `onePageRendered` promise has resolved, + // since it'd be overwritten anyway; won't happen in the default viewer. + return; + } + this._optionalContentConfig = val; + + const optionalContentConfigPromise = Promise.resolve(val); + for (const pageView of this._pages) { + pageView.update( + pageView.scale, + pageView.rotation, + optionalContentConfigPromise + ); + } + this.update(); + } + + async getOptionalContentConfig() { + if (!this.pdfDocument) { + throw new Error("getOptionalContentConfig - setDocument was not called."); + } + if (!this._optionalContentConfig) { + // Prevent issues if the method is called *before* the `onePageRendered` + // promise has resolved; won't happen in the default viewer. + return this.pdfDocument.getOptionalContentConfig(); + } + return this._optionalContentConfig; + } + /** * @type {number} One of the values in {ScrollMode}. */ diff --git a/web/firefox_print_service.js b/web/firefox_print_service.js index d293140ad18029..ba154ae7d2e5b0 100644 --- a/web/firefox_print_service.js +++ b/web/firefox_print_service.js @@ -19,7 +19,13 @@ import { PDFPrintServiceFactory } from "./app.js"; import { shadow } from "pdfjs-lib"; // Creates a placeholder with div and canvas with right size for the page. -function composePage(pdfDocument, pageNumber, size, printContainer) { +function composePage( + pdfDocument, + pageNumber, + size, + printContainer, + optionalContentConfigPromise +) { const canvas = document.createElement("canvas"); // The size of the canvas in pixels for printing. @@ -54,6 +60,7 @@ function composePage(pdfDocument, pageNumber, size, printContainer) { viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }), intent: "print", annotationStorage: pdfDocument.annotationStorage.getAll(), + optionalContentConfigPromise, }; return pdfPage.render(renderContext).promise; }) @@ -76,21 +83,39 @@ function composePage(pdfDocument, pageNumber, size, printContainer) { }; } -function FirefoxPrintService(pdfDocument, pagesOverview, printContainer) { +function FirefoxPrintService( + pdfDocument, + pagesOverview, + printContainer, + optionalContentConfigPromise = null +) { this.pdfDocument = pdfDocument; this.pagesOverview = pagesOverview; this.printContainer = printContainer; + this._optionalContentConfigPromise = + optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); } FirefoxPrintService.prototype = { layout() { - const { pdfDocument, pagesOverview, printContainer } = this; + const { + pdfDocument, + pagesOverview, + printContainer, + _optionalContentConfigPromise, + } = this; const body = document.querySelector("body"); body.setAttribute("data-pdfjsprinting", true); for (let i = 0, ii = pagesOverview.length; i < ii; ++i) { - composePage(pdfDocument, i + 1, pagesOverview[i], printContainer); + composePage( + pdfDocument, + i + 1, + pagesOverview[i], + printContainer, + _optionalContentConfigPromise + ); } }, @@ -110,8 +135,18 @@ PDFPrintServiceFactory.instance = { return shadow(this, "supportsPrinting", value); }, - createPrintService(pdfDocument, pagesOverview, printContainer) { - return new FirefoxPrintService(pdfDocument, pagesOverview, printContainer); + createPrintService( + pdfDocument, + pagesOverview, + printContainer, + optionalContentConfigPromise + ) { + return new FirefoxPrintService( + pdfDocument, + pagesOverview, + printContainer, + optionalContentConfigPromise + ); }, }; diff --git a/web/images/toolbarButton-viewLayers.png b/web/images/toolbarButton-viewLayers.png new file mode 100644 index 0000000000000000000000000000000000000000..9a644e9c5c9c3a69d954dc86861fc092095d3a8c GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPVyRZO1_uhvalYl}znIRD+&iT2ysd*(pE(61!b(>}a zsbo(V#}JFtXD4qIJYc}VymaQ7f5HcRJcYTQGpHN7hP=LS_nJq<>&pIwU6|ieU%o}58z{t+84^+AoS&PUnpXnkGBE5}w`nGj z+T`it7!q;#?Q}=J1_d6LSb66E^?WsM`c(@YbGw(C7UruhGgexc82OJU#oEY-IjW#s zf+3jk-DNMA4{8z0Oxzn>Sf(ZPailf8cQFvYBitzd=gh0vw14Xrj;Qf`mTc2pWDpwT zm3cr@Rcyz#TQSe)q-r}HGznJ{@wqJgvX0fic}2&Ux-!vX_Tq!Dw61j(eCv4;uMnHH zS!NZ9V+uICZW=($X { + this._optionalContentConfig.toggleVisibility(groupId, input.checked); + + this.eventBus.dispatch("optionalcontentconfig", { + source: this, + optionalContentConfig: this._optionalContentConfig, + }); + }; + + input.onclick = evt => { + toggleVisibility(); + + evt.stopPropagation(); + return true; + }; + element.onclick = evt => { + if (evt.target !== element) { + return true; // The target is the "label", which is handled above. + } + input.checked = !input.checked; + toggleVisibility(); + + return false; + }; + } + + /** + * @private + */ + async _setNestedName(element, { name = null }) { + if (typeof name === "string") { + element.textContent = this._normalizeTextContent(name); + return; + } + element.textContent = await this.l10n.get( + "additional_layers", + null, + "Additional Layers" + ); + element.style.fontStyle = "italic"; + } + + /** + * @private + */ + _addToggleButton(div, { name = null }) { + super._addToggleButton(div, /* hidden = */ name === null); + } + + /** + * @private + */ + _toggleAllTreeItems() { + if (!this._optionalContentConfig) { + return; + } + super._toggleAllTreeItems(); + } + + /** + * @param {PDFLayerViewerRenderParameters} params + */ + render({ optionalContentConfig, pdfDocument }) { + if (this._optionalContentConfig) { + this.reset(); + } + this._optionalContentConfig = optionalContentConfig || null; + this._pdfDocument = pdfDocument || null; + + const groups = optionalContentConfig && optionalContentConfig.getGroups(); + if (!groups) { + this._dispatchEvent(/* layersCount = */ 0); + return; + } + + const fragment = document.createDocumentFragment(), + queue = [{ parent: fragment, groups }]; + let layersCount = 0, + hasAnyNesting = false; + while (queue.length > 0) { + const levelData = queue.shift(); + for (const groupId of levelData.groups) { + const div = document.createElement("div"); + div.className = "treeItem"; + + const element = document.createElement("a"); + div.appendChild(element); + + if (typeof groupId === "object") { + hasAnyNesting = true; + this._addToggleButton(div, groupId); + this._setNestedName(element, groupId); + + const itemsDiv = document.createElement("div"); + itemsDiv.className = "treeItems"; + div.appendChild(itemsDiv); + + queue.push({ parent: itemsDiv, groups: groupId.order }); + } else { + const group = optionalContentConfig.getGroup(groupId); + + const input = document.createElement("input"); + this._bindLink(element, { groupId, input }); + input.type = "checkbox"; + input.id = groupId; + input.checked = group.visible; + + const label = document.createElement("label"); + label.setAttribute("for", groupId); + label.textContent = this._normalizeTextContent(group.name); + + element.appendChild(input); + element.appendChild(label); + + layersCount++; + } + + levelData.parent.appendChild(div); + } + } + if (hasAnyNesting) { + this.container.classList.add("treeWithDeepNesting"); + + this._lastToggleIsShow = + fragment.querySelectorAll(".treeItemsHidden").length === 0; + } + + this.container.appendChild(fragment); + + this._dispatchEvent(layersCount); + } + + /** + * @private + */ + async _resetLayers() { + if (!this._optionalContentConfig) { + return; + } + // Fetch the default optional content configuration... + const optionalContentConfig = await this._pdfDocument.getOptionalContentConfig(); + + this.eventBus.dispatch("optionalcontentconfig", { + source: this, + optionalContentConfig, + }); + + // ... and reset the sidebarView to the default state. + this.render({ + optionalContentConfig, + pdfDocument: this._pdfDocument, + }); + } +} + +export { PDFLayerViewer }; diff --git a/web/pdf_outline_viewer.js b/web/pdf_outline_viewer.js index 780f080467072e..dc401a1baccc62 100644 --- a/web/pdf_outline_viewer.js +++ b/web/pdf_outline_viewer.js @@ -150,6 +150,7 @@ class PDFOutlineViewer extends BaseTreeViewer { const itemsDiv = document.createElement("div"); itemsDiv.className = "treeItems"; div.appendChild(itemsDiv); + queue.push({ parent: itemsDiv, items: item.items }); } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index ecf0326f8d8992..6ac25e041f1d81 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -38,6 +38,8 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js"; * @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} optionalContentConfigPromise - A promise that is resolved + * with an {OptionalContentConfig} instance. The default value is `null`. * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. * @property {IPDFTextLayerFactory} textLayerFactory * @property {number} [textLayerMode] - Controls if the text layer used for @@ -82,6 +84,8 @@ class PDFPageView { this.scale = options.scale || DEFAULT_SCALE; this.viewport = defaultViewport; this.pdfPageRotate = defaultViewport.rotation; + this._optionalContentConfigPromise = + options.optionalContentConfigPromise || null; this.hasRestrictedScaling = false; this.textLayerMode = Number.isInteger(options.textLayerMode) ? options.textLayerMode @@ -212,12 +216,16 @@ class PDFPageView { div.appendChild(this.loadingIconDiv); } - update(scale, rotation) { + update(scale, rotation, optionalContentConfigPromise) { this.scale = scale || this.scale; // The rotation may be zero. if (typeof rotation !== "undefined") { this.rotation = rotation; } + // The optionalContentConfigPromise is optional. + if (typeof optionalContentConfigPromise !== "undefined") { + this._optionalContentConfigPromise = optionalContentConfigPromise; + } const totalRotation = (this.rotation + this.pdfPageRotate) % 360; this.viewport = this.viewport.clone({ @@ -635,6 +643,7 @@ class PDFPageView { viewport: this.viewport, enableWebGL: this.enableWebGL, renderInteractiveForms: this.renderInteractiveForms, + optionalContentConfigPromise: this._optionalContentConfigPromise, }; const renderTask = this.pdfPage.render(renderContext); renderTask.onContinue = function (cont) { diff --git a/web/pdf_print_service.js b/web/pdf_print_service.js index 6d0b4a368e4dbe..1713fe377b18cf 100644 --- a/web/pdf_print_service.js +++ b/web/pdf_print_service.js @@ -22,7 +22,13 @@ let overlayManager = null; // Renders the page to the canvas of the given print service, and returns // the suggested dimensions of the output page. -function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) { +function renderPage( + activeServiceOnEntry, + pdfDocument, + pageNumber, + size, + optionalContentConfigPromise +) { const scratchCanvas = activeService.scratchCanvas; // The size of the canvas in pixels for printing. @@ -50,6 +56,7 @@ function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) { viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }), intent: "print", annotationStorage: pdfDocument.annotationStorage.getAll(), + optionalContentConfigPromise, }; return pdfPage.render(renderContext).promise; }) @@ -61,10 +68,18 @@ function renderPage(activeServiceOnEntry, pdfDocument, pageNumber, size) { }); } -function PDFPrintService(pdfDocument, pagesOverview, printContainer, l10n) { +function PDFPrintService( + pdfDocument, + pagesOverview, + printContainer, + optionalContentConfigPromise = null, + l10n +) { this.pdfDocument = pdfDocument; this.pagesOverview = pagesOverview; this.printContainer = printContainer; + this._optionalContentConfigPromise = + optionalContentConfigPromise || pdfDocument.getOptionalContentConfig(); this.l10n = l10n || NullL10n; this.disableCreateObjectURL = AppOptions.get("disableCreateObjectURL"); this.currentPage = -1; @@ -154,7 +169,13 @@ PDFPrintService.prototype = { } const index = this.currentPage; renderProgress(index, pageCount, this.l10n); - renderPage(this, this.pdfDocument, index + 1, this.pagesOverview[index]) + renderPage( + this, + this.pdfDocument, + /* pageNumber = */ index + 1, + this.pagesOverview[index], + this._optionalContentConfigPromise + ) .then(this.useRenderedPage.bind(this)) .then(function () { renderNextPage(resolve, reject); @@ -347,7 +368,13 @@ function ensureOverlay() { PDFPrintServiceFactory.instance = { supportsPrinting: true, - createPrintService(pdfDocument, pagesOverview, printContainer, l10n) { + createPrintService( + pdfDocument, + pagesOverview, + printContainer, + optionalContentConfigPromise, + l10n + ) { if (activeService) { throw new Error("The print service is created and active."); } @@ -355,6 +382,7 @@ PDFPrintServiceFactory.instance = { pdfDocument, pagesOverview, printContainer, + optionalContentConfigPromise, l10n ); return activeService; diff --git a/web/pdf_sidebar.js b/web/pdf_sidebar.js index c7a16251dec91e..49ccb39f606891 100644 --- a/web/pdf_sidebar.js +++ b/web/pdf_sidebar.js @@ -52,12 +52,16 @@ const SidebarView = { * the outline view. * @property {HTMLButtonElement} attachmentsButton - The button used to show * the attachments view. + * @property {HTMLButtonElement} layersButton - The button used to show + * the layers view. * @property {HTMLDivElement} thumbnailView - The container in which * the thumbnails are placed. * @property {HTMLDivElement} outlineView - The container in which * the outline is placed. * @property {HTMLDivElement} attachmentsView - The container in which * the attachments are placed. + * @property {HTMLDivElement} layersView - The container in which + * the layers are placed. */ class PDFSidebar { @@ -92,10 +96,12 @@ class PDFSidebar { this.thumbnailButton = elements.thumbnailButton; this.outlineButton = elements.outlineButton; this.attachmentsButton = elements.attachmentsButton; + this.layersButton = elements.layersButton; this.thumbnailView = elements.thumbnailView; this.outlineView = elements.outlineView; this.attachmentsView = elements.attachmentsView; + this.layersView = elements.layersView; this.eventBus = eventBus; this.l10n = l10n; @@ -112,6 +118,7 @@ class PDFSidebar { this.outlineButton.disabled = false; this.attachmentsButton.disabled = false; + this.layersButton.disabled = false; } /** @@ -133,6 +140,10 @@ class PDFSidebar { return this.isOpen && this.active === SidebarView.ATTACHMENTS; } + get isLayersViewVisible() { + return this.isOpen && this.active === SidebarView.LAYERS; + } + /** * @param {number} view - The sidebar view that should become visible, * must be one of the values in {SidebarView}. @@ -196,6 +207,11 @@ class PDFSidebar { return false; } break; + case SidebarView.LAYERS: + if (this.layersButton.disabled) { + return false; + } + break; default: console.error(`PDFSidebar._switchView: "${view}" is not a valid view.`); return false; @@ -217,6 +233,7 @@ class PDFSidebar { "toggled", view === SidebarView.ATTACHMENTS ); + this.layersButton.classList.toggle("toggled", view === SidebarView.LAYERS); // ... and for all views. this.thumbnailView.classList.toggle("hidden", view !== SidebarView.THUMBS); this.outlineView.classList.toggle("hidden", view !== SidebarView.OUTLINE); @@ -224,6 +241,7 @@ class PDFSidebar { "hidden", view !== SidebarView.ATTACHMENTS ); + this.layersView.classList.toggle("hidden", view !== SidebarView.LAYERS); if (forceOpen && !this.isOpen) { this.open(); @@ -331,9 +349,9 @@ class PDFSidebar { this.l10n .get( - "toggle_sidebar_notification.title", + "toggle_sidebar_notification2.title", null, - "Toggle Sidebar (document contains outline/attachments)" + "Toggle Sidebar (document contains outline/attachments/layers)" ) .then(msg => { this.toggleButton.title = msg; @@ -356,6 +374,9 @@ class PDFSidebar { case SidebarView.ATTACHMENTS: this.attachmentsButton.classList.add(UI_NOTIFICATION_CLASS); break; + case SidebarView.LAYERS: + this.layersButton.classList.add(UI_NOTIFICATION_CLASS); + break; } } @@ -375,6 +396,9 @@ class PDFSidebar { case SidebarView.ATTACHMENTS: this.attachmentsButton.classList.remove(UI_NOTIFICATION_CLASS); break; + case SidebarView.LAYERS: + this.layersButton.classList.remove(UI_NOTIFICATION_CLASS); + break; } }; @@ -429,6 +453,13 @@ class PDFSidebar { this.switchView(SidebarView.ATTACHMENTS); }); + this.layersButton.addEventListener("click", () => { + this.switchView(SidebarView.LAYERS); + }); + this.layersButton.addEventListener("dblclick", () => { + this.eventBus.dispatch("resetlayers", { source: this }); + }); + // Disable/enable views. const onTreeLoaded = (count, button, view) => { button.disabled = !count; @@ -454,6 +485,10 @@ class PDFSidebar { ); }); + this.eventBus._on("layersloaded", evt => { + onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS); + }); + // Update the thumbnailViewer, if visible, when exiting presentation mode. this.eventBus._on("presentationmodechanged", evt => { if (!evt.active && !evt.switchInProgress && this.isThumbnailViewVisible) { diff --git a/web/viewer.css b/web/viewer.css index c33e5674be4797..46d372042a22e9 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -884,6 +884,10 @@ html[dir="rtl"] #viewOutline.toolbarButton::before { content: url(images/toolbarButton-viewAttachments.png); } +#viewLayers.toolbarButton::before { + content: url(images/toolbarButton-viewLayers.png); +} + #viewFind.toolbarButton::before { content: url(images/toolbarButton-search.png); } @@ -1164,7 +1168,8 @@ a:focus > .thumbnail > .thumbnailSelectionRing, } #outlineView, -#attachmentsView { +#attachmentsView, +#layersView { position: absolute; width: calc(100% - 8px); top: 0; @@ -1208,6 +1213,16 @@ html[dir='rtl'] .treeItem > a { padding: 2px 4px 5px 0; } +.treeItem > a > label { + cursor: pointer; +} +html[dir='ltr'] .treeItem > a > label { + padding-left: 4px; +} +html[dir='rtl'] .treesItem > a > label { + padding-right: 4px; +} + .treeItemToggler { position: relative; height: 0; @@ -1665,6 +1680,10 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * { content: url(images/toolbarButton-viewAttachments@2x.png); } + #viewLayers.toolbarButton::before { + content: url(images/toolbarButton-viewLayers@2x.png); + } + #viewFind.toolbarButton::before { content: url(images/toolbarButton-search@2x.png); } diff --git a/web/viewer.html b/web/viewer.html index 75beacd30b87dc..7df42a827b5fe3 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -86,6 +86,9 @@ +
@@ -95,6 +98,8 @@
+ diff --git a/web/viewer.js b/web/viewer.js index f51cdd44e1d7dc..44f20ecc20a8e4 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -121,10 +121,12 @@ function getViewerConfiguration() { thumbnailButton: document.getElementById("viewThumbnail"), outlineButton: document.getElementById("viewOutline"), attachmentsButton: document.getElementById("viewAttachments"), + layersButton: document.getElementById("viewLayers"), // Views thumbnailView: document.getElementById("thumbnailView"), outlineView: document.getElementById("outlineView"), attachmentsView: document.getElementById("attachmentsView"), + layersView: document.getElementById("layersView"), }, sidebarResizer: { outerContainer: document.getElementById("outerContainer"),