Merge pull request #12170 from Snuffleupagus/optional-content-viewer-2

[api-minor] Add support for toggling of Optional Content in the viewer (issue 12096)
This commit is contained in:
Tim van der Meij 2020-08-30 21:14:24 +02:00 committed by GitHub
commit aa27e7fb8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 556 additions and 39 deletions

View File

@ -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)

View File

@ -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=Ytterligare 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)

View File

@ -355,6 +355,67 @@ class Catalog {
return onParsed;
}
function parseOrder(refs, nestedLevels = 0) {
if (!Array.isArray(refs)) {
return null;
}
const order = [];
for (const value of refs) {
if (isRef(value) && contentGroupRefs.includes(value)) {
parsedOrderRefs.put(value); // Handle "hidden" groups, see below.
order.push(value.toString());
continue;
}
// Handle nested /Order arrays (see e.g. issue 9462 and bug 1240641).
const nestedOrder = parseNestedOrder(value, nestedLevels);
if (nestedOrder) {
order.push(nestedOrder);
}
}
if (nestedLevels > 0) {
return order;
}
const hiddenGroups = [];
for (const groupRef of contentGroupRefs) {
if (parsedOrderRefs.has(groupRef)) {
continue;
}
hiddenGroups.push(groupRef.toString());
}
if (hiddenGroups.length) {
order.push({ name: null, order: hiddenGroups });
}
return order;
}
function parseNestedOrder(ref, nestedLevels) {
if (++nestedLevels > MAX_NESTED_LEVELS) {
warn("parseNestedOrder - reached MAX_NESTED_LEVELS.");
return null;
}
const value = xref.fetchIfRef(ref);
if (!Array.isArray(value)) {
return null;
}
const nestedName = xref.fetchIfRef(value[0]);
if (typeof nestedName !== "string") {
return null;
}
const nestedOrder = parseOrder(value.slice(1), nestedLevels);
if (!nestedOrder || !nestedOrder.length) {
return null;
}
return { name: stringToPDFString(nestedName), order: nestedOrder };
}
const xref = this.xref,
parsedOrderRefs = new RefSet(),
MAX_NESTED_LEVELS = 10;
return {
name: isString(config.get("Name"))
? stringToPDFString(config.get("Name"))
@ -367,6 +428,8 @@ class Catalog {
: null,
on: parseOnOff(config.get("ON")),
off: parseOnOff(config.get("OFF")),
order: parseOrder(config.get("Order")),
groups: null,
};
}

View File

@ -778,9 +778,9 @@ class PDFDocumentProxy {
}
/**
* @returns {Promise<OptionalContentConfig | null>} A promise that is resolved
* with an {@link OptionalContentConfig} that has all the optional content
* groups, or `null` if the document does not have any.
* @returns {Promise<OptionalContentConfig>} A promise that is resolved with
* an {@link OptionalContentConfig} that contains all the optional content
* groups (assuming that the document has any).
*/
getOptionalContentConfig() {
return this._transport.getOptionalContentConfig();

View File

@ -26,42 +26,44 @@ class OptionalContentConfig {
constructor(data) {
this.name = null;
this.creator = null;
this.groups = new Map();
this._order = null;
this._groups = new Map();
if (data === null) {
return;
}
this.name = data.name;
this.creator = data.creator;
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) {
for (const group of this._groups) {
group.visible = false;
}
}
for (const on of data.on) {
this.groups.get(on).visible = true;
this._groups.get(on).visible = true;
}
for (const off of data.off) {
this.groups.get(off).visible = false;
this._groups.get(off).visible = false;
}
}
isVisible(group) {
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. Until
// we implement this, just fallback to using the group policy for now.
@ -71,44 +73,44 @@ class OptionalContentConfig {
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;
}
}
@ -120,6 +122,35 @@ class OptionalContentConfig {
warn(`Unknown group type ${group.type}.`);
return true;
}
setVisibility(id, visible = true) {
if (!this._groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return;
}
this._groups.get(id).visible = !!visible;
}
getOrder() {
if (!this._groups.size) {
return null;
}
if (this._order) {
return this._order.slice();
}
return Array.from(this._groups.keys());
}
getGroups() {
if (!this._groups.size) {
return null;
}
return Object.fromEntries(this._groups);
}
getGroup(id) {
return this._groups.get(id) || null;
}
}
export { OptionalContentConfig };

