mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 23:04:02 +02:00
Add the possibility to merge/update acroforms when merging/extracting (bug 2015853)
This commit is contained in:
parent
a4fcd830cc
commit
baf8647b1f
@ -208,6 +208,38 @@ function getParentToUpdate(dict, ref, xref) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function deepCompare(a, b) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a instanceof Dict && b instanceof Dict) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (const [key, value1] of a.getRawEntries()) {
|
||||
const value2 = b.getRaw(key);
|
||||
if (value2 === undefined || !deepCompare(value1, value2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0, ii = a.length; i < ii; i++) {
|
||||
if (!deepCompare(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const ROMAN_NUMBER_MAP = [
|
||||
"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM",
|
||||
@ -745,6 +777,7 @@ export {
|
||||
arrayBuffersToBytes,
|
||||
codePointIter,
|
||||
collectActions,
|
||||
deepCompare,
|
||||
encodeToXmlString,
|
||||
escapePDFName,
|
||||
escapeString,
|
||||
|
||||
@ -17,13 +17,17 @@
|
||||
/** @typedef {import("../document.js").Page} Page */
|
||||
/** @typedef {import("../xref.js").XRef} XRef */
|
||||
|
||||
import {
|
||||
deepCompare,
|
||||
getInheritableProperty,
|
||||
stringToAsciiOrUTF16BE,
|
||||
} from "../core_utils.js";
|
||||
import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
|
||||
import { getModificationDate, stringToPDFString } from "../../shared/util.js";
|
||||
import { incrementalUpdate, writeValue } from "../writer.js";
|
||||
import { NameTree, NumberTree } from "../name_number_tree.js";
|
||||
import { BaseStream } from "../base_stream.js";
|
||||
import { StringStream } from "../stream.js";
|
||||
import { stringToAsciiOrUTF16BE } from "../core_utils.js";
|
||||
|
||||
const MAX_LEAVES_PER_PAGES_NODE = 16;
|
||||
const MAX_IN_NAME_TREE_NODE = 64;
|
||||
@ -60,6 +64,12 @@ class DocumentData {
|
||||
this.namespaces = null;
|
||||
this.structTreeAF = null;
|
||||
this.structTreePronunciationLexicon = [];
|
||||
this.acroForm = null;
|
||||
this.acroFormDefaultAppearance = "";
|
||||
this.acroFormDefaultResources = null;
|
||||
this.acroFormQ = 0;
|
||||
this.hasSignatureAnnotations = false;
|
||||
this.fieldToParent = new RefSetCache();
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +134,20 @@ class PDFEditor {
|
||||
|
||||
structTreePronunciationLexicon = [];
|
||||
|
||||
fields = [];
|
||||
|
||||
acroFormDefaultAppearance = "";
|
||||
|
||||
acroFormDefaultResources = null;
|
||||
|
||||
acroFormNeedAppearances = false;
|
||||
|
||||
acroFormSigFlags = 0;
|
||||
|
||||
acroFormCalculationOrder = null;
|
||||
|
||||
acroFormQ = 0;
|
||||
|
||||
constructor({ useObjectStreams = true, title = "", author = "" } = {}) {
|
||||
[this.rootRef, this.rootDict] = this.newDict;
|
||||
[this.infoRef, this.infoDict] = this.newDict;
|
||||
@ -625,6 +649,7 @@ class PDFEditor {
|
||||
|
||||
this.#fixPostponedRefCopies(allDocumentData);
|
||||
await this.#mergeStructTrees(allDocumentData);
|
||||
await this.#mergeAcroForms(allDocumentData);
|
||||
|
||||
return this.writePDF();
|
||||
}
|
||||
@ -648,6 +673,9 @@ class PDFEditor {
|
||||
pdfManager
|
||||
.ensureCatalog("structTreeRoot")
|
||||
.then(structTreeRoot => (documentData.structTreeRoot = structTreeRoot)),
|
||||
pdfManager
|
||||
.ensureCatalog("acroForm")
|
||||
.then(acroForm => (documentData.acroForm = acroForm)),
|
||||
]);
|
||||
const structTreeRoot = documentData.structTreeRoot;
|
||||
if (structTreeRoot) {
|
||||
@ -683,7 +711,12 @@ class PDFEditor {
|
||||
async #postCollectPageData(pageData) {
|
||||
const {
|
||||
page: { xref, annotations },
|
||||
documentData: { pagesMap, destinations, usedNamedDestinations },
|
||||
documentData: {
|
||||
pagesMap,
|
||||
destinations,
|
||||
usedNamedDestinations,
|
||||
fieldToParent,
|
||||
},
|
||||
} = pageData;
|
||||
|
||||
if (!annotations) {
|
||||
@ -693,6 +726,7 @@ class PDFEditor {
|
||||
const promises = [];
|
||||
let newAnnotations = [];
|
||||
let newIndex = 0;
|
||||
let { hasSignatureAnnotations } = pageData.documentData;
|
||||
|
||||
// Filter out annotations that are linking to deleted pages.
|
||||
for (const annotationRef of annotations) {
|
||||
@ -700,6 +734,20 @@ class PDFEditor {
|
||||
promises.push(
|
||||
xref.fetchIfRefAsync(annotationRef).then(async annotationDict => {
|
||||
if (!isName(annotationDict.get("Subtype"), "Link")) {
|
||||
if (isName(annotationDict.get("Subtype"), "Widget")) {
|
||||
hasSignatureAnnotations ||= isName(
|
||||
annotationDict.get("FT"),
|
||||
"Sig"
|
||||
);
|
||||
const parentRef = annotationDict.get("Parent") || null;
|
||||
// We remove the parent to avoid visiting it when cloning the
|
||||
// annotation.
|
||||
// It'll be fixed later in #mergeAcroForms when merging the
|
||||
// AcroForms.
|
||||
annotationDict.delete("Parent");
|
||||
fieldToParent.put(annotationRef, parentRef);
|
||||
}
|
||||
|
||||
newAnnotations[newAnnotationIndex] = annotationRef;
|
||||
return;
|
||||
}
|
||||
@ -735,6 +783,7 @@ class PDFEditor {
|
||||
await Promise.all(promises);
|
||||
newAnnotations = newAnnotations.filter(annot => !!annot);
|
||||
pageData.annotations = newAnnotations.length > 0 ? newAnnotations : null;
|
||||
pageData.documentData.hasSignatureAnnotations ||= hasSignatureAnnotations;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -813,46 +862,52 @@ class PDFEditor {
|
||||
}
|
||||
const pageRef = this.newPages[i];
|
||||
const pageDict = this.xref[pageRef.num];
|
||||
const visited = new RefSet();
|
||||
visited.put(pageRef);
|
||||
|
||||
// Visit the new page in order to collect used StructParent entries.
|
||||
this.#visitObject(pageDict, dict => {
|
||||
const structParent =
|
||||
dict.get("StructParent") ?? dict.get("StructParents");
|
||||
if (typeof structParent !== "number") {
|
||||
return;
|
||||
}
|
||||
usedStructParents.add(structParent);
|
||||
let parent = parentTree.get(structParent);
|
||||
const parentRef = parent instanceof Ref ? parent : null;
|
||||
if (parentRef) {
|
||||
const array = xref.fetch(parentRef);
|
||||
if (Array.isArray(array)) {
|
||||
parent = array;
|
||||
this.#visitObject(
|
||||
pageDict,
|
||||
dict => {
|
||||
const structParent =
|
||||
dict.get("StructParent") ?? dict.get("StructParents");
|
||||
if (typeof structParent !== "number") {
|
||||
return;
|
||||
}
|
||||
usedStructParents.add(structParent);
|
||||
let parent = parentTree.get(structParent);
|
||||
const parentRef = parent instanceof Ref ? parent : null;
|
||||
if (parentRef) {
|
||||
const array = xref.fetch(parentRef);
|
||||
if (Array.isArray(array)) {
|
||||
parent = array;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(parent) && parent.every(ref => ref === null)) {
|
||||
parent = null;
|
||||
}
|
||||
if (!parent) {
|
||||
if (dict.has("StructParent")) {
|
||||
dict.delete("StructParent");
|
||||
} else {
|
||||
dict.delete("StructParents");
|
||||
}
|
||||
return;
|
||||
}
|
||||
let newStructParent = oldStructParentMapping.get(structParent);
|
||||
if (newStructParent === undefined) {
|
||||
newStructParent = newStructParentId++;
|
||||
oldStructParentMapping.set(structParent, newStructParent);
|
||||
newParentTree.set(newStructParent, [oldRefMapping, parent]);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(parent) && parent.every(ref => ref === null)) {
|
||||
parent = null;
|
||||
}
|
||||
if (!parent) {
|
||||
if (dict.has("StructParent")) {
|
||||
dict.delete("StructParent");
|
||||
dict.set("StructParent", newStructParent);
|
||||
} else {
|
||||
dict.delete("StructParents");
|
||||
dict.set("StructParents", newStructParent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let newStructParent = oldStructParentMapping.get(structParent);
|
||||
if (newStructParent === undefined) {
|
||||
newStructParent = newStructParentId++;
|
||||
oldStructParentMapping.set(structParent, newStructParent);
|
||||
newParentTree.set(newStructParent, [oldRefMapping, parent]);
|
||||
}
|
||||
if (dict.has("StructParent")) {
|
||||
dict.set("StructParent", newStructParent);
|
||||
} else {
|
||||
dict.set("StructParents", newStructParent);
|
||||
}
|
||||
});
|
||||
},
|
||||
visited
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
@ -1159,6 +1214,361 @@ class PDFEditor {
|
||||
}
|
||||
}
|
||||
|
||||
async #mergeAcroForms(allDocumentData) {
|
||||
this.#setAcroFormDefaultBasicValues(allDocumentData);
|
||||
this.#setAcroFormDefaultAppearance(allDocumentData);
|
||||
this.#setAcroFormQ(allDocumentData);
|
||||
await this.#setAcroFormDefaultResources(allDocumentData);
|
||||
const newFields = this.fields;
|
||||
for (const documentData of allDocumentData) {
|
||||
let fields = documentData.acroForm?.get("Fields") || null;
|
||||
if (!fields && documentData.fieldToParent.size > 0) {
|
||||
fields = this.#fixFields(
|
||||
documentData.fieldToParent,
|
||||
documentData.document.xref
|
||||
);
|
||||
}
|
||||
if (Array.isArray(fields) && fields.length > 0) {
|
||||
this.currentDocument = documentData;
|
||||
await this.#cloneFields(newFields, fields);
|
||||
this.currentDocument = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#setAcroFormQ(allDocumentData) {
|
||||
let firstQ = 0;
|
||||
let firstDocData = null;
|
||||
for (const documentData of allDocumentData) {
|
||||
const q = documentData.acroForm?.get("Q");
|
||||
if (typeof q !== "number" || q === 0) {
|
||||
continue;
|
||||
}
|
||||
if (firstDocData?.acroFormQ > 0) {
|
||||
documentData.acroFormQ = q;
|
||||
continue;
|
||||
}
|
||||
if (firstQ === 0) {
|
||||
firstQ = q;
|
||||
firstDocData = documentData;
|
||||
continue;
|
||||
}
|
||||
if (q === firstQ) {
|
||||
continue;
|
||||
}
|
||||
firstDocData.acroFormQ ||= firstQ;
|
||||
documentData.acroFormQ = q;
|
||||
firstQ = 0;
|
||||
}
|
||||
|
||||
if (firstQ > 0) {
|
||||
this.acroFormQ = firstQ;
|
||||
}
|
||||
}
|
||||
|
||||
#setAcroFormDefaultBasicValues(allDocumentData) {
|
||||
let sigFlags = 0;
|
||||
let needAppearances = false;
|
||||
const calculationOrder = [];
|
||||
for (const documentData of allDocumentData) {
|
||||
if (!documentData.acroForm) {
|
||||
continue;
|
||||
}
|
||||
const sf = documentData.acroForm.get("SigFlags");
|
||||
if (typeof sf === "number" && documentData.hasSignatureAnnotations) {
|
||||
sigFlags |= sf;
|
||||
}
|
||||
if (documentData.acroForm.get("NeedAppearances") === true) {
|
||||
needAppearances = true;
|
||||
}
|
||||
const co = documentData.acroForm.get("CO") || null;
|
||||
if (!Array.isArray(co)) {
|
||||
continue;
|
||||
}
|
||||
const { oldRefMapping } = documentData;
|
||||
for (const coRef of co) {
|
||||
const newCoRef = oldRefMapping.get(coRef);
|
||||
if (newCoRef) {
|
||||
calculationOrder.push(newCoRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.acroFormSigFlags = sigFlags;
|
||||
this.acroFormNeedAppearances = needAppearances;
|
||||
this.acroFormCalculationOrder =
|
||||
calculationOrder.length > 0 ? calculationOrder : null;
|
||||
}
|
||||
|
||||
#setAcroFormDefaultAppearance(allDocumentData) {
|
||||
// If all the DAs are the same we just use it in the AcroForm. Otherwise, we
|
||||
// set the DA for each documentData and use for any annotations that don't
|
||||
// have their own DA.
|
||||
let firstDA = null;
|
||||
let firstDocData = null;
|
||||
for (const documentData of allDocumentData) {
|
||||
const da = documentData.acroForm?.get("DA") || null;
|
||||
if (!da || typeof da !== "string") {
|
||||
continue;
|
||||
}
|
||||
if (firstDocData?.acroFormDefaultAppearance) {
|
||||
documentData.acroFormDefaultAppearance = da;
|
||||
continue;
|
||||
}
|
||||
if (!firstDA) {
|
||||
firstDA = da;
|
||||
firstDocData = documentData;
|
||||
continue;
|
||||
}
|
||||
if (da === firstDA) {
|
||||
continue;
|
||||
}
|
||||
firstDocData.acroFormDefaultAppearance ||= firstDA;
|
||||
documentData.acroFormDefaultAppearance = da;
|
||||
firstDA = null;
|
||||
}
|
||||
|
||||
if (firstDA) {
|
||||
this.acroFormDefaultAppearance = firstDA;
|
||||
}
|
||||
}
|
||||
|
||||
async #setAcroFormDefaultResources(allDocumentData) {
|
||||
let firstDR = null;
|
||||
let firstDRRef = null;
|
||||
let firstDocData = null;
|
||||
for (const documentData of allDocumentData) {
|
||||
const dr = documentData.acroForm?.get("DR") || null;
|
||||
if (!dr || !(dr instanceof Dict)) {
|
||||
continue;
|
||||
}
|
||||
if (firstDocData?.acroFormDefaultResources) {
|
||||
documentData.acroFormDefaultResources = dr;
|
||||
continue;
|
||||
}
|
||||
if (!firstDR) {
|
||||
firstDR = dr;
|
||||
firstDRRef = documentData.acroForm.getRaw("DR");
|
||||
firstDocData = documentData;
|
||||
continue;
|
||||
}
|
||||
if (deepCompare(firstDR, dr)) {
|
||||
continue;
|
||||
}
|
||||
firstDocData.acroFormDefaultResources ||= firstDR;
|
||||
documentData.acroFormDefaultResources = dr;
|
||||
firstDR = null;
|
||||
firstDRRef = null;
|
||||
}
|
||||
|
||||
if (firstDR) {
|
||||
this.currentDocument = firstDocData;
|
||||
this.acroFormDefaultResources = await this.#collectDependencies(
|
||||
firstDRRef,
|
||||
true,
|
||||
firstDocData.document.xref
|
||||
);
|
||||
this.currentDocument = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the document has some fields but no Fields entry in the AcroForm, we
|
||||
* need to fix that by creating a Fields entry with the oldest parent field
|
||||
* for each field.
|
||||
* @param {Map<Ref, Ref>} fieldToParent
|
||||
* @param {XRef} xref
|
||||
* @returns {Array<Ref>}
|
||||
*/
|
||||
#fixFields(fieldToParent, xref) {
|
||||
const newFields = [];
|
||||
const processed = new RefSet();
|
||||
for (const [fieldRef, parentRef] of fieldToParent) {
|
||||
if (!parentRef) {
|
||||
newFields.push(fieldRef);
|
||||
continue;
|
||||
}
|
||||
let parent = parentRef;
|
||||
let lastNonNullParent = parentRef;
|
||||
while (true) {
|
||||
parent = xref.fetchIfRef(parent)?.get("Parent") || null;
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
lastNonNullParent = parent;
|
||||
}
|
||||
if (!processed.has(lastNonNullParent)) {
|
||||
newFields.push(lastNonNullParent);
|
||||
processed.put(lastNonNullParent);
|
||||
}
|
||||
}
|
||||
return newFields;
|
||||
}
|
||||
|
||||
async #cloneFields(newFields, fields) {
|
||||
const processed = new RefSet();
|
||||
const stack = [
|
||||
{
|
||||
kids: fields,
|
||||
newKids: newFields,
|
||||
pos: 0,
|
||||
oldParentRef: null,
|
||||
parentRef: null,
|
||||
parent: null,
|
||||
},
|
||||
];
|
||||
const {
|
||||
document: { xref },
|
||||
oldRefMapping,
|
||||
fieldToParent,
|
||||
acroFormDefaultAppearance,
|
||||
acroFormDefaultResources,
|
||||
acroFormQ,
|
||||
} = this.currentDocument;
|
||||
const daToFix = [];
|
||||
const drToFix = [];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const data = stack.at(-1);
|
||||
const { kids, newKids, parent, pos } = data;
|
||||
if (pos === kids.length) {
|
||||
stack.pop();
|
||||
if (newKids.length === 0 || !parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parentDict = (this.xref[data.parentRef.num] =
|
||||
this.cloneDict(parent));
|
||||
parentDict.delete("Parent");
|
||||
parentDict.delete("Kids");
|
||||
await this.#collectDependencies(parentDict, false, xref);
|
||||
parentDict.set("Kids", newKids);
|
||||
|
||||
if (stack.length > 0) {
|
||||
const lastData = stack.at(-1);
|
||||
if (!lastData.parentRef && lastData.oldParentRef) {
|
||||
const parentRef = (lastData.parentRef = this.newRef);
|
||||
parentDict.set("Parent", parentRef);
|
||||
oldRefMapping.put(lastData.oldParentRef, parentRef);
|
||||
}
|
||||
lastData.newKids.push(data.parentRef);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const oldKidRef = kids[data.pos++];
|
||||
if (!(oldKidRef instanceof Ref) || processed.has(oldKidRef)) {
|
||||
continue;
|
||||
}
|
||||
processed.put(oldKidRef);
|
||||
const kid = xref.fetchIfRef(oldKidRef);
|
||||
if (kid.has("Kids")) {
|
||||
const kidsArray = kid.get("Kids");
|
||||
if (!Array.isArray(kidsArray)) {
|
||||
continue;
|
||||
}
|
||||
stack.push({
|
||||
kids: kidsArray,
|
||||
newKids: [],
|
||||
pos: 0,
|
||||
oldParentRef: oldKidRef,
|
||||
parentRef: null,
|
||||
parent: kid,
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fieldToParent.has(oldKidRef)) {
|
||||
continue;
|
||||
}
|
||||
const newRef = oldRefMapping.get(oldKidRef);
|
||||
if (!newRef) {
|
||||
continue;
|
||||
}
|
||||
newKids.push(newRef);
|
||||
if (!data.parentRef && data.oldParentRef) {
|
||||
data.parentRef = this.newRef;
|
||||
oldRefMapping.put(data.oldParentRef, data.parentRef);
|
||||
}
|
||||
const newKid = this.xref[newRef.num];
|
||||
if (data.parentRef) {
|
||||
newKid.set("Parent", data.parentRef);
|
||||
}
|
||||
if (
|
||||
acroFormDefaultAppearance &&
|
||||
isName(newKid.get("FT"), "Tx") &&
|
||||
!newKid.has("DA")
|
||||
) {
|
||||
// Fix the DA later since we need to have all the fields tree.
|
||||
daToFix.push(newKid);
|
||||
}
|
||||
if (
|
||||
acroFormDefaultResources &&
|
||||
!newKid.has("Kids") &&
|
||||
newKid.get("AP") instanceof Dict
|
||||
) {
|
||||
// Fix the DR later since we need to have all the fields tree.
|
||||
drToFix.push(newKid);
|
||||
}
|
||||
if (acroFormQ && !newKid.has("Q")) {
|
||||
newKid.set("Q", acroFormQ);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of daToFix) {
|
||||
const da = getInheritableProperty({ dict: field, key: "DA" });
|
||||
if (!da) {
|
||||
// No DA in a parent field, we can set the default one.
|
||||
field.set("DA", acroFormDefaultAppearance);
|
||||
}
|
||||
}
|
||||
const resourcesValuesCache = new Map();
|
||||
for (const field of drToFix) {
|
||||
const ap = field.get("AP");
|
||||
for (const value of ap.getValues()) {
|
||||
if (!(value instanceof BaseStream)) {
|
||||
continue;
|
||||
}
|
||||
let resources = value.dict.getRaw("Resources");
|
||||
if (!resources) {
|
||||
const newResourcesRef =
|
||||
await resourcesValuesCache.getOrInsertComputed(
|
||||
acroFormDefaultResources,
|
||||
() => this.#cloneObject(acroFormDefaultResources, xref)
|
||||
);
|
||||
value.dict.set("Resources", newResourcesRef);
|
||||
continue;
|
||||
}
|
||||
|
||||
resources = xref.fetchIfRef(resources);
|
||||
for (const [
|
||||
resKey,
|
||||
resValue,
|
||||
] of acroFormDefaultResources.getRawEntries()) {
|
||||
if (!resources.has(resKey)) {
|
||||
let newResValue = resValue;
|
||||
if (resValue instanceof Ref) {
|
||||
newResValue = await this.#collectDependencies(
|
||||
resValue,
|
||||
true,
|
||||
xref
|
||||
);
|
||||
} else if (
|
||||
resValue instanceof Dict ||
|
||||
resValue instanceof BaseStream ||
|
||||
Array.isArray(resValue)
|
||||
) {
|
||||
newResValue = await resourcesValuesCache.getOrInsertComputed(
|
||||
resValue,
|
||||
() => this.#cloneObject(resValue, xref)
|
||||
);
|
||||
}
|
||||
resources.set(resKey, newResValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #collectPageLabels() {
|
||||
// We can only preserve page labels when editing a single PDF file.
|
||||
// This is consistent with behavior in Adobe Acrobat.
|
||||
@ -1484,6 +1894,33 @@ class PDFEditor {
|
||||
rootDict.set("StructTreeRoot", structTreeRef);
|
||||
}
|
||||
|
||||
#makeAcroForm() {
|
||||
if (this.fields.length === 0) {
|
||||
return;
|
||||
}
|
||||
const { rootDict } = this;
|
||||
const acroFormRef = this.newRef;
|
||||
const acroForm = (this.xref[acroFormRef.num] = new Dict());
|
||||
rootDict.set("AcroForm", acroFormRef);
|
||||
acroForm.set("Fields", this.fields);
|
||||
if (this.acroFormNeedAppearances) {
|
||||
acroForm.set("NeedAppearances", true);
|
||||
}
|
||||
if (this.acroFormSigFlags > 0) {
|
||||
acroForm.set("SigFlags", this.acroFormSigFlags);
|
||||
}
|
||||
acroForm.setIfArray("CO", this.acroFormCalculationOrder);
|
||||
acroForm.setIfDict("DR", this.acroFormDefaultResources);
|
||||
if (this.acroFormDefaultAppearance) {
|
||||
acroForm.set("DA", this.acroFormDefaultAppearance);
|
||||
}
|
||||
if (this.acroFormQ > 0) {
|
||||
acroForm.set("Q", this.acroFormQ);
|
||||
}
|
||||
// We don't merge XFA stuff because it'd require to parse, extract and merge
|
||||
// all the data, which is a lot of work for a deprecated feature (i.e. XFA).
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the root dictionary.
|
||||
* @returns {Promise<void>}
|
||||
@ -1492,6 +1929,7 @@ class PDFEditor {
|
||||
const { rootDict } = this;
|
||||
rootDict.setIfName("Type", "Catalog");
|
||||
rootDict.setIfName("Version", this.version);
|
||||
this.#makeAcroForm();
|
||||
this.#makePageTree();
|
||||
this.#makePageLabelsTree();
|
||||
this.#makeDestinationsTree();
|
||||
|
||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -878,3 +878,4 @@
|
||||
!two_pages.pdf
|
||||
!sci-notation.pdf
|
||||
!nested_outline.pdf
|
||||
!form_two_pages.pdf
|
||||
|
||||
BIN
test/pdfs/form_two_pages.pdf
Normal file
BIN
test/pdfs/form_two_pages.pdf
Normal file
Binary file not shown.
@ -6223,5 +6223,112 @@ small scripts as well as for`);
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AcroForm", function () {
|
||||
it("extract page 2 and check AcroForm Fields T entries", async function () {
|
||||
let loadingTask = getDocument(
|
||||
buildGetDocumentParams("form_two_pages.pdf")
|
||||
);
|
||||
let pdfDoc = await loadingTask.promise;
|
||||
|
||||
// Collect the fieldNames (derived from T entries) of annotations on
|
||||
// page 2 of the original document.
|
||||
const origPage2 = await pdfDoc.getPage(2);
|
||||
const origAnnotations = await origPage2.getAnnotations();
|
||||
const origFieldNames = origAnnotations
|
||||
.filter(a => a.fieldName)
|
||||
.map(a => a.fieldName)
|
||||
.sort();
|
||||
|
||||
// Extract only page 2 (0-based index = 1).
|
||||
const data = await pdfDoc.extractPages([
|
||||
{ document: null, includePages: [1] },
|
||||
]);
|
||||
await loadingTask.destroy();
|
||||
|
||||
loadingTask = getDocument(data);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
|
||||
expect(pdfDoc.numPages).toEqual(1);
|
||||
|
||||
// The AcroForm Fields in the new PDF should correspond exactly to the
|
||||
// annotations that were on page 2 of the original document, with the
|
||||
// same T entries (encoded in fieldName).
|
||||
const page = await pdfDoc.getPage(1);
|
||||
const annotations = await page.getAnnotations();
|
||||
const fieldNames = annotations
|
||||
.filter(a => a.fieldName)
|
||||
.map(a => a.fieldName)
|
||||
.sort();
|
||||
|
||||
expect(fieldNames).toEqual(origFieldNames);
|
||||
|
||||
// Also verify the AcroForm Fields via getFieldObjects, which directly
|
||||
// reflects the T entries of the fields in the AcroForm dictionary.
|
||||
const fieldObjects = await pdfDoc.getFieldObjects();
|
||||
expect(fieldObjects).not.toBeNull();
|
||||
expect(Object.keys(fieldObjects).sort()).toEqual(origFieldNames);
|
||||
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
|
||||
it("merge pages 2 and 1 and check AcroForm Fields T entries", async function () {
|
||||
let loadingTask = getDocument(
|
||||
buildGetDocumentParams("form_two_pages.pdf")
|
||||
);
|
||||
let pdfDoc = await loadingTask.promise;
|
||||
|
||||
// Collect fieldNames from each page of the original document.
|
||||
const origPage1 = await pdfDoc.getPage(1);
|
||||
const origPage1FieldNames = (await origPage1.getAnnotations())
|
||||
.filter(a => a.fieldName)
|
||||
.map(a => a.fieldName)
|
||||
.sort();
|
||||
|
||||
const origPage2 = await pdfDoc.getPage(2);
|
||||
const origPage2FieldNames = (await origPage2.getAnnotations())
|
||||
.filter(a => a.fieldName)
|
||||
.map(a => a.fieldName)
|
||||
.sort();
|
||||
|
||||
// Extract page 2 first, then page 1.
|
||||
const data = await pdfDoc.extractPages([
|
||||
{ document: null, includePages: [1] },
|
||||
{ document: null, includePages: [0] },
|
||||
]);
|
||||
await loadingTask.destroy();
|
||||
|
||||
loadingTask = getDocument(data);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
|
||||
expect(pdfDoc.numPages).toEqual(2);
|
||||
|
||||
// Page 1 of the new PDF should have the fields from original page 2.
|
||||
const page1 = await pdfDoc.getPage(1);
|
||||
const page1FieldNames = (await page1.getAnnotations())
|
||||
.filter(a => a.fieldName)
|
||||
.map(a => a.fieldName)
|
||||
.sort();
|
||||
expect(page1FieldNames).toEqual(origPage2FieldNames);
|
||||
|
||||
// Page 2 of the new PDF should have the fields from original page 1.
|
||||
const page2 = await pdfDoc.getPage(2);
|
||||
const page2FieldNames = (await page2.getAnnotations())
|
||||
.filter(a => a.fieldName)
|
||||
.map(a => a.fieldName)
|
||||
.sort();
|
||||
expect(page2FieldNames).toEqual(origPage1FieldNames);
|
||||
|
||||
// The AcroForm Fields should contain all fields from both pages.
|
||||
const fieldObjects = await pdfDoc.getFieldObjects();
|
||||
expect(fieldObjects).not.toBeNull();
|
||||
const allOrigFieldNames = [
|
||||
...new Set([...origPage1FieldNames, ...origPage2FieldNames]),
|
||||
].sort();
|
||||
expect(Object.keys(fieldObjects).sort()).toEqual(allOrigFieldNames);
|
||||
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
|
||||
import {
|
||||
arrayBuffersToBytes,
|
||||
deepCompare,
|
||||
encodeToXmlString,
|
||||
escapePDFName,
|
||||
escapeString,
|
||||
@ -474,6 +475,103 @@ describe("core_utils", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function () {
|
||||
it("should return true for the same reference", function () {
|
||||
const dict = new Dict();
|
||||
expect(deepCompare(dict, dict)).toBeTrue();
|
||||
const arr = [1, 2, 3];
|
||||
expect(deepCompare(arr, arr)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return true for identical primitive values", function () {
|
||||
expect(deepCompare(1, 1)).toBeTrue();
|
||||
expect(deepCompare("hello", "hello")).toBeTrue();
|
||||
expect(deepCompare(null, null)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false for different primitive values", function () {
|
||||
expect(deepCompare(1, 2)).toBeFalse();
|
||||
expect(deepCompare("hello", "world")).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return true for two equal empty Dicts", function () {
|
||||
expect(deepCompare(new Dict(), new Dict())).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false for Dicts with different sizes", function () {
|
||||
const a = new Dict();
|
||||
a.set("key", 1);
|
||||
expect(deepCompare(a, new Dict())).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return true for Dicts with same Ref values", function () {
|
||||
const ref = Ref.get(10, 0);
|
||||
const a = new Dict();
|
||||
a.set("Foo", ref);
|
||||
const b = new Dict();
|
||||
b.set("Foo", ref);
|
||||
expect(deepCompare(a, b)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false for Dicts with different Ref values", function () {
|
||||
const a = new Dict();
|
||||
a.set("Foo", Ref.get(10, 0));
|
||||
const b = new Dict();
|
||||
b.set("Foo", Ref.get(20, 0));
|
||||
expect(deepCompare(a, b)).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return false for Dicts with different numeric values", function () {
|
||||
const a = new Dict();
|
||||
a.set("Foo", 1);
|
||||
const b = new Dict();
|
||||
b.set("Foo", 2);
|
||||
expect(deepCompare(a, b)).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return true for equal nested Dicts", function () {
|
||||
const inner1 = new Dict();
|
||||
inner1.set("Bar", Ref.get(5, 0));
|
||||
const outer1 = new Dict();
|
||||
outer1.set("Foo", inner1);
|
||||
|
||||
const inner2 = new Dict();
|
||||
inner2.set("Bar", Ref.get(5, 0));
|
||||
const outer2 = new Dict();
|
||||
outer2.set("Foo", inner2);
|
||||
|
||||
expect(deepCompare(outer1, outer2)).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false for Dicts with the same key but different nested Dicts", function () {
|
||||
const inner1 = new Dict();
|
||||
inner1.set("Bar", Ref.get(5, 0));
|
||||
const outer1 = new Dict();
|
||||
outer1.set("Foo", inner1);
|
||||
|
||||
const inner2 = new Dict();
|
||||
inner2.set("Bar", Ref.get(99, 0));
|
||||
const outer2 = new Dict();
|
||||
outer2.set("Foo", inner2);
|
||||
|
||||
expect(deepCompare(outer1, outer2)).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return true for equal arrays", function () {
|
||||
const ref = Ref.get(1, 0);
|
||||
expect(deepCompare([ref, ref], [ref, ref])).toBeTrue();
|
||||
});
|
||||
|
||||
it("should return false for arrays with different lengths", function () {
|
||||
const ref = Ref.get(1, 0);
|
||||
expect(deepCompare([ref, ref], [ref])).toBeFalse();
|
||||
});
|
||||
|
||||
it("should return false for arrays with different values", function () {
|
||||
expect(deepCompare([Ref.get(1, 0)], [Ref.get(2, 0)])).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSizeInBytes", function () {
|
||||
it("should get the size in bytes to use to represent a positive integer", function () {
|
||||
expect(getSizeInBytes(0)).toEqual(0);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user