Add support for saving forms
This commit is contained in:
parent
3380f2a7fc
commit
1a6816ba98
@ -22,6 +22,7 @@ import {
|
|||||||
AnnotationType,
|
AnnotationType,
|
||||||
assert,
|
assert,
|
||||||
escapeString,
|
escapeString,
|
||||||
|
getModificationDate,
|
||||||
isString,
|
isString,
|
||||||
OPS,
|
OPS,
|
||||||
stringToPDFString,
|
stringToPDFString,
|
||||||
@ -29,11 +30,12 @@ import {
|
|||||||
warn,
|
warn,
|
||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { Catalog, FileSpec, ObjectLoader } from "./obj.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 { ColorSpace } from "./colorspace.js";
|
||||||
import { getInheritableProperty } from "./core_utils.js";
|
import { getInheritableProperty } from "./core_utils.js";
|
||||||
import { OperatorList } from "./operator_list.js";
|
import { OperatorList } from "./operator_list.js";
|
||||||
import { StringStream } from "./stream.js";
|
import { StringStream } from "./stream.js";
|
||||||
|
import { writeDict } from "./writer.js";
|
||||||
|
|
||||||
class AnnotationFactory {
|
class AnnotationFactory {
|
||||||
/**
|
/**
|
||||||
@ -68,6 +70,7 @@ class AnnotationFactory {
|
|||||||
if (!isDict(dict)) {
|
if (!isDict(dict)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = isRef(ref) ? ref.toString() : `annot_${idFactory.createObjId()}`;
|
const id = isRef(ref) ? ref.toString() : `annot_${idFactory.createObjId()}`;
|
||||||
|
|
||||||
// Determine the annotation's subtype.
|
// Determine the annotation's subtype.
|
||||||
@ -77,6 +80,7 @@ class AnnotationFactory {
|
|||||||
// Return the right annotation object based on the subtype and field type.
|
// Return the right annotation object based on the subtype and field type.
|
||||||
const parameters = {
|
const parameters = {
|
||||||
xref,
|
xref,
|
||||||
|
ref,
|
||||||
dict,
|
dict,
|
||||||
subtype,
|
subtype,
|
||||||
id,
|
id,
|
||||||
@ -550,6 +554,10 @@ class Annotation {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async save(evaluator, task, annotationStorage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -791,6 +799,7 @@ class WidgetAnnotation extends Annotation {
|
|||||||
|
|
||||||
const dict = params.dict;
|
const dict = params.dict;
|
||||||
const data = this.data;
|
const data = this.data;
|
||||||
|
this.ref = params.ref;
|
||||||
|
|
||||||
data.annotationType = AnnotationType.WIDGET;
|
data.annotationType = AnnotationType.WIDGET;
|
||||||
data.fieldName = this._constructFieldName(dict);
|
data.fieldName = this._constructFieldName(dict);
|
||||||
@ -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) {
|
async _getAppearance(evaluator, task, annotationStorage) {
|
||||||
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
|
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
|
||||||
if (!annotationStorage || isPassword) {
|
if (!annotationStorage || isPassword) {
|
||||||
@ -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) {
|
_processCheckBox(params) {
|
||||||
if (isName(this.data.fieldValue)) {
|
if (isName(this.data.fieldValue)) {
|
||||||
this.data.fieldValue = this.data.fieldValue.name;
|
this.data.fieldValue = this.data.fieldValue.name;
|
||||||
@ -1354,6 +1540,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation {
|
|||||||
if (isDict(fieldParent) && fieldParent.has("V")) {
|
if (isDict(fieldParent) && fieldParent.has("V")) {
|
||||||
const fieldParentValue = fieldParent.get("V");
|
const fieldParentValue = fieldParent.get("V");
|
||||||
if (isName(fieldParentValue)) {
|
if (isName(fieldParentValue)) {
|
||||||
|
this.parent = params.dict.getRaw("Parent");
|
||||||
this.data.fieldValue = fieldParentValue.name;
|
this.data.fieldValue = fieldParentValue.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +73,7 @@ var ARCFourCipher = (function ARCFourCipherClosure() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
|
ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
|
||||||
|
ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock;
|
||||||
|
|
||||||
return ARCFourCipher;
|
return ARCFourCipher;
|
||||||
})();
|
})();
|
||||||
@ -699,6 +700,9 @@ var NullCipher = (function NullCipherClosure() {
|
|||||||
decryptBlock: function NullCipher_decryptBlock(data) {
|
decryptBlock: function NullCipher_decryptBlock(data) {
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
encrypt: function NullCipher_encrypt(data) {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return NullCipher;
|
return NullCipher;
|
||||||
@ -1097,6 +1101,7 @@ class AESBaseCipher {
|
|||||||
if (bufferLength < 16) {
|
if (bufferLength < 16) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let j = 0; j < 16; ++j) {
|
for (let j = 0; j < 16; ++j) {
|
||||||
buffer[j] ^= iv[j];
|
buffer[j] ^= iv[j];
|
||||||
}
|
}
|
||||||
@ -1474,6 +1479,42 @@ var CipherTransform = (function CipherTransformClosure() {
|
|||||||
data = cipher.decryptBlock(data, true);
|
data = cipher.decryptBlock(data, true);
|
||||||
return bytesToString(data);
|
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;
|
return CipherTransform;
|
||||||
})();
|
})();
|
||||||
|
@ -227,6 +227,43 @@ class Page {
|
|||||||
return stream;
|
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) {
|
loadResources(keys) {
|
||||||
if (!this.resourcesPromise) {
|
if (!this.resourcesPromise) {
|
||||||
// TODO: add async `_getInheritableProperty` and remove this.
|
// TODO: add async `_getInheritableProperty` and remove this.
|
||||||
|
@ -1211,9 +1211,21 @@ var XRef = (function XRefClosure() {
|
|||||||
streamTypes: Object.create(null),
|
streamTypes: Object.create(null),
|
||||||
fontTypes: Object.create(null),
|
fontTypes: Object.create(null),
|
||||||
};
|
};
|
||||||
|
this._newRefNum = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
XRef.prototype = {
|
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) {
|
setStartXRef: function XRef_setStartXRef(startXRef) {
|
||||||
// Store the starting positions of xref tables as we process them
|
// Store the starting positions of xref tables as we process them
|
||||||
// so we can recover from missing data errors
|
// so we can recover from missing data errors
|
||||||
|
@ -21,9 +21,11 @@ import {
|
|||||||
getVerbosityLevel,
|
getVerbosityLevel,
|
||||||
info,
|
info,
|
||||||
InvalidPDFException,
|
InvalidPDFException,
|
||||||
|
isString,
|
||||||
MissingPDFException,
|
MissingPDFException,
|
||||||
PasswordException,
|
PasswordException,
|
||||||
setVerbosityLevel,
|
setVerbosityLevel,
|
||||||
|
stringToPDFString,
|
||||||
UnexpectedResponseException,
|
UnexpectedResponseException,
|
||||||
UnknownErrorException,
|
UnknownErrorException,
|
||||||
UNSUPPORTED_FEATURES,
|
UNSUPPORTED_FEATURES,
|
||||||
@ -32,6 +34,7 @@ import {
|
|||||||
} from "../shared/util.js";
|
} from "../shared/util.js";
|
||||||
import { clearPrimitiveCaches, Ref } from "./primitives.js";
|
import { clearPrimitiveCaches, Ref } from "./primitives.js";
|
||||||
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
||||||
|
import { incrementalUpdate } from "./writer.js";
|
||||||
import { isNodeJS } from "../shared/is_node.js";
|
import { isNodeJS } from "../shared/is_node.js";
|
||||||
import { MessageHandler } from "../shared/message_handler.js";
|
import { MessageHandler } from "../shared/message_handler.js";
|
||||||
import { PDFWorkerStream } from "./worker_stream.js";
|
import { PDFWorkerStream } from "./worker_stream.js";
|
||||||
@ -513,6 +516,67 @@ class WorkerMessageHandler {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handler.on("SaveDocument", function ({
|
||||||
|
numPages,
|
||||||
|
annotationStorage,
|
||||||
|
filename,
|
||||||
|
}) {
|
||||||
|
pdfManager.requestLoadedStream();
|
||||||
|
const promises = [pdfManager.onLoadedStream()];
|
||||||
|
const document = pdfManager.pdfDocument;
|
||||||
|
for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
|
||||||
|
promises.push(
|
||||||
|
pdfManager.getPage(pageIndex).then(function (page) {
|
||||||
|
const task = new WorkerTask(`Save: page ${pageIndex}`);
|
||||||
|
return page.save(handler, task, annotationStorage);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).then(([stream, ...refs]) => {
|
||||||
|
let newRefs = [];
|
||||||
|
for (const ref of refs) {
|
||||||
|
newRefs = ref
|
||||||
|
.filter(x => x !== null)
|
||||||
|
.reduce((a, b) => a.concat(b), newRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newRefs.length === 0) {
|
||||||
|
// No new refs so just return the initial bytes
|
||||||
|
return stream.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xref = document.xref;
|
||||||
|
let newXrefInfo = Object.create(null);
|
||||||
|
if (xref.trailer) {
|
||||||
|
// Get string info from Info in order to compute fileId
|
||||||
|
const _info = Object.create(null);
|
||||||
|
const xrefInfo = xref.trailer.get("Info") || null;
|
||||||
|
if (xrefInfo) {
|
||||||
|
xrefInfo.forEach((key, value) => {
|
||||||
|
if (isString(key) && isString(value)) {
|
||||||
|
_info[key] = stringToPDFString(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newXrefInfo = {
|
||||||
|
rootRef: xref.trailer.getRaw("Root") || null,
|
||||||
|
encrypt: xref.trailer.getRaw("Encrypt") || null,
|
||||||
|
newRef: xref.getNewRef(),
|
||||||
|
infoRef: xref.trailer.getRaw("Info") || null,
|
||||||
|
info: _info,
|
||||||
|
fileIds: xref.trailer.getRaw("ID") || null,
|
||||||
|
startXRef: document.startXRef,
|
||||||
|
filename,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
xref.resetNewRef();
|
||||||
|
|
||||||
|
return incrementalUpdate(stream.bytes, newXrefInfo, newRefs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
handler.on(
|
handler.on(
|
||||||
"GetOperatorList",
|
"GetOperatorList",
|
||||||
function wphSetupRenderPage(data, sink) {
|
function wphSetupRenderPage(data, sink) {
|
||||||
|
221
src/core/writer.js
Normal file
221
src/core/writer.js
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
/* Copyright 2020 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
/* eslint no-var: error */
|
||||||
|
|
||||||
|
import { bytesToString, escapeString } from "../shared/util.js";
|
||||||
|
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
|
||||||
|
import { calculateMD5 } from "./crypto.js";
|
||||||
|
|
||||||
|
function writeDict(dict, buffer, transform) {
|
||||||
|
buffer.push("<<");
|
||||||
|
for (const key of dict.getKeys()) {
|
||||||
|
buffer.push(` /${key} `);
|
||||||
|
writeValue(dict.getRaw(key), buffer, transform);
|
||||||
|
}
|
||||||
|
buffer.push(">>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStream(stream, buffer, transform) {
|
||||||
|
writeDict(stream.dict, buffer, transform);
|
||||||
|
buffer.push(" stream\n");
|
||||||
|
let string = bytesToString(stream.getBytes());
|
||||||
|
if (transform !== null) {
|
||||||
|
string = transform.encryptString(string);
|
||||||
|
}
|
||||||
|
buffer.push(string);
|
||||||
|
buffer.push("\nendstream\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeArray(array, buffer, transform) {
|
||||||
|
buffer.push("[");
|
||||||
|
let first = true;
|
||||||
|
for (const val of array) {
|
||||||
|
if (!first) {
|
||||||
|
buffer.push(" ");
|
||||||
|
} else {
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
writeValue(val, buffer, transform);
|
||||||
|
}
|
||||||
|
buffer.push("]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberToString(value) {
|
||||||
|
if (Number.isInteger(value)) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedValue = Math.round(value * 100);
|
||||||
|
if (roundedValue % 100 === 0) {
|
||||||
|
return (roundedValue / 100).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundedValue % 10 === 0) {
|
||||||
|
return value.toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeValue(value, buffer, transform) {
|
||||||
|
if (isName(value)) {
|
||||||
|
buffer.push(`/${value.name}`);
|
||||||
|
} else if (isRef(value)) {
|
||||||
|
buffer.push(`${value.num} ${value.gen} R`);
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
writeArray(value, buffer, transform);
|
||||||
|
} else if (typeof value === "string") {
|
||||||
|
if (transform !== null) {
|
||||||
|
value = transform.encryptString(value);
|
||||||
|
}
|
||||||
|
buffer.push(`(${escapeString(value)})`);
|
||||||
|
} else if (typeof value === "number") {
|
||||||
|
buffer.push(numberToString(value));
|
||||||
|
} else if (isDict(value)) {
|
||||||
|
writeDict(value, buffer, transform);
|
||||||
|
} else if (isStream(value)) {
|
||||||
|
writeStream(value, buffer, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeInt(number, size, offset, buffer) {
|
||||||
|
for (let i = size + offset - 1; i > offset - 1; i--) {
|
||||||
|
buffer[i] = number & 0xff;
|
||||||
|
number >>= 8;
|
||||||
|
}
|
||||||
|
return offset + size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeString(string, offset, buffer) {
|
||||||
|
for (let i = 0, len = string.length; i < len; i++) {
|
||||||
|
buffer[offset + i] = string.charCodeAt(i) & 0xff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeMD5(filesize, xrefInfo) {
|
||||||
|
const time = Math.floor(Date.now() / 1000);
|
||||||
|
const filename = xrefInfo.filename || "";
|
||||||
|
const md5Buffer = [time.toString(), filename, filesize.toString()];
|
||||||
|
let md5BufferLen = md5Buffer.reduce((a, str) => a + str.length, 0);
|
||||||
|
for (const value of Object.values(xrefInfo.info)) {
|
||||||
|
md5Buffer.push(value);
|
||||||
|
md5BufferLen += value.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const array = new Uint8Array(md5BufferLen);
|
||||||
|
let offset = 0;
|
||||||
|
for (const str of md5Buffer) {
|
||||||
|
writeString(str, offset, array);
|
||||||
|
offset += str.length;
|
||||||
|
}
|
||||||
|
return bytesToString(calculateMD5(array));
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementalUpdate(originalData, xrefInfo, newRefs) {
|
||||||
|
const newXref = new Dict(null);
|
||||||
|
const refForXrefTable = xrefInfo.newRef;
|
||||||
|
|
||||||
|
let buffer, baseOffset;
|
||||||
|
const lastByte = originalData[originalData.length - 1];
|
||||||
|
if (lastByte === /* \n */ 0x0a || lastByte === /* \r */ 0x0d) {
|
||||||
|
buffer = [];
|
||||||
|
baseOffset = originalData.length;
|
||||||
|
} else {
|
||||||
|
// Avoid to concatenate %%EOF with an object definition
|
||||||
|
buffer = ["\n"];
|
||||||
|
baseOffset = originalData.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
newXref.set("Size", refForXrefTable.num + 1);
|
||||||
|
newXref.set("Prev", xrefInfo.startXRef);
|
||||||
|
newXref.set("Type", Name.get("XRef"));
|
||||||
|
|
||||||
|
if (xrefInfo.rootRef !== null) {
|
||||||
|
newXref.set("Root", xrefInfo.rootRef);
|
||||||
|
}
|
||||||
|
if (xrefInfo.infoRef !== null) {
|
||||||
|
newXref.set("Info", xrefInfo.infoRef);
|
||||||
|
}
|
||||||
|
if (xrefInfo.encrypt !== null) {
|
||||||
|
newXref.set("Encrypt", xrefInfo.encrypt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a ref for the new xref and sort them
|
||||||
|
newRefs.push({ ref: refForXrefTable, data: "" });
|
||||||
|
newRefs = newRefs.sort((a, b) => {
|
||||||
|
// compare the refs
|
||||||
|
return a.ref.num - b.ref.num;
|
||||||
|
});
|
||||||
|
|
||||||
|
const xrefTableData = [[0, 1, 0xffff]];
|
||||||
|
const indexes = [0, 1];
|
||||||
|
let maxOffset = 0;
|
||||||
|
for (const { ref, data } of newRefs) {
|
||||||
|
maxOffset = Math.max(maxOffset, baseOffset);
|
||||||
|
xrefTableData.push([1, baseOffset, Math.min(ref.gen, 0xffff)]);
|
||||||
|
baseOffset += data.length;
|
||||||
|
indexes.push(ref.num);
|
||||||
|
indexes.push(1);
|
||||||
|
buffer.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
newXref.set("Index", indexes);
|
||||||
|
|
||||||
|
if (xrefInfo.fileIds.length !== 0) {
|
||||||
|
const md5 = computeMD5(baseOffset, xrefInfo);
|
||||||
|
newXref.set("ID", [xrefInfo.fileIds[0], md5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetSize = Math.ceil(Math.log2(maxOffset) / 8);
|
||||||
|
const sizes = [1, offsetSize, 2];
|
||||||
|
const structSize = sizes[0] + sizes[1] + sizes[2];
|
||||||
|
const tableLength = structSize * xrefTableData.length;
|
||||||
|
newXref.set("W", sizes);
|
||||||
|
newXref.set("Length", tableLength);
|
||||||
|
|
||||||
|
buffer.push(`${refForXrefTable.num} ${refForXrefTable.gen} obj\n`);
|
||||||
|
writeDict(newXref, buffer, null);
|
||||||
|
buffer.push(" stream\n");
|
||||||
|
|
||||||
|
const bufferLen = buffer.reduce((a, str) => a + str.length, 0);
|
||||||
|
const footer = `\nendstream\nendobj\nstartxref\n${baseOffset}\n%%EOF\n`;
|
||||||
|
const array = new Uint8Array(
|
||||||
|
originalData.length + bufferLen + tableLength + footer.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// Original data
|
||||||
|
array.set(originalData);
|
||||||
|
let offset = originalData.length;
|
||||||
|
|
||||||
|
// New data
|
||||||
|
for (const str of buffer) {
|
||||||
|
writeString(str, offset, array);
|
||||||
|
offset += str.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New xref table
|
||||||
|
for (const [type, objOffset, gen] of xrefTableData) {
|
||||||
|
offset = writeInt(type, sizes[0], offset, array);
|
||||||
|
offset = writeInt(objOffset, sizes[1], offset, array);
|
||||||
|
offset = writeInt(gen, sizes[2], offset, array);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the footer
|
||||||
|
writeString(footer, offset, array);
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { writeDict, incrementalUpdate };
|
@ -867,6 +867,16 @@ class PDFDocumentProxy {
|
|||||||
get loadingTask() {
|
get loadingTask() {
|
||||||
return this._transport.loadingTask;
|
return this._transport.loadingTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AnnotationStorage} annotationStorage - Storage for annotation
|
||||||
|
* data in forms.
|
||||||
|
* @returns {Promise<Uint8Array>} A promise that is resolved with a
|
||||||
|
* {Uint8Array} containing the full data of the saved document.
|
||||||
|
*/
|
||||||
|
saveDocument(annotationStorage) {
|
||||||
|
return this._transport.saveDocument(annotationStorage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2520,6 +2530,15 @@ class WorkerTransport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveDocument(annotationStorage) {
|
||||||
|
return this.messageHandler.sendWithPromise("SaveDocument", {
|
||||||
|
numPages: this._numPages,
|
||||||
|
annotationStorage:
|
||||||
|
(annotationStorage && annotationStorage.getAll()) || null,
|
||||||
|
filename: this._fullReader.filename,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getDestinations() {
|
getDestinations() {
|
||||||
return this.messageHandler.sendWithPromise("GetDestinations", null);
|
return this.messageHandler.sendWithPromise("GetDestinations", null);
|
||||||
}
|
}
|
||||||
|
@ -832,6 +832,19 @@ function isArrayEqual(arr1, arr2) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getModificationDate(date = new Date(Date.now())) {
|
||||||
|
const buffer = [
|
||||||
|
date.getUTCFullYear().toString(),
|
||||||
|
(date.getUTCMonth() + 1).toString().padStart(2, "0"),
|
||||||
|
(date.getUTCDate() + 1).toString().padStart(2, "0"),
|
||||||
|
date.getUTCHours().toString().padStart(2, "0"),
|
||||||
|
date.getUTCMinutes().toString().padStart(2, "0"),
|
||||||
|
date.getUTCSeconds().toString().padStart(2, "0"),
|
||||||
|
];
|
||||||
|
|
||||||
|
return buffer.join("");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promise Capability object.
|
* Promise Capability object.
|
||||||
*
|
*
|
||||||
@ -934,6 +947,7 @@ export {
|
|||||||
createPromiseCapability,
|
createPromiseCapability,
|
||||||
createObjectURL,
|
createObjectURL,
|
||||||
escapeString,
|
escapeString,
|
||||||
|
getModificationDate,
|
||||||
getVerbosityLevel,
|
getVerbosityLevel,
|
||||||
info,
|
info,
|
||||||
isArrayBuffer,
|
isArrayBuffer,
|
||||||
|
@ -1821,6 +1821,46 @@ describe("annotation", function () {
|
|||||||
done();
|
done();
|
||||||
}, done.fail);
|
}, done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should save text", function (done) {
|
||||||
|
const textWidgetRef = Ref.get(123, 0);
|
||||||
|
const xref = new XRefMock([{ ref: textWidgetRef, data: textWidgetDict }]);
|
||||||
|
partialEvaluator.xref = xref;
|
||||||
|
const task = new WorkerTask("test save");
|
||||||
|
|
||||||
|
AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
textWidgetRef,
|
||||||
|
pdfManagerMock,
|
||||||
|
idFactoryMock
|
||||||
|
)
|
||||||
|
.then(annotation => {
|
||||||
|
const annotationStorage = {};
|
||||||
|
annotationStorage[annotation.data.id] = "hello world";
|
||||||
|
return annotation.save(partialEvaluator, task, annotationStorage);
|
||||||
|
}, done.fail)
|
||||||
|
.then(data => {
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
const [oldData, newData] = data;
|
||||||
|
expect(oldData.ref).toEqual(Ref.get(123, 0));
|
||||||
|
expect(newData.ref).toEqual(Ref.get(1, 0));
|
||||||
|
|
||||||
|
oldData.data = oldData.data.replace(/\(D:[0-9]+\)/, "(date)");
|
||||||
|
expect(oldData.data).toEqual(
|
||||||
|
"123 0 obj\n" +
|
||||||
|
"<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " +
|
||||||
|
"<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " +
|
||||||
|
"/V (hello world) /AP << /N 1 0 R>> /M (date)>>\nendobj\n"
|
||||||
|
);
|
||||||
|
expect(newData.data).toEqual(
|
||||||
|
"1 0 obj\n<< /Length 77 /Subtype /Form /Resources " +
|
||||||
|
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10]>> stream\n" +
|
||||||
|
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 2.00 Td (hello world) Tj " +
|
||||||
|
"ET Q EMC\nendstream\nendobj\n"
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
}, done.fail);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ButtonWidgetAnnotation", function () {
|
describe("ButtonWidgetAnnotation", function () {
|
||||||
@ -1977,6 +2017,65 @@ describe("annotation", function () {
|
|||||||
}, done.fail);
|
}, done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should save checkboxes", function (done) {
|
||||||
|
const appearanceStatesDict = new Dict();
|
||||||
|
const exportValueOptionsDict = new Dict();
|
||||||
|
const normalAppearanceDict = new Dict();
|
||||||
|
|
||||||
|
normalAppearanceDict.set("Checked", Ref.get(314, 0));
|
||||||
|
normalAppearanceDict.set("Off", Ref.get(271, 0));
|
||||||
|
exportValueOptionsDict.set("Off", 0);
|
||||||
|
exportValueOptionsDict.set("Checked", 1);
|
||||||
|
appearanceStatesDict.set("D", exportValueOptionsDict);
|
||||||
|
appearanceStatesDict.set("N", normalAppearanceDict);
|
||||||
|
|
||||||
|
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||||
|
buttonWidgetDict.set("V", Name.get("Off"));
|
||||||
|
|
||||||
|
const buttonWidgetRef = Ref.get(123, 0);
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||||
|
]);
|
||||||
|
partialEvaluator.xref = xref;
|
||||||
|
const task = new WorkerTask("test save");
|
||||||
|
|
||||||
|
AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
buttonWidgetRef,
|
||||||
|
pdfManagerMock,
|
||||||
|
idFactoryMock
|
||||||
|
)
|
||||||
|
.then(annotation => {
|
||||||
|
const annotationStorage = {};
|
||||||
|
annotationStorage[annotation.data.id] = true;
|
||||||
|
return Promise.all([
|
||||||
|
annotation,
|
||||||
|
annotation.save(partialEvaluator, task, annotationStorage),
|
||||||
|
]);
|
||||||
|
}, done.fail)
|
||||||
|
.then(([annotation, [oldData]]) => {
|
||||||
|
oldData.data = oldData.data.replace(/\(D:[0-9]+\)/, "(date)");
|
||||||
|
expect(oldData.ref).toEqual(Ref.get(123, 0));
|
||||||
|
expect(oldData.data).toEqual(
|
||||||
|
"123 0 obj\n" +
|
||||||
|
"<< /Type /Annot /Subtype /Widget /FT /Btn " +
|
||||||
|
"/AP << /D << /Off 0 /Checked 1>> " +
|
||||||
|
"/N << /Checked 314 0 R /Off 271 0 R>>>> " +
|
||||||
|
"/V /Checked /AS /Checked /M (date)>>\nendobj\n"
|
||||||
|
);
|
||||||
|
return annotation;
|
||||||
|
}, done.fail)
|
||||||
|
.then(annotation => {
|
||||||
|
const annotationStorage = {};
|
||||||
|
annotationStorage[annotation.data.id] = false;
|
||||||
|
return annotation.save(partialEvaluator, task, annotationStorage);
|
||||||
|
}, done.fail)
|
||||||
|
.then(data => {
|
||||||
|
expect(data).toEqual(null);
|
||||||
|
done();
|
||||||
|
}, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
it("should handle radio buttons with a field value", function (done) {
|
it("should handle radio buttons with a field value", function (done) {
|
||||||
const parentDict = new Dict();
|
const parentDict = new Dict();
|
||||||
parentDict.set("V", Name.get("1"));
|
parentDict.set("V", Name.get("1"));
|
||||||
@ -2127,6 +2226,83 @@ describe("annotation", function () {
|
|||||||
done();
|
done();
|
||||||
}, done.fail);
|
}, done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should save radio buttons", function (done) {
|
||||||
|
const appearanceStatesDict = new Dict();
|
||||||
|
const exportValueOptionsDict = new Dict();
|
||||||
|
const normalAppearanceDict = new Dict();
|
||||||
|
|
||||||
|
normalAppearanceDict.set("Checked", Ref.get(314, 0));
|
||||||
|
normalAppearanceDict.set("Off", Ref.get(271, 0));
|
||||||
|
exportValueOptionsDict.set("Off", 0);
|
||||||
|
exportValueOptionsDict.set("Checked", 1);
|
||||||
|
appearanceStatesDict.set("D", exportValueOptionsDict);
|
||||||
|
appearanceStatesDict.set("N", normalAppearanceDict);
|
||||||
|
|
||||||
|
buttonWidgetDict.set("Ff", AnnotationFieldFlag.RADIO);
|
||||||
|
buttonWidgetDict.set("AP", appearanceStatesDict);
|
||||||
|
|
||||||
|
const buttonWidgetRef = Ref.get(123, 0);
|
||||||
|
const parentRef = Ref.get(456, 0);
|
||||||
|
|
||||||
|
const parentDict = new Dict();
|
||||||
|
parentDict.set("V", Name.get("Off"));
|
||||||
|
parentDict.set("Kids", [buttonWidgetRef]);
|
||||||
|
buttonWidgetDict.set("Parent", parentRef);
|
||||||
|
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: buttonWidgetRef, data: buttonWidgetDict },
|
||||||
|
{ ref: parentRef, data: parentDict },
|
||||||
|
]);
|
||||||
|
|
||||||
|
parentDict.xref = xref;
|
||||||
|
buttonWidgetDict.xref = xref;
|
||||||
|
partialEvaluator.xref = xref;
|
||||||
|
const task = new WorkerTask("test save");
|
||||||
|
|
||||||
|
AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
buttonWidgetRef,
|
||||||
|
pdfManagerMock,
|
||||||
|
idFactoryMock
|
||||||
|
)
|
||||||
|
.then(annotation => {
|
||||||
|
const annotationStorage = {};
|
||||||
|
annotationStorage[annotation.data.id] = true;
|
||||||
|
return Promise.all([
|
||||||
|
annotation,
|
||||||
|
annotation.save(partialEvaluator, task, annotationStorage),
|
||||||
|
]);
|
||||||
|
}, done.fail)
|
||||||
|
.then(([annotation, data]) => {
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
const [radioData, parentData] = data;
|
||||||
|
radioData.data = radioData.data.replace(/\(D:[0-9]+\)/, "(date)");
|
||||||
|
expect(radioData.ref).toEqual(Ref.get(123, 0));
|
||||||
|
expect(radioData.data).toEqual(
|
||||||
|
"123 0 obj\n" +
|
||||||
|
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 32768 " +
|
||||||
|
"/AP << /D << /Off 0 /Checked 1>> " +
|
||||||
|
"/N << /Checked 314 0 R /Off 271 0 R>>>> " +
|
||||||
|
"/Parent 456 0 R /AS /Checked /M (date)>>\nendobj\n"
|
||||||
|
);
|
||||||
|
expect(parentData.ref).toEqual(Ref.get(456, 0));
|
||||||
|
expect(parentData.data).toEqual(
|
||||||
|
"456 0 obj\n<< /V /Checked /Kids [123 0 R]>>\nendobj\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
return annotation;
|
||||||
|
}, done.fail)
|
||||||
|
.then(annotation => {
|
||||||
|
const annotationStorage = {};
|
||||||
|
annotationStorage[annotation.data.id] = false;
|
||||||
|
return annotation.save(partialEvaluator, task, annotationStorage);
|
||||||
|
}, done.fail)
|
||||||
|
.then(data => {
|
||||||
|
expect(data).toEqual(null);
|
||||||
|
done();
|
||||||
|
}, done.fail);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ChoiceWidgetAnnotation", function () {
|
describe("ChoiceWidgetAnnotation", function () {
|
||||||
@ -2448,6 +2624,53 @@ describe("annotation", function () {
|
|||||||
done();
|
done();
|
||||||
}, done.fail);
|
}, done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should save choice", function (done) {
|
||||||
|
choiceWidgetDict.set("Opt", ["A", "B", "C"]);
|
||||||
|
choiceWidgetDict.set("V", "A");
|
||||||
|
|
||||||
|
const choiceWidgetRef = Ref.get(123, 0);
|
||||||
|
const xref = new XRefMock([
|
||||||
|
{ ref: choiceWidgetRef, data: choiceWidgetDict },
|
||||||
|
]);
|
||||||
|
partialEvaluator.xref = xref;
|
||||||
|
const task = new WorkerTask("test save");
|
||||||
|
|
||||||
|
AnnotationFactory.create(
|
||||||
|
xref,
|
||||||
|
choiceWidgetRef,
|
||||||
|
pdfManagerMock,
|
||||||
|
idFactoryMock
|
||||||
|
)
|
||||||
|
.then(annotation => {
|
||||||
|
const annotationStorage = {};
|
||||||
|
annotationStorage[annotation.data.id] = "C";
|
||||||
|
return annotation.save(partialEvaluator, task, annotationStorage);
|
||||||
|
}, done.fail)
|
||||||
|
.then(data => {
|
||||||
|
expect(data.length).toEqual(2);
|
||||||
|
const [oldData, newData] = data;
|
||||||
|
expect(oldData.ref).toEqual(Ref.get(123, 0));
|
||||||
|
expect(newData.ref).toEqual(Ref.get(1, 0));
|
||||||
|
|
||||||
|
oldData.data = oldData.data.replace(/\(D:[0-9]+\)/, "(date)");
|
||||||
|
expect(oldData.data).toEqual(
|
||||||
|
"123 0 obj\n" +
|
||||||
|
"<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " +
|
||||||
|
"<< /Font << /Helv 314 0 R>>>> " +
|
||||||
|
"/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " +
|
||||||
|
"/AP << /N 1 0 R>> /M (date)>>\nendobj\n"
|
||||||
|
);
|
||||||
|
expect(newData.data).toEqual(
|
||||||
|
"1 0 obj\n" +
|
||||||
|
"<< /Length 67 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " +
|
||||||
|
"/BBox [0 0 32 10]>> stream\n" +
|
||||||
|
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2.00 2.00 Td (C) Tj ET Q EMC\n" +
|
||||||
|
"endstream\nendobj\n"
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
}, done.fail);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("LineAnnotation", function () {
|
describe("LineAnnotation", function () {
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"type1_parser_spec.js",
|
"type1_parser_spec.js",
|
||||||
"ui_utils_spec.js",
|
"ui_utils_spec.js",
|
||||||
"unicode_spec.js",
|
"unicode_spec.js",
|
||||||
"util_spec.js"
|
"util_spec.js",
|
||||||
|
"writer_spec.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -599,7 +599,16 @@ describe("CipherTransformFactory", function () {
|
|||||||
done.fail("Password should be rejected.");
|
done.fail("Password should be rejected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileId1, fileId2, dict1, dict2;
|
function ensureEncryptDecryptIsIdentity(dict, fileId, password, string) {
|
||||||
|
const factory = new CipherTransformFactory(dict, fileId, password);
|
||||||
|
const cipher = factory.createCipherTransform(123, 0);
|
||||||
|
const encrypted = cipher.encryptString(string);
|
||||||
|
const decrypted = cipher.decryptString(encrypted);
|
||||||
|
|
||||||
|
expect(string).toEqual(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileId1, fileId2, dict1, dict2, dict3;
|
||||||
var aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict;
|
var aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict;
|
||||||
|
|
||||||
beforeAll(function (done) {
|
beforeAll(function (done) {
|
||||||
@ -636,7 +645,7 @@ describe("CipherTransformFactory", function () {
|
|||||||
P: -1084,
|
P: -1084,
|
||||||
R: 4,
|
R: 4,
|
||||||
});
|
});
|
||||||
aes256Dict = buildDict({
|
dict3 = {
|
||||||
Filter: Name.get("Standard"),
|
Filter: Name.get("Standard"),
|
||||||
V: 5,
|
V: 5,
|
||||||
Length: 256,
|
Length: 256,
|
||||||
@ -661,7 +670,8 @@ describe("CipherTransformFactory", function () {
|
|||||||
Perms: unescape("%D8%FC%844%E5e%0DB%5D%7Ff%FD%3COMM"),
|
Perms: unescape("%D8%FC%844%E5e%0DB%5D%7Ff%FD%3COMM"),
|
||||||
P: -1084,
|
P: -1084,
|
||||||
R: 5,
|
R: 5,
|
||||||
});
|
};
|
||||||
|
aes256Dict = buildDict(dict3);
|
||||||
aes256IsoDict = buildDict({
|
aes256IsoDict = buildDict({
|
||||||
Filter: Name.get("Standard"),
|
Filter: Name.get("Standard"),
|
||||||
V: 5,
|
V: 5,
|
||||||
@ -742,7 +752,7 @@ describe("CipherTransformFactory", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(function () {
|
||||||
fileId1 = fileId2 = dict1 = dict2 = null;
|
fileId1 = fileId2 = dict1 = dict2 = dict3 = null;
|
||||||
aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
|
aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -799,4 +809,61 @@ describe("CipherTransformFactory", function () {
|
|||||||
ensurePasswordCorrect(done, dict2, fileId2);
|
ensurePasswordCorrect(done, dict2, fileId2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Encrypt and decrypt", function () {
|
||||||
|
it("should encrypt and decrypt using ARCFour", function (done) {
|
||||||
|
dict3.CF = buildDict({
|
||||||
|
Identity: buildDict({
|
||||||
|
CFM: Name.get("V2"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const dict = buildDict(dict3);
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "hello world");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
it("should encrypt and decrypt using AES128", function (done) {
|
||||||
|
dict3.CF = buildDict({
|
||||||
|
Identity: buildDict({
|
||||||
|
CFM: Name.get("AESV2"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const dict = buildDict(dict3);
|
||||||
|
// 1 char
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "a");
|
||||||
|
// 2 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aa");
|
||||||
|
// 16 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaaaaaaaaaaaaa");
|
||||||
|
// 19 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(
|
||||||
|
dict,
|
||||||
|
fileId1,
|
||||||
|
"user",
|
||||||
|
"aaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
it("should encrypt and decrypt using AES256", function (done) {
|
||||||
|
dict3.CF = buildDict({
|
||||||
|
Identity: buildDict({
|
||||||
|
CFM: Name.get("AESV3"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const dict = buildDict(dict3);
|
||||||
|
// 4 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaa");
|
||||||
|
// 5 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaa");
|
||||||
|
// 16 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(dict, fileId1, "user", "aaaaaaaaaaaaaaaa");
|
||||||
|
// 22 chars
|
||||||
|
ensureEncryptDecryptIsIdentity(
|
||||||
|
dict,
|
||||||
|
fileId1,
|
||||||
|
"user",
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -80,6 +80,7 @@ function initializePDFJS(callback) {
|
|||||||
"pdfjs-test/unit/ui_utils_spec.js",
|
"pdfjs-test/unit/ui_utils_spec.js",
|
||||||
"pdfjs-test/unit/unicode_spec.js",
|
"pdfjs-test/unit/unicode_spec.js",
|
||||||
"pdfjs-test/unit/util_spec.js",
|
"pdfjs-test/unit/util_spec.js",
|
||||||
|
"pdfjs-test/unit/writer_spec.js",
|
||||||
].map(function (moduleName) {
|
].map(function (moduleName) {
|
||||||
// eslint-disable-next-line no-unsanitized/method
|
// eslint-disable-next-line no-unsanitized/method
|
||||||
return SystemJS.import(moduleName);
|
return SystemJS.import(moduleName);
|
||||||
|
@ -13,10 +13,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { isRef, Ref } from "../../src/core/primitives.js";
|
||||||
import { Page, PDFDocument } from "../../src/core/document.js";
|
import { Page, PDFDocument } from "../../src/core/document.js";
|
||||||
import { assert } from "../../src/shared/util.js";
|
import { assert } from "../../src/shared/util.js";
|
||||||
import { isNodeJS } from "../../src/shared/is_node.js";
|
import { isNodeJS } from "../../src/shared/is_node.js";
|
||||||
import { isRef } from "../../src/core/primitives.js";
|
|
||||||
import { StringStream } from "../../src/core/stream.js";
|
import { StringStream } from "../../src/core/stream.js";
|
||||||
|
|
||||||
class DOMFileReaderFactory {
|
class DOMFileReaderFactory {
|
||||||
@ -70,6 +70,7 @@ class XRefMock {
|
|||||||
streamTypes: Object.create(null),
|
streamTypes: Object.create(null),
|
||||||
fontTypes: Object.create(null),
|
fontTypes: Object.create(null),
|
||||||
};
|
};
|
||||||
|
this._newRefNum = null;
|
||||||
|
|
||||||
for (const key in array) {
|
for (const key in array) {
|
||||||
const obj = array[key];
|
const obj = array[key];
|
||||||
@ -77,6 +78,17 @@ class XRefMock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNewRef() {
|
||||||
|
if (this._newRefNum === null) {
|
||||||
|
this._newRefNum = Object.keys(this._map).length;
|
||||||
|
}
|
||||||
|
return Ref.get(this._newRefNum++, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetNewRef() {
|
||||||
|
this.newRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
fetch(ref) {
|
fetch(ref) {
|
||||||
return this._map[ref.toString()];
|
return this._map[ref.toString()];
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
createPromiseCapability,
|
createPromiseCapability,
|
||||||
createValidAbsoluteUrl,
|
createValidAbsoluteUrl,
|
||||||
escapeString,
|
escapeString,
|
||||||
|
getModificationDate,
|
||||||
isArrayBuffer,
|
isArrayBuffer,
|
||||||
isBool,
|
isBool,
|
||||||
isNum,
|
isNum,
|
||||||
@ -323,4 +324,11 @@ describe("util", function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getModificationDate", function () {
|
||||||
|
it("should get a correctly formatted date", function () {
|
||||||
|
const date = new Date(Date.UTC(3141, 5, 9, 2, 6, 53));
|
||||||
|
expect(getModificationDate(date)).toEqual("31410610020653");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
99
test/unit/writer_spec.js
Normal file
99
test/unit/writer_spec.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* Copyright 2020 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Dict, Name, Ref } from "../../src/core/primitives.js";
|
||||||
|
import { incrementalUpdate, writeDict } from "../../src/core/writer.js";
|
||||||
|
import { bytesToString } from "../../src/shared/util.js";
|
||||||
|
import { StringStream } from "../../src/core/stream.js";
|
||||||
|
|
||||||
|
describe("Writer", function () {
|
||||||
|
describe("Incremental update", function () {
|
||||||
|
it("should update a file with new objects", function (done) {
|
||||||
|
const originalData = new Uint8Array();
|
||||||
|
const newRefs = [
|
||||||
|
{ ref: Ref.get(123, 0x2d), data: "abc\n" },
|
||||||
|
{ ref: Ref.get(456, 0x4e), data: "defg\n" },
|
||||||
|
];
|
||||||
|
const xrefInfo = {
|
||||||
|
newRef: Ref.get(789, 0),
|
||||||
|
startXRef: 314,
|
||||||
|
fileIds: ["id", ""],
|
||||||
|
rootRef: null,
|
||||||
|
infoRef: null,
|
||||||
|
encrypt: null,
|
||||||
|
filename: "foo.pdf",
|
||||||
|
info: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = incrementalUpdate(originalData, xrefInfo, newRefs);
|
||||||
|
data = bytesToString(data);
|
||||||
|
|
||||||
|
const expected =
|
||||||
|
"\nabc\n" +
|
||||||
|
"defg\n" +
|
||||||
|
"789 0 obj\n" +
|
||||||
|
"<< /Size 790 /Prev 314 /Type /XRef /Index [0 1 123 1 456 1 789 1] " +
|
||||||
|
"/ID [(id) (\x01#Eg\x89\xab\xcd\xef\xfe\xdc\xba\x98vT2\x10)] " +
|
||||||
|
"/W [1 1 2] /Length 16>> stream\n" +
|
||||||
|
"\x00\x01\xff\xff" +
|
||||||
|
"\x01\x01\x00\x2d" +
|
||||||
|
"\x01\x05\x00\x4e" +
|
||||||
|
"\x01\x0a\x00\x00\n" +
|
||||||
|
"endstream\n" +
|
||||||
|
"endobj\n" +
|
||||||
|
"startxref\n" +
|
||||||
|
"10\n" +
|
||||||
|
"%%EOF\n";
|
||||||
|
|
||||||
|
expect(data).toEqual(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("writeDict", function () {
|
||||||
|
it("should write a Dict", function (done) {
|
||||||
|
const dict = new Dict(null);
|
||||||
|
dict.set("A", Name.get("B"));
|
||||||
|
dict.set("B", Ref.get(123, 456));
|
||||||
|
dict.set("C", 789);
|
||||||
|
dict.set("D", "hello world");
|
||||||
|
dict.set("E", "(hello\\world)");
|
||||||
|
dict.set("F", [1.23001, 4.50001, 6]);
|
||||||
|
|
||||||
|
const gdict = new Dict(null);
|
||||||
|
gdict.set("H", 123.00001);
|
||||||
|
const string = "a stream";
|
||||||
|
const stream = new StringStream(string);
|
||||||
|
stream.dict = new Dict(null);
|
||||||
|
stream.dict.set("Length", string.length);
|
||||||
|
gdict.set("I", stream);
|
||||||
|
|
||||||
|
dict.set("G", gdict);
|
||||||
|
|
||||||
|
const buffer = [];
|
||||||
|
writeDict(dict, buffer, null);
|
||||||
|
|
||||||
|
const expected =
|
||||||
|
"<< /A /B /B 123 456 R /C 789 /D (hello world) " +
|
||||||
|
"/E (\\(hello\\\\world\\)) /F [1.23 4.5 6] " +
|
||||||
|
"/G << /H 123 /I << /Length 8>> stream\n" +
|
||||||
|
"a stream\n" +
|
||||||
|
"endstream\n>>>>";
|
||||||
|
|
||||||
|
expect(buffer.join("")).toEqual(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
48
web/app.js
48
web/app.js
@ -236,6 +236,7 @@ const PDFViewerApplication = {
|
|||||||
_boundEvents: {},
|
_boundEvents: {},
|
||||||
contentDispositionFilename: null,
|
contentDispositionFilename: null,
|
||||||
triggerDelayedFallback: null,
|
triggerDelayedFallback: null,
|
||||||
|
_saveInProgress: false,
|
||||||
|
|
||||||
// Called once when the document is loaded.
|
// Called once when the document is loaded.
|
||||||
async initialize(appConfig) {
|
async initialize(appConfig) {
|
||||||
@ -730,6 +731,7 @@ const PDFViewerApplication = {
|
|||||||
this.baseUrl = "";
|
this.baseUrl = "";
|
||||||
this.contentDispositionFilename = null;
|
this.contentDispositionFilename = null;
|
||||||
this.triggerDelayedFallback = null;
|
this.triggerDelayedFallback = null;
|
||||||
|
this._saveInProgress = false;
|
||||||
|
|
||||||
this.pdfSidebar.reset();
|
this.pdfSidebar.reset();
|
||||||
this.pdfOutlineViewer.reset();
|
this.pdfOutlineViewer.reset();
|
||||||
@ -904,6 +906,43 @@ const PDFViewerApplication = {
|
|||||||
.catch(downloadByUrl); // Error occurred, try downloading with the URL.
|
.catch(downloadByUrl); // Error occurred, try downloading with the URL.
|
||||||
},
|
},
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (this._saveInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.baseUrl;
|
||||||
|
// Use this.url instead of this.baseUrl to perform filename detection based
|
||||||
|
// on the reference fragment as ultimate fallback if needed.
|
||||||
|
const filename =
|
||||||
|
this.contentDispositionFilename || getPDFFileNameFromURL(this.url);
|
||||||
|
const downloadManager = this.downloadManager;
|
||||||
|
downloadManager.onerror = err => {
|
||||||
|
// This error won't really be helpful because it's likely the
|
||||||
|
// fallback won't work either (or is already open).
|
||||||
|
this.error(`PDF failed to be saved: ${err}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the PDF document isn't ready, or the PDF file is still downloading,
|
||||||
|
// simply download using the URL.
|
||||||
|
if (!this.pdfDocument || !this.downloadComplete) {
|
||||||
|
this.download();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._saveInProgress = true;
|
||||||
|
this.pdfDocument
|
||||||
|
.saveDocument(this.pdfDocument.annotationStorage)
|
||||||
|
.then(data => {
|
||||||
|
const blob = new Blob([data], { type: "application/pdf" });
|
||||||
|
downloadManager.download(blob, url, filename);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.download();
|
||||||
|
}); // Error occurred, try downloading with the URL.
|
||||||
|
this._saveInProgress = false;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For PDF documents that contain e.g. forms and javaScript, we should only
|
* For PDF documents that contain e.g. forms and javaScript, we should only
|
||||||
* trigger the fallback bar once the user has interacted with the page.
|
* trigger the fallback bar once the user has interacted with the page.
|
||||||
@ -2265,7 +2304,14 @@ function webViewerPrint() {
|
|||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
function webViewerDownload() {
|
function webViewerDownload() {
|
||||||
PDFViewerApplication.download();
|
if (
|
||||||
|
PDFViewerApplication.pdfDocument &&
|
||||||
|
PDFViewerApplication.pdfDocument.annotationStorage.size > 0
|
||||||
|
) {
|
||||||
|
PDFViewerApplication.save();
|
||||||
|
} else {
|
||||||
|
PDFViewerApplication.download();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function webViewerFirstPage() {
|
function webViewerFirstPage() {
|
||||||
if (PDFViewerApplication.pdfDocument) {
|
if (PDFViewerApplication.pdfDocument) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user