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:
parent
3380f2a7fc
commit
1a6816ba98
16 changed files with 1060 additions and 8 deletions
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue