Skip to content

Commit cdc4ffa

Browse files
committed
Add support for saving forms
1 parent 6620861 commit cdc4ffa

17 files changed

+1064
-9
lines changed

src/core/annotation.js

+188-1
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ import {
2222
AnnotationType,
2323
assert,
2424
escapeString,
25+
getModificationDate,
2526
isString,
2627
OPS,
2728
stringToPDFString,
2829
Util,
2930
warn,
3031
} from "../shared/util.js";
3132
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
32-
import { Dict, isDict, isName, isRef, isStream } from "./primitives.js";
33+
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
3334
import { ColorSpace } from "./colorspace.js";
3435
import { getInheritableProperty } from "./core_utils.js";
3536
import { OperatorList } from "./operator_list.js";
3637
import { StringStream } from "./stream.js";
38+
import { writeDict } from "./writer.js";
3739

3840
class AnnotationFactory {
3941
/**
@@ -68,6 +70,7 @@ class AnnotationFactory {
6870
if (!isDict(dict)) {
6971
return undefined;
7072
}
73+
7174
const id = isRef(ref) ? ref.toString() : `annot_${idFactory.createObjId()}`;
7275

7376
// Determine the annotation's subtype.
@@ -77,6 +80,7 @@ class AnnotationFactory {
7780
// Return the right annotation object based on the subtype and field type.
7881
const parameters = {
7982
xref,
83+
ref,
8084
dict,
8185
subtype,
8286
id,
@@ -550,6 +554,10 @@ class Annotation {
550554
});
551555
});
552556
}
557+
558+
async save(evaluator, task, annotationStorage) {
559+
return null;
560+
}
553561
}
554562

555563
/**
@@ -791,6 +799,7 @@ class WidgetAnnotation extends Annotation {
791799

792800
const dict = params.dict;
793801
const data = this.data;
802+
this.ref = params.ref;
794803

795804
data.annotationType = AnnotationType.WIDGET;
796805
data.fieldName = this._constructFieldName(dict);
@@ -953,6 +962,78 @@ class WidgetAnnotation extends Annotation {
953962
);
954963
}
955964

965+
async save(evaluator, task, annotationStorage) {
966+
if (this.data.fieldValue === annotationStorage[this.data.id]) {
967+
return null;
968+
}
969+
970+
let appearance = await this._getAppearance(
971+
evaluator,
972+
task,
973+
annotationStorage
974+
);
975+
if (appearance === null) {
976+
return null;
977+
}
978+
979+
const dict = evaluator.xref.fetchIfRef(this.ref);
980+
if (!isDict(dict)) {
981+
return null;
982+
}
983+
984+
const bbox = [
985+
0,
986+
0,
987+
this.data.rect[2] - this.data.rect[0],
988+
this.data.rect[3] - this.data.rect[1],
989+
];
990+
991+
const newRef = evaluator.xref.getNewRef();
992+
const AP = new Dict(evaluator.xref);
993+
AP.set("N", newRef);
994+
995+
const value = annotationStorage[this.data.id];
996+
const encrypt = evaluator.xref.encrypt;
997+
let originalTransform = null;
998+
let newTransform = null;
999+
if (encrypt) {
1000+
originalTransform = encrypt.createCipherTransform(
1001+
this.ref.num,
1002+
this.ref.gen
1003+
);
1004+
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
1005+
appearance = newTransform.encryptString(appearance);
1006+
}
1007+
1008+
dict.set("V", value);
1009+
dict.set("AP", AP);
1010+
dict.set("M", `D:${getModificationDate()}`);
1011+
1012+
const appearanceDict = new Dict(evaluator.xref);
1013+
appearanceDict.set("Length", appearance.length);
1014+
appearanceDict.set("Subtype", Name.get("Form"));
1015+
appearanceDict.set("Resources", this.fieldResources);
1016+
appearanceDict.set("BBox", bbox);
1017+
1018+
const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
1019+
writeDict(dict, bufferOriginal, originalTransform);
1020+
bufferOriginal.push("\nendobj\n");
1021+
1022+
const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
1023+
writeDict(appearanceDict, bufferNew, newTransform);
1024+
bufferNew.push(" stream\n");
1025+
bufferNew.push(appearance);
1026+
bufferNew.push("\nendstream\nendobj\n");
1027+
1028+
return [
1029+
// data for the original object
1030+
// V field changed + reference for new AP
1031+
{ ref: this.ref, data: bufferOriginal.join("") },
1032+
// data for the new AP
1033+
{ ref: newRef, data: bufferNew.join("") },
1034+
];
1035+
}
1036+
9561037
async _getAppearance(evaluator, task, annotationStorage) {
9571038
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
9581039
if (!annotationStorage || isPassword) {
@@ -1312,6 +1393,111 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
13121393
);
13131394
}
13141395

1396+
async save(evaluator, task, annotationStorage) {
1397+
if (this.data.checkBox) {
1398+
return this._saveCheckbox(evaluator, task, annotationStorage);
1399+
}
1400+
1401+
if (this.data.radioButton) {
1402+
return this._saveRadioButton(evaluator, task, annotationStorage);
1403+
}
1404+
1405+
return super.save(evaluator, task, annotationStorage);
1406+
}
1407+
1408+
async _saveCheckbox(evaluator, task, annotationStorage) {
1409+
const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off";
1410+
const value = annotationStorage[this.data.id];
1411+
1412+
if (defaultValue === value) {
1413+
return null;
1414+
}
1415+
1416+
const dict = evaluator.xref.fetchIfRef(this.ref);
1417+
if (!isDict(dict)) {
1418+
return null;
1419+
}
1420+
1421+
const name = Name.get(value ? this.data.exportValue : "Off");
1422+
dict.set("V", name);
1423+
dict.set("AS", name);
1424+
dict.set("M", `D:${getModificationDate()}`);
1425+
1426+
const encrypt = evaluator.xref.encrypt;
1427+
let originalTransform = null;
1428+
if (encrypt) {
1429+
originalTransform = encrypt.createCipherTransform(
1430+
this.ref.num,
1431+
this.ref.gen
1432+
);
1433+
}
1434+
1435+
const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
1436+
writeDict(dict, buffer, originalTransform);
1437+
buffer.push("\nendobj\n");
1438+
1439+
return [{ ref: this.ref, data: buffer.join("") }];
1440+
}
1441+
1442+
async _saveRadioButton(evaluator, task, annotationStorage) {
1443+
const defaultValue = this.data.fieldValue === this.data.buttonValue;
1444+
const value = annotationStorage[this.data.id];
1445+
1446+
if (defaultValue === value) {
1447+
return null;
1448+
}
1449+
1450+
const dict = evaluator.xref.fetchIfRef(this.ref);
1451+
if (!isDict(dict)) {
1452+
return null;
1453+
}
1454+
1455+
const name = Name.get(value ? this.data.buttonValue : "Off");
1456+
let parentBuffer = null;
1457+
const encrypt = evaluator.xref.encrypt;
1458+
1459+
if (value) {
1460+
if (isRef(this.parent)) {
1461+
const parent = evaluator.xref.fetch(this.parent);
1462+
let parentTransform = null;
1463+
if (encrypt) {
1464+
parentTransform = encrypt.createCipherTransform(
1465+
this.parent.num,
1466+
this.parent.gen
1467+
);
1468+
}
1469+
parent.set("V", name);
1470+
parentBuffer = [`${this.parent.num} ${this.parent.gen} obj\n`];
1471+
writeDict(parent, parentBuffer, parentTransform);
1472+
parentBuffer.push("\nendobj\n");
1473+
} else if (isDict(this.parent)) {
1474+
this.parent.set("V", name);
1475+
}
1476+
}
1477+
1478+
dict.set("AS", name);
1479+
dict.set("M", `D:${getModificationDate()}`);
1480+
1481+
let originalTransform = null;
1482+
if (encrypt) {
1483+
originalTransform = encrypt.createCipherTransform(
1484+
this.ref.num,
1485+
this.ref.gen
1486+
);
1487+
}
1488+
1489+
const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
1490+
writeDict(dict, buffer, originalTransform);
1491+
buffer.push("\nendobj\n");
1492+
1493+
const newRefs = [{ ref: this.ref, data: buffer.join("") }];
1494+
if (parentBuffer !== null) {
1495+
newRefs.push({ ref: this.parent, data: parentBuffer.join("") });
1496+
}
1497+
1498+
return newRefs;
1499+
}
1500+
13151501
_processCheckBox(params) {
13161502
if (isName(this.data.fieldValue)) {
13171503
this.data.fieldValue = this.data.fieldValue.name;
@@ -1354,6 +1540,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
13541540
if (isDict(fieldParent) && fieldParent.has("V")) {
13551541
const fieldParentValue = fieldParent.get("V");
13561542
if (isName(fieldParentValue)) {
1543+
this.parent = params.dict.getRaw("Parent");
13571544
this.data.fieldValue = fieldParentValue.name;
13581545
}
13591546
}

src/core/crypto.js

+41
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var ARCFourCipher = (function ARCFourCipherClosure() {
7373
},
7474
};
7575
ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
76+
ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock;
7677

7778
return ARCFourCipher;
7879
})();
@@ -699,6 +700,9 @@ var NullCipher = (function NullCipherClosure() {
699700
decryptBlock: function NullCipher_decryptBlock(data) {
700701
return data;
701702
},
703+
encrypt: function NullCipher_encrypt(data) {
704+
return data;
705+
},
702706
};
703707

704708
return NullCipher;
@@ -1097,6 +1101,7 @@ class AESBaseCipher {
10971101
if (bufferLength < 16) {
10981102
continue;
10991103
}
1104+
11001105
for (let j = 0; j < 16; ++j) {
11011106
buffer[j] ^= iv[j];
11021107
}
@@ -1474,6 +1479,42 @@ var CipherTransform = (function CipherTransformClosure() {
14741479
data = cipher.decryptBlock(data, true);
14751480
return bytesToString(data);
14761481
},
1482+
encryptString: function CipherTransform_encryptString(s) {
1483+
const cipher = new this.StringCipherConstructor();
1484+
if (cipher instanceof AESBaseCipher) {
1485+
// Append some chars equal to "16 - (M mod 16)"
1486+
// where M is the string length (see section 7.6.2 in PDF specification)
1487+
// to have a final string where the length is a multiple of 16.
1488+
const strLen = s.length;
1489+
const pad = 16 - (strLen % 16);
1490+
if (pad !== 16) {
1491+
s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad));
1492+
}
1493+
1494+
// Generate an initialization vector
1495+
const iv = new Uint8Array(16);
1496+
if (typeof crypto !== "undefined") {
1497+
crypto.getRandomValues(iv);
1498+
} else {
1499+
for (let i = 0; i < 16; i++) {
1500+
iv[i] = Math.floor(256 * Math.random());
1501+
}
1502+
}
1503+
1504+
let data = stringToBytes(s);
1505+
data = cipher.encrypt(data, iv);
1506+
1507+
const buf = new Uint8Array(16 + data.length);
1508+
buf.set(iv);
1509+
buf.set(data, 16);
1510+
1511+
return bytesToString(buf);
1512+
}
1513+
1514+
let data = stringToBytes(s);
1515+
data = cipher.encrypt(data);
1516+
return bytesToString(data);
1517+
},
14771518
};
14781519
return CipherTransform;
14791520
})();

src/core/document.js

+37
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,43 @@ class Page {
227227
return stream;
228228
}
229229

230+
save(handler, task, annotationStorage) {
231+
const partialEvaluator = new PartialEvaluator({
232+
xref: this.xref,
233+
handler,
234+
pageIndex: this.pageIndex,
235+
idFactory: this._localIdFactory,
236+
fontCache: this.fontCache,
237+
builtInCMapCache: this.builtInCMapCache,
238+
globalImageCache: this.globalImageCache,
239+
options: this.evaluatorOptions,
240+
});
241+
242+
// Fetch the page's annotations and save the content
243+
// in case of interactive form fields.
244+
return this._parsedAnnotations.then(function (annotations) {
245+
const newRefsPromises = [];
246+
for (const annotation of annotations) {
247+
if (!isAnnotationRenderable(annotation, "print")) {
248+
continue;
249+
}
250+
newRefsPromises.push(
251+
annotation
252+
.save(partialEvaluator, task, annotationStorage)
253+
.catch(function (reason) {
254+
warn(
255+
"save - ignoring annotation data during " +
256+
`"${task.name}" task: "${reason}".`
257+
);
258+
return null;
259+
})
260+
);
261+
}
262+
263+
return Promise.all(newRefsPromises);
264+
});
265+
}
266+
230267
loadResources(keys) {
231268
if (!this.resourcesPromise) {
232269
// TODO: add async `_getInheritableProperty` and remove this.

src/core/obj.js

+12
Original file line numberDiff line numberDiff line change
@@ -1211,9 +1211,21 @@ var XRef = (function XRefClosure() {
12111211
streamTypes: Object.create(null),
12121212
fontTypes: Object.create(null),
12131213
};
1214+
this._newRefNum = null;
12141215
}
12151216

12161217
XRef.prototype = {
1218+
getNewRef: function XRef_getNewRef() {
1219+
if (this._newRefNum === null) {
1220+
this._newRefNum = this.entries.length;
1221+
}
1222+
return Ref.get(this._newRefNum++, 0);
1223+
},
1224+
1225+
resetNewRef: function XRef_resetNewRef() {
1226+
this._newRefNum = null;
1227+
},
1228+
12171229
setStartXRef: function XRef_setStartXRef(startXRef) {
12181230
// Store the starting positions of xref tables as we process them
12191231
// so we can recover from missing data errors

0 commit comments

Comments
 (0)