Add support for optional marked content. - #12095
Conversation
|
/botio test |
From: Bot.io (Windows)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.215.176.217:8877/6ff05971f6f1fd5/output.txt |
From: Bot.io (Linux m4)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.67.70.0:8877/48e5f33dc64066f/output.txt |
From: Bot.io (Linux m4)FailedFull output at http://54.67.70.0:8877/48e5f33dc64066f/output.txt Total script time: 26.89 mins
Image differences available at: http://54.67.70.0:8877/48e5f33dc64066f/reftest-analyzer.html#web=eq.log |
From: Bot.io (Windows)FailedFull output at http://54.215.176.217:8877/6ff05971f6f1fd5/output.txt Total script time: 31.32 mins
Image differences available at: http://54.215.176.217:8877/6ff05971f6f1fd5/reftest-analyzer.html#web=eq.log |
|
/botio test |
From: Bot.io (Windows)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.215.176.217:8877/ef23cf7e4380623/output.txt |
From: Bot.io (Linux m4)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.67.70.0:8877/d7647d4e6988ad1/output.txt |
| } else if (isDict(contentProperties)) { | ||
| optionalContent = contentProperties; | ||
| } else { | ||
| throw new Error("Optional content properties malformed."); |
There was a problem hiding this comment.
Should be throw new FormatError(...), since that's used for errors with the PDF data itself.
| throw new FormatError(`Unknown PatternName: ${patternName}`); | ||
| } | ||
|
|
||
| parseMarkedContentProps(contentProperties, resources) { |
There was a problem hiding this comment.
Please make this method async, since the data fetching below (i.e. the Dict.get(...) calls) could throw which would thus break the OperatorList parsing, and that way you'll be able to (easier) deal with any errors gracefully (relying on the existing ignoreErrors functionality).
| warn(`Expected name for beginMarkedContentProps arg0=${args[0]}`); | ||
| continue; | ||
| } | ||
| fn = OPS.beginMarkedContentProps; |
There was a problem hiding this comment.
This line looks superfluous, since fn === OPS.beginMarkedContentProps must hold in order for this block to have been entered!?
| 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; |
There was a problem hiding this comment.
Similar to all other methods here, see e.g. get permissions() above, please wrap this in try...catch to prevent breaking errors if data fetching fails (since Dict.get(...) calls could throw).
There was a problem hiding this comment.
Will get optionalContentConfig() always return the same data for a document with Optional Content groups?
If so, please shadow the return values to avoid having to repeatedly re-parsing this for every single page. (Or at least, please shadow the return null; cases.)
| } | ||
|
|
||
| _readOptionalContentConfig(config, contentGroupRefs) { | ||
| // console.log(contentGroupRefs); |
There was a problem hiding this comment.
Left-over debugging statement?
| } | ||
| fn = OPS.beginMarkedContentProps; | ||
| if (args[0].name === "OC") { | ||
| args = ["OC", self.parseMarkedContentProps(args[1], resources)]; |
There was a problem hiding this comment.
With parseMarkedContentProps being asynchronous, as suggested above, this would need to become something like this (which thus handles any errors gracefully):
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: /* Some new UNSUPPORTED_FEATURES id */,
});
warn(`getOperatorList - ignoring beginMarkedContentProps: "${reason}".`);
return;
}
throw reason;
})
);
return;| }; | ||
|
|
||
| if (dict.has("OC")) { | ||
| groupOptions.optionalContent = this.parseMarkedContentProps( |
There was a problem hiding this comment.
Nit: With parseMarkedContentProps being asynchronous, as suggested below, this would need to be groupOptions.optionalContent = await this.parseMarkedContentProps( instead.
| if (simpleFillText && !accent && this.contentVisible) { | ||
| // common case | ||
| ctx.fillText(character, scaledX, scaledY); | ||
| } else { | ||
| } else if (this.contentVisible) { |
There was a problem hiding this comment.
It would in my opinion be, a little bit, clearer if you simply enclosed this entire code-block
Lines 1698 to 1717 in 6c39aff
in
if (this.contentVisible) { ... } rather than "inlining" these checks.
There was a problem hiding this comment.
Also, unless I'm mistaken there's no similar checks in the showType3Text below. Don't you need to account for this.contentVisible there as well?
| if (this._canvas) { | ||
| if (canvasInRendering.has(this._canvas)) { | ||
| this.capability.reject( | ||
| new Error( | ||
| "Cannot use the same canvas during multiple render() operations. " + | ||
| "Use different canvas or ensure previous operations were " + | ||
| "cancelled or completed." | ||
| ) | ||
| ); | ||
| } else { | ||
| canvasInRendering.add(this._canvas); | ||
| } | ||
| } |
There was a problem hiding this comment.
Which test-case required moving this code, and is there another way to deal with that?
(Since the commit message says: - Reject the InternalRenderTask earlier now that InitializeGraphics is waits for another promise.)
I've not actually tested it, but based on reading the code I'm not entirely convinced that moving this code will actually do the right thing in general.
Currently initializeGraphics will throw
Lines 2668 to 2672 in 6c39aff
Lines 1118 to 1121 in 6c39aff
Lines 1054 to 1081 in 6c39aff
With your changes here, it doesn't seem like you're actually guaranteed that complete is ever called correctly!?
There was a problem hiding this comment.
It was the test that calls render twice. It doesn't seem to be failing anymore. This code is quite the mix of callbacks and old style promises.
From: Bot.io (Linux m4)FailedFull output at http://54.67.70.0:8877/d7647d4e6988ad1/output.txt Total script time: 26.87 mins
Image differences available at: http://54.67.70.0:8877/d7647d4e6988ad1/reftest-analyzer.html#web=eq.log |
From: Bot.io (Windows)FailedFull output at http://54.215.176.217:8877/ef23cf7e4380623/output.txt Total script time: 30.57 mins
Image differences available at: http://54.215.176.217:8877/ef23cf7e4380623/reftest-analyzer.html#web=eq.log |
| continue; | ||
| } | ||
| groupRefs.push(groupRef); | ||
| const group = properties.xref.fetchIfRef(groupRef); |
There was a problem hiding this comment.
I missed this previously: You want this.xref.fetchIfRef(groupRef); instead here, since this.xref is guaranteed to be defined whereas a Dict instance may not always have an xref property.
| if (ex instanceof MissingDataException) { | ||
| throw ex; | ||
| } | ||
| warn("Unable to optional content config. " + ex); |
There was a problem hiding this comment.
Missing e.g. the word "read" here?
warn(`Unable to read optional content config: ${ex}`);| name: group.get("Name"), | ||
| intent: group.get("Intent"), // TODO validate this. |
There was a problem hiding this comment.
Since this data is being sent between threads, whatever data this contains much be compatible with the "structured clone algorithm". Hence you may want to add at least some basic validation of these two properties since otherwise a corrupt PDF document might refuse to render altogether.
| return { | ||
| type: optionalContentType, | ||
| ids: groupIds, | ||
| policy: optionalContent.get("P"), |
There was a problem hiding this comment.
Since this data is being sent between threads, whatever data this contains much be compatible with the "structured clone algorithm". Hence you may want to add at least some basic validation of the P property since otherwise a corrupt PDF document might refuse to render altogether.
There was a problem hiding this comment.
According to the specification, https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf#G7.3883825, this should be a Name. In the OptionalContentConfig class you're accessing them as if they were mere strings, which I don't think will work!?
I believe that you'll need to either validate that it's a name and (essentially) return optionalContent.get("P").name, here, or possibly return the name as-is a and fix the lookup in OptionalContentConfig instead.
There was a problem hiding this comment.
I thought there was a test PDF for this, but appears there aren't any with a policy attribute. If I have some time I'll try to make some more tests.
| name: config.get("Name"), | ||
| creator: config.get("Creator"), | ||
| baseState: config.get("BaseState"), | ||
| on: parseOnOff(config.get("ON")), | ||
| off: parseOnOff(config.get("OFF")), |
There was a problem hiding this comment.
Since this data is being sent between threads, whatever data this contains much be compatible with the "structured clone algorithm". Hence you may want to add at least some basic validation of the Name/Creator/BaseState properties since otherwise a corrupt PDF document might refuse to render altogether.
| return this.messageHandler | ||
| .sendWithPromise("GetOptionalContentConfig", null) | ||
| .then(results => { | ||
| const config = new OptionalContentConfig(); |
There was a problem hiding this comment.
Just wondering: Any particular reason to not simply pass the results when creating the OptionalContentConfig instance, and thus essentially inline the initialize method in the constructor?
There was a problem hiding this comment.
I had plans of doing some unit testing this differently. Not going to do that now, so I'll merge it.
| const group = properties.xref.fetchIfRef(groupRef); | ||
| groups.push({ | ||
| id: groupRef.toString(), | ||
| name: group.get("Name"), |
There was a problem hiding this comment.
Since this is, I believe, a string; don't you need to wrap it in stringToPDFString() to avoid issues?
Edit: Issue #7283 is one example which requires this.
| name: config.get("Name"), | ||
| creator: config.get("Creator"), |
There was a problem hiding this comment.
Since these are, I believe, strings; don't you need to wrap them in stringToPDFString() to avoid issues?
Edit: Issue #7283 is one example which requires this.
| return { | ||
| name: config.get("Name"), | ||
| creator: config.get("Creator"), | ||
| baseState: config.get("BaseState"), |
There was a problem hiding this comment.
According to the specification, https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf#G7.3883884, this should be a Name. In the OptionalContentConfig class you're accessing them as if they were mere strings, which I don't think will work!?
I believe that you'll need to either validate that it's a name and (essentially) return config.get("BaseState").name, here, or possibly return the name as-is a and fix the lookup in OptionalContentConfig instead.
| return { | ||
| type: optionalContentType, | ||
| ids: groupIds, | ||
| policy: optionalContent.get("P"), |
There was a problem hiding this comment.
According to the specification, https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf#G7.3883825, this should be a Name. In the OptionalContentConfig class you're accessing them as if they were mere strings, which I don't think will work!?
I believe that you'll need to either validate that it's a name and (essentially) return optionalContent.get("P").name, here, or possibly return the name as-is a and fix the lookup in OptionalContentConfig instead.
| ctx.drawImage(mask, 0, 0); | ||
| } | ||
|
|
||
| function isContentVisible(stack) { |
There was a problem hiding this comment.
Given that every call-sites seem to pass in this.markedContentStack manually, any reason not to make this a "normal" method and simply handle the stack internally instead?
E.g. something like this
_computeContentVisible() {
const state = this.markedContentStack;
for (let i = stack.length - 1; i >= 0; i--) {
if (!stack[i].visible) {
return false;
}
}
return true;
},and the call-sites could then simply read this.contentVisible = this._computeContentVisible();
| continue; | ||
| case OPS.beginMarkedContentProps: | ||
| if (!isName(args[0])) { | ||
| warn(`Expected name for beginMarkedContentProps arg0=${args[0]}`); |
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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.
Snuffleupagus
left a comment
There was a problem hiding this comment.
r=me, with the commits squashed and all tests passing.
| const defaultConfig = properties.get("D"); | ||
| if (!defaultConfig) { | ||
| return shadow(this, "optionalContentConfig", null); | ||
| } |
There was a problem hiding this comment.
Nit: Would it make sense to move this to occur earlier in the method, since the groupsData parsing above seems pointless in the !defaultConfig case?
timvandermeij
left a comment
There was a problem hiding this comment.
Minor comments, also looks good to me after this.
|
|
||
| let expression = null; | ||
| if (optionalContent.get("VE")) { | ||
| // TODO support content epxressions. |
|
|
||
| handler.on( | ||
| "GetOptionalContentConfig", | ||
| function wphSetupGetOptionalContentConfig(data) { |
There was a problem hiding this comment.
This handler can be shortened a bit by using an anonymous function like the one below.
| /** | ||
| * @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} |
There was a problem hiding this comment.
Shouldn't @see be on a new line for JSDoc?
| * @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 |
There was a problem hiding this comment.
Nit: automatically be fetched
| @@ -0,0 +1,107 @@ | |||
| /* Copyright 2012 Mozilla Foundation | |||
|
/botio test |
From: Bot.io (Linux m4)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.67.70.0:8877/871b55504c08464/output.txt |
From: Bot.io (Windows)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.215.176.217:8877/d3650f17b24da81/output.txt |
From: Bot.io (Linux m4)FailedFull output at http://54.67.70.0:8877/871b55504c08464/output.txt Total script time: 26.85 mins
Image differences available at: http://54.67.70.0:8877/871b55504c08464/reftest-analyzer.html#web=eq.log |
From: Bot.io (Windows)FailedFull output at http://54.215.176.217:8877/d3650f17b24da81/output.txt Total script time: 30.81 mins
Image differences available at: http://54.215.176.217:8877/d3650f17b24da81/reftest-analyzer.html#web=eq.log |
There was a problem hiding this comment.
According to the test logs, there's two actual errors thrown during rendering:
TEST-UNEXPECTED-FAIL | test failed issue7769 | in firefox | page1 round 1 | render : Error: Optional content group must be found.
TEST-UNEXPECTED-FAIL | test failed issue7769 | in chrome | page1 round 1 | render : Error: Optional content group must be found.
Unfortunately, it appears that this particular error occurs in such a way that the failures aren't displayed in the "Reftest analyzer" (and I thus missed it previously).
| optionalContent: null, | ||
| }; | ||
|
|
||
| if (dict.has("OC")) { |
There was a problem hiding this comment.
All form XObjects can have an OC entry, not just transparency groups that have a Group entry. Then you may need to pass the parsed OC value also to paintFormXObjectBegin/End and handle it in canvas.js.
There was a problem hiding this comment.
Good catch, I'll just insert a beginMarkedContentProps before the form object so I don't have to handle this for both.
|
You should push a Line 2444 in d69fb44 "EMC" is the end of both "BMC" and "BDC" sections. "BMC" is not related to optional content but some PDFs may use it and the marked content stack might go wrong. |
|
@brendandahl Given that this PR seems really close to being done, since there's just a few more comments to fix (such that all tests pass), do you perhaps have time to fix the remaining things such that this can land? I've been playing around with this patch a fair bit locally[1], and it seems to work really well in practice :-) [1] I've got, more-or-less, finished patches for the viewer integration. |
cbec96b to
5c370a5
Compare
|
/botio test |
From: Bot.io (Linux m4)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.67.70.0:8877/48c34ec27dcef8c/output.txt |
From: Bot.io (Windows)ReceivedCommand cmd_test from @brendandahl received. Current queue size: 0 Live output at: http://54.215.176.217:8877/0f7ae373a4b37f7/output.txt |
From: Bot.io (Linux m4)FailedFull output at http://54.67.70.0:8877/48c34ec27dcef8c/output.txt Total script time: 26.94 mins
Image differences available at: http://54.67.70.0:8877/48c34ec27dcef8c/reftest-analyzer.html#web=eq.log |
timvandermeij
left a comment
There was a problem hiding this comment.
I'm leaving the review for @Snuffleupagus, but I'm just checking the comments/types here. For context, we have been working a lot on that the last few days in #12102 and follow-up pull requests. The main priority was to get TypeScript definitions generated automatically from the source code and to get the comments in a consistent format, so this requires a small number of changes here to make sure that once #12156 lands everything is still consistent and the generated types are correct. Thanks!
|
|
||
| /** | ||
| * @see {OptionalContentConfig} | ||
| * @returns {Promise<OptionalContentConfig>} A promise that is resolved with |
There was a problem hiding this comment.
I think this should be @returns {Promise<OptionalContentConfig | null>} looking at the catalog code. Moreover, @link should be used to refer to other types/code and it should be formatted in the standard way. In summary, this is my suggestion based on the existing comments:
/**
* @returns {Promise<OptionalContentConfig | null>} A promise that is resolved
* with an {@link OptionalContentConfig} that will have all the optional
* content groups, or `null` if the document does not have any.
*/| } | ||
|
|
||
| /** | ||
| * @see {OptionalContentConfig} |
There was a problem hiding this comment.
This line can be removed since the type is in the @returns already so JSDoc will pick it up (we don't have this elsewhere too).
| * 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 |
There was a problem hiding this comment.
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.
From: Bot.io (Windows)FailedFull output at http://54.215.176.217:8877/0f7ae373a4b37f7/output.txt Total script time: 31.48 mins
Image differences available at: http://54.215.176.217:8877/0f7ae373a4b37f7/reftest-analyzer.html#web=eq.log |
|
By the way, the unit test failures (and most likely the reference test failures as well) are known intermittents and are tracked in #12123 (the Windows one is a new one that I now mentioned there). |
|
/botio test |
From: Bot.io (Linux m4)ReceivedCommand cmd_test from @Snuffleupagus received. Current queue size: 0 Live output at: http://54.67.70.0:8877/fc2151d724bc8a7/output.txt |
From: Bot.io (Windows)ReceivedCommand cmd_test from @Snuffleupagus received. Current queue size: 0 Live output at: http://54.215.176.217:8877/eacea1f25df0912/output.txt |
From: Bot.io (Linux m4)FailedFull output at http://54.67.70.0:8877/fc2151d724bc8a7/output.txt Total script time: 27.04 mins
Image differences available at: http://54.67.70.0:8877/fc2151d724bc8a7/reftest-analyzer.html#web=eq.log |
From: Bot.io (Windows)FailedFull output at http://54.215.176.217:8877/eacea1f25df0912/output.txt Total script time: 32.10 mins
Image differences available at: http://54.215.176.217:8877/eacea1f25df0912/reftest-analyzer.html#web=eq.log |
Snuffleupagus
left a comment
There was a problem hiding this comment.
Looks good to me, thank you!
| @@ -2446,15 +2496,28 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { | |||
| }, | |||
| beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) { | |||
| // TODO Marked content. | |||
There was a problem hiding this comment.
Nit: Remove this comment?
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. 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.
|
/botio makeref |
From: Bot.io (Linux m4)ReceivedCommand cmd_makeref from @brendandahl received. Current queue size: 0 Live output at: http://54.67.70.0:8877/540ff90a3e49b5e/output.txt |
From: Bot.io (Windows)ReceivedCommand cmd_makeref from @brendandahl received. Current queue size: 0 Live output at: http://54.215.176.217:8877/20d3f22d0d860ed/output.txt |
From: Bot.io (Linux m4)SuccessFull output at http://54.67.70.0:8877/540ff90a3e49b5e/output.txt Total script time: 25.47 mins
|
From: Bot.io (Windows)SuccessFull output at http://54.215.176.217:8877/20d3f22d0d860ed/output.txt Total script time: 27.44 mins
|
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.