Skip to content

Commit

Permalink
Merge pull request #13627 from calixteman/save
Browse files Browse the repository at this point in the history
XFA - Save filled data in the pdf when downloading the file (Bug 1716288)
  • Loading branch information
calixteman authored Jun 25, 2021
2 parents dc7faa2 + 429ffdc commit 9de0916
Show file tree
Hide file tree
Showing 17 changed files with 393 additions and 113 deletions.
7 changes: 7 additions & 0 deletions src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,13 @@ class PDFDocument {
this.xfaFactory.setFonts(pdfFonts);
}

async serializeXfaData(annotationStorage) {
if (this.xfaFactory) {
return this.xfaFactory.serializeData(annotationStorage);
}
return null;
}

get formInfo() {
const formInfo = {
hasFields: false,
Expand Down
4 changes: 4 additions & 0 deletions src/core/pdf_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class BasePdfManager {
return this.pdfDocument.loadXfaFonts(handler, task);
}

serializeXfaData(annotationStorage) {
return this.pdfDocument.serializeXfaData(annotationStorage);
}

cleanup(manuallyTriggered = false) {
return this.pdfDocument.cleanup(manuallyTriggered);
}
Expand Down
56 changes: 34 additions & 22 deletions src/core/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,28 +564,31 @@ class WorkerMessageHandler {

handler.on(
"SaveDocument",
function ({ numPages, annotationStorage, filename }) {
function ({ isPureXfa, numPages, annotationStorage, filename }) {
pdfManager.requestLoadedStream();

const promises = [
pdfManager.onLoadedStream(),
pdfManager.ensureCatalog("acroForm"),
pdfManager.ensureDoc("xref"),
pdfManager.ensureDoc("startXRef"),
];

for (let pageIndex = 0; pageIndex < numPages; pageIndex++) {
promises.push(
pdfManager.getPage(pageIndex).then(function (page) {
const task = new WorkerTask(`Save: page ${pageIndex}`);
startWorkerTask(task);

return page
.save(handler, task, annotationStorage)
.finally(function () {
finishWorkerTask(task);
});
})
);
if (isPureXfa) {
promises.push(pdfManager.serializeXfaData(annotationStorage));
} else {
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)
.finally(function () {
finishWorkerTask(task);
});
})
);
}
}

return Promise.all(promises).then(function ([
Expand All @@ -596,15 +599,23 @@ class WorkerMessageHandler {
...refs
]) {
let newRefs = [];
for (const ref of refs) {
newRefs = ref
.filter(x => x !== null)
.reduce((a, b) => a.concat(b), newRefs);
}
let xfaData = null;
if (isPureXfa) {
xfaData = refs[0];
if (!xfaData) {
return stream.bytes;
}
} else {
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;
if (newRefs.length === 0) {
// No new refs so just return the initial bytes
return stream.bytes;
}
}

const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || [];
Expand Down Expand Up @@ -652,6 +663,7 @@ class WorkerMessageHandler {
newRefs,
xref,
datasetsRef: xfaDatasets,
xfaData,
});
});
}
Expand Down
28 changes: 17 additions & 11 deletions src/core/writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,7 @@ function computeMD5(filesize, xrefInfo) {
return bytesToString(calculateMD5(array));
}

function updateXFA(datasetsRef, newRefs, xref) {
if (datasetsRef === null || xref === null) {
return;
}
const datasets = xref.fetchIfRef(datasetsRef);
const str = datasets.getString();
function writeXFADataForAcroform(str, newRefs) {
const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str);

for (const { xfa } of newRefs) {
Expand All @@ -148,20 +143,30 @@ function updateXFA(datasetsRef, newRefs, xref) {
}
const buffer = [];
xml.documentElement.dump(buffer);
let updatedXml = buffer.join("");
return buffer.join("");
}

function updateXFA(xfaData, datasetsRef, newRefs, xref) {
if (datasetsRef === null || xref === null) {
return;
}
if (xfaData === null) {
const datasets = xref.fetchIfRef(datasetsRef);
xfaData = writeXFADataForAcroform(datasets.getString(), newRefs);
}

const encrypt = xref.encrypt;
if (encrypt) {
const transform = encrypt.createCipherTransform(
datasetsRef.num,
datasetsRef.gen
);
updatedXml = transform.encryptString(updatedXml);
xfaData = transform.encryptString(xfaData);
}
const data =
`${datasetsRef.num} ${datasetsRef.gen} obj\n` +
`<< /Type /EmbeddedFile /Length ${updatedXml.length}>>\nstream\n` +
updatedXml +
`<< /Type /EmbeddedFile /Length ${xfaData.length}>>\nstream\n` +
xfaData +
"\nendstream\nendobj\n";

newRefs.push({ ref: datasetsRef, data });
Expand All @@ -173,8 +178,9 @@ function incrementalUpdate({
newRefs,
xref = null,
datasetsRef = null,
xfaData = null,
}) {
updateXFA(datasetsRef, newRefs, xref);
updateXFA(xfaData, datasetsRef, newRefs, xref);

const newXref = new Dict(null);
const refForXrefTable = xrefInfo.newRef;
Expand Down
11 changes: 8 additions & 3 deletions src/core/xfa/bind.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
$hasSettableValue,
$indexOf,
$insertAt,
$isBindable,
$isDataValue,
$isDescendent,
$namespaceId,
Expand Down Expand Up @@ -87,12 +88,12 @@ class Binder {
// data node (through $data property): we'll use it
// to save form data.

formNode[$data] = data;
if (formNode[$hasSettableValue]()) {
if (data[$isDataValue]()) {
const value = data[$getDataValue]();
// TODO: use picture.
formNode[$setValue](createText(value));
formNode[$data] = data;
} else if (
formNode instanceof Field &&
formNode.ui &&
Expand All @@ -103,13 +104,11 @@ class Binder {
.map(child => child[$content].trim())
.join("\n");
formNode[$setValue](createText(value));
formNode[$data] = data;
} else if (this._isConsumeData()) {
warn(`XFA - Nodes haven't the same type.`);
}
} else if (!data[$isDataValue]() || this._isMatchTemplate()) {
this._bindElement(formNode, data);
formNode[$data] = data;
} else {
warn(`XFA - Nodes haven't the same type.`);
}
Expand Down Expand Up @@ -496,6 +495,12 @@ class Binder {
continue;
}

if (!child[$isBindable]()) {
// The node cannot contain some new data so there is nothing
// to create in the data node.
continue;
}

let global = false;
let picture = null;
let ref = null;
Expand Down
82 changes: 82 additions & 0 deletions src/core/xfa/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* Copyright 2021 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 {
$getAttributes,
$getChildren,
$nodeName,
$setValue,
$toString,
$uid,
} from "./xfa_object.js";

class DataHandler {
constructor(root, data) {
this.data = data;
this.dataset = root.datasets || null;
}

serialize(storage) {
const stack = [[-1, this.data[$getChildren]()]];

while (stack.length > 0) {
const last = stack[stack.length - 1];
const [i, children] = last;
if (i + 1 === children.length) {
stack.pop();
continue;
}

const child = children[++last[0]];
const storageEntry = storage.get(child[$uid]);
if (storageEntry) {
child[$setValue](storageEntry);
} else {
const attributes = child[$getAttributes]();
for (const value of attributes.values()) {
const entry = storage.get(value[$uid]);
if (entry) {
value[$setValue](entry);
break;
}
}
}

const nodes = child[$getChildren]();
if (nodes.length > 0) {
stack.push([-1, nodes]);
}
}

const buf = [
`<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">`,
];
if (this.dataset) {
// Dump nodes other than data: they can contains for example
// some data for choice lists.
for (const child of this.dataset[$getChildren]()) {
if (child[$nodeName] !== "data") {
child[$toString](buf);
}
}
}
this.data[$toString](buf);
buf.push("</xfa:datasets>");

return buf.join("");
}
}

export { DataHandler };
9 changes: 8 additions & 1 deletion src/core/xfa/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import { $globalData, $toHTML } from "./xfa_object.js";
import { Binder } from "./bind.js";
import { DataHandler } from "./data.js";
import { FontFinder } from "./fonts.js";
import { warn } from "../../shared/util.js";
import { XFAParser } from "./parser.js";
Expand All @@ -23,7 +24,9 @@ class XFAFactory {
constructor(data) {
try {
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
const binder = new Binder(this.root);
this.form = binder.bind();
this.dataHandler = new DataHandler(this.root, binder.getData());
this.form[$globalData].template = this.form;
} catch (e) {
warn(`XFA - an error occured during parsing and binding: ${e}`);
Expand Down Expand Up @@ -70,6 +73,10 @@ class XFAFactory {
return pages;
}

serializeData(storage) {
return this.dataHandler.serialize(storage);
}

static _createDocument(data) {
if (!data["/xdp:xdp"]) {
return data["xdp:xdp"];
Expand Down
Loading

0 comments on commit 9de0916

Please sign in to comment.