Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-minor] Implement basic support for OptionalContent Usage dicts (issue 5764, bug 1826783) #17726

Merged
merged 2 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 62 additions & 14 deletions src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)) {
Expand Down
55 changes: 35 additions & 20 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down
81 changes: 73 additions & 8 deletions src/display/optional_content_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand All @@ -53,7 +83,9 @@ class OptionalContentConfig {

#order = null;

constructor(data) {
constructor(data, renderingIntent = RenderingIntentFlag.DISPLAY) {
this.renderingIntent = renderingIntent;

this.name = null;
this.creator = null;

Expand All @@ -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)
);
}

Expand Down Expand Up @@ -198,11 +230,44 @@ class OptionalContentConfig {
}

setVisibility(id, visible = true) {
if (!this.#groups.has(id)) {
const group = this.#groups.get(id);
if (!group) {
warn(`Optional content group not found: ${id}`);
return;
}
this.#groups.get(id)._setVisible(INTERNAL, !!visible);
group._setVisible(INTERNAL, !!visible, /* userSet = */ true);

this.#cachedGetHash = null;
}

setOCGState({ state, preserveRB }) {
let operator;

for (const elem of state) {
switch (elem) {
case "ON":
case "OFF":
case "Toggle":
operator = elem;
continue;
}

const group = this.#groups.get(elem);
if (!group) {
continue;
}
switch (operator) {
case "ON":
group._setVisible(INTERNAL, true);
break;
case "OFF":
group._setVisible(INTERNAL, false);
break;
case "Toggle":
group._setVisible(INTERNAL, !group.visible);
break;
}
}

this.#cachedGetHash = null;
}
Expand Down
4 changes: 3 additions & 1 deletion test/driver.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,9 @@ class Driver {
}

task.pdfDoc = doc;
task.optionalContentConfigPromise = doc.getOptionalContentConfig();
task.optionalContentConfigPromise = doc.getOptionalContentConfig({
intent: task.print ? "print" : "display",
});

if (task.optionalContent) {
const entries = Object.entries(task.optionalContent),
Expand Down
1 change: 1 addition & 0 deletions test/pdfs/bug1826783.pdf.link
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://bugzilla.mozilla.org/attachment.cgi?id=9327375
17 changes: 17 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4016,6 +4016,23 @@
"lastPage": 5,
"type": "eq"
},
{
"id": "bug1826783-display",
"file": "pdfs/bug1826783.pdf",
"md5": "93e706efee15dd7b32d32d66f15a3ea2",
"rounds": 1,
"link": true,
"type": "eq"
},
{
"id": "bug1826783-print",
"file": "pdfs/bug1826783.pdf",
"md5": "93e706efee15dd7b32d32d66f15a3ea2",
"rounds": 1,
"link": true,
"type": "eq",
"print": true
},
{
"id": "issue8586",
"file": "pdfs/issue8586.pdf",
Expand Down
1 change: 0 additions & 1 deletion web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1796,7 +1796,6 @@ const PDFViewerApplication = {
pagesOverview: this.pdfViewer.getPagesOverview(),
printContainer: this.appConfig.printContainer,
printResolution: AppOptions.get("printResolution"),
optionalContentConfigPromise: this.pdfViewer.optionalContentConfigPromise,
printAnnotationStoragePromise: this._printAnnotationStoragePromise,
});
this.forceRendering();
Expand Down