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 mozilla#269.
  • Loading branch information
Brendan Dahl committed Jul 14, 2020
1 parent 6c39aff commit cfa898a
Show file tree
Hide file tree
Showing 12 changed files with 448 additions and 46 deletions.
81 changes: 78 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 = 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,61 @@ class PartialEvaluator {
throw new FormatError(`Unknown PatternName: ${patternName}`);
}

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 Error("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 content epxressions.
expression = true;
}

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

getOperatorList({
stream,
task,
Expand Down Expand Up @@ -1683,9 +1746,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 +1755,21 @@ 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;
}
fn = OPS.beginMarkedContentProps;
if (args[0].name === "OC") {
args = ["OC", self.parseMarkedContentProps(args[1], resources)];
} else {
// 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
56 changes: 56 additions & 0 deletions src/core/obj.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,62 @@ class Catalog {
return permissions;
}

get optionalContentConfig() {
const properties = this.catDict.get("OCProperties");
if (!properties) {
return null;
}
const groupsData = properties.get("OCGs");
if (!Array.isArray(groupsData)) {
return 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 = properties.xref.fetchIfRef(groupRef);
groups.push({
id: groupRef.toString(),
name: group.get("Name"),
intent: group.get("Intent"), // TODO validate this.
});
}
const defaultConfig = properties.get("D");
if (!defaultConfig) {
return null;
}
const config = this._readOptionalContentConfig(defaultConfig, groupRefs);
config.groups = groups;
return config;
}

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

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

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

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

handler.on("GetPermissions", function (data) {
return pdfManager.ensureCatalog("permissions");
});
Expand Down
47 changes: 42 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,15 @@ class PDFDocumentProxy {
return this._transport.getOutline();
}

/**
* @returns {Promise} A promise that is resolved with an
* {OptionalContentConfig} that will have all the optional content groups (if
* the document has any). @see {OptionalContentConfig}
*/
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 +892,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 automatically fetched with the default
* visibility states set.
*/

/**
Expand Down Expand Up @@ -1004,6 +1019,7 @@ class PDFPageProxy {
imageLayer = null,
canvasFactory = null,
background = null,
optionalContentConfigPromise = null,
}) {
if (this._stats) {
this._stats.time("Overall");
Expand All @@ -1014,6 +1030,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 +1126,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 +2473,16 @@ class WorkerTransport {
return this.messageHandler.sendWithPromise("GetOutline", null);
}

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

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

initializeGraphics(transparency = false) {
initializeGraphics({ transparency = false, optionalContentConfig }) {
if (this.cancelled) {
return;
}
Expand Down Expand Up @@ -2697,7 +2733,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 cfa898a

Please sign in to comment.