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.
This commit is contained in:
parent
d7fdb72a3f
commit
429ffdcd2f
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -564,8 +564,9 @@ class WorkerMessageHandler {
|
||||
|
||||
handler.on(
|
||||
"SaveDocument",
|
||||
function ({ numPages, annotationStorage, filename }) {
|
||||
function ({ isPureXfa, numPages, annotationStorage, filename }) {
|
||||
pdfManager.requestLoadedStream();
|
||||
|
||||
const promises = [
|
||||
pdfManager.onLoadedStream(),
|
||||
pdfManager.ensureCatalog("acroForm"),
|
||||
@ -573,19 +574,21 @@ class WorkerMessageHandler {
|
||||
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 ([
|
||||
@ -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")) || [];
|
||||
@ -652,6 +663,7 @@ class WorkerMessageHandler {
|
||||
newRefs,
|
||||
xref,
|
||||
datasetsRef: xfaDatasets,
|
||||
xfaData,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -148,7 +143,17 @@ 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) {
|
||||
@ -156,12 +161,12 @@ function updateXFA(datasetsRef, newRefs, xref) {
|
||||
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 });
|
||||
@ -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;
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
$hasSettableValue,
|
||||
$indexOf,
|
||||
$insertAt,
|
||||
$isBindable,
|
||||
$isDataValue,
|
||||
$isDescendent,
|
||||
$namespaceId,
|
||||
@ -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 &&
|
||||
@ -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.`);
|
||||
}
|
||||
@ -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;
|
||||
|
82
src/core/xfa/data.js
Normal file
82
src/core/xfa/data.js
Normal file
@ -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 };
|
@ -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";
|
||||
@ -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}`);
|
||||
@ -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"];
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
$clean,
|
||||
$cleanPage,
|
||||
$content,
|
||||
$data,
|
||||
$extra,
|
||||
$finalize,
|
||||
$flushHTML,
|
||||
@ -32,9 +33,9 @@ import {
|
||||
$getSubformParent,
|
||||
$getTemplateRoot,
|
||||
$globalData,
|
||||
$hasItem,
|
||||
$hasSettableValue,
|
||||
$ids,
|
||||
$isBindable,
|
||||
$isCDATAXml,
|
||||
$isSplittable,
|
||||
$isTransparent,
|
||||
@ -572,7 +573,7 @@ class BooleanElement extends Option01 {
|
||||
}
|
||||
|
||||
[$toHTML](availableSpace) {
|
||||
return valueToHtml(this[$content] === 1);
|
||||
return valueToHtml(this[$content] === 1 ? "1" : "0");
|
||||
}
|
||||
}
|
||||
|
||||
@ -950,17 +951,31 @@ class CheckButton extends XFAObject {
|
||||
let type;
|
||||
let className;
|
||||
let groupId;
|
||||
let id;
|
||||
const fieldId = this[$getParent]()[$getParent]()[$uid];
|
||||
const container = this[$getParent]()[$getParent]()[$getParent]();
|
||||
const field = this[$getParent]()[$getParent]();
|
||||
const items =
|
||||
(field.items.children.length &&
|
||||
field.items.children[0][$toHTML]().html) ||
|
||||
[];
|
||||
const exportedValue = {
|
||||
on: (items[0] || "on").toString(),
|
||||
off: (items[1] || "off").toString(),
|
||||
};
|
||||
|
||||
const value = (field.value && field.value[$text]()) || "off";
|
||||
const checked = value === exportedValue.on || undefined;
|
||||
const container = field[$getParent]();
|
||||
const fieldId = field[$uid];
|
||||
let dataId;
|
||||
|
||||
if (container instanceof ExclGroup) {
|
||||
groupId = container[$uid];
|
||||
type = "radio";
|
||||
className = "xfaRadio";
|
||||
id = `${fieldId}-radio`;
|
||||
dataId = container[$data] && container[$data][$uid];
|
||||
} else {
|
||||
type = "checkbox";
|
||||
className = "xfaCheckbox";
|
||||
dataId = field[$data] && field[$data][$uid];
|
||||
}
|
||||
|
||||
const input = {
|
||||
@ -969,14 +984,13 @@ class CheckButton extends XFAObject {
|
||||
class: [className],
|
||||
style,
|
||||
fieldId,
|
||||
dataId,
|
||||
type,
|
||||
checked,
|
||||
xfaOn: exportedValue.on,
|
||||
},
|
||||
};
|
||||
|
||||
if (id) {
|
||||
input.attributes.id = id;
|
||||
}
|
||||
|
||||
if (groupId) {
|
||||
input.attributes.name = groupId;
|
||||
}
|
||||
@ -1022,25 +1036,36 @@ class ChoiceList extends XFAObject {
|
||||
const children = [];
|
||||
|
||||
if (field.items.children.length > 0) {
|
||||
const displayed = field.items.children[0][$toHTML]().html;
|
||||
const values = field.items.children[1]
|
||||
? field.items.children[1][$toHTML]().html
|
||||
: [];
|
||||
const items = field.items;
|
||||
let displayedIndex = 0;
|
||||
let saveIndex = 0;
|
||||
if (items.children.length === 2) {
|
||||
displayedIndex = items.children[0].save;
|
||||
saveIndex = 1 - displayedIndex;
|
||||
}
|
||||
const displayed = items.children[displayedIndex][$toHTML]().html;
|
||||
const values = items.children[saveIndex][$toHTML]().html;
|
||||
|
||||
const value = (field.value && field.value[$text]()) || "";
|
||||
for (let i = 0, ii = displayed.length; i < ii; i++) {
|
||||
children.push({
|
||||
const option = {
|
||||
name: "option",
|
||||
attributes: {
|
||||
value: values[i] || displayed[i],
|
||||
},
|
||||
value: displayed[i],
|
||||
});
|
||||
};
|
||||
if (values[i] === value) {
|
||||
option.attributes.selected = true;
|
||||
}
|
||||
children.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
const selectAttributes = {
|
||||
class: ["xfaSelect"],
|
||||
fieldId: this[$getParent]()[$getParent]()[$uid],
|
||||
fieldId: field[$uid],
|
||||
dataId: field[$data] && field[$data][$uid],
|
||||
style,
|
||||
};
|
||||
|
||||
@ -1272,11 +1297,13 @@ class DateTimeEdit extends XFAObject {
|
||||
// When the picker is host we should use type=date for the input
|
||||
// but we need to put the buttons outside the text-field.
|
||||
const style = toStyle(this, "border", "font", "margin");
|
||||
const field = this[$getParent]()[$getParent]();
|
||||
const html = {
|
||||
name: "input",
|
||||
attributes: {
|
||||
type: "text",
|
||||
fieldId: this[$getParent]()[$getParent]()[$uid],
|
||||
fieldId: field[$uid],
|
||||
dataId: field[$data] && field[$data][$uid],
|
||||
class: ["xfaTextfield"],
|
||||
style,
|
||||
},
|
||||
@ -1976,6 +2003,10 @@ class ExclGroup extends XFAObject {
|
||||
this.setProperty = new XFAObjectArray();
|
||||
}
|
||||
|
||||
[$isBindable]() {
|
||||
return true;
|
||||
}
|
||||
|
||||
[$hasSettableValue]() {
|
||||
return true;
|
||||
}
|
||||
@ -1988,17 +2019,7 @@ class ExclGroup extends XFAObject {
|
||||
field.value = nodeValue;
|
||||
}
|
||||
|
||||
const nodeBoolean = new BooleanElement({});
|
||||
nodeBoolean[$content] = 0;
|
||||
|
||||
for (const item of field.items.children) {
|
||||
if (item[$hasItem](value)) {
|
||||
nodeBoolean[$content] = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
field.value[$setValue](nodeBoolean);
|
||||
field.value[$setValue](value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2312,6 +2333,10 @@ class Field extends XFAObject {
|
||||
this.setProperty = new XFAObjectArray();
|
||||
}
|
||||
|
||||
[$isBindable]() {
|
||||
return true;
|
||||
}
|
||||
|
||||
[$setValue](value) {
|
||||
_setValue(this, value);
|
||||
}
|
||||
@ -2906,15 +2931,6 @@ class Items extends XFAObject {
|
||||
this.time = new XFAObjectArray();
|
||||
}
|
||||
|
||||
[$hasItem](value) {
|
||||
return (
|
||||
this.hasOwnProperty(value[$nodeName]) &&
|
||||
this[value[$nodeName]].children.some(
|
||||
node => node[$content] === value[$content]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[$toHTML]() {
|
||||
const output = [];
|
||||
for (const child of this[$getChildren]()) {
|
||||
@ -3182,11 +3198,13 @@ class NumericEdit extends XFAObject {
|
||||
[$toHTML](availableSpace) {
|
||||
// TODO: incomplete.
|
||||
const style = toStyle(this, "border", "font", "margin");
|
||||
const field = this[$getParent]()[$getParent]();
|
||||
const html = {
|
||||
name: "input",
|
||||
attributes: {
|
||||
type: "text",
|
||||
fieldId: this[$getParent]()[$getParent]()[$uid],
|
||||
fieldId: field[$uid],
|
||||
dataId: field[$data] && field[$data][$uid],
|
||||
class: ["xfaTextfield"],
|
||||
style,
|
||||
},
|
||||
@ -4151,6 +4169,10 @@ class Subform extends XFAObject {
|
||||
this.subformSet = new XFAObjectArray();
|
||||
}
|
||||
|
||||
[$isBindable]() {
|
||||
return true;
|
||||
}
|
||||
|
||||
*[$getContainedChildren]() {
|
||||
// This function is overriden in order to fake that subforms under
|
||||
// this set are in fact under parent subform.
|
||||
@ -4924,11 +4946,13 @@ class TextEdit extends XFAObject {
|
||||
// TODO: incomplete.
|
||||
const style = toStyle(this, "border", "font", "margin");
|
||||
let html;
|
||||
const field = this[$getParent]()[$getParent]();
|
||||
if (this.multiLine === 1) {
|
||||
html = {
|
||||
name: "textarea",
|
||||
attributes: {
|
||||
fieldId: this[$getParent]()[$getParent]()[$uid],
|
||||
dataId: field[$data] && field[$data][$uid],
|
||||
fieldId: field[$uid],
|
||||
class: ["xfaTextfield"],
|
||||
style,
|
||||
},
|
||||
@ -4938,7 +4962,8 @@ class TextEdit extends XFAObject {
|
||||
name: "input",
|
||||
attributes: {
|
||||
type: "text",
|
||||
fieldId: this[$getParent]()[$getParent]()[$uid],
|
||||
dataId: field[$data] && field[$data][$uid],
|
||||
fieldId: field[$uid],
|
||||
class: ["xfaTextfield"],
|
||||
style,
|
||||
},
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
import { getInteger, getKeyword, HTMLResult } from "./utils.js";
|
||||
import { shadow, warn } from "../../shared/util.js";
|
||||
import { encodeToXmlString } from "../core_utils.js";
|
||||
import { NamespaceIds } from "./namespaces.js";
|
||||
import { searchNode } from "./som.js";
|
||||
|
||||
@ -36,6 +37,7 @@ const $extra = Symbol("extra");
|
||||
const $finalize = Symbol();
|
||||
const $flushHTML = Symbol();
|
||||
const $getAttributeIt = Symbol();
|
||||
const $getAttributes = Symbol();
|
||||
const $getAvailableSpace = Symbol();
|
||||
const $getChildrenByClass = Symbol();
|
||||
const $getChildrenByName = Symbol();
|
||||
@ -50,12 +52,12 @@ const $getParent = Symbol();
|
||||
const $getTemplateRoot = Symbol();
|
||||
const $global = Symbol();
|
||||
const $globalData = Symbol();
|
||||
const $hasItem = Symbol();
|
||||
const $hasSettableValue = Symbol();
|
||||
const $ids = Symbol();
|
||||
const $indexOf = Symbol();
|
||||
const $insertAt = Symbol();
|
||||
const $isCDATAXml = Symbol();
|
||||
const $isBindable = Symbol();
|
||||
const $isDataValue = Symbol();
|
||||
const $isDescendent = Symbol();
|
||||
const $isSplittable = Symbol();
|
||||
@ -78,6 +80,7 @@ const $setSetAttributes = Symbol();
|
||||
const $setValue = Symbol();
|
||||
const $text = Symbol();
|
||||
const $toHTML = Symbol();
|
||||
const $toString = Symbol();
|
||||
const $toStyle = Symbol();
|
||||
const $uid = Symbol("uid");
|
||||
|
||||
@ -101,6 +104,8 @@ const _validator = Symbol();
|
||||
|
||||
let uid = 0;
|
||||
|
||||
const NS_DATASETS = NamespaceIds.datasets.id;
|
||||
|
||||
class XFAObject {
|
||||
constructor(nsId, name, hasChildren = false) {
|
||||
this[$namespaceId] = nsId;
|
||||
@ -161,6 +166,10 @@ class XFAObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$isBindable]() {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$setId](ids) {
|
||||
if (this.id && this[$namespaceId] === NamespaceIds.template.id) {
|
||||
ids.set(this.id, this);
|
||||
@ -207,10 +216,6 @@ class XFAObject {
|
||||
}
|
||||
}
|
||||
|
||||
[$hasItem]() {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$indexOf](child) {
|
||||
return this[_children].indexOf(child);
|
||||
}
|
||||
@ -599,6 +604,7 @@ class XFAObject {
|
||||
shadow(clone, $symbol, this[$symbol]);
|
||||
}
|
||||
}
|
||||
clone[$uid] = `${clone[$nodeName]}${uid++}`;
|
||||
clone[_children] = [];
|
||||
|
||||
for (const name of Object.getOwnPropertyNames(this)) {
|
||||
@ -720,6 +726,7 @@ class XFAAttribute {
|
||||
this[$nodeName] = name;
|
||||
this[$content] = value;
|
||||
this[$consumed] = false;
|
||||
this[$uid] = `attribute${uid++}`;
|
||||
}
|
||||
|
||||
[$getParent]() {
|
||||
@ -730,6 +737,11 @@ class XFAAttribute {
|
||||
return true;
|
||||
}
|
||||
|
||||
[$setValue](value) {
|
||||
value = value.value || "";
|
||||
this[$content] = value.toString();
|
||||
}
|
||||
|
||||
[$text]() {
|
||||
return this[$content];
|
||||
}
|
||||
@ -765,6 +777,44 @@ class XmlObject extends XFAObject {
|
||||
this[$consumed] = false;
|
||||
}
|
||||
|
||||
[$toString](buf) {
|
||||
const tagName = this[$nodeName];
|
||||
if (tagName === "#text") {
|
||||
buf.push(encodeToXmlString(this[$content]));
|
||||
return;
|
||||
}
|
||||
const prefix = this[$namespaceId] === NS_DATASETS ? "xfa:" : "";
|
||||
buf.push(`<${prefix}${tagName}`);
|
||||
for (const [name, value] of this[_attributes].entries()) {
|
||||
buf.push(` ${name}="${encodeToXmlString(value[$content])}"`);
|
||||
}
|
||||
if (this[_dataValue] !== null) {
|
||||
if (this[_dataValue]) {
|
||||
buf.push(` xfa:dataNode="dataValue"`);
|
||||
} else {
|
||||
buf.push(` xfa:dataNode="dataGroup"`);
|
||||
}
|
||||
}
|
||||
if (!this[$content] && this[_children].length === 0) {
|
||||
buf.push("/>");
|
||||
return;
|
||||
}
|
||||
|
||||
buf.push(">");
|
||||
if (this[$content]) {
|
||||
if (typeof this[$content] === "string") {
|
||||
buf.push(encodeToXmlString(this[$content]));
|
||||
} else {
|
||||
this[$content][$toString](buf);
|
||||
}
|
||||
} else {
|
||||
for (const child of this[_children]) {
|
||||
child[$toString](buf);
|
||||
}
|
||||
}
|
||||
buf.push(`</${prefix}${tagName}>`);
|
||||
}
|
||||
|
||||
[$onChild](child) {
|
||||
if (this[$content]) {
|
||||
const node = new XmlObject(this[$namespaceId], "#text");
|
||||
@ -808,6 +858,10 @@ class XmlObject extends XFAObject {
|
||||
return this[_children].filter(c => c[$nodeName] === name);
|
||||
}
|
||||
|
||||
[$getAttributes]() {
|
||||
return this[_attributes];
|
||||
}
|
||||
|
||||
[$getChildrenByClass](name) {
|
||||
const value = this[_attributes].get(name);
|
||||
if (value !== undefined) {
|
||||
@ -882,6 +936,11 @@ class XmlObject extends XFAObject {
|
||||
return this[$content].trim();
|
||||
}
|
||||
|
||||
[$setValue](value) {
|
||||
value = value.value || "";
|
||||
this[$content] = value.toString();
|
||||
}
|
||||
|
||||
[$dump]() {
|
||||
const dumped = Object.create(null);
|
||||
if (this[$content]) {
|
||||
@ -993,6 +1052,7 @@ export {
|
||||
$finalize,
|
||||
$flushHTML,
|
||||
$getAttributeIt,
|
||||
$getAttributes,
|
||||
$getAvailableSpace,
|
||||
$getChildren,
|
||||
$getChildrenByClass,
|
||||
@ -1007,11 +1067,11 @@ export {
|
||||
$getTemplateRoot,
|
||||
$global,
|
||||
$globalData,
|
||||
$hasItem,
|
||||
$hasSettableValue,
|
||||
$ids,
|
||||
$indexOf,
|
||||
$insertAt,
|
||||
$isBindable,
|
||||
$isCDATAXml,
|
||||
$isDataValue,
|
||||
$isDescendent,
|
||||
@ -1034,6 +1094,7 @@ export {
|
||||
$setValue,
|
||||
$text,
|
||||
$toHTML,
|
||||
$toString,
|
||||
$toStyle,
|
||||
$uid,
|
||||
ContentObject,
|
||||
|
@ -2790,6 +2790,7 @@ class WorkerTransport {
|
||||
saveDocument() {
|
||||
return this.messageHandler
|
||||
.sendWithPromise("SaveDocument", {
|
||||
isPureXfa: !!this._htmlForXfa,
|
||||
numPages: this._numPages,
|
||||
annotationStorage: this.annotationStorage.serializable,
|
||||
filename: this._fullReader?.filename ?? null,
|
||||
|
@ -14,8 +14,8 @@
|
||||
*/
|
||||
|
||||
class XfaLayer {
|
||||
static setupStorage(html, fieldId, element, storage, intent) {
|
||||
const storedData = storage.getValue(fieldId, { value: null });
|
||||
static setupStorage(html, id, element, storage, intent) {
|
||||
const storedData = storage.getValue(id, { value: null });
|
||||
switch (element.name) {
|
||||
case "textarea":
|
||||
if (storedData.value !== null) {
|
||||
@ -25,36 +25,22 @@ class XfaLayer {
|
||||
break;
|
||||
}
|
||||
html.addEventListener("input", event => {
|
||||
storage.setValue(fieldId, { value: event.target.value });
|
||||
storage.setValue(id, { value: event.target.value });
|
||||
});
|
||||
break;
|
||||
case "input":
|
||||
if (element.attributes.type === "radio") {
|
||||
if (storedData.value) {
|
||||
if (
|
||||
element.attributes.type === "radio" ||
|
||||
element.attributes.type === "checkbox"
|
||||
) {
|
||||
if (storedData.value === element.attributes.exportedValue) {
|
||||
html.setAttribute("checked", true);
|
||||
}
|
||||
if (intent === "print") {
|
||||
break;
|
||||
}
|
||||
html.addEventListener("change", event => {
|
||||
const { target } = event;
|
||||
for (const radio of document.getElementsByName(target.name)) {
|
||||
if (radio !== target) {
|
||||
const id = radio.id;
|
||||
storage.setValue(id.split("-")[0], { value: false });
|
||||
}
|
||||
}
|
||||
storage.setValue(fieldId, { value: target.checked });
|
||||
});
|
||||
} else if (element.attributes.type === "checkbox") {
|
||||
if (storedData.value) {
|
||||
html.setAttribute("checked", true);
|
||||
}
|
||||
if (intent === "print") {
|
||||
break;
|
||||
}
|
||||
html.addEventListener("input", event => {
|
||||
storage.setValue(fieldId, { value: event.target.checked });
|
||||
storage.setValue(id, { value: event.target.getAttribute("xfaOn") });
|
||||
});
|
||||
} else {
|
||||
if (storedData.value !== null) {
|
||||
@ -64,7 +50,7 @@ class XfaLayer {
|
||||
break;
|
||||
}
|
||||
html.addEventListener("input", event => {
|
||||
storage.setValue(fieldId, { value: event.target.value });
|
||||
storage.setValue(id, { value: event.target.value });
|
||||
});
|
||||
}
|
||||
break;
|
||||
@ -80,9 +66,9 @@ class XfaLayer {
|
||||
const options = event.target.options;
|
||||
const value =
|
||||
options.selectedIndex === -1
|
||||
? null
|
||||
? ""
|
||||
: options[options.selectedIndex].value;
|
||||
storage.setValue(fieldId, { value });
|
||||
storage.setValue(id, { value });
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -96,7 +82,7 @@ class XfaLayer {
|
||||
attributes.name = `${attributes.name}-${intent}`;
|
||||
}
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
if (value === null || value === undefined || key === "fieldId") {
|
||||
if (value === null || value === undefined || key === "dataId") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -115,8 +101,8 @@ class XfaLayer {
|
||||
|
||||
// Set the value after the others to be sure overwrite
|
||||
// any other values.
|
||||
if (storage && attributes.fieldId !== undefined) {
|
||||
this.setupStorage(html, attributes.fieldId, element, storage);
|
||||
if (storage && attributes.dataId) {
|
||||
this.setupStorage(html, attributes.dataId, element, storage);
|
||||
}
|
||||
}
|
||||
|
||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -67,6 +67,7 @@
|
||||
!issue8229.pdf
|
||||
!issue8276_reduced.pdf
|
||||
!issue8372.pdf
|
||||
!xfa_filled_imm1344e.pdf
|
||||
!issue8424.pdf
|
||||
!issue8480.pdf
|
||||
!bug1650302_reduced.pdf
|
||||
|
71171
test/pdfs/xfa_filled_imm1344e.pdf
Normal file
71171
test/pdfs/xfa_filled_imm1344e.pdf
Normal file
File diff suppressed because one or more lines are too long
@ -930,6 +930,14 @@
|
||||
"link": true,
|
||||
"type": "load"
|
||||
},
|
||||
{ "id": "xfa_filled_imm1344e",
|
||||
"file": "pdfs/xfa_filled_imm1344e.pdf",
|
||||
"md5": "0576d16692fcd8ef2366cb48bf296e81",
|
||||
"rounds": 1,
|
||||
"enableXfa": true,
|
||||
"lastPage": 2,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "xfa_bug1717681",
|
||||
"file": "pdfs/xfa_bug1717681.pdf",
|
||||
"md5": "435b1eae7e017b1a932fe204d1ba8be5",
|
||||
|
@ -42,6 +42,7 @@
|
||||
"writer_spec.js",
|
||||
"xfa_formcalc_spec.js",
|
||||
"xfa_parser_spec.js",
|
||||
"xfa_serialize_data_spec.js",
|
||||
"xfa_tohtml_spec.js",
|
||||
"xml_spec.js"
|
||||
]
|
||||
|
@ -88,6 +88,7 @@ async function initializePDFJS(callback) {
|
||||
"pdfjs-test/unit/writer_spec.js",
|
||||
"pdfjs-test/unit/xfa_formcalc_spec.js",
|
||||
"pdfjs-test/unit/xfa_parser_spec.js",
|
||||
"pdfjs-test/unit/xfa_serialize_data_spec.js",
|
||||
"pdfjs-test/unit/xfa_tohtml_spec.js",
|
||||
"pdfjs-test/unit/xml_spec.js",
|
||||
].map(function (moduleName) {
|
||||
|
73
test/unit/xfa_serialize_data_spec.js
Normal file
73
test/unit/xfa_serialize_data_spec.js
Normal file
@ -0,0 +1,73 @@
|
||||
/* 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 { $uid } from "../../src/core/xfa/xfa_object.js";
|
||||
import { DataHandler } from "../../src/core/xfa/data.js";
|
||||
import { searchNode } from "../../src/core/xfa/som.js";
|
||||
import { XFAParser } from "../../src/core/xfa/parser.js";
|
||||
|
||||
describe("Data serializer", function () {
|
||||
it("should serialize data with an annotationStorage", function () {
|
||||
const xml = `
|
||||
<?xml version="1.0"?>
|
||||
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
||||
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
|
||||
<foo>bar</foo>
|
||||
<xfa:data>
|
||||
<Receipt>
|
||||
<Page>1</Page>
|
||||
<Detail PartNo="GS001">
|
||||
<Description>Giant Slingshot</Description>
|
||||
<Units>1</Units>
|
||||
<Unit_Price>250.00</Unit_Price>
|
||||
<Total_Price>250.00</Total_Price>
|
||||
</Detail>
|
||||
<Page>2</Page>
|
||||
<Detail PartNo="RRB-LB">
|
||||
<Description>Road Runner Bait, large bag</Description>
|
||||
<Units>5</Units>
|
||||
<Unit_Price>12.00</Unit_Price>
|
||||
<Total_Price>60.00</Total_Price>
|
||||
</Detail>
|
||||
<Sub_Total>310.00</Sub_Total>
|
||||
<Tax>24.80</Tax>
|
||||
<Total_Price>334.80</Total_Price>
|
||||
</Receipt>
|
||||
</xfa:data>
|
||||
<bar>foo</bar>
|
||||
</xfa:datasets>
|
||||
</xdp:xdp>
|
||||
`;
|
||||
const root = new XFAParser().parse(xml);
|
||||
const data = root.datasets.data;
|
||||
const dataHandler = new DataHandler(root, data);
|
||||
|
||||
const storage = new Map();
|
||||
for (const [path, value] of [
|
||||
["Receipt.Detail[0].Units", "12&3"],
|
||||
["Receipt.Detail[0].Unit_Price", "456>"],
|
||||
["Receipt.Detail[0].Total_Price", "789"],
|
||||
["Receipt.Detail[1].PartNo", "foo-bar😀"],
|
||||
["Receipt.Detail[1].Description", "hello world"],
|
||||
]) {
|
||||
storage.set(searchNode(root, data, path)[0][$uid], { value });
|
||||
}
|
||||
|
||||
const serialized = dataHandler.serialize(storage);
|
||||
const expected = `<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><foo>bar</foo><bar>foo</bar><xfa:data><Receipt><Page>1</Page><Detail PartNo="GS001"><Description>Giant Slingshot</Description><Units>12&3</Units><Unit_Price>456></Unit_Price><Total_Price>789</Total_Price></Detail><Page>2</Page><Detail PartNo="foo-bar😀"><Description>hello world</Description><Units>5</Units><Unit_Price>12.00</Unit_Price><Total_Price>60.00</Total_Price></Detail><Sub_Total>310.00</Sub_Total><Tax>24.80</Tax><Total_Price>334.80</Total_Price></Receipt></xfa:data></xfa:datasets>`;
|
||||
|
||||
expect(serialized).toEqual(expected);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user