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:
Calixte Denizet 2021-06-25 14:31:55 +02:00
parent d7fdb72a3f
commit 429ffdcd2f
17 changed files with 71564 additions and 113 deletions

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@ -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"];

View File

@ -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,
},

View File

@ -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,

View File

@ -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,

View File

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

View File

@ -67,6 +67,7 @@
!issue8229.pdf
!issue8276_reduced.pdf
!issue8372.pdf
!xfa_filled_imm1344e.pdf
!issue8424.pdf
!issue8480.pdf
!bug1650302_reduced.pdf

File diff suppressed because one or more lines are too long

View File

@ -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",

View File

@ -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"
]

View File

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

View 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&amp;3</Units><Unit_Price>456&gt;</Unit_Price><Total_Price>789</Total_Price></Detail><Page>2</Page><Detail PartNo="foo-bar&#x1F600;"><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);
});
});