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

Add support for optional marked content. #12095

Merged
merged 1 commit into from
Aug 4, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 108 additions & 3 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,14 @@ class PartialEvaluator {
} else {
bbox = null;
}
let optionalContent = null;
if (dict.has("OC")) {
optionalContent = await this.parseMarkedContentProps(
dict.get("OC"),
resources
);
operatorList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
}
var group = dict.get("Group");
if (group) {
var groupOptions = {
Expand Down Expand Up @@ -449,6 +457,10 @@ class PartialEvaluator {
if (group) {
operatorList.addOp(OPS.endGroup, [groupOptions]);
}

if (optionalContent) {
operatorList.addOp(OPS.endMarkedContent, []);
}
});
}

Expand Down Expand Up @@ -1202,6 +1214,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 @@ -1704,9 +1773,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 @@ -1716,6 +1782,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]}`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it actually make sense to simply move this check into the next handling below and throw a FormatError instead?
I.e. something similar to the next code that's used e.g. when parsing OPS.paintXObject earlier in this file.

Really sorry about the back-and-forth in some of these comments, but it's quite a lot of added code/functionality to "unpack" here and at first glance I unfortunately overlooked some of the finer details.
The overall functionality seem to work very nicely though :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started to do do this, but sometimes the marked content isn't an "OC" which then causes us to wait async when we really don't need to.

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 @@ -481,6 +481,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
45 changes: 40 additions & 5 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,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 @@ -788,6 +789,15 @@ class PDFDocumentProxy {
return this._transport.getOutline();
}

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

/**
* @returns {Promise<Array<string | null>>} A promise that is resolved with
* an {Array} that contains the permission flags for the PDF document, or
Expand Down Expand Up @@ -965,6 +975,11 @@ class PDFDocumentProxy {
* image). The default value is 'rgb(255,255,255)'.
* @property {Object} [annotationStorage] - Storage for annotation data in
* forms.
* @property {Promise} [optionalContentConfigPromise] - A promise that should
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once #12156 lands, this should be changed to mention the promise type and to have each line start with two spaces as per the standard.

* 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 @@ -1088,6 +1103,7 @@ class PDFPageProxy {
canvasFactory = null,
background = null,
annotationStorage = null,
optionalContentConfigPromise = null,
}) {
if (this._stats) {
this._stats.time("Overall");
Expand All @@ -1098,6 +1114,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 @@ -1191,16 +1211,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 @@ -2546,6 +2572,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 @@ -2759,7 +2793,7 @@ const InternalRenderTask = (function InternalRenderTaskClosure() {
});
}

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