1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-20 15:18:08 +02:00

Add support for saving forms

This commit is contained in:
Calixte Denizet 2020-08-03 19:44:04 +02:00 committed by calixteman
parent 3380f2a7fc
commit 1a6816ba98
16 changed files with 1060 additions and 8 deletions

View file

@ -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 () {

View file

@ -36,6 +36,7 @@
"type1_parser_spec.js",
"ui_utils_spec.js",
"unicode_spec.js",
"util_spec.js"
"util_spec.js",
"writer_spec.js"
]
}

View file

@ -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();
});
});
});

View file

@ -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);

View file

@ -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()];
}

View file

@ -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
View 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();
});
});
});