mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-22 16:18:08 +02:00
[api-minor] Implement basic support for OptionalContent Usage
dicts (issue 5764, bug 1826783)
The following are some highlights of this patch: - In the Worker we only extract a *subset* of the potential contents of the `Usage` dictionary, to avoid having to implement/test a bunch of code that'd be completely unused in the viewer. - In order to still allow the user to *manually* override the default visible layers in the viewer, the viewable/printable state is purposely *not* enforced during initialization in the `OptionalContentConfig` constructor. - Printing will now always use the *default* visible layers, rather than using the same state as the viewer (as was the case previously). This ensures that the printing-output will correctly take the `Usage` dictionary into account, and in practice toggling of visible layers rarely seem to be necessary except in the viewer itself (if at all).[1] --- [1] In the unlikely case that it'd ever be deemed necessary to support fine-grained control of optional content visibility during printing, some new (additional) UI would likely be needed to support that case.
This commit is contained in:
parent
e647311a89
commit
3c78ff5fb0
13 changed files with 186 additions and 62 deletions
|
@ -445,20 +445,10 @@ class Catalog {
|
|||
continue;
|
||||
}
|
||||
groupRefs.put(groupRef);
|
||||
const group = this.xref.fetch(groupRef);
|
||||
groups.push({
|
||||
id: groupRef.toString(),
|
||||
name:
|
||||
typeof group.get("Name") === "string"
|
||||
? stringToPDFString(group.get("Name"))
|
||||
: null,
|
||||
intent:
|
||||
typeof group.get("Intent") === "string"
|
||||
? stringToPDFString(group.get("Intent"))
|
||||
: null,
|
||||
});
|
||||
|
||||
groups.push(this.#readOptionalContentGroup(groupRef));
|
||||
}
|
||||
config = this._readOptionalContentConfig(defaultConfig, groupRefs);
|
||||
config = this.#readOptionalContentConfig(defaultConfig, groupRefs);
|
||||
config.groups = groups;
|
||||
} catch (ex) {
|
||||
if (ex instanceof MissingDataException) {
|
||||
|
@ -469,7 +459,65 @@ class Catalog {
|
|||
return shadow(this, "optionalContentConfig", config);
|
||||
}
|
||||
|
||||
_readOptionalContentConfig(config, contentGroupRefs) {
|
||||
#readOptionalContentGroup(groupRef) {
|
||||
const group = this.xref.fetch(groupRef);
|
||||
const obj = {
|
||||
id: groupRef.toString(),
|
||||
name: null,
|
||||
intent: null,
|
||||
usage: {
|
||||
print: null,
|
||||
view: null,
|
||||
},
|
||||
};
|
||||
|
||||
const name = group.get("Name");
|
||||
if (typeof name === "string") {
|
||||
obj.name = stringToPDFString(name);
|
||||
}
|
||||
|
||||
let intent = group.getArray("Intent");
|
||||
if (!Array.isArray(intent)) {
|
||||
intent = [intent];
|
||||
}
|
||||
if (intent.every(i => i instanceof Name)) {
|
||||
obj.intent = intent.map(i => i.name);
|
||||
}
|
||||
|
||||
const usage = group.get("Usage");
|
||||
if (!(usage instanceof Dict)) {
|
||||
return obj;
|
||||
}
|
||||
const usageObj = obj.usage;
|
||||
|
||||
const print = usage.get("Print");
|
||||
if (print instanceof Dict) {
|
||||
const printState = print.get("PrintState");
|
||||
if (printState instanceof Name) {
|
||||
switch (printState.name) {
|
||||
case "ON":
|
||||
case "OFF":
|
||||
usageObj.print = { printState: printState.name };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const view = usage.get("View");
|
||||
if (view instanceof Dict) {
|
||||
const viewState = view.get("ViewState");
|
||||
if (viewState instanceof Name) {
|
||||
switch (viewState.name) {
|
||||
case "ON":
|
||||
case "OFF":
|
||||
usageObj.view = { viewState: viewState.name };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
#readOptionalContentConfig(config, contentGroupRefs) {
|
||||
function parseOnOff(refs) {
|
||||
const onParsed = [];
|
||||
if (Array.isArray(refs)) {
|
||||
|
|
|
@ -949,12 +949,26 @@ class PDFDocumentProxy {
|
|||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} GetOptionalContentConfigParameters
|
||||
* @property {string} [intent] - Determines the optional content groups that
|
||||
* are visible by default; valid values are:
|
||||
* - 'display' (viewable groups).
|
||||
* - 'print' (printable groups).
|
||||
* - 'any' (all groups).
|
||||
* The default value is 'display'.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {GetOptionalContentConfigParameters} [params] - Optional content
|
||||
* config parameters.
|
||||
* @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();
|
||||
getOptionalContentConfig({ intent = "display" } = {}) {
|
||||
const { renderingIntent } = this._transport.getRenderingIntent(intent);
|
||||
|
||||
return this._transport.getOptionalContentConfig(renderingIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1340,17 +1354,14 @@ class PDFPageProxy {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {GetAnnotationsParameters} params - Annotation parameters.
|
||||
* @param {GetAnnotationsParameters} [params] - Annotation parameters.
|
||||
* @returns {Promise<Array<any>>} A promise that is resolved with an
|
||||
* {Array} of the annotation objects.
|
||||
*/
|
||||
getAnnotations({ intent = "display" } = {}) {
|
||||
const intentArgs = this._transport.getRenderingIntent(intent);
|
||||
const { renderingIntent } = this._transport.getRenderingIntent(intent);
|
||||
|
||||
return this._transport.getAnnotations(
|
||||
this._pageIndex,
|
||||
intentArgs.renderingIntent
|
||||
);
|
||||
return this._transport.getAnnotations(this._pageIndex, renderingIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1411,20 +1422,20 @@ class PDFPageProxy {
|
|||
annotationMode,
|
||||
printAnnotationStorage
|
||||
);
|
||||
const { renderingIntent, cacheKey } = intentArgs;
|
||||
// If there was a pending destroy, cancel it so no cleanup happens during
|
||||
// this call to render...
|
||||
this.#pendingCleanup = false;
|
||||
// ... and ensure that a delayed cleanup is always aborted.
|
||||
this.#abortDelayedCleanup();
|
||||
|
||||
if (!optionalContentConfigPromise) {
|
||||
optionalContentConfigPromise = this._transport.getOptionalContentConfig();
|
||||
}
|
||||
optionalContentConfigPromise ||=
|
||||
this._transport.getOptionalContentConfig(renderingIntent);
|
||||
|
||||
let intentState = this._intentStates.get(intentArgs.cacheKey);
|
||||
let intentState = this._intentStates.get(cacheKey);
|
||||
if (!intentState) {
|
||||
intentState = Object.create(null);
|
||||
this._intentStates.set(intentArgs.cacheKey, intentState);
|
||||
this._intentStates.set(cacheKey, intentState);
|
||||
}
|
||||
|
||||
// Ensure that a pending `streamReader` cancel timeout is always aborted.
|
||||
|
@ -1433,9 +1444,7 @@ class PDFPageProxy {
|
|||
intentState.streamReaderCancelTimeout = null;
|
||||
}
|
||||
|
||||
const intentPrint = !!(
|
||||
intentArgs.renderingIntent & RenderingIntentFlag.PRINT
|
||||
);
|
||||
const intentPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
|
||||
|
||||
// If there's no displayReadyCapability yet, then the operatorList
|
||||
// was never requested before. Make the request and create the promise.
|
||||
|
@ -1512,6 +1521,12 @@ class PDFPageProxy {
|
|||
}
|
||||
this._stats?.time("Rendering");
|
||||
|
||||
if (!(optionalContentConfig.renderingIntent & renderingIntent)) {
|
||||
throw new Error(
|
||||
"Must use the same `intent`-argument when calling the `PDFPageProxy.render` " +
|
||||
"and `PDFDocumentProxy.getOptionalContentConfig` methods."
|
||||
);
|
||||
}
|
||||
internalRenderTask.initializeGraphics({
|
||||
transparency,
|
||||
optionalContentConfig,
|
||||
|
@ -2994,10 +3009,10 @@ class WorkerTransport {
|
|||
return this.messageHandler.sendWithPromise("GetOutline", null);
|
||||
}
|
||||
|
||||
getOptionalContentConfig() {
|
||||
return this.messageHandler
|
||||
.sendWithPromise("GetOptionalContentConfig", null)
|
||||
.then(results => new OptionalContentConfig(results));
|
||||
getOptionalContentConfig(renderingIntent) {
|
||||
return this.#cacheSimpleMethod("GetOptionalContentConfig").then(
|
||||
data => new OptionalContentConfig(data, renderingIntent)
|
||||
);
|
||||
}
|
||||
|
||||
getPermissions() {
|
||||
|
|
|
@ -13,33 +13,63 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { info, objectFromMap, unreachable, warn } from "../shared/util.js";
|
||||
import {
|
||||
info,
|
||||
objectFromMap,
|
||||
RenderingIntentFlag,
|
||||
unreachable,
|
||||
warn,
|
||||
} from "../shared/util.js";
|
||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||
|
||||
const INTERNAL = Symbol("INTERNAL");
|
||||
|
||||
class OptionalContentGroup {
|
||||
#isDisplay = false;
|
||||
|
||||
#isPrint = false;
|
||||
|
||||
#userSet = false;
|
||||
|
||||
#visible = true;
|
||||
|
||||
constructor(name, intent) {
|
||||
constructor(renderingIntent, { name, intent, usage }) {
|
||||
this.#isDisplay = !!(renderingIntent & RenderingIntentFlag.DISPLAY);
|
||||
this.#isPrint = !!(renderingIntent & RenderingIntentFlag.PRINT);
|
||||
|
||||
this.name = name;
|
||||
this.intent = intent;
|
||||
this.usage = usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get visible() {
|
||||
return this.#visible;
|
||||
if (this.#userSet) {
|
||||
return this.#visible;
|
||||
}
|
||||
if (!this.#visible) {
|
||||
return false;
|
||||
}
|
||||
const { print, view } = this.usage;
|
||||
|
||||
if (this.#isDisplay) {
|
||||
return view?.viewState !== "OFF";
|
||||
} else if (this.#isPrint) {
|
||||
return print?.printState !== "OFF";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
_setVisible(internal, visible) {
|
||||
_setVisible(internal, visible, userSet = false) {
|
||||
if (internal !== INTERNAL) {
|
||||
unreachable("Internal method `_setVisible` called.");
|
||||
}
|
||||
this.#userSet = userSet;
|
||||
this.#visible = visible;
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +83,9 @@ class OptionalContentConfig {
|
|||
|
||||
#order = null;
|
||||
|
||||
constructor(data) {
|
||||
constructor(data, renderingIntent = RenderingIntentFlag.DISPLAY) {
|
||||
this.renderingIntent = renderingIntent;
|
||||
|
||||
this.name = null;
|
||||
this.creator = null;
|
||||
|
||||
|
@ -66,7 +98,7 @@ class OptionalContentConfig {
|
|||
for (const group of data.groups) {
|
||||
this.#groups.set(
|
||||
group.id,
|
||||
new OptionalContentGroup(group.name, group.intent)
|
||||
new OptionalContentGroup(renderingIntent, group)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -202,7 +234,7 @@ class OptionalContentConfig {
|
|||
warn(`Optional content group not found: ${id}`);
|
||||
return;
|
||||
}
|
||||
this.#groups.get(id)._setVisible(INTERNAL, !!visible);
|
||||
this.#groups.get(id)._setVisible(INTERNAL, !!visible, /* userSet = */ true);
|
||||
|
||||
this.#cachedGetHash = null;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue