Skip to content

Commit 6881752

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

17 files changed

+944
-9
lines changed

src/core/annotation.js

+119-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,75 @@ 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+
const dict = evaluator.xref.fetchIfRef(this.ref);
977+
if (!isDict(dict)) {
978+
return null;
979+
}
980+
981+
const bbox = [
982+
0,
983+
0,
984+
this.data.rect[2] - this.data.rect[0],
985+
this.data.rect[3] - this.data.rect[1],
986+
];
987+
988+
const newRef = evaluator.xref.getNewRef();
989+
const AP = new Dict(null);
990+
AP.set("N", newRef);
991+
992+
const annotationString = annotationStorage[this.data.id];
993+
const encrypt = evaluator.xref.encrypt;
994+
let transfOriginal = null;
995+
let transfNew = null;
996+
if (encrypt) {
997+
transfOriginal = encrypt.createCipherTransform(
998+
this.ref.num,
999+
this.ref.gen
1000+
);
1001+
transfNew = encrypt.createCipherTransform(newRef.num, newRef.gen);
1002+
appearance = transfNew.encryptString(appearance);
1003+
}
1004+
1005+
dict.set("V", annotationString);
1006+
dict.set("AP", AP);
1007+
dict.set("M", `D:${getModificationDate()}`);
1008+
1009+
const appearanceDict = new Dict(null);
1010+
appearanceDict.set("Length", appearance.length);
1011+
appearanceDict.set("Subtype", Name.get("Form"));
1012+
appearanceDict.set("Resources", this.fieldResources);
1013+
appearanceDict.set("BBox", bbox);
1014+
appearanceDict.set("M", `D:${getModificationDate()}`);
1015+
1016+
const bufferOriginal = [`${this.ref.num} 0 obj\n`];
1017+
writeDict(dict, bufferOriginal, transfOriginal);
1018+
bufferOriginal.push("\nendobj\n");
1019+
1020+
const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
1021+
writeDict(appearanceDict, bufferNew, transfNew);
1022+
bufferNew.push(" stream\n");
1023+
bufferNew.push(appearance);
1024+
bufferNew.push("\nendstream\nendobj\n");
1025+
1026+
return [
1027+
{ ref: this.ref.num, data: bufferOriginal.join("") },
1028+
{ ref: newRef.num, data: bufferNew.join("") },
1029+
];
1030+
}
1031+
return null;
1032+
}
1033+
9561034
async _getAppearance(evaluator, task, annotationStorage) {
9571035
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
9581036
if (!annotationStorage || isPassword) {
@@ -1312,6 +1390,46 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
13121390
);
13131391
}
13141392

1393+
async save(evaluator, task, annotationStorage) {
1394+
if (!this.data.checkBox && !this.data.radioButton) {
1395+
return null;
1396+
}
1397+
1398+
const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off";
1399+
const isChecked = annotationStorage[this.data.id];
1400+
if (defaultValue === isChecked) {
1401+
return null;
1402+
}
1403+
1404+
const dict = evaluator.xref.fetchIfRef(this.ref);
1405+
if (!isDict(dict)) {
1406+
return null;
1407+
}
1408+
1409+
const value = this.data.checkBox
1410+
? this.data.exportValue
1411+
: this.data.buttonValue;
1412+
const name = Name.get(isChecked ? value : "Off");
1413+
dict.set("V", name);
1414+
dict.set("AS", name);
1415+
dict.set("M", `D:${getModificationDate()}`);
1416+
1417+
const encrypt = evaluator.xref.encrypt;
1418+
let transfOriginal = null;
1419+
if (encrypt) {
1420+
transfOriginal = encrypt.createCipherTransform(
1421+
this.ref.num,
1422+
this.ref.gen
1423+
);
1424+
}
1425+
1426+
const buffer = [`${this.ref.num} 0 obj\n`];
1427+
writeDict(dict, buffer, transfOriginal);
1428+
buffer.push("\nendobj\n");
1429+
1430+
return [{ ref: this.ref.num, data: buffer.join("") }];
1431+
}
1432+
13151433
_processCheckBox(params) {
13161434
if (isName(this.data.fieldValue)) {
13171435
this.data.fieldValue = this.data.fieldValue.name;

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 7.6.2)
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 iv 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

+36
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,42 @@ 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 add their operator lists to the
243+
// page's operator list to render them.
244+
return this._parsedAnnotations.then(function (annotations) {
245+
const newRefsPromises = [];
246+
for (const annotation of annotations) {
247+
if (isAnnotationRenderable(annotation, "print")) {
248+
newRefsPromises.push(
249+
annotation
250+
.save(partialEvaluator, task, annotationStorage)
251+
.catch(function (reason) {
252+
warn(
253+
"save - ignoring annotation data during " +
254+
`"${task.name}" task: "${reason}".`
255+
);
256+
return null;
257+
})
258+
);
259+
}
260+
}
261+
262+
return Promise.all(newRefsPromises);
263+
});
264+
}
265+
230266
loadResources(keys) {
231267
if (!this.resourcesPromise) {
232268
// 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.newRef = null;
12141215
}
12151216

12161217
XRef.prototype = {
1218+
getNewRef: function XRef_getNewRef() {
1219+
if (this.newRef === null) {
1220+
this.newRef = this.entries.length;
1221+
}
1222+
return Ref.get(this.newRef++, 0);
1223+
},
1224+
1225+
resetNewRef: function XRef_resetNewRef() {
1226+
this.newRef = 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)