mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-02 16:21:01 +02:00
Add support for Optional Content in the AnnotationLayer (issue 20433)
This commit is contained in:
parent
25c7d9eaac
commit
fb9758303b
@ -76,6 +76,7 @@ import { FileSpec } from "./file_spec.js";
|
||||
import { JpegStream } from "./jpeg_stream.js";
|
||||
import { ObjectLoader } from "./object_loader.js";
|
||||
import { OperatorList } from "./operator_list.js";
|
||||
import { parseMarkedContentProps } from "./evaluator_utils.js";
|
||||
import { XFAFactory } from "./xfa/factory.js";
|
||||
|
||||
class AnnotationFactory {
|
||||
@ -663,6 +664,8 @@ function getTransformMatrix(rect, bbox, matrix) {
|
||||
}
|
||||
|
||||
class Annotation {
|
||||
_oc = undefined;
|
||||
|
||||
constructor(params) {
|
||||
const { annotationGlobals, dict, orphanFields, ref, subtype, xref } =
|
||||
params;
|
||||
@ -679,7 +682,7 @@ class Annotation {
|
||||
this.setColor(dict.getArray("C"));
|
||||
this.setBorderStyle(dict);
|
||||
this.setAppearance(dict);
|
||||
this.setOptionalContent(dict);
|
||||
this.#setOptionalContent(xref, dict);
|
||||
|
||||
const MK = dict.get("MK");
|
||||
this.setBorderAndBackgroundColors(MK);
|
||||
@ -710,6 +713,7 @@ class Annotation {
|
||||
hasAppearance: !!this.appearance,
|
||||
id: params.id,
|
||||
modificationDate: this.modificationDate,
|
||||
oc: this._oc,
|
||||
rect: this.rectangle,
|
||||
subtype,
|
||||
hasOwnCanvas: false,
|
||||
@ -1169,14 +1173,17 @@ class Annotation {
|
||||
}
|
||||
}
|
||||
|
||||
setOptionalContent(dict) {
|
||||
this.oc = null;
|
||||
|
||||
const oc = dict.get("OC");
|
||||
if (oc instanceof Name) {
|
||||
warn("setOptionalContent: Support for /Name-entry is not implemented.");
|
||||
} else if (oc instanceof Dict) {
|
||||
this.oc = oc;
|
||||
#setOptionalContent(xref, dict) {
|
||||
if (dict.has("OC")) {
|
||||
try {
|
||||
this._oc = parseMarkedContentProps(
|
||||
xref,
|
||||
dict.get("OC"),
|
||||
/* resources = */ null
|
||||
);
|
||||
} catch (ex) {
|
||||
warn(`#setOptionalContent: ${ex}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1229,13 +1236,7 @@ class Annotation {
|
||||
|
||||
const opList = new OperatorList();
|
||||
|
||||
let optionalContent;
|
||||
if (this.oc) {
|
||||
optionalContent = await evaluator.parseMarkedContentProps(
|
||||
this.oc,
|
||||
/* resources = */ null
|
||||
);
|
||||
}
|
||||
const optionalContent = this._oc;
|
||||
if (optionalContent !== undefined) {
|
||||
opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
|
||||
}
|
||||
@ -2110,13 +2111,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const bbox = [0, 0, this.width, this.height];
|
||||
const transform = getTransformMatrix(this.data.rect, bbox, matrix);
|
||||
|
||||
let optionalContent;
|
||||
if (this.oc) {
|
||||
optionalContent = await evaluator.parseMarkedContentProps(
|
||||
this.oc,
|
||||
/* resources = */ null
|
||||
);
|
||||
}
|
||||
const optionalContent = this._oc;
|
||||
if (optionalContent !== undefined) {
|
||||
opList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
|
||||
}
|
||||
|
||||
@ -87,6 +87,7 @@ import { getGlyphsUnicode } from "./glyphlist.js";
|
||||
import { getMetrics } from "./metrics.js";
|
||||
import { getUnicodeForGlyph } from "./unicode.js";
|
||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||
import { parseMarkedContentProps } from "./evaluator_utils.js";
|
||||
import { PDFImage } from "./image.js";
|
||||
import { Stream } from "./stream.js";
|
||||
import { stringToPDFString } from "./string_utils.js";
|
||||
@ -1651,105 +1652,8 @@ class PartialEvaluator {
|
||||
throw new FormatError(`Unknown PatternName: ${patternName}`);
|
||||
}
|
||||
|
||||
_parseVisibilityExpression(array, nestingCounter, currentResult) {
|
||||
const MAX_NESTING = 10;
|
||||
if (++nestingCounter > MAX_NESTING) {
|
||||
warn("Visibility expression is too deeply nested");
|
||||
return;
|
||||
}
|
||||
const length = array.length;
|
||||
const operator = this.xref.fetchIfRef(array[0]);
|
||||
if (length < 2 || !(operator instanceof Name)) {
|
||||
warn("Invalid visibility expression");
|
||||
return;
|
||||
}
|
||||
switch (operator.name) {
|
||||
case "And":
|
||||
case "Or":
|
||||
case "Not":
|
||||
currentResult.push(operator.name);
|
||||
break;
|
||||
default:
|
||||
warn(`Invalid operator ${operator.name} in visibility expression`);
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i < length; i++) {
|
||||
const raw = array[i];
|
||||
const object = this.xref.fetchIfRef(raw);
|
||||
if (Array.isArray(object)) {
|
||||
const nestedResult = [];
|
||||
currentResult.push(nestedResult);
|
||||
// Recursively parse a subarray.
|
||||
this._parseVisibilityExpression(object, nestingCounter, nestedResult);
|
||||
} else if (raw instanceof Ref) {
|
||||
// Reference to an OCG dictionary.
|
||||
currentResult.push(raw.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async parseMarkedContentProps(contentProperties, resources) {
|
||||
let optionalContent;
|
||||
if (contentProperties instanceof Name) {
|
||||
const properties = resources.get("Properties");
|
||||
optionalContent = properties.get(contentProperties.name);
|
||||
} else if (contentProperties instanceof Dict) {
|
||||
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 expression = optionalContent.get("VE");
|
||||
if (Array.isArray(expression)) {
|
||||
const result = [];
|
||||
this._parseVisibilityExpression(expression, 0, result);
|
||||
if (result.length > 0) {
|
||||
return {
|
||||
type: "OCMD",
|
||||
expression: result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const optionalContentGroups = optionalContent.get("OCGs");
|
||||
if (
|
||||
Array.isArray(optionalContentGroups) ||
|
||||
optionalContentGroups instanceof Dict
|
||||
) {
|
||||
const groupIds = [];
|
||||
if (Array.isArray(optionalContentGroups)) {
|
||||
for (const ocg of optionalContentGroups) {
|
||||
groupIds.push(ocg.toString());
|
||||
}
|
||||
} else {
|
||||
// Dictionary, just use the obj id.
|
||||
groupIds.push(optionalContentGroups.objId);
|
||||
}
|
||||
|
||||
return {
|
||||
type: optionalContentType,
|
||||
ids: groupIds,
|
||||
policy:
|
||||
optionalContent.get("P") instanceof Name
|
||||
? optionalContent.get("P").name
|
||||
: null,
|
||||
expression: null,
|
||||
};
|
||||
} else if (optionalContentGroups instanceof Ref) {
|
||||
return {
|
||||
type: optionalContentType,
|
||||
id: optionalContentGroups.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return parseMarkedContentProps(this.xref, contentProperties, resources);
|
||||
}
|
||||
|
||||
async getOperatorList({
|
||||
|
||||
123
src/core/evaluator_utils.js
Normal file
123
src/core/evaluator_utils.js
Normal file
@ -0,0 +1,123 @@
|
||||
/* Copyright 2012 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 { Dict, Name, Ref } from "./primitives.js";
|
||||
import { FormatError, warn } from "../shared/util.js";
|
||||
|
||||
function _parseVisibilityExpression(
|
||||
xref,
|
||||
array,
|
||||
nestingCounter,
|
||||
currentResult
|
||||
) {
|
||||
const MAX_NESTING = 10;
|
||||
if (++nestingCounter > MAX_NESTING) {
|
||||
warn("Visibility expression is too deeply nested");
|
||||
return;
|
||||
}
|
||||
const length = array.length;
|
||||
const operator = xref.fetchIfRef(array[0]);
|
||||
if (length < 2 || !(operator instanceof Name)) {
|
||||
warn("Invalid visibility expression");
|
||||
return;
|
||||
}
|
||||
switch (operator.name) {
|
||||
case "And":
|
||||
case "Or":
|
||||
case "Not":
|
||||
currentResult.push(operator.name);
|
||||
break;
|
||||
default:
|
||||
warn(`Invalid operator ${operator.name} in visibility expression`);
|
||||
return;
|
||||
}
|
||||
for (let i = 1; i < length; i++) {
|
||||
const raw = array[i];
|
||||
const object = xref.fetchIfRef(raw);
|
||||
if (Array.isArray(object)) {
|
||||
const nestedResult = [];
|
||||
currentResult.push(nestedResult);
|
||||
// Recursively parse a subarray.
|
||||
_parseVisibilityExpression(xref, object, nestingCounter, nestedResult);
|
||||
} else if (raw instanceof Ref) {
|
||||
// Reference to an OCG dictionary.
|
||||
currentResult.push(raw.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseMarkedContentProps(xref, contentProperties, resources) {
|
||||
let optionalContent;
|
||||
if (contentProperties instanceof Name) {
|
||||
const properties = resources.get("Properties");
|
||||
optionalContent = properties.get(contentProperties.name);
|
||||
} else if (contentProperties instanceof Dict) {
|
||||
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 expression = optionalContent.get("VE");
|
||||
if (Array.isArray(expression)) {
|
||||
const result = [];
|
||||
_parseVisibilityExpression(xref, expression, 0, result);
|
||||
if (result.length > 0) {
|
||||
return {
|
||||
type: "OCMD",
|
||||
expression: result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const optionalContentGroups = optionalContent.get("OCGs");
|
||||
if (
|
||||
Array.isArray(optionalContentGroups) ||
|
||||
optionalContentGroups instanceof Dict
|
||||
) {
|
||||
const groupIds = [];
|
||||
if (Array.isArray(optionalContentGroups)) {
|
||||
for (const ocg of optionalContentGroups) {
|
||||
groupIds.push(ocg.toString());
|
||||
}
|
||||
} else {
|
||||
// Dictionary, just use the obj id.
|
||||
groupIds.push(optionalContentGroups.objId);
|
||||
}
|
||||
const p = optionalContent.get("P");
|
||||
|
||||
return {
|
||||
type: optionalContentType,
|
||||
ids: groupIds,
|
||||
policy: p instanceof Name ? p.name : null,
|
||||
expression: null,
|
||||
};
|
||||
} else if (optionalContentGroups instanceof Ref) {
|
||||
return {
|
||||
type: optionalContentType,
|
||||
id: optionalContentGroups.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export { parseMarkedContentProps };
|
||||
@ -16,6 +16,8 @@
|
||||
/** @typedef {import("./api").PDFPageProxy} PDFPageProxy */
|
||||
/** @typedef {import("./page_viewport").PageViewport} PageViewport */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/optional_content_config").OptionalContentConfig} OptionalContentConfig */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../../web/text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../src/display/editor/tools.js").AnnotationEditorUIManager} AnnotationEditorUIManager */
|
||||
@ -886,6 +888,18 @@ class AnnotationElement {
|
||||
});
|
||||
}
|
||||
|
||||
updateOC(optionalContentConfig) {
|
||||
if (!this.data.oc || !optionalContentConfig) {
|
||||
return;
|
||||
}
|
||||
const isVisible = optionalContentConfig.isVisible(this.data.oc);
|
||||
if (isVisible) {
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
get width() {
|
||||
return this.data.rect[2] - this.data.rect[0];
|
||||
}
|
||||
@ -3755,7 +3769,7 @@ class FileAttachmentAnnotationElement extends AnnotationElement {
|
||||
* @property {TextAccessibilityManager} [accessibilityManager]
|
||||
* @property {AnnotationEditorUIManager} [annotationEditorUIManager]
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
* @property {CommentManager} [commentManager] - The comment manager instance.
|
||||
* @property {OptionalContentConfig} [optionalContentConfig]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -3827,7 +3841,7 @@ class AnnotationLayer {
|
||||
* @memberof AnnotationLayer
|
||||
*/
|
||||
async render(params) {
|
||||
const { annotations } = params;
|
||||
const { annotations, optionalContentConfig } = params;
|
||||
const layer = this.div;
|
||||
setLayerDimensions(layer, this.viewport);
|
||||
|
||||
@ -3892,6 +3906,7 @@ class AnnotationLayer {
|
||||
if (data.hidden) {
|
||||
rendered.style.visibility = "hidden";
|
||||
}
|
||||
element.updateOC(optionalContentConfig);
|
||||
|
||||
if (element._isEditable) {
|
||||
this.#editableAnnotations.set(element.data.id, element);
|
||||
@ -4052,11 +4067,14 @@ class AnnotationLayer {
|
||||
* @param {AnnotationLayerParameters} viewport
|
||||
* @memberof AnnotationLayer
|
||||
*/
|
||||
update({ viewport }) {
|
||||
update({ viewport, optionalContentConfig }) {
|
||||
const layer = this.div;
|
||||
this.viewport = viewport;
|
||||
setLayerDimensions(layer, { rotation: viewport.rotation });
|
||||
|
||||
for (const element of this.#elements) {
|
||||
element.updateOC(optionalContentConfig);
|
||||
}
|
||||
this.#setAnnotationCanvasMap();
|
||||
layer.hidden = false;
|
||||
}
|
||||
|
||||
@ -242,7 +242,8 @@ class Rasterize {
|
||||
fieldObjects,
|
||||
page,
|
||||
imageResourcesPath,
|
||||
renderForms = false
|
||||
renderForms = false,
|
||||
optionalContentConfigPromise = null
|
||||
) {
|
||||
try {
|
||||
const { svg, foreignObject, style, div } = this.createContainer(viewport);
|
||||
@ -263,6 +264,7 @@ class Rasterize {
|
||||
imageResourcesPath,
|
||||
renderForms,
|
||||
fieldObjects,
|
||||
optionalContentConfig: await optionalContentConfigPromise,
|
||||
};
|
||||
|
||||
// Ensure that the annotationLayer gets translated.
|
||||
@ -1355,7 +1357,8 @@ class Driver {
|
||||
task.fieldObjects,
|
||||
page,
|
||||
IMAGE_RESOURCES_PATH,
|
||||
renderForms
|
||||
renderForms,
|
||||
task.optionalContentConfigPromise
|
||||
).then(() => {
|
||||
completeRender(false);
|
||||
});
|
||||
|
||||
1
test/pdfs/issue20433.pdf.link
Normal file
1
test/pdfs/issue20433.pdf.link
Normal file
@ -0,0 +1 @@
|
||||
https://web.archive.org/web/20251107005559/https://octopdf.com/octopdf-sample.pdf
|
||||
@ -1956,6 +1956,42 @@
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{
|
||||
"id": "issue20433-initial",
|
||||
"file": "pdfs/issue20433.pdf",
|
||||
"md5": "3a550da7807540982ed457397667db79",
|
||||
"link": true,
|
||||
"rounds": 1,
|
||||
"firstPage": 2,
|
||||
"type": "eq",
|
||||
"forms": true
|
||||
},
|
||||
{
|
||||
"id": "issue20433-no-form",
|
||||
"file": "pdfs/issue20433.pdf",
|
||||
"md5": "3a550da7807540982ed457397667db79",
|
||||
"link": true,
|
||||
"rounds": 1,
|
||||
"firstPage": 2,
|
||||
"type": "eq",
|
||||
"forms": true,
|
||||
"optionalContent": {
|
||||
"73R": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "issue20433-no-mathml",
|
||||
"file": "pdfs/issue20433.pdf",
|
||||
"md5": "3a550da7807540982ed457397667db79",
|
||||
"link": true,
|
||||
"rounds": 1,
|
||||
"firstPage": 2,
|
||||
"type": "eq",
|
||||
"forms": true,
|
||||
"optionalContent": {
|
||||
"115R": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "issue13845",
|
||||
"file": "pdfs/issue13845.pdf",
|
||||
|
||||
@ -63,6 +63,7 @@ import { PresentationModeState } from "./ui_utils.js";
|
||||
* @property {PageViewport} viewport
|
||||
* @property {string} [intent] - The default value is "display".
|
||||
* @property {StructTreeLayerBuilder} [structTreeLayer]
|
||||
* @property {Promise} [optionalContentConfigPromise]
|
||||
*/
|
||||
|
||||
class AnnotationLayerBuilder {
|
||||
@ -125,8 +126,15 @@ class AnnotationLayerBuilder {
|
||||
* @returns {Promise<void>} A promise that is resolved when rendering of the
|
||||
* annotations is complete.
|
||||
*/
|
||||
async render({ viewport, intent = "display", structTreeLayer = null }) {
|
||||
async render({
|
||||
viewport,
|
||||
intent = "display",
|
||||
structTreeLayer = null,
|
||||
optionalContentConfigPromise = null,
|
||||
}) {
|
||||
if (this.div) {
|
||||
const optionalContentConfig = await optionalContentConfigPromise;
|
||||
|
||||
if (this._cancelled || !this.annotationLayer) {
|
||||
return;
|
||||
}
|
||||
@ -134,15 +142,18 @@ class AnnotationLayerBuilder {
|
||||
// transformation matrices.
|
||||
this.annotationLayer.update({
|
||||
viewport: viewport.clone({ dontFlip: true }),
|
||||
optionalContentConfig,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const [annotations, hasJSActions, fieldObjects] = await Promise.all([
|
||||
this.pdfPage.getAnnotations({ intent }),
|
||||
this._hasJSActionsPromise,
|
||||
this._fieldObjectsPromise,
|
||||
]);
|
||||
const [annotations, hasJSActions, fieldObjects, optionalContentConfig] =
|
||||
await Promise.all([
|
||||
this.pdfPage.getAnnotations({ intent }),
|
||||
this._hasJSActionsPromise,
|
||||
this._fieldObjectsPromise,
|
||||
optionalContentConfigPromise,
|
||||
]);
|
||||
if (this._cancelled) {
|
||||
return;
|
||||
}
|
||||
@ -169,6 +180,7 @@ class AnnotationLayerBuilder {
|
||||
enableScripting: this.enableScripting,
|
||||
hasJSActions,
|
||||
fieldObjects,
|
||||
optionalContentConfig,
|
||||
});
|
||||
|
||||
this.#annotations = annotations;
|
||||
|
||||
@ -464,6 +464,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
viewport: this.viewport,
|
||||
intent: "display",
|
||||
structTreeLayer: this.structTreeLayer,
|
||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
});
|
||||
} catch (ex) {
|
||||
console.error("#renderAnnotationLayer:", ex);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user