Add support for optional marked content.

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.
This commit is contained in:
Brendan Dahl 2020-07-14 15:17:27 -07:00
parent e68ac05f18
commit ac494a2278
14 changed files with 1179 additions and 54 deletions

View File

@ -392,6 +392,14 @@ class PartialEvaluator {
} else { } else {
bbox = null; 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"); var group = dict.get("Group");
if (group) { if (group) {
var groupOptions = { var groupOptions = {
@ -449,6 +457,10 @@ class PartialEvaluator {
if (group) { if (group) {
operatorList.addOp(OPS.endGroup, [groupOptions]); operatorList.addOp(OPS.endGroup, [groupOptions]);
} }
if (optionalContent) {
operatorList.addOp(OPS.endMarkedContent, []);
}
}); });
} }
@ -1202,6 +1214,63 @@ class PartialEvaluator {
throw new FormatError(`Unknown PatternName: ${patternName}`); 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({ getOperatorList({
stream, stream,
task, task,
@ -1704,9 +1773,6 @@ class PartialEvaluator {
continue; continue;
case OPS.markPoint: case OPS.markPoint:
case OPS.markPointProps: case OPS.markPointProps:
case OPS.beginMarkedContent:
case OPS.beginMarkedContentProps:
case OPS.endMarkedContent:
case OPS.beginCompat: case OPS.beginCompat:
case OPS.endCompat: case OPS.endCompat:
// Ignore operators where the corresponding handlers are known to // Ignore operators where the corresponding handlers are known to
@ -1716,6 +1782,45 @@ class PartialEvaluator {
// e.g. as done in https://github.com/mozilla/pdf.js/pull/6266, // e.g. as done in https://github.com/mozilla/pdf.js/pull/6266,
// but doing so is meaningless without knowing the semantics. // but doing so is meaningless without knowing the semantics.
continue; 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: default:
// Note: Ignore the operator if it has `Dict` arguments, since // Note: Ignore the operator if it has `Dict` arguments, since
// those are non-serializable, otherwise postMessage will throw // those are non-serializable, otherwise postMessage will throw

View File

@ -254,6 +254,82 @@ class Catalog {
return permissions; 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() { get numPages() {
const obj = this.toplevelPagesDict.get("Count"); const obj = this.toplevelPagesDict.get("Count");
if (!Number.isInteger(obj)) { if (!Number.isInteger(obj)) {

View File

@ -481,6 +481,10 @@ class WorkerMessageHandler {
return pdfManager.ensureCatalog("documentOutline"); return pdfManager.ensureCatalog("documentOutline");
}); });
handler.on("GetOptionalContentConfig", function (data) {
return pdfManager.ensureCatalog("optionalContentConfig");
});
handler.on("GetPermissions", function (data) { handler.on("GetPermissions", function (data) {
return pdfManager.ensureCatalog("permissions"); return pdfManager.ensureCatalog("permissions");
}); });

View File

@ -55,6 +55,7 @@ import { GlobalWorkerOptions } from "./worker_options.js";
import { isNodeJS } from "../shared/is_node.js"; import { isNodeJS } from "../shared/is_node.js";
import { MessageHandler } from "../shared/message_handler.js"; import { MessageHandler } from "../shared/message_handler.js";
import { Metadata } from "./metadata.js"; import { Metadata } from "./metadata.js";
import { OptionalContentConfig } from "./optional_content_config.js";
import { PDFDataTransportStream } from "./transport_stream.js"; import { PDFDataTransportStream } from "./transport_stream.js";
import { WebGLContext } from "./webgl.js"; import { WebGLContext } from "./webgl.js";
@ -788,6 +789,15 @@ class PDFDocumentProxy {
return this._transport.getOutline(); 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 * @returns {Promise<Array<string | null>>} A promise that is resolved with
* an {Array} that contains the permission flags for the PDF document, or * an {Array} that contains the permission flags for the PDF document, or
@ -965,6 +975,11 @@ class PDFDocumentProxy {
* image). The default value is 'rgb(255,255,255)'. * image). The default value is 'rgb(255,255,255)'.
* @property {Object} [annotationStorage] - Storage for annotation data in * @property {Object} [annotationStorage] - Storage for annotation data in
* forms. * forms.
* @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.
*/ */
/** /**
@ -1088,6 +1103,7 @@ class PDFPageProxy {
canvasFactory = null, canvasFactory = null,
background = null, background = null,
annotationStorage = null, annotationStorage = null,
optionalContentConfigPromise = null,
}) { }) {
if (this._stats) { if (this._stats) {
this._stats.time("Overall"); this._stats.time("Overall");
@ -1098,6 +1114,10 @@ class PDFPageProxy {
// this call to render. // this call to render.
this.pendingCleanup = false; this.pendingCleanup = false;
if (!optionalContentConfigPromise) {
optionalContentConfigPromise = this._transport.getOptionalContentConfig();
}
let intentState = this._intentStates.get(renderingIntent); let intentState = this._intentStates.get(renderingIntent);
if (!intentState) { if (!intentState) {
intentState = Object.create(null); intentState = Object.create(null);
@ -1191,8 +1211,11 @@ class PDFPageProxy {
intentState.renderTasks.push(internalRenderTask); intentState.renderTasks.push(internalRenderTask);
const renderTask = internalRenderTask.task; const renderTask = internalRenderTask.task;
intentState.displayReadyCapability.promise Promise.all([
.then(transparency => { intentState.displayReadyCapability.promise,
optionalContentConfigPromise,
])
.then(([transparency, optionalContentConfig]) => {
if (this.pendingCleanup) { if (this.pendingCleanup) {
complete(); complete();
return; return;
@ -1200,7 +1223,10 @@ class PDFPageProxy {
if (this._stats) { if (this._stats) {
this._stats.time("Rendering"); this._stats.time("Rendering");
} }
internalRenderTask.initializeGraphics(transparency); internalRenderTask.initializeGraphics({
transparency,
optionalContentConfig,
});
internalRenderTask.operatorListChanged(); internalRenderTask.operatorListChanged();
}) })
.catch(complete); .catch(complete);
@ -2546,6 +2572,14 @@ class WorkerTransport {
return this.messageHandler.sendWithPromise("GetOutline", null); return this.messageHandler.sendWithPromise("GetOutline", null);
} }
getOptionalContentConfig() {
return this.messageHandler
.sendWithPromise("GetOptionalContentConfig", null)
.then(results => {
return new OptionalContentConfig(results);
});
}
getPermissions() { getPermissions() {
return this.messageHandler.sendWithPromise("GetPermissions", null); return this.messageHandler.sendWithPromise("GetPermissions", null);
} }
@ -2759,7 +2793,7 @@ const InternalRenderTask = (function InternalRenderTaskClosure() {
}); });
} }
initializeGraphics(transparency = false) { initializeGraphics({ transparency = false, optionalContentConfig }) {
if (this.cancelled) { if (this.cancelled) {
return; return;
} }
@ -2797,7 +2831,8 @@ const InternalRenderTask = (function InternalRenderTaskClosure() {
this.objs, this.objs,
this.canvasFactory, this.canvasFactory,
this.webGLContext, this.webGLContext,
imageLayer imageLayer,
optionalContentConfig
); );
this.gfx.beginDrawing({ this.gfx.beginDrawing({
transform, transform,

View File

@ -447,7 +447,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
objs, objs,
canvasFactory, canvasFactory,
webGLContext, webGLContext,
imageLayer imageLayer,
optionalContentConfig
) { ) {
this.ctx = canvasCtx; this.ctx = canvasCtx;
this.current = new CanvasExtraState(); this.current = new CanvasExtraState();
@ -471,6 +472,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
this.smaskStack = []; this.smaskStack = [];
this.smaskCounter = 0; this.smaskCounter = 0;
this.tempSMask = null; this.tempSMask = null;
this.contentVisible = true;
this.markedContentStack = [];
this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory); this.cachedCanvases = new CachedCanvases(this.canvasFactory);
if (canvasCtx) { if (canvasCtx) {
// NOTE: if mozCurrentTransform is polyfilled, then the current state of // NOTE: if mozCurrentTransform is polyfilled, then the current state of
@ -1262,34 +1266,36 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
// For stroke we want to temporarily change the global alpha to the // For stroke we want to temporarily change the global alpha to the
// stroking alpha. // stroking alpha.
ctx.globalAlpha = this.current.strokeAlpha; ctx.globalAlpha = this.current.strokeAlpha;
if ( if (this.contentVisible) {
strokeColor && if (
strokeColor.hasOwnProperty("type") && strokeColor &&
strokeColor.type === "Pattern" strokeColor.hasOwnProperty("type") &&
) { strokeColor.type === "Pattern"
// for patterns, we transform to pattern space, calculate ) {
// the pattern, call stroke, and restore to user space // for patterns, we transform to pattern space, calculate
ctx.save(); // the pattern, call stroke, and restore to user space
// The current transform will be replaced while building the pattern, ctx.save();
// but the line width needs to be adjusted by the current transform, so // The current transform will be replaced while building the pattern,
// we must scale it. To properly fix this we should be using a pattern // but the line width needs to be adjusted by the current transform,
// transform instead (see #10955). // so we must scale it. To properly fix this we should be using a
const transform = ctx.mozCurrentTransform; // pattern transform instead (see #10955).
const scale = Util.singularValueDecompose2dScale(transform)[0]; const transform = ctx.mozCurrentTransform;
ctx.strokeStyle = strokeColor.getPattern(ctx, this); const scale = Util.singularValueDecompose2dScale(transform)[0];
ctx.lineWidth = Math.max( ctx.strokeStyle = strokeColor.getPattern(ctx, this);
this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, ctx.lineWidth = Math.max(
this.current.lineWidth * scale this.getSinglePixelWidth() * MIN_WIDTH_FACTOR,
); this.current.lineWidth * scale
ctx.stroke(); );
ctx.restore(); ctx.stroke();
} else { ctx.restore();
// Prevent drawing too thin lines by enforcing a minimum line width. } else {
ctx.lineWidth = Math.max( // Prevent drawing too thin lines by enforcing a minimum line width.
this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, ctx.lineWidth = Math.max(
this.current.lineWidth this.getSinglePixelWidth() * MIN_WIDTH_FACTOR,
); this.current.lineWidth
ctx.stroke(); );
ctx.stroke();
}
} }
if (consumePath) { if (consumePath) {
this.consumePath(); this.consumePath();
@ -1317,11 +1323,13 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
needRestore = true; needRestore = true;
} }
if (this.pendingEOFill) { if (this.contentVisible) {
ctx.fill("evenodd"); if (this.pendingEOFill) {
this.pendingEOFill = false; ctx.fill("evenodd");
} else { this.pendingEOFill = false;
ctx.fill(); } else {
ctx.fill();
}
} }
if (needRestore) { if (needRestore) {
@ -1700,7 +1708,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
// Only attempt to draw the glyph if it is actually in the embedded font // Only attempt to draw the glyph if it is actually in the embedded font
// file or if there isn't a font file so the fallback font is shown. // file or if there isn't a font file so the fallback font is shown.
if (glyph.isInFont || font.missingFile) { if (this.contentVisible && (glyph.isInFont || font.missingFile)) {
if (simpleFillText && !accent) { if (simpleFillText && !accent) {
// common case // common case
ctx.fillText(character, scaledX, scaledY); ctx.fillText(character, scaledX, scaledY);
@ -1782,12 +1790,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
warn(`Type3 character "${glyph.operatorListId}" is not available.`); warn(`Type3 character "${glyph.operatorListId}" is not available.`);
continue; continue;
} }
this.processingType3 = glyph; if (this.contentVisible) {
this.save(); this.processingType3 = glyph;
ctx.scale(fontSize, fontSize); this.save();
ctx.transform.apply(ctx, fontMatrix); ctx.scale(fontSize, fontSize);
this.executeOperatorList(operatorList); ctx.transform.apply(ctx, fontMatrix);
this.restore(); this.executeOperatorList(operatorList);
this.restore();
}
var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); var transformed = Util.applyTransform([glyph.width, 0], fontMatrix);
width = transformed[0] * fontSize + spacing; width = transformed[0] * fontSize + spacing;
@ -1869,6 +1879,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}, },
shadingFill: function CanvasGraphics_shadingFill(patternIR) { shadingFill: function CanvasGraphics_shadingFill(patternIR) {
if (!this.contentVisible) {
return;
}
var ctx = this.ctx; var ctx = this.ctx;
this.save(); this.save();
@ -1917,6 +1930,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
matrix, matrix,
bbox bbox
) { ) {
if (!this.contentVisible) {
return;
}
this.save(); this.save();
this.baseTransformStack.push(this.baseTransform); this.baseTransformStack.push(this.baseTransform);
@ -1936,11 +1952,18 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}, },
paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() { paintFormXObjectEnd: function CanvasGraphics_paintFormXObjectEnd() {
if (!this.contentVisible) {
return;
}
this.restore(); this.restore();
this.baseTransform = this.baseTransformStack.pop(); this.baseTransform = this.baseTransformStack.pop();
}, },
beginGroup: function CanvasGraphics_beginGroup(group) { beginGroup: function CanvasGraphics_beginGroup(group) {
if (!this.contentVisible) {
return;
}
this.save(); this.save();
var currentCtx = this.ctx; var currentCtx = this.ctx;
// TODO non-isolated groups - according to Rik at adobe non-isolated // TODO non-isolated groups - according to Rik at adobe non-isolated
@ -2062,6 +2085,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}, },
endGroup: function CanvasGraphics_endGroup(group) { endGroup: function CanvasGraphics_endGroup(group) {
if (!this.contentVisible) {
return;
}
this.groupLevel--; this.groupLevel--;
var groupCtx = this.ctx; var groupCtx = this.ctx;
this.ctx = this.groupStack.pop(); this.ctx = this.groupStack.pop();
@ -2117,6 +2143,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}, },
paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) { paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) {
if (!this.contentVisible) {
return;
}
var ctx = this.ctx; var ctx = this.ctx;
var width = img.width, var width = img.width,
height = img.height; height = img.height;
@ -2168,6 +2197,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
scaleY, scaleY,
positions positions
) { ) {
if (!this.contentVisible) {
return;
}
var width = imgData.width; var width = imgData.width;
var height = imgData.height; var height = imgData.height;
var fillColor = this.current.fillColor; var fillColor = this.current.fillColor;
@ -2212,6 +2244,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup( paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(
images images
) { ) {
if (!this.contentVisible) {
return;
}
var ctx = this.ctx; var ctx = this.ctx;
var fillColor = this.current.fillColor; var fillColor = this.current.fillColor;
@ -2249,6 +2284,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}, },
paintImageXObject: function CanvasGraphics_paintImageXObject(objId) { paintImageXObject: function CanvasGraphics_paintImageXObject(objId) {
if (!this.contentVisible) {
return;
}
const imgData = objId.startsWith("g_") const imgData = objId.startsWith("g_")
? this.commonObjs.get(objId) ? this.commonObjs.get(objId)
: this.objs.get(objId); : this.objs.get(objId);
@ -2266,6 +2304,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
scaleY, scaleY,
positions positions
) { ) {
if (!this.contentVisible) {
return;
}
const imgData = objId.startsWith("g_") const imgData = objId.startsWith("g_")
? this.commonObjs.get(objId) ? this.commonObjs.get(objId)
: this.objs.get(objId); : this.objs.get(objId);
@ -2292,6 +2333,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
paintInlineImageXObject: function CanvasGraphics_paintInlineImageXObject( paintInlineImageXObject: function CanvasGraphics_paintInlineImageXObject(
imgData imgData
) { ) {
if (!this.contentVisible) {
return;
}
var width = imgData.width; var width = imgData.width;
var height = imgData.height; var height = imgData.height;
var ctx = this.ctx; var ctx = this.ctx;
@ -2394,6 +2438,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
imgData, imgData,
map map
) { ) {
if (!this.contentVisible) {
return;
}
var ctx = this.ctx; var ctx = this.ctx;
var w = imgData.width; var w = imgData.width;
var h = imgData.height; var h = imgData.height;
@ -2433,6 +2480,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
}, },
paintSolidColorImageMask: function CanvasGraphics_paintSolidColorImageMask() { paintSolidColorImageMask: function CanvasGraphics_paintSolidColorImageMask() {
if (!this.contentVisible) {
return;
}
this.ctx.fillRect(0, 0, 1, 1); this.ctx.fillRect(0, 0, 1, 1);
}, },
@ -2445,16 +2495,28 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
// TODO Marked content. // TODO Marked content.
}, },
beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) { beginMarkedContent: function CanvasGraphics_beginMarkedContent(tag) {
// TODO Marked content. this.markedContentStack.push({
visible: true,
});
}, },
beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps( beginMarkedContentProps: function CanvasGraphics_beginMarkedContentProps(
tag, tag,
properties properties
) { ) {
// TODO Marked content. if (tag === "OC") {
this.markedContentStack.push({
visible: this.optionalContentConfig.isVisible(properties),
});
} else {
this.markedContentStack.push({
visible: true,
});
}
this.contentVisible = this.isContentVisible();
}, },
endMarkedContent: function CanvasGraphics_endMarkedContent() { endMarkedContent: function CanvasGraphics_endMarkedContent() {
// TODO Marked content. this.markedContentStack.pop();
this.contentVisible = this.isContentVisible();
}, },
// Compatibility // Compatibility
@ -2500,6 +2562,15 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
transform[1] * x + transform[3] * y + transform[5], transform[1] * x + transform[3] * y + transform[5],
]; ];
}, },
isContentVisible: function CanvasGraphics_isContentVisible() {
for (let i = this.markedContentStack.length - 1; i >= 0; i--) {
if (!this.markedContentStack[i].visible) {
return false;
}
}
return true;
},
}; };
for (var op in OPS) { for (var op in OPS) {

View File

@ -0,0 +1,125 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { warn } from "../shared/util.js";
class OptionalContentGroup {
constructor(name, intent) {
this.visible = true;
this.name = name;
this.intent = intent;
}
}
class OptionalContentConfig {
constructor(data) {
this.name = null;
this.creator = null;
this.groups = new Map();
if (data === null) {
return;
}
this.name = data.name;
this.creator = data.creator;
for (const group of data.groups) {
this.groups.set(
group.id,
new OptionalContentGroup(group.name, group.intent)
);
}
if (data.baseState === "OFF") {
for (const group of this.groups) {
group.visible = false;
}
}
for (const on of data.on) {
this.groups.get(on).visible = true;
}
for (const off of data.off) {
this.groups.get(off).visible = false;
}
}
isVisible(group) {
if (group.type === "OCG") {
if (!this.groups.has(group.id)) {
warn(`Optional content group not found: ${group.id}`);
return true;
}
return this.groups.get(group.id).visible;
} else if (group.type === "OCMD") {
// Per the spec, the expression should be preferred if available. Until
// we implement this, just fallback to using the group policy for now.
if (group.expression) {
warn("Visibility expression not supported yet.");
}
if (!group.policy || group.policy === "AnyOn") {
// Default
for (const id of group.ids) {
if (!this.groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (this.groups.get(id).visible) {
return true;
}
}
return false;
} else if (group.policy === "AllOn") {
for (const id of group.ids) {
if (!this.groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (!this.groups.get(id).visible) {
return false;
}
}
return true;
} else if (group.policy === "AnyOff") {
for (const id of group.ids) {
if (!this.groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (!this.groups.get(id).visible) {
return true;
}
}
return false;
} else if (group.policy === "AllOff") {
for (const id of group.ids) {
if (!this.groups.has(id)) {
warn(`Optional content group not found: ${id}`);
return true;
}
if (this.groups.get(id).visible) {
return false;
}
}
return true;
}
warn(`Unknown optional content policy ${group.policy}.`);
return true;
}
warn(`Unknown group type ${group.type}.`);
return true;
}
}
export { OptionalContentConfig };

View File

@ -302,6 +302,7 @@ const UNSUPPORTED_FEATURES = {
errorFontToUnicode: "errorFontToUnicode", errorFontToUnicode: "errorFontToUnicode",
errorFontLoadNative: "errorFontLoadNative", errorFontLoadNative: "errorFontLoadNative",
errorFontGetPath: "errorFontGetPath", errorFontGetPath: "errorFontGetPath",
errorMarkedContent: "errorMarkedContent",
}; };
const PasswordResponses = { const PasswordResponses = {

View File

@ -52,6 +52,7 @@
!issue7835.pdf !issue7835.pdf
!issue11922_reduced.pdf !issue11922_reduced.pdf
!issue7855.pdf !issue7855.pdf
!issue11144_reduced.pdf
!issue7872.pdf !issue7872.pdf
!issue7901.pdf !issue7901.pdf
!issue8061.pdf !issue8061.pdf
@ -296,6 +297,7 @@
!issue3371.pdf !issue3371.pdf
!issue2956.pdf !issue2956.pdf
!issue2537r.pdf !issue2537r.pdf
!issue269_1.pdf
!bug946506.pdf !bug946506.pdf
!issue3885.pdf !issue3885.pdf
!issue11697_reduced.pdf !issue11697_reduced.pdf
@ -331,6 +333,7 @@
!issue5481.pdf !issue5481.pdf
!issue5567.pdf !issue5567.pdf
!issue5701.pdf !issue5701.pdf
!issue12007_reduced.pdf
!issue5896.pdf !issue5896.pdf
!issue6010_1.pdf !issue6010_1.pdf
!issue6010_2.pdf !issue6010_2.pdf
@ -352,6 +355,7 @@
!issue9278.pdf !issue9278.pdf
!annotation-text-without-popup.pdf !annotation-text-without-popup.pdf
!annotation-underline.pdf !annotation-underline.pdf
!issue269_2.pdf
!annotation-strikeout.pdf !annotation-strikeout.pdf
!annotation-squiggly.pdf !annotation-squiggly.pdf
!annotation-highlight.pdf !annotation-highlight.pdf

Binary file not shown.

Binary file not shown.

676
test/pdfs/issue269_1.pdf Normal file

File diff suppressed because one or more lines are too long

BIN
test/pdfs/issue269_2.pdf Normal file

Binary file not shown.

View File

@ -926,6 +926,34 @@
"link": false, "link": false,
"type": "eq" "type": "eq"
}, },
{ "id": "issue269_1",
"file": "pdfs/issue269_1.pdf",
"md5": "ab932f697b4d2e2bf700de15a8efea9c",
"rounds": 1,
"type": "eq",
"about": "Optional marked content."
},
{ "id": "issue269_2",
"file": "pdfs/issue269_2.pdf",
"md5": "0f553510850ee17c87fbab3fac564165",
"rounds": 1,
"type": "eq",
"about": "Optional marked content."
},
{ "id": "issue11144_reduced",
"file": "pdfs/issue11144_reduced.pdf",
"md5": "09e3e771ebd6867558074e900adb54b9",
"rounds": 1,
"type": "eq",
"about": "Optional marked content."
},
{ "id": "issue12007_reduced",
"file": "pdfs/issue12007_reduced.pdf",
"md5": "3aa9d8a0c5ff8594245149f9c7379613",
"rounds": 1,
"type": "eq",
"about": "Optional marked content."
},
{ "id": "issue10438", { "id": "issue10438",
"file": "pdfs/issue10438_reduced.pdf", "file": "pdfs/issue10438_reduced.pdf",
"md5": "bb26f68493e33af17b256a6ffe777a24", "md5": "bb26f68493e33af17b256a6ffe777a24",

View File

@ -1636,8 +1636,8 @@ describe("api", function () {
const result1 = loadingTask1.promise.then(pdfDoc => { const result1 = loadingTask1.promise.then(pdfDoc => {
return pdfDoc.getPage(1).then(pdfPage => { return pdfDoc.getPage(1).then(pdfPage => {
return pdfPage.getOperatorList().then(opList => { return pdfPage.getOperatorList().then(opList => {
expect(opList.fnArray.length).toEqual(722); expect(opList.fnArray.length).toBeGreaterThan(100);
expect(opList.argsArray.length).toEqual(722); expect(opList.argsArray.length).toBeGreaterThan(100);
expect(opList.lastChunk).toEqual(true); expect(opList.lastChunk).toEqual(true);
return loadingTask1.destroy(); return loadingTask1.destroy();