Skip to content

Commit 21396c0

Browse files
committed
Add support for saving forms
1 parent e832a80 commit 21396c0

16 files changed

+611
-17
lines changed

l10n/en-US/viewer.properties

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ print.title=Print
4141
print_label=Print
4242
download.title=Download
4343
download_label=Download
44+
save.title=Save
45+
save_label=Save
4446
bookmark.title=Current view (copy or open in new window)
4547
bookmark_label=Current View
4648

src/core/annotation.js

+87
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { ColorSpace } from "./colorspace.js";
3434
import { getInheritableProperty } from "./core_utils.js";
3535
import { OperatorList } from "./operator_list.js";
3636
import { StringStream } from "./stream.js";
37+
import { writeDict } from "./writer.js";
3738

3839
class AnnotationFactory {
3940
/**
@@ -68,6 +69,7 @@ class AnnotationFactory {
6869
if (!isDict(dict)) {
6970
return undefined;
7071
}
72+
7173
const id = isRef(ref) ? ref.toString() : `annot_${idFactory.createObjId()}`;
7274

7375
// Determine the annotation's subtype.
@@ -77,6 +79,7 @@ class AnnotationFactory {
7779
// Return the right annotation object based on the subtype and field type.
7880
const parameters = {
7981
xref,
82+
ref,
8083
dict,
8184
subtype,
8285
id,
@@ -792,6 +795,8 @@ class WidgetAnnotation extends Annotation {
792795

793796
const dict = params.dict;
794797
const data = this.data;
798+
this.dict = dict;
799+
this.ref = params.ref;
795800

796801
data.annotationType = AnnotationType.WIDGET;
797802
data.fieldName = this._constructFieldName(dict);
@@ -954,6 +959,70 @@ class WidgetAnnotation extends Annotation {
954959
);
955960
}
956961

962+
async save(evaluator, task, annotationStorage) {
963+
if (this.data.fieldValue === annotationStorage[this.data.id]) {
964+
return null;
965+
}
966+
967+
let appearance = await this.getAppearance(
968+
evaluator,
969+
task,
970+
annotationStorage
971+
);
972+
if (appearance !== null) {
973+
const bbox = [
974+
0,
975+
0,
976+
this.data.rect[2] - this.data.rect[0],
977+
this.data.rect[3] - this.data.rect[1],
978+
];
979+
const newRef = evaluator.xref.getNewRef();
980+
const AP = new Dict(null);
981+
AP.set("N", newRef);
982+
983+
let annotationString = annotationStorage[this.data.id];
984+
const encrypt = evaluator.xref.encrypt;
985+
if (encrypt) {
986+
const transf = encrypt.createCipherTransform(
987+
this.ref.num,
988+
this.ref.gen
989+
);
990+
annotationString = transf.encryptString(annotationString);
991+
const da = this.dict.get("DA") || null;
992+
if (da !== null) {
993+
this.dict.set("DA", transf.encryptString(da));
994+
}
995+
appearance = encrypt
996+
.createCipherTransform(newRef.num, newRef.gen)
997+
.encryptString(appearance);
998+
}
999+
1000+
this.dict.set("V", annotationString);
1001+
this.dict.set("AP", AP);
1002+
1003+
const appearanceDict = new Dict(null);
1004+
appearanceDict.set("Length", appearance.length);
1005+
appearanceDict.set("Subtype", { name: "Form" });
1006+
appearanceDict.set("Resources", this.fieldResources);
1007+
appearanceDict.set("BBox", bbox);
1008+
1009+
let bufferOriginal = `${this.ref.num} 0 obj\n`;
1010+
bufferOriginal = writeDict(this.dict, bufferOriginal);
1011+
bufferOriginal += "\nendobj\n";
1012+
1013+
let bufferNew = `${newRef.num} ${newRef.gen} obj\n`;
1014+
bufferNew = writeDict(appearanceDict, bufferNew);
1015+
bufferNew += ` stream\n${appearance}`;
1016+
bufferNew += "\nendstream\nendobj\n";
1017+
1018+
return [
1019+
{ ref: this.ref.num, data: bufferOriginal },
1020+
{ ref: newRef.num, data: bufferNew },
1021+
];
1022+
}
1023+
return null;
1024+
}
1025+
9571026
async getAppearance(evaluator, task, annotationStorage) {
9581027
// If it's a password textfield then no rendering to avoid to leak it.
9591028
// see 12.7.4.3, table 228
@@ -1288,6 +1357,24 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
12881357
);
12891358
}
12901359

1360+
async save(evaluator, task, annotationStorage) {
1361+
const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off";
1362+
const isChecked = annotationStorage[this.data.id];
1363+
if (defaultValue === isChecked) {
1364+
return null;
1365+
}
1366+
1367+
const value = isChecked ? this.data.exportValue : "Off";
1368+
this.dict.set("V", { name: value });
1369+
this.dict.set("AS", { name: value });
1370+
1371+
let buffer = `${this.ref.num} 0 obj\n`;
1372+
buffer = writeDict(this.dict, buffer);
1373+
buffer += "\nendobj\n";
1374+
1375+
return [{ ref: this.ref.num, data: buffer }];
1376+
}
1377+
12911378
_processCheckBox(params) {
12921379
if (isName(this.data.fieldValue)) {
12931380
this.data.fieldValue = this.data.fieldValue.name;

src/core/crypto.js

+7
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
})();
@@ -1474,6 +1475,12 @@ var CipherTransform = (function CipherTransformClosure() {
14741475
data = cipher.decryptBlock(data, true);
14751476
return bytesToString(data);
14761477
},
1478+
encryptString: function CipherTransform_encryptString(s) {
1479+
var cipher = new this.StringCipherConstructor();
1480+
var data = stringToBytes(s);
1481+
data = cipher.encrypt(data);
1482+
return bytesToString(data);
1483+
},
14771484
};
14781485
return CipherTransform;
14791486
})();

src/core/document.js

+40
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,46 @@ 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+
if (annotations.length === 0) {
246+
return null;
247+
}
248+
249+
const newRefsPromises = [];
250+
for (const annotation of annotations) {
251+
if (isAnnotationRenderable(annotation, "print")) {
252+
newRefsPromises.push(
253+
annotation
254+
.save(partialEvaluator, task, annotationStorage)
255+
.catch(function (reason) {
256+
warn(
257+
"save - ignoring annotation data during " +
258+
`"${task.name}" task: "${reason}".`
259+
);
260+
return null;
261+
})
262+
);
263+
}
264+
}
265+
266+
return Promise.all(newRefsPromises);
267+
});
268+
}
269+
230270
loadResources(keys) {
231271
if (!this.resourcesPromise) {
232272
// TODO: add async `_getInheritableProperty` and remove this.

src/core/obj.js

+12
Original file line numberDiff line numberDiff line change
@@ -1135,9 +1135,21 @@ var XRef = (function XRefClosure() {
11351135
streamTypes: Object.create(null),
11361136
fontTypes: Object.create(null),
11371137
};
1138+
this.newRef = null;
11381139
}
11391140

11401141
XRef.prototype = {
1142+
getNewRef: function XRef_getNewRef() {
1143+
if (this.newRef === null) {
1144+
this.newRef = this.entries.length;
1145+
}
1146+
return Ref.get(this.newRef++, 0);
1147+
},
1148+
1149+
resetNewRef: function XRef_resetNewRef() {
1150+
this.newRef = null;
1151+
},
1152+
11411153
setStartXRef: function XRef_setStartXRef(startXRef) {
11421154
// Store the starting positions of xref tables as we process them
11431155
// so we can recover from missing data errors

src/core/worker.js

+55
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import {
2121
getVerbosityLevel,
2222
info,
2323
InvalidPDFException,
24+
isString,
2425
MissingPDFException,
2526
PasswordException,
2627
setVerbosityLevel,
28+
stringToPDFString,
2729
UnexpectedResponseException,
2830
UnknownErrorException,
2931
UNSUPPORTED_FEATURES,
@@ -35,6 +37,7 @@ import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
3537
import { isNodeJS } from "../shared/is_node.js";
3638
import { MessageHandler } from "../shared/message_handler.js";
3739
import { PDFWorkerStream } from "./worker_stream.js";
40+
import { writeXRef } from "./writer.js";
3841
import { XRefParseException } from "./core_utils.js";
3942

4043
class WorkerTask {
@@ -513,6 +516,58 @@ class WorkerMessageHandler {
513516
});
514517
});
515518

519+
handler.on("SavePage", function (data) {
520+
var pageIndex = data.pageIndex;
521+
return pdfManager.getPage(pageIndex).then(function (page) {
522+
const task = new WorkerTask(`Save: page ${pageIndex}`);
523+
return page.save(handler, task, data.annotationStorage);
524+
});
525+
});
526+
527+
handler.on("SaveDocument", function (data) {
528+
pdfManager.requestLoadedStream();
529+
530+
const newRefs = data.newRefs;
531+
if (newRefs.length === 0) {
532+
// No new refs so just return the initial bytes
533+
return pdfManager.onLoadedStream().then(function (stream) {
534+
return stream.bytes;
535+
});
536+
}
537+
538+
const xref = pdfManager.pdfDocument.xref;
539+
let newXrefInfo = Object.create(null);
540+
if (xref.trailer) {
541+
// Get string info from Info in order to compute fileId
542+
const _info = Object.create(null);
543+
const xrefInfo = xref.trailer.get("Info") || null;
544+
if (xrefInfo) {
545+
xrefInfo.forEach((key, value) => {
546+
if (isString(value)) {
547+
_info[key] =
548+
typeof value !== "string" ? value : stringToPDFString(value);
549+
}
550+
});
551+
}
552+
553+
newXrefInfo = {
554+
rootRef: xref.trailer.getRaw("Root") || null,
555+
encrypt: xref.trailer.getRaw("Encrypt") || null,
556+
newRef: xref.getNewRef(),
557+
infoRef: xref.trailer.getRaw("Info") || null,
558+
info: _info,
559+
fileIds: xref.trailer.getRaw("ID") || null,
560+
startXRef: pdfManager.pdfDocument.startXRef,
561+
filename: data.filename,
562+
};
563+
}
564+
xref.resetNewRef();
565+
566+
return pdfManager.onLoadedStream().then(function (stream) {
567+
return writeXRef(stream.bytes, newXrefInfo, newRefs);
568+
});
569+
});
570+
516571
handler.on(
517572
"GetOperatorList",
518573
function wphSetupRenderPage(data, sink) {

0 commit comments

Comments
 (0)