View File

@ -397,6 +397,8 @@ var Driver = (function DriverClosure() {
loadingTask.promise.then(
doc => {
task.pdfDoc = doc;
task.optionalContentConfigPromise = doc.getOptionalContentConfig();
this._nextPage(task, failure);
},
err => {
@ -605,6 +607,7 @@ var Driver = (function DriverClosure() {
canvasContext: ctx,
viewport,
renderInteractiveForms: renderForms,
optionalContentConfigPromise: task.optionalContentConfigPromise,
};
if (renderPrint) {
const annotationStorage = task.annotationStorage;

View File

@ -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";
@ -209,6 +210,8 @@ const PDFViewerApplication = {
pdfOutlineViewer: null,
/** @type {PDFAttachmentViewer} */
pdfAttachmentViewer: null,
/** @type {PDFLayerViewer} */
pdfLayerViewer: null,
/** @type {PDFCursorTools} */
pdfCursorTools: null,
/** @type {ViewHistory} */
@ -445,6 +448,7 @@ const PDFViewerApplication = {
this.pdfThumbnailViewer = new PDFThumbnailViewer({
container: appConfig.sidebar.thumbnailView,
eventBus,
renderingQueue: pdfRenderingQueue,
linkService: pdfLinkService,
l10n: this.l10n,
@ -509,6 +513,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,
@ -737,6 +747,7 @@ const PDFViewerApplication = {
this.pdfSidebar.reset();
this.pdfOutlineViewer.reset();
this.pdfAttachmentViewer.reset();
this.pdfLayerViewer.reset();
if (this.pdfHistory) {
this.pdfHistory.reset();
@ -1318,6 +1329,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.optionalContentConfigPromise.then(optionalContentConfig => {
this.pdfLayerViewer.render({ optionalContentConfig, pdfDocument });
});
});
this._initializePageLabels(pdfDocument);
@ -1667,12 +1683,15 @@ const PDFViewerApplication = {
const pagesOverview = this.pdfViewer.getPagesOverview();
const printContainer = this.appConfig.printContainer;
const printResolution = AppOptions.get("printResolution");
const optionalContentConfigPromise = this.pdfViewer
.optionalContentConfigPromise;
const printService = PDFPrintServiceFactory.instance.createPrintService(
this.pdfDocument,
pagesOverview,
printContainer,
printResolution,
optionalContentConfigPromise,
this.l10n
);
this.printService = printService;
@ -1748,6 +1767,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);
@ -1827,6 +1847,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);
@ -2169,12 +2190,15 @@ function webViewerPageMode({ mode }) {
view = SidebarView.THUMBS;
break;
case "bookmarks":
case "outline":
case "outline": // non-standard
view = SidebarView.OUTLINE;
break;
case "attachments":
case "attachments": // non-standard
view = SidebarView.ATTACHMENTS;
break;
case "layers": // non-standard
view = SidebarView.LAYERS;
break;
case "none":
view = SidebarView.NONE;
break;
@ -2420,6 +2444,9 @@ function webViewerRotateCw() {
function webViewerRotateCcw() {
PDFViewerApplication.rotatePages(-90);
}
function webViewerOptionalContentConfig(evt) {
PDFViewerApplication.pdfViewer.optionalContentConfigPromise = evt.promise;
}
function webViewerSwitchScrollMode(evt) {
PDFViewerApplication.pdfViewer.scrollMode = evt.mode;
}
@ -3013,7 +3040,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.
}

View File

@ -439,6 +439,7 @@ class BaseViewer {
const firstPagePromise = pdfDocument.getPage(1);
const annotationStorage = pdfDocument.annotationStorage;
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
this._pagesCapability.promise.then(() => {
this.eventBus.dispatch("pagesloaded", {
@ -474,6 +475,7 @@ class BaseViewer {
firstPagePromise
.then(firstPdfPage => {
this._firstPageCapability.resolve(firstPdfPage);
this._optionalContentConfigPromise = optionalContentConfigPromise;
const scale = this.currentScale;
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
@ -486,8 +488,9 @@ class BaseViewer {
eventBus: this.eventBus,
id: pageNum,
scale,
annotationStorage,
defaultViewport: viewport.clone(),
annotationStorage,
optionalContentConfigPromise,
renderingQueue: this.renderingQueue,
textLayerFactory,
textLayerMode: this.textLayerMode,
@ -605,6 +608,7 @@ class BaseViewer {
this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE);
this._location = null;
this._pagesRotation = 0;
this._optionalContentConfigPromise = null;
this._pagesRequests = new WeakMap();
this._firstPageCapability = createPromiseCapability();
this._onePageRenderedCapability = createPromiseCapability();
@ -1222,6 +1226,50 @@ class BaseViewer {
});
}
/**
* @type {Promise<OptionalContentConfig | null>}
*/
get optionalContentConfigPromise() {
if (!this.pdfDocument) {
return Promise.resolve(null);
}
if (!this._optionalContentConfigPromise) {
// 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();
}
return this._optionalContentConfigPromise;
}
/**
* @param {Promise<OptionalContentConfig>} promise - A promise that is
* resolved with an {@link OptionalContentConfig} instance.
*/
set optionalContentConfigPromise(promise) {
if (!(promise instanceof Promise)) {
throw new Error(`Invalid optionalContentConfigPromise: ${promise}`);
}
if (!this.pdfDocument) {
return;
}
if (!this._optionalContentConfigPromise) {
// Ignore the setter *before* the `onePageRendered` promise has resolved,
// since it'll be overwritten anyway; won't happen in the default viewer.
return;
}
this._optionalContentConfigPromise = promise;
for (const pageView of this._pages) {
pageView.update(pageView.scale, pageView.rotation, promise);
}
this.update();
this.eventBus.dispatch("optionalcontentconfigchanged", {
source: this,
promise,
});
}
/**
* @type {number} One of the values in {ScrollMode}.
*/

View File

@ -23,7 +23,8 @@ function composePage(
pageNumber,
size,
printContainer,
printResolution
printResolution,
optionalContentConfigPromise
) {
const canvas = document.createElement("canvas");
@ -58,6 +59,7 @@ function composePage(
viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
intent: "print",
annotationStorage: pdfDocument.annotationStorage,
optionalContentConfigPromise,
};
return pdfPage.render(renderContext).promise;
})
@ -84,12 +86,15 @@ function FirefoxPrintService(
pdfDocument,
pagesOverview,
printContainer,
printResolution
printResolution,
optionalContentConfigPromise = null
) {
this.pdfDocument = pdfDocument;
this.pagesOverview = pagesOverview;
this.printContainer = printContainer;
this._printResolution = printResolution || 150;
this._optionalContentConfigPromise =
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
}
FirefoxPrintService.prototype = {
@ -99,6 +104,7 @@ FirefoxPrintService.prototype = {
pagesOverview,
printContainer,
_printResolution,
_optionalContentConfigPromise,
} = this;
const body = document.querySelector("body");
@ -110,7 +116,8 @@ FirefoxPrintService.prototype = {
/* pageNumber = */ i + 1,
pagesOverview[i],
printContainer,
_printResolution
_printResolution,
_optionalContentConfigPromise
);
}
},
@ -135,13 +142,15 @@ PDFPrintServiceFactory.instance = {
pdfDocument,
pagesOverview,
printContainer,
printResolution
printResolution,
optionalContentConfigPromise
) {
return new FirefoxPrintService(
pdfDocument,
pagesOverview,
printContainer,
printResolution
printResolution,
optionalContentConfigPromise
);
},
};

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.233 4.233" height="16" width="16" fill="rgba(255,255,255,1)"><path d="M.15 2.992c-.198.1-.2.266-.002.365l1.604.802a.93.93 0 00.729-.001l1.602-.801c.198-.1.197-.264 0-.364l-.695-.348c-1.306.595-2.542 0-2.542 0m-.264.53l.658-.329c.6.252 1.238.244 1.754 0l.659.329-1.536.768zM.15 1.935c-.198.1-.198.265 0 .364l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363l-.694-.35c-1.14.56-2.546.001-2.546.001m-.264.53l.664-.332c.52.266 1.261.235 1.75.002l.659.33-1.537.768zM.15.877c-.198.099-.198.264 0 .363l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363L2.481.075a.926.926 0 00-.727 0zm.43.182L2.117.29l1.538.769-1.538.768z"/></svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4.233 4.233" height="16" width="16"><path d="M.15 2.992c-.198.1-.2.266-.002.365l1.604.802a.93.93 0 00.729-.001l1.602-.801c.198-.1.197-.264 0-.364l-.695-.348c-1.306.595-2.542 0-2.542 0m-.264.53l.658-.329c.6.252 1.238.244 1.754 0l.659.329-1.536.768zM.15 1.935c-.198.1-.198.265 0 .364l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363l-.694-.35c-1.14.56-2.546.001-2.546.001m-.264.53l.664-.332c.52.266 1.261.235 1.75.002l.659.33-1.537.768zM.15.877c-.198.099-.198.264 0 .363l1.604.802a.926.926 0 00.727 0l1.603-.802c.198-.099.198-.264 0-.363L2.481.075a.926.926 0 00-.727 0zm.43.182L2.117.29l1.538.769-1.538.768z"/></svg>

After

Width:  |  Height:  |  Size: 685 B

212
web/pdf_layer_viewer.js Normal file
View File

@ -0,0 +1,212 @@
/* Copyright 2020 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 { BaseTreeViewer } from "./base_tree_viewer.js";
/**
* @typedef {Object} PDFLayerViewerOptions
* @property {HTMLDivElement} container - The viewer element.
* @property {EventBus} eventBus - The application event bus.
* @property {IL10n} l10n - Localization service.
*/
/**
* @typedef {Object} PDFLayerViewerRenderParameters
* @property {OptionalContentConfig|null} optionalContentConfig - An
* {OptionalContentConfig} instance.
* @property {PDFDocument} pdfDocument - A {PDFDocument} instance.
*/
class PDFLayerViewer extends BaseTreeViewer {
constructor(options) {
super(options);
this.l10n = options.l10n;
this.eventBus._on("resetlayers", this._resetLayers.bind(this));
this.eventBus._on("togglelayerstree", this._toggleAllTreeItems.bind(this));
}
reset() {
super.reset();
this._optionalContentConfig = null;
this._pdfDocument = null;
}
/**
* @private
*/
_dispatchEvent(layersCount) {
this.eventBus.dispatch("layersloaded", {
source: this,
layersCount,
});
}
/**
* @private
*/
_bindLink(element, { groupId, input }) {
const setVisibility = () => {
this._optionalContentConfig.setVisibility(groupId, input.checked);
this.eventBus.dispatch("optionalcontentconfig", {
source: this,
promise: Promise.resolve(this._optionalContentConfig),
});
};
element.onclick = evt => {
if (evt.target === input) {
setVisibility();
return true;
} else if (evt.target !== element) {
return true; // The target is the "label", which is handled above.
}
input.checked = !input.checked;
setVisibility();
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.getOrder();
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,
promise: Promise.resolve(optionalContentConfig),
});
// ... and reset the sidebarView to the default state.
this.render({
optionalContentConfig,
pdfDocument: this._pdfDocument,
});
}
}
export { PDFLayerViewer };

View File

@ -40,6 +40,9 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js";
* @property {PageViewport} defaultViewport - The page viewport.
* @property {AnnotationStorage} [annotationStorage] - Storage for annotation
* data in forms. The default value is `null`.
* @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 {IPDFTextLayerFactory} textLayerFactory
* @property {number} [textLayerMode] - Controls if the text layer used for
@ -83,8 +86,10 @@ class PDFPageView {
this.rotation = 0;
this.scale = options.scale || DEFAULT_SCALE;
this.viewport = defaultViewport;
this._annotationStorage = options.annotationStorage || null;
this.pdfPageRotate = defaultViewport.rotation;
this._annotationStorage = options.annotationStorage || null;
this._optionalContentConfigPromise =
options.optionalContentConfigPromise || null;
this.hasRestrictedScaling = false;
this.textLayerMode = Number.isInteger(options.textLayerMode)
? options.textLayerMode
@ -236,12 +241,15 @@ class PDFPageView {
div.appendChild(this.loadingIconDiv);
}
update(scale, rotation) {
update(scale, rotation, optionalContentConfigPromise = null) {
this.scale = scale || this.scale;
// The rotation may be zero.
if (typeof rotation !== "undefined") {
this.rotation = rotation;
}
if (optionalContentConfigPromise instanceof Promise) {
this._optionalContentConfigPromise = optionalContentConfigPromise;
}
const totalRotation = (this.rotation + this.pdfPageRotate) % 360;
this.viewport = this.viewport.clone({
@ -660,6 +668,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) {

View File

@ -27,7 +27,8 @@ function renderPage(
pdfDocument,
pageNumber,
size,
printResolution
printResolution,
optionalContentConfigPromise
) {
const scratchCanvas = activeService.scratchCanvas;
@ -55,6 +56,7 @@ function renderPage(
viewport: pdfPage.getViewport({ scale: 1, rotation: size.rotation }),
intent: "print",
annotationStorage: pdfDocument.annotationStorage,
optionalContentConfigPromise,
};
return pdfPage.render(renderContext).promise;
})
@ -71,12 +73,15 @@ function PDFPrintService(
pagesOverview,
printContainer,
printResolution,
optionalContentConfigPromise = null,
l10n
) {
this.pdfDocument = pdfDocument;
this.pagesOverview = pagesOverview;
this.printContainer = printContainer;
this._printResolution = printResolution || 150;
this._optionalContentConfigPromise =
optionalContentConfigPromise || pdfDocument.getOptionalContentConfig();
this.l10n = l10n || NullL10n;
this.currentPage = -1;
// The temporary canvas where renderPage paints one page at a time.
@ -170,7 +175,8 @@ PDFPrintService.prototype = {
this.pdfDocument,
/* pageNumber = */ index + 1,
this.pagesOverview[index],
this._printResolution
this._printResolution,
this._optionalContentConfigPromise
)
.then(this.useRenderedPage.bind(this))
.then(function () {
@ -372,6 +378,7 @@ PDFPrintServiceFactory.instance = {
pagesOverview,
printContainer,
printResolution,
optionalContentConfigPromise,
l10n
) {
if (activeService) {
@ -382,6 +389,7 @@ PDFPrintServiceFactory.instance = {
pagesOverview,
printContainer,
printResolution,
optionalContentConfigPromise,
l10n
);
return activeService;

View File

@ -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) {

View File

@ -29,8 +29,12 @@ const THUMBNAIL_WIDTH = 98; // px
* @property {HTMLDivElement} container - The viewer element.
* @property {number} id - The thumbnail's unique ID (normally its number).
* @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 {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {function} checkSetImageDisabled
* @property {boolean} [disableCanvasToImageConversion] - Don't convert the
* canvas thumbnails to images. This prevents `toDataURL` calls, but
* increases the overall memory usage. The default value is `false`.
@ -91,8 +95,10 @@ class PDFThumbnailView {
container,
id,
defaultViewport,
optionalContentConfigPromise,
linkService,
renderingQueue,
checkSetImageDisabled,
disableCanvasToImageConversion = false,
l10n = NullL10n,
}) {
@ -104,6 +110,7 @@ class PDFThumbnailView {
this.rotation = 0;
this.viewport = defaultViewport;
this.pdfPageRotate = defaultViewport.rotation;
this._optionalContentConfigPromise = optionalContentConfigPromise || null;
this.linkService = linkService;
this.renderingQueue = renderingQueue;
@ -111,6 +118,11 @@ class PDFThumbnailView {
this.renderTask = null;
this.renderingState = RenderingStates.INITIAL;
this.resume = null;
this._checkSetImageDisabled =
checkSetImageDisabled ||
function () {
return false;
};
this.disableCanvasToImageConversion = disableCanvasToImageConversion;
this.pageWidth = this.viewport.width;
@ -345,6 +357,7 @@ class PDFThumbnailView {
const renderContext = {
canvasContext: ctx,
viewport: drawViewport,
optionalContentConfigPromise: this._optionalContentConfigPromise,
};
const renderTask = (this.renderTask = pdfPage.render(renderContext));
renderTask.onContinue = renderContinueCallback;
@ -361,6 +374,9 @@ class PDFThumbnailView {
}
setImage(pageView) {
if (this._checkSetImageDisabled()) {
return;
}
if (this.renderingState !== RenderingStates.INITIAL) {
return;
}

View File

@ -29,6 +29,7 @@ const THUMBNAIL_SELECTED_CLASS = "selected";
* @typedef {Object} PDFThumbnailViewerOptions
* @property {HTMLDivElement} container - The container for the thumbnail
* elements.
* @property {EventBus} eventBus - The application event bus.
* @property {IPDFLinkService} linkService - The navigation/linking service.
* @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
* @property {IL10n} l10n - Localization service.
@ -43,7 +44,13 @@ class PDFThumbnailViewer {
/**
* @param {PDFThumbnailViewerOptions} options
*/
constructor({ container, linkService, renderingQueue, l10n = NullL10n }) {
constructor({
container,
eventBus,
linkService,
renderingQueue,
l10n = NullL10n,
}) {
this.container = container;
this.linkService = linkService;
this.renderingQueue = renderingQueue;
@ -51,6 +58,12 @@ 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;
});
}
/**
@ -151,7 +164,9 @@ class PDFThumbnailViewer {
this._currentPageNumber = 1;
this._pageLabels = null;
this._pagesRotation = 0;
this._optionalContentConfigPromise = null;
this._pagesRequests = new WeakMap();
this._setImageDisabled = false;
// Remove the thumbnails from the DOM.
this.container.textContent = "";
@ -167,19 +182,28 @@ class PDFThumbnailViewer {
if (!pdfDocument) {
return;
}
const firstPagePromise = pdfDocument.getPage(1);
const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig();
pdfDocument
.getPage(1)
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({
container: this.container,
id: pageNum,
defaultViewport: viewport.clone(),
optionalContentConfigPromise,
linkService: this.linkService,
renderingQueue: this.renderingQueue,
checkSetImageDisabled,
disableCanvasToImageConversion: false,
l10n: this.l10n,
});

View File

@ -73,6 +73,7 @@
--toolbarButton-viewThumbnail-icon: url(images/toolbarButton-viewThumbnail.svg);
--toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline.svg);
--toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments.svg);
--toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers.svg);
--toolbarButton-search-icon: url(images/toolbarButton-search.svg);
--findbarButton-previous-icon: url(images/findbarButton-previous.svg);
--findbarButton-next-icon: url(images/findbarButton-next.svg);
@ -143,6 +144,7 @@
--toolbarButton-viewThumbnail-icon: url(images/toolbarButton-viewThumbnail-dark.svg);
--toolbarButton-viewOutline-icon: url(images/toolbarButton-viewOutline-dark.svg);
--toolbarButton-viewAttachments-icon: url(images/toolbarButton-viewAttachments-dark.svg);
--toolbarButton-viewLayers-icon: url(images/toolbarButton-viewLayers-dark.svg);
--toolbarButton-search-icon: url(images/toolbarButton-search-dark.svg);
--findbarButton-previous-icon: url(images/findbarButton-previous-dark.svg);
--findbarButton-next-icon: url(images/findbarButton-next-dark.svg);
@ -1066,6 +1068,10 @@ html[dir="rtl"] #viewOutline.toolbarButton::before {
content: var(--toolbarButton-viewAttachments-icon);
}
#viewLayers.toolbarButton::before {
content: var(--toolbarButton-viewLayers-icon);
}
#viewFind.toolbarButton::before {
content: var(--toolbarButton-search-icon);
}
@ -1339,7 +1345,8 @@ a:focus > .thumbnail > .thumbnailSelectionRing,
}
#outlineView,
#attachmentsView {
#attachmentsView,
#layersView {
position: absolute;
width: calc(100% - 8px);
top: 0;
@ -1383,6 +1390,16 @@ html[dir='rtl'] .treeItem > a {
padding: 2px 4px 5px 0;
}
#layersView .treeItem > a > * {
cursor: pointer;
}
html[dir='ltr'] #layersView .treeItem > a > label {
padding-left: 4px;
}
html[dir='rtl'] #layersView .treesItem > a > label {
padding-right: 4px;
}
.treeItemToggler {
position: relative;
height: 0;

View File

@ -86,6 +86,9 @@ See https://github.com/adobe-type-tools/cmap-resources
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4" data-l10n-id="attachments">
<span data-l10n-id="attachments_label">Attachments</span>
</button>
<button id="viewLayers" class="toolbarButton" title="Show Layers (double-click to reset all layers to the default state)" tabindex="5" data-l10n-id="layers">
<span data-l10n-id="layers_label">Layers</span>
</button>
</div>
</div>
<div id="sidebarContent">
@ -95,6 +98,8 @@ See https://github.com/adobe-type-tools/cmap-resources
</div>
<div id="attachmentsView" class="hidden">
</div>
<div id="layersView" class="hidden">
</div>
</div>
<div id="sidebarResizer" class="hidden"></div>
</div> <!-- sidebarContainer -->

View File

@ -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"),