Skip to content

Commit

Permalink
Add support for optional marked content.
Browse files Browse the repository at this point in the history
Add a new method to the API to get the optional content configuration. Add
a new render task param that accepts the above configuration.
For now, the optional content is not controllable by the user in
the viewer, but renders with the default configuration in the PDF.

All of the test files added exhibit different uses of optional content.

Fixes #269.

Fix test to work with optional content.

- Change the stopAtErrors test to ensure the operator list has something,
  instead of asserting the exact number of operators.
  • Loading branch information
Brendan Dahl authored and brendandahl committed Jul 17, 2020
1 parent 6c39aff commit 9bf3bf0
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 53 deletions.
107 changes: 104 additions & 3 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,16 @@ class PartialEvaluator {
smask,
isolated: false,
knockout: false,
optionalContent: null,
};

if (dict.has("OC")) {
groupOptions.optionalContent = await this.parseMarkedContentProps(
dict.get("OC"),
resources
);
}

var groupSubtype = group.get("S");
var colorSpace = null;
if (isName(groupSubtype, "Transparency")) {
Expand Down Expand Up @@ -1224,6 +1232,63 @@ class PartialEvaluator {
throw new FormatError(`Unknown PatternName: ${patternName}`);
}

async parseMarkedContentProps(contentProperties, resources) {
let optionalContent;
if (isName(contentProperties)) {
const properties = resources.get("Properties");
optionalContent = properties.get(contentProperties.name);
} else if (isDict(contentProperties)) {
optionalContent = contentProperties;
} else {
throw new FormatError("Optional content properties malformed.");
}

const optionalContentType = optionalContent.get("Type").name;
if (optionalContentType === "OCG") {
return {
type: optionalContentType,
id: optionalContent.objId,
};
} else if (optionalContentType === "OCMD") {
const optionalContentGroups = optionalContent.get("OCGs");
if (
Array.isArray(optionalContentGroups) ||
isDict(optionalContentGroups)
) {
const groupIds = [];
if (Array.isArray(optionalContentGroups)) {
optionalContent.get("OCGs").forEach(ocg => {
groupIds.push(ocg.toString());
});
} else {
// Dictionary, just use the obj id.
groupIds.push(optionalContentGroups.objId);
}

let expression = null;
if (optionalContent.get("VE")) {
// TODO support visibility expression.
expression = true;
}

return {
type: optionalContentType,
ids: groupIds,
policy: isName(optionalContent.get("P"))
? optionalContent.get("P").name
: null,
expression,
};
} else if (isRef(optionalContentGroups)) {
return {
type: optionalContentType,
id: optionalContentGroups.toString(),
};
}
}
return null;
}

getOperatorList({
stream,
task,
Expand Down Expand Up @@ -1683,9 +1748,6 @@ class PartialEvaluator {
continue;
case OPS.markPoint:
case OPS.markPointProps:
case OPS.beginMarkedContent:
case OPS.beginMarkedContentProps:
case OPS.endMarkedContent:
case OPS.beginCompat:
case OPS.endCompat:
// Ignore operators where the corresponding handlers are known to
Expand All @@ -1695,6 +1757,45 @@ class PartialEvaluator {
// e.g. as done in https://github.com/mozilla/pdf.js/pull/6266,
// but doing so is meaningless without knowing the semantics.
continue;
case OPS.beginMarkedContentProps:
if (!isName(args[0])) {
warn(`Expected name for beginMarkedContentProps arg0=${args[0]}`);
continue;
}
if (args[0].name === "OC") {
next(
self
.parseMarkedContentProps(args[1], resources)
.then(data => {
operatorList.addOp(OPS.beginMarkedContentProps, [
"OC",
data,
]);
})
.catch(reason => {
if (reason instanceof AbortException) {
return;
}
if (self.options.ignoreErrors) {
self.handler.send("UnsupportedFeature", {
featureId: UNSUPPORTED_FEATURES.errorMarkedContent,
});
warn(
`getOperatorList - ignoring beginMarkedContentProps: "${reason}".`
);
return;
}
throw reason;
})
);
return;
}
// Other marked content types aren't supported yet.
args = [args[0].name];

break;
case OPS.beginMarkedContent:
case OPS.endMarkedContent:
default:
// Note: Ignore the operator if it has `Dict` arguments, since
// those are non-serializable, otherwise postMessage will throw
Expand Down
76 changes: 76 additions & 0 deletions src/core/obj.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,82 @@ class Catalog {
return permissions;
}

get optionalContentConfig() {
let config = null;
try {
const properties = this.catDict.get("OCProperties");
if (!properties) {
return shadow(this, "optionalContentConfig", null);
}
const defaultConfig = properties.get("D");
if (!defaultConfig) {
return shadow(this, "optionalContentConfig", null);
}
const groupsData = properties.get("OCGs");
if (!Array.isArray(groupsData)) {
return shadow(this, "optionalContentConfig", null);
}
const groups = [];
const groupRefs = [];
// Ensure all the optional content groups are valid.
for (const groupRef of groupsData) {
if (!isRef(groupRef)) {
continue;
}
groupRefs.push(groupRef);
const group = this.xref.fetchIfRef(groupRef);
groups.push({
id: groupRef.toString(),
name: isString(group.get("Name"))
? stringToPDFString(group.get("Name"))
: null,
intent: isString(group.get("Intent"))
? stringToPDFString(group.get("Intent"))
: null,
});
}
config = this._readOptionalContentConfig(defaultConfig, groupRefs);
config.groups = groups;
} catch (ex) {
if (ex instanceof MissingDataException) {
throw ex;
}
warn(`Unable to read optional content config: ${ex}`);
}
return shadow(this, "optionalContentConfig", config);
}

_readOptionalContentConfig(config, contentGroupRefs) {
function parseOnOff(refs) {
const onParsed = [];
if (Array.isArray(refs)) {
for (const value of refs) {
if (!isRef(value)) {
continue;
}
if (contentGroupRefs.includes(value)) {
onParsed.push(value.toString());
}
}
}
return onParsed;
}

return {
name: isString(config.get("Name"))
? stringToPDFString(config.get("Name"))
: null,
creator: isString(config.get("Creator"))
? stringToPDFString(config.get("Creator"))
: null,
baseState: isName(config.get("BaseState"))
? config.get("BaseState").name
: null,
on: parseOnOff(config.get("ON")),
off: parseOnOff(config.get("OFF")),
};
}

get numPages() {
const obj = this.toplevelPagesDict.get("Count");
if (!Number.isInteger(obj)) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,10 @@ class WorkerMessageHandler {
return pdfManager.ensureCatalog("documentOutline");
});

handler.on("GetOptionalContentConfig", function (data) {
return pdfManager.ensureCatalog("optionalContentConfig");
});

handler.on("GetPermissions", function (data) {
return pdfManager.ensureCatalog("permissions");
});
Expand Down
46 changes: 41 additions & 5 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { GlobalWorkerOptions } from "./worker_options.js";
import { isNodeJS } from "../shared/is_node.js";
import { MessageHandler } from "../shared/message_handler.js";
import { Metadata } from "./metadata.js";
import { OptionalContentConfig } from "./optional_content_config.js";
import { PDFDataTransportStream } from "./transport_stream.js";
import { WebGLContext } from "./webgl.js";

Expand Down Expand Up @@ -709,6 +710,16 @@ class PDFDocumentProxy {
return this._transport.getOutline();
}

/**
* @see {OptionalContentConfig}
* @returns {Promise} A promise that is resolved with an
* {OptionalContentConfig} that will have all the optional content groups (if
* the document has any).
*/
getOptionalContentConfig() {
return this._transport.getOptionalContentConfig();
}

/**
* @returns {Promise} A promise that is resolved with an {Array} that contains
* the permission flags for the PDF document, or `null` when
Expand Down Expand Up @@ -882,6 +893,11 @@ class PDFDocumentProxy {
* CSS <color> value, a CanvasGradient object (a linear or
* radial gradient) or a CanvasPattern object (a repetitive
* image). The default value is 'rgb(255,255,255)'.
* @property {Promise} [optionalContentConfigPromise] - A promise that should
* resolve with an {OptionalContentConfig} created from
* PDFDocumentProxy.getOptionalContentConfig. If null, the
* config will be automatically fetched with the default
* visibility states set.
*/

/**
Expand Down Expand Up @@ -1004,6 +1020,7 @@ class PDFPageProxy {
imageLayer = null,
canvasFactory = null,
background = null,
optionalContentConfigPromise = null,
}) {
if (this._stats) {
this._stats.time("Overall");
Expand All @@ -1014,6 +1031,10 @@ class PDFPageProxy {
// this call to render.
this.pendingCleanup = false;

if (!optionalContentConfigPromise) {
optionalContentConfigPromise = this._transport.getOptionalContentConfig();
}

let intentState = this._intentStates.get(renderingIntent);
if (!intentState) {
intentState = Object.create(null);
Expand Down Expand Up @@ -1106,16 +1127,22 @@ class PDFPageProxy {
intentState.renderTasks.push(internalRenderTask);
const renderTask = internalRenderTask.task;

intentState.displayReadyCapability.promise
.then(transparency => {
Promise.all([
intentState.displayReadyCapability.promise,
optionalContentConfigPromise,
])
.then(([transparency, optionalContentConfig]) => {
if (this.pendingCleanup) {
complete();
return;
}
if (this._stats) {
this._stats.time("Rendering");
}
internalRenderTask.initializeGraphics(transparency);
internalRenderTask.initializeGraphics({
transparency,
optionalContentConfig,
});
internalRenderTask.operatorListChanged();
})
.catch(complete);
Expand Down Expand Up @@ -2447,6 +2474,14 @@ class WorkerTransport {
return this.messageHandler.sendWithPromise("GetOutline", null);
}

getOptionalContentConfig() {
return this.messageHandler
.sendWithPromise("GetOptionalContentConfig", null)
.then(results => {
return new OptionalContentConfig(results);
});
}

getPermissions() {
return this.messageHandler.sendWithPromise("GetPermissions", null);
}
Expand Down Expand Up @@ -2659,7 +2694,7 @@ const InternalRenderTask = (function InternalRenderTaskClosure() {
});
}

initializeGraphics(transparency = false) {
initializeGraphics({ transparency = false, optionalContentConfig }) {
if (this.cancelled) {
return;
}
Expand Down Expand Up @@ -2697,7 +2732,8 @@ const InternalRenderTask = (function InternalRenderTaskClosure() {
this.objs,
this.canvasFactory,
this.webGLContext,
imageLayer
imageLayer,
optionalContentConfig
);
this.gfx.beginDrawing({
transform,
Expand Down
Loading

0 comments on commit 9bf3bf0

Please sign in to comment.