Add support for saving forms
This commit is contained in:
parent
3380f2a7fc
commit
1a6816ba98
@ -22,6 +22,7 @@ import {
|
||||
AnnotationType,
|
||||
assert,
|
||||
escapeString,
|
||||
getModificationDate,
|
||||
isString,
|
||||
OPS,
|
||||
stringToPDFString,
|
||||
@ -29,11 +30,12 @@ import {
|
||||
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 {
|
||||
/**
|
||||
@ -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.
|
||||
@ -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,
|
||||
@ -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 data = this.data;
|
||||
this.ref = params.ref;
|
||||
|
||||
data.annotationType = AnnotationType.WIDGET;
|
||||
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) {
|
||||
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
|
||||
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) {
|
||||
if (isName(this.data.fieldValue)) {
|
||||
this.data.fieldValue = this.data.fieldValue.name;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ var ARCFourCipher = (function ARCFourCipherClosure() {
|
||||
},
|
||||
};
|
||||
ARCFourCipher.prototype.decryptBlock = ARCFourCipher.prototype.encryptBlock;
|
||||
ARCFourCipher.prototype.encrypt = ARCFourCipher.prototype.encryptBlock;
|
||||
|
||||
return ARCFourCipher;
|
||||
})();
|
||||
@ -699,6 +700,9 @@ var NullCipher = (function NullCipherClosure() {
|
||||
decryptBlock: function NullCipher_decryptBlock(data) {
|
||||
return data;
|
||||
},
|
||||
encrypt: function NullCipher_encrypt(data) {
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
return NullCipher;
|
||||
@ -1097,6 +1101,7 @@ class AESBaseCipher {
|
||||
if (bufferLength < 16) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < 16; ++j) {
|
||||
buffer[j] ^= iv[j];
|
||||
}
|
||||
@ -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;
|
||||
})();
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -21,9 +21,11 @@ import {
|
||||
getVerbosityLevel,
|
||||
info,
|
||||
InvalidPDFException,
|
||||
isString,
|
||||
MissingPDFException,
|
||||
PasswordException,
|
||||
setVerbosityLevel,
|
||||
stringToPDFString,
|
||||
UnexpectedResponseException,
|
||||
UnknownErrorException,
|
||||
UNSUPPORTED_FEATURES,
|
||||
@ -32,6 +34,7 @@ import {
|
||||
} from "../shared/util.js";
|
||||
import { clearPrimitiveCaches, Ref } from "./primitives.js";
|
||||
import { LocalPdfManager, NetworkPdfManager } from "./pdf_manager.js";
|
||||
import { incrementalUpdate } from "./writer.js";
|
||||
import { isNodeJS } from "../shared/is_node.js";
|
||||
import { MessageHandler } from "../shared/message_handler.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(
|
||||
"GetOperatorList",
|
||||
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() {
|
||||
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() {
|
||||
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.
|
||||
*
|
||||
@ -934,6 +947,7 @@ export {
|
||||
createPromiseCapability,
|
||||
createObjectURL,
|
||||
escapeString,
|
||||
getModificationDate,
|
||||
getVerbosityLevel,
|
||||
info,
|
||||
isArrayBuffer,
|
||||
|
@ -1821,6 +1821,46 @@ describe("annotation", function () {
|
||||
done();
|
||||
}, 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 () {
|
||||
@ -1977,6 +2017,65 @@ describe("annotation", function () {
|
||||
}, 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) {
|
||||
const parentDict = new Dict();
|
||||
parentDict.set("V", Name.get("1"));
|
||||
@ -2127,6 +2226,83 @@ describe("annotation", function () {
|
||||
done();
|
||||
}, 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 () {
|
||||
@ -2448,6 +2624,53 @@ describe("annotation", function () {
|
||||
done();
|
||||
}, 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 () {
|
||||
|
@ -36,6 +36,7 @@
|
||||
"type1_parser_spec.js",
|
||||
"ui_utils_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.");
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
beforeAll(function (done) {
|
||||
@ -636,7 +645,7 @@ describe("CipherTransformFactory", function () {
|
||||
P: -1084,
|
||||
R: 4,
|
||||
});
|
||||
aes256Dict = buildDict({
|
||||
dict3 = {
|
||||
Filter: Name.get("Standard"),
|
||||
V: 5,
|
||||
Length: 256,
|
||||
@ -661,7 +670,8 @@ describe("CipherTransformFactory", function () {
|
||||
Perms: unescape("%D8%FC%844%E5e%0DB%5D%7Ff%FD%3COMM"),
|
||||
P: -1084,
|
||||
R: 5,
|
||||
});
|
||||
};
|
||||
aes256Dict = buildDict(dict3);
|
||||
aes256IsoDict = buildDict({
|
||||
Filter: Name.get("Standard"),
|
||||
V: 5,
|
||||
@ -742,7 +752,7 @@ describe("CipherTransformFactory", function () {
|
||||
});
|
||||
|
||||
afterAll(function () {
|
||||
fileId1 = fileId2 = dict1 = dict2 = null;
|
||||
fileId1 = fileId2 = dict1 = dict2 = dict3 = null;
|
||||
aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
|
||||
});
|
||||
|
||||
@ -799,4 +809,61 @@ describe("CipherTransformFactory", function () {
|
||||
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/unicode_spec.js",
|
||||
"pdfjs-test/unit/util_spec.js",
|
||||
"pdfjs-test/unit/writer_spec.js",
|
||||
].map(function (moduleName) {
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
return SystemJS.import(moduleName);
|
||||
|
@ -13,10 +13,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isRef, Ref } from "../../src/core/primitives.js";
|
||||
import { Page, PDFDocument } from "../../src/core/document.js";
|
||||
import { assert } from "../../src/shared/util.js";
|
||||
import { isNodeJS } from "../../src/shared/is_node.js";
|
||||
import { isRef } from "../../src/core/primitives.js";
|
||||
import { StringStream } from "../../src/core/stream.js";
|
||||
|
||||
class DOMFileReaderFactory {
|
||||
@ -70,6 +70,7 @@ class XRefMock {
|
||||
streamTypes: Object.create(null),
|
||||
fontTypes: Object.create(null),
|
||||
};
|
||||
this._newRefNum = null;
|
||||
|
||||
for (const key in array) {
|
||||
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) {
|
||||
return this._map[ref.toString()];
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
createPromiseCapability,
|
||||
createValidAbsoluteUrl,
|
||||
escapeString,
|
||||
getModificationDate,
|
||||
isArrayBuffer,
|
||||
isBool,
|
||||
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: {},
|
||||
contentDispositionFilename: null,
|
||||
triggerDelayedFallback: null,
|
||||
_saveInProgress: false,
|
||||
|
||||
// Called once when the document is loaded.
|
||||
async initialize(appConfig) {
|
||||
@ -730,6 +731,7 @@ const PDFViewerApplication = {
|
||||
this.baseUrl = "";
|
||||
this.contentDispositionFilename = null;
|
||||
this.triggerDelayedFallback = null;
|
||||
this._saveInProgress = false;
|
||||
|
||||
this.pdfSidebar.reset();
|
||||
this.pdfOutlineViewer.reset();
|
||||
@ -904,6 +906,43 @@ const PDFViewerApplication = {
|
||||
.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
|
||||
* trigger the fallback bar once the user has interacted with the page.
|
||||
@ -2265,7 +2304,14 @@ function webViewerPrint() {
|
||||
window.print();
|
||||
}
|
||||
function webViewerDownload() {
|
||||
PDFViewerApplication.download();
|
||||
if (
|
||||
PDFViewerApplication.pdfDocument &&
|
||||
PDFViewerApplication.pdfDocument.annotationStorage.size > 0
|
||||
) {
|
||||
PDFViewerApplication.save();
|
||||
} else {
|
||||
PDFViewerApplication.download();
|
||||
}
|
||||
}
|
||||
function webViewerFirstPage() {
|
||||
if (PDFViewerApplication.pdfDocument) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user