Skip to content

Commit

Permalink
Add support for saving forms
Browse files Browse the repository at this point in the history
  • Loading branch information
calixteman committed Aug 12, 2020
1 parent 3380f2a commit 1a6816b
Show file tree
Hide file tree
Showing 16 changed files with 1,060 additions and 8 deletions.
189 changes: 188 additions & 1 deletion src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,20 @@ import {
AnnotationType,
assert,
escapeString,
getModificationDate,
isString,
OPS,
stringToPDFString,
Util,
warn,
} from "../shared/util.js";
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
import { Dict, isDict, isName, isRef, isStream } from "./primitives.js";
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
import { ColorSpace } from "./colorspace.js";
import { getInheritableProperty } from "./core_utils.js";
import { OperatorList } from "./operator_list.js";
import { StringStream } from "./stream.js";
import { writeDict } from "./writer.js";

class AnnotationFactory {
/**
Expand Down Expand Up @@ -68,6 +70,7 @@ class AnnotationFactory {
if (!isDict(dict)) {
return undefined;
}

const id = isRef(ref) ? ref.toString() : `annot_${idFactory.createObjId()}`;

// Determine the annotation's subtype.
Expand All @@ -77,6 +80,7 @@ class AnnotationFactory {
// Return the right annotation object based on the subtype and field type.
const parameters = {
xref,
ref,
dict,
subtype,
id,
Expand Down Expand Up @@ -550,6 +554,10 @@ class Annotation {
});
});
}

async save(evaluator, task, annotationStorage) {
return null;
}
}

/**
Expand Down Expand Up @@ -791,6 +799,7 @@ class WidgetAnnotation extends Annotation {

const dict = params.dict;
const data = this.data;
this.ref = params.ref;

data.annotationType = AnnotationType.WIDGET;
data.fieldName = this._constructFieldName(dict);
Expand Down Expand Up @@ -953,6 +962,78 @@ class WidgetAnnotation extends Annotation {
);
}

async save(evaluator, task, annotationStorage) {
if (this.data.fieldValue === annotationStorage[this.data.id]) {
return null;
}

let appearance = await this._getAppearance(
evaluator,
task,
annotationStorage
);
if (appearance === null) {
return null;
}

const dict = evaluator.xref.fetchIfRef(this.ref);
if (!isDict(dict)) {
return null;
}

const bbox = [
0,
0,
this.data.rect[2] - this.data.rect[0],
this.data.rect[3] - this.data.rect[1],
];

const newRef = evaluator.xref.getNewRef();
const AP = new Dict(evaluator.xref);
AP.set("N", newRef);

const value = annotationStorage[this.data.id];
const encrypt = evaluator.xref.encrypt;
let originalTransform = null;
let newTransform = null;
if (encrypt) {
originalTransform = encrypt.createCipherTransform(
this.ref.num,
this.ref.gen
);
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
appearance = newTransform.encryptString(appearance);
}

dict.set("V", value);
dict.set("AP", AP);
dict.set("M", `D:${getModificationDate()}`);

const appearanceDict = new Dict(evaluator.xref);
appearanceDict.set("Length", appearance.length);
appearanceDict.set("Subtype", Name.get("Form"));
appearanceDict.set("Resources", this.fieldResources);
appearanceDict.set("BBox", bbox);

const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
writeDict(dict, bufferOriginal, originalTransform);
bufferOriginal.push("\nendobj\n");

const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
writeDict(appearanceDict, bufferNew, newTransform);
bufferNew.push(" stream\n");
bufferNew.push(appearance);
bufferNew.push("\nendstream\nendobj\n");

return [
// data for the original object
// V field changed + reference for new AP
{ ref: this.ref, data: bufferOriginal.join("") },
// data for the new AP
{ ref: newRef, data: bufferNew.join("") },
];
}

async _getAppearance(evaluator, task, annotationStorage) {
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
if (!annotationStorage || isPassword) {
Expand Down Expand Up @@ -1312,6 +1393,111 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
);
}

async save(evaluator, task, annotationStorage) {
if (this.data.checkBox) {
return this._saveCheckbox(evaluator, task, annotationStorage);
}

if (this.data.radioButton) {
return this._saveRadioButton(evaluator, task, annotationStorage);
}

return super.save(evaluator, task, annotationStorage);
}

async _saveCheckbox(evaluator, task, annotationStorage) {
const defaultValue = this.data.fieldValue && this.data.fieldValue !== "Off";
const value = annotationStorage[this.data.id];

if (defaultValue === value) {
return null;
}

const dict = evaluator.xref.fetchIfRef(this.ref);
if (!isDict(dict)) {
return null;
}

const name = Name.get(value ? this.data.exportValue : "Off");
dict.set("V", name);
dict.set("AS", name);
dict.set("M", `D:${getModificationDate()}`);

const encrypt = evaluator.xref.encrypt;
let originalTransform = null;
if (encrypt) {
originalTransform = encrypt.createCipherTransform(
this.ref.num,
this.ref.gen
);
}

const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
writeDict(dict, buffer, originalTransform);
buffer.push("\nendobj\n");

return [{ ref: this.ref, data: buffer.join("") }];
}

async _saveRadioButton(evaluator, task, annotationStorage) {
const defaultValue = this.data.fieldValue === this.data.buttonValue;
const value = annotationStorage[this.data.id];

if (defaultValue === value) {
return null;
}

const dict = evaluator.xref.fetchIfRef(this.ref);
if (!isDict(dict)) {
return null;
}

const name = Name.get(value ? this.data.buttonValue : "Off");
let parentBuffer = null;
const encrypt = evaluator.xref.encrypt;

if (value) {
if (isRef(this.parent)) {
const parent = evaluator.xref.fetch(this.parent);
let parentTransform = null;
if (encrypt) {
parentTransform = encrypt.createCipherTransform(
this.parent.num,
this.parent.gen
);
}
parent.set("V", name);
parentBuffer = [`${this.parent.num} ${this.parent.gen} obj\n`];
writeDict(parent, parentBuffer, parentTransform);
parentBuffer.push("\nendobj\n");
} else if (isDict(this.parent)) {
this.parent.set("V", name);
}
}

dict.set("AS", name);
dict.set("M", `D:${getModificationDate()}`);

let originalTransform = null;
if (encrypt) {
originalTransform = encrypt.createCipherTransform(
this.ref.num,
this.ref.gen
);
}

const buffer = [`${this.ref.num} ${this.ref.gen} obj\n`];
writeDict(dict, buffer, originalTransform);
buffer.push("\nendobj\n");

const newRefs = [{ ref: this.ref, data: buffer.join("") }];
if (parentBuffer !== null) {
newRefs.push({ ref: this.parent, data: parentBuffer.join("") });
}

return newRefs;
}

_processCheckBox(params) {
if (isName(this.data.fieldValue)) {
this.data.fieldValue = this.data.fieldValue.name;
Expand Down Expand Up @@ -1354,6 +1540,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
if (isDict(fieldParent) && fieldParent.has("V")) {
const fieldParentValue = fieldParent.get("V");
if (isName(fieldParentValue)) {
this.parent = params.dict.getRaw("Parent");
this.data.fieldValue = fieldParentValue.name;
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/core/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var ARCFourCipher = (function ARCFourCipherClosure() {
},
};
ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock;

return ARCFourCipher;
})();
Expand Down Expand Up @@ -699,6 +700,9 @@ var NullCipher = (function NullCipherClosure() {
decryptBlock: function NullCipher_decryptBlock(data) {
return data;
},
encrypt: function NullCipher_encrypt(data) {
return data;
},
};

return NullCipher;
Expand Down Expand Up @@ -1097,6 +1101,7 @@ class AESBaseCipher {
if (bufferLength < 16) {
continue;
}

for (let j = 0; j < 16; ++j) {
buffer[j] ^= iv[j];
}
Expand Down Expand Up @@ -1474,6 +1479,42 @@ var CipherTransform = (function CipherTransformClosure() {
data = cipher.decryptBlock(data, true);
return bytesToString(data);
},
encryptString: function CipherTransform_encryptString(s) {
const cipher = new this.StringCipherConstructor();
if (cipher instanceof AESBaseCipher) {
// Append some chars equal to "16 - (M mod 16)"
// where M is the string length (see section 7.6.2 in PDF specification)
// to have a final string where the length is a multiple of 16.
const strLen = s.length;
const pad = 16 - (strLen % 16);
if (pad !== 16) {
s = s.padEnd(16 * Math.ceil(strLen / 16), String.fromCharCode(pad));
}

// Generate an initialization vector
const iv = new Uint8Array(16);
if (typeof crypto !== "undefined") {
crypto.getRandomValues(iv);
} else {
for (let i = 0; i < 16; i++) {
iv[i] = Math.floor(256 * Math.random());
}
}

let data = stringToBytes(s);
data = cipher.encrypt(data, iv);

const buf = new Uint8Array(16 + data.length);
buf.set(iv);
buf.set(data, 16);

return bytesToString(buf);
}

let data = stringToBytes(s);
data = cipher.encrypt(data);
return bytesToString(data);
},
};
return CipherTransform;
})();
Expand Down
37 changes: 37 additions & 0 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,43 @@ class Page {
return stream;
}

save(handler, task, annotationStorage) {
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
handler,
pageIndex: this.pageIndex,
idFactory: this._localIdFactory,
fontCache: this.fontCache,
builtInCMapCache: this.builtInCMapCache,
globalImageCache: this.globalImageCache,
options: this.evaluatorOptions,
});

// Fetch the page's annotations and save the content
// in case of interactive form fields.
return this._parsedAnnotations.then(function (annotations) {
const newRefsPromises = [];
for (const annotation of annotations) {
if (!isAnnotationRenderable(annotation, "print")) {
continue;
}
newRefsPromises.push(
annotation
.save(partialEvaluator, task, annotationStorage)
.catch(function (reason) {
warn(
"save - ignoring annotation data during " +
`"${task.name}" task: "${reason}".`
);
return null;
})
);
}

return Promise.all(newRefsPromises);
});
}

loadResources(keys) {
if (!this.resourcesPromise) {
// TODO: add async `_getInheritableProperty` and remove this.
Expand Down
12 changes: 12 additions & 0 deletions src/core/obj.js
Original file line number Diff line number Diff line change
Expand Up @@ -1211,9 +1211,21 @@ var XRef = (function XRefClosure() {
streamTypes: Object.create(null),
fontTypes: Object.create(null),
};
this._newRefNum = null;
}

XRef.prototype = {
getNewRef: function XRef_getNewRef() {
if (this._newRefNum === null) {
this._newRefNum = this.entries.length;
}
return Ref.get(this._newRefNum++, 0);
},

resetNewRef: function XRef_resetNewRef() {
this._newRefNum = null;
},

setStartXRef: function XRef_setStartXRef(startXRef) {
// Store the starting positions of xref tables as we process them
// so we can recover from missing data errors
Expand Down
Loading

0 comments on commit 1a6816b

Please sign in to comment.