Skip to content

Commit 429ffdc

Browse files
committed
XFA - Save filled data in the pdf when downloading the file (Bug 1716288)
- when binding (after parsing) we get a map between some template nodes and some data nodes; - so set user data in input handlers in using data node uids in the annotation storage; - to save the form, just put the value we have in the storage in the correct data nodes, serialize the xml as a string and then write the string at the end of the pdf using src/core/writer.js; - fix few bugs around data bindings: - the "Off" issue in Bug 1716980.
1 parent d7fdb72 commit 429ffdc

17 files changed

+393
-113
lines changed

src/core/document.js

+7
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,13 @@ class PDFDocument {
963963
this.xfaFactory.setFonts(pdfFonts);
964964
}
965965

966+
async serializeXfaData(annotationStorage) {
967+
if (this.xfaFactory) {
968+
return this.xfaFactory.serializeData(annotationStorage);
969+
}
970+
return null;
971+
}
972+
966973
get formInfo() {
967974
const formInfo = {
968975
hasFields: false,

src/core/pdf_manager.js

+4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ class BasePdfManager {
7777
return this.pdfDocument.loadXfaFonts(handler, task);
7878
}
7979

80+
serializeXfaData(annotationStorage) {
81+
return this.pdfDocument.serializeXfaData(annotationStorage);
82+
}
83+
8084
cleanup(manuallyTriggered = false) {
8185
return this.pdfDocument.cleanup(manuallyTriggered);
8286
}

src/core/worker.js

+34-22
Original file line numberDiff line numberDiff line change
@@ -564,28 +564,31 @@ class WorkerMessageHandler {
564564

565565
handler.on(
566566
"SaveDocument",
567-
function ({ numPages, annotationStorage, filename }) {
567+
function ({ isPureXfa, numPages, annotationStorage, filename }) {
568568
pdfManager.requestLoadedStream();
569+
569570
const promises = [
570571
pdfManager.onLoadedStream(),
571572
pdfManager.ensureCatalog("acroForm"),
572573
pdfManager.ensureDoc("xref"),
573574
pdfManager.ensureDoc("startXRef"),
574575
];
575576

576-
for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
577-
promises.push(
578-
pdfManager.getPage(pageIndex).then(function (page) {
579-
const task = new WorkerTask(`Save: page ${pageIndex}`);
580-
startWorkerTask(task);
581-
582-
return page
583-
.save(handler, task, annotationStorage)
584-
.finally(function () {
585-
finishWorkerTask(task);
586-
});
587-
})
588-
);
577+
if (isPureXfa) {
578+
promises.push(pdfManager.serializeXfaData(annotationStorage));
579+
} else {
580+
for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
581+
promises.push(
582+
pdfManager.getPage(pageIndex).then(function (page) {
583+
const task = new WorkerTask(`Save: page ${pageIndex}`);
584+
return page
585+
.save(handler, task, annotationStorage)
586+
.finally(function () {
587+
finishWorkerTask(task);
588+
});
589+
})
590+
);
591+
}
589592
}
590593

591594
return Promise.all(promises).then(function ([
@@ -596,15 +599,23 @@ class WorkerMessageHandler {
596599
...refs
597600
]) {
598601
let newRefs = [];
599-
for (const ref of refs) {
600-
newRefs = ref
601-
.filter(x => x !== null)
602-
.reduce((a, b) => a.concat(b), newRefs);
603-
}
602+
let xfaData = null;
603+
if (isPureXfa) {
604+
xfaData = refs[0];
605+
if (!xfaData) {
606+
return stream.bytes;
607+
}
608+
} else {
609+
for (const ref of refs) {
610+
newRefs = ref
611+
.filter(x => x !== null)
612+
.reduce((a, b) => a.concat(b), newRefs);
613+
}
604614

605-
if (newRefs.length === 0) {
606-
// No new refs so just return the initial bytes
607-
return stream.bytes;
615+
if (newRefs.length === 0) {
616+
// No new refs so just return the initial bytes
617+
return stream.bytes;
618+
}
608619
}
609620

610621
const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || [];
@@ -652,6 +663,7 @@ class WorkerMessageHandler {
652663
newRefs,
653664
xref,
654665
datasetsRef: xfaDatasets,
666+
xfaData,
655667
});
656668
});
657669
}

src/core/writer.js

+17-11
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,7 @@ function computeMD5(filesize, xrefInfo) {
123123
return bytesToString(calculateMD5(array));
124124
}
125125

126-
function updateXFA(datasetsRef, newRefs, xref) {
127-
if (datasetsRef === null || xref === null) {
128-
return;
129-
}
130-
const datasets = xref.fetchIfRef(datasetsRef);
131-
const str = datasets.getString();
126+
function writeXFADataForAcroform(str, newRefs) {
132127
const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str);
133128

134129
for (const { xfa } of newRefs) {
@@ -148,20 +143,30 @@ function updateXFA(datasetsRef, newRefs, xref) {
148143
}
149144
const buffer = [];
150145
xml.documentElement.dump(buffer);
151-
let updatedXml = buffer.join("");
146+
return buffer.join("");
147+
}
148+
149+
function updateXFA(xfaData, datasetsRef, newRefs, xref) {
150+
if (datasetsRef === null || xref === null) {
151+
return;
152+
}
153+
if (xfaData === null) {
154+
const datasets = xref.fetchIfRef(datasetsRef);
155+
xfaData = writeXFADataForAcroform(datasets.getString(), newRefs);
156+
}
152157

153158
const encrypt = xref.encrypt;
154159
if (encrypt) {
155160
const transform = encrypt.createCipherTransform(
156161
datasetsRef.num,
157162
datasetsRef.gen
158163
);
159-
updatedXml = transform.encryptString(updatedXml);
164+
xfaData = transform.encryptString(xfaData);
160165
}
161166
const data =
162167
`${datasetsRef.num} ${datasetsRef.gen} obj\n` +
163-
`<< /Type /EmbeddedFile /Length ${updatedXml.length}>>\nstream\n` +
164-
updatedXml +
168+
`<< /Type /EmbeddedFile /Length ${xfaData.length}>>\nstream\n` +
169+
xfaData +
165170
"\nendstream\nendobj\n";
166171

167172
newRefs.push({ ref: datasetsRef, data });
@@ -173,8 +178,9 @@ function incrementalUpdate({
173178
newRefs,
174179
xref = null,
175180
datasetsRef = null,
181+
xfaData = null,
176182
}) {
177-
updateXFA(datasetsRef, newRefs, xref);
183+
updateXFA(xfaData, datasetsRef, newRefs, xref);
178184

179185
const newXref = new Dict(null);
180186
const refForXrefTable = xrefInfo.newRef;

src/core/xfa/bind.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
$hasSettableValue,
3030
$indexOf,
3131
$insertAt,
32+
$isBindable,
3233
$isDataValue,
3334
$isDescendent,
3435
$namespaceId,
@@ -87,12 +88,12 @@ class Binder {
8788
// data node (through $data property): we'll use it
8889
// to save form data.
8990

91+
formNode[$data] = data;
9092
if (formNode[$hasSettableValue]()) {
9193
if (data[$isDataValue]()) {
9294
const value = data[$getDataValue]();
9395
// TODO: use picture.
9496
formNode[$setValue](createText(value));
95-
formNode[$data] = data;
9697
} else if (
9798
formNode instanceof Field &&
9899
formNode.ui &&
@@ -103,13 +104,11 @@ class Binder {
103104
.map(child => child[$content].trim())
104105
.join("\n");
105106
formNode[$setValue](createText(value));
106-
formNode[$data] = data;
107107
} else if (this._isConsumeData()) {
108108
warn(`XFA - Nodes haven't the same type.`);
109109
}
110110
} else if (!data[$isDataValue]() || this._isMatchTemplate()) {
111111
this._bindElement(formNode, data);
112-
formNode[$data] = data;
113112
} else {
114113
warn(`XFA - Nodes haven't the same type.`);
115114
}
@@ -496,6 +495,12 @@ class Binder {
496495
continue;
497496
}
498497

498+
if (!child[$isBindable]()) {
499+
// The node cannot contain some new data so there is nothing
500+
// to create in the data node.
501+
continue;
502+
}
503+
499504
let global = false;
500505
let picture = null;
501506
let ref = null;

src/core/xfa/data.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* Copyright 2021 Mozilla Foundation
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import {
17+
$getAttributes,
18+
$getChildren,
19+
$nodeName,
20+
$setValue,
21+
$toString,
22+
$uid,
23+
} from "./xfa_object.js";
24+
25+
class DataHandler {
26+
constructor(root, data) {
27+
this.data = data;
28+
this.dataset = root.datasets || null;
29+
}
30+
31+
serialize(storage) {
32+
const stack = [[-1, this.data[$getChildren]()]];
33+
34+
while (stack.length > 0) {
35+
const last = stack[stack.length - 1];
36+
const [i, children] = last;
37+
if (i + 1 === children.length) {
38+
stack.pop();
39+
continue;
40+
}
41+
42+
const child = children[++last[0]];
43+
const storageEntry = storage.get(child[$uid]);
44+
if (storageEntry) {
45+
child[$setValue](storageEntry);
46+
} else {
47+
const attributes = child[$getAttributes]();
48+
for (const value of attributes.values()) {
49+
const entry = storage.get(value[$uid]);
50+
if (entry) {
51+
value[$setValue](entry);
52+
break;
53+
}
54+
}
55+
}
56+
57+
const nodes = child[$getChildren]();
58+
if (nodes.length > 0) {
59+
stack.push([-1, nodes]);
60+
}
61+
}
62+
63+
const buf = [
64+
`<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">`,
65+
];
66+
if (this.dataset) {
67+
// Dump nodes other than data: they can contains for example
68+
// some data for choice lists.
69+
for (const child of this.dataset[$getChildren]()) {
70+
if (child[$nodeName] !== "data") {
71+
child[$toString](buf);
72+
}
73+
}
74+
}
75+
this.data[$toString](buf);
76+
buf.push("</xfa:datasets>");
77+
78+
return buf.join("");
79+
}
80+
}
81+
82+
export { DataHandler };

src/core/xfa/factory.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import { $globalData, $toHTML } from "./xfa_object.js";
1717
import { Binder } from "./bind.js";
18+
import { DataHandler } from "./data.js";
1819
import { FontFinder } from "./fonts.js";
1920
import { warn } from "../../shared/util.js";
2021
import { XFAParser } from "./parser.js";
@@ -23,7 +24,9 @@ class XFAFactory {
2324
constructor(data) {
2425
try {
2526
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
26-
this.form = new Binder(this.root).bind();
27+
const binder = new Binder(this.root);
28+
this.form = binder.bind();
29+
this.dataHandler = new DataHandler(this.root, binder.getData());
2730
this.form[$globalData].template = this.form;
2831
} catch (e) {
2932
warn(`XFA - an error occured during parsing and binding: ${e}`);
@@ -70,6 +73,10 @@ class XFAFactory {
7073
return pages;
7174
}
7275

76+
serializeData(storage) {
77+
return this.dataHandler.serialize(storage);
78+
}
79+
7380
static _createDocument(data) {
7481
if (!data["/xdp:xdp"]) {
7582
return data["xdp:xdp"];

0 commit comments

Comments
 (0)