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); |     this.xfaFactory.setFonts(pdfFonts); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async serializeXfaData(annotationStorage) { | ||||||
|  |     if (this.xfaFactory) { | ||||||
|  |       return this.xfaFactory.serializeData(annotationStorage); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   get formInfo() { |   get formInfo() { | ||||||
|     const formInfo = { |     const formInfo = { | ||||||
|       hasFields: false, |       hasFields: false, | ||||||
|  | |||||||
| @ -77,6 +77,10 @@ class BasePdfManager { | |||||||
|     return this.pdfDocument.loadXfaFonts(handler, task); |     return this.pdfDocument.loadXfaFonts(handler, task); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   serializeXfaData(annotationStorage) { | ||||||
|  |     return this.pdfDocument.serializeXfaData(annotationStorage); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   cleanup(manuallyTriggered = false) { |   cleanup(manuallyTriggered = false) { | ||||||
|     return this.pdfDocument.cleanup(manuallyTriggered); |     return this.pdfDocument.cleanup(manuallyTriggered); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -564,8 +564,9 @@ class WorkerMessageHandler { | |||||||
| 
 | 
 | ||||||
|     handler.on( |     handler.on( | ||||||
|       "SaveDocument", |       "SaveDocument", | ||||||
|       function ({ numPages, annotationStorage, filename }) { |       function ({ isPureXfa, numPages, annotationStorage, filename }) { | ||||||
|         pdfManager.requestLoadedStream(); |         pdfManager.requestLoadedStream(); | ||||||
|  | 
 | ||||||
|         const promises = [ |         const promises = [ | ||||||
|           pdfManager.onLoadedStream(), |           pdfManager.onLoadedStream(), | ||||||
|           pdfManager.ensureCatalog("acroForm"), |           pdfManager.ensureCatalog("acroForm"), | ||||||
| @ -573,19 +574,21 @@ class WorkerMessageHandler { | |||||||
|           pdfManager.ensureDoc("startXRef"), |           pdfManager.ensureDoc("startXRef"), | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         for (let pageIndex = 0; pageIndex < numPages; pageIndex++) { |         if (isPureXfa) { | ||||||
|           promises.push( |           promises.push(pdfManager.serializeXfaData(annotationStorage)); | ||||||
|             pdfManager.getPage(pageIndex).then(function (page) { |         } else { | ||||||
|               const task = new WorkerTask(`Save: page ${pageIndex}`); |           for (let pageIndex = 0; pageIndex < numPages; pageIndex++) { | ||||||
|               startWorkerTask(task); |             promises.push( | ||||||
| 
 |               pdfManager.getPage(pageIndex).then(function (page) { | ||||||
|               return page |                 const task = new WorkerTask(`Save: page ${pageIndex}`); | ||||||
|                 .save(handler, task, annotationStorage) |                 return page | ||||||
|                 .finally(function () { |                   .save(handler, task, annotationStorage) | ||||||
|                   finishWorkerTask(task); |                   .finally(function () { | ||||||
|                 }); |                     finishWorkerTask(task); | ||||||
|             }) |                   }); | ||||||
|           ); |               }) | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return Promise.all(promises).then(function ([ |         return Promise.all(promises).then(function ([ | ||||||
| @ -596,15 +599,23 @@ class WorkerMessageHandler { | |||||||
|           ...refs |           ...refs | ||||||
|         ]) { |         ]) { | ||||||
|           let newRefs = []; |           let newRefs = []; | ||||||
|           for (const ref of refs) { |           let xfaData = null; | ||||||
|             newRefs = ref |           if (isPureXfa) { | ||||||
|               .filter(x => x !== null) |             xfaData = refs[0]; | ||||||
|               .reduce((a, b) => a.concat(b), newRefs); |             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) { |             if (newRefs.length === 0) { | ||||||
|             // No new refs so just return the initial bytes
 |               // No new refs so just return the initial bytes
 | ||||||
|             return stream.bytes; |               return stream.bytes; | ||||||
|  |             } | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || []; |           const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || []; | ||||||
| @ -652,6 +663,7 @@ class WorkerMessageHandler { | |||||||
|             newRefs, |             newRefs, | ||||||
|             xref, |             xref, | ||||||
|             datasetsRef: xfaDatasets, |             datasetsRef: xfaDatasets, | ||||||
|  |             xfaData, | ||||||
|           }); |           }); | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -123,12 +123,7 @@ function computeMD5(filesize, xrefInfo) { | |||||||
|   return bytesToString(calculateMD5(array)); |   return bytesToString(calculateMD5(array)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function updateXFA(datasetsRef, newRefs, xref) { | function writeXFADataForAcroform(str, newRefs) { | ||||||
|   if (datasetsRef === null || xref === null) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   const datasets = xref.fetchIfRef(datasetsRef); |  | ||||||
|   const str = datasets.getString(); |  | ||||||
|   const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str); |   const xml = new SimpleXMLParser({ hasAttributes: true }).parseFromString(str); | ||||||
| 
 | 
 | ||||||
|   for (const { xfa } of newRefs) { |   for (const { xfa } of newRefs) { | ||||||
| @ -148,7 +143,17 @@ function updateXFA(datasetsRef, newRefs, xref) { | |||||||
|   } |   } | ||||||
|   const buffer = []; |   const buffer = []; | ||||||
|   xml.documentElement.dump(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; |   const encrypt = xref.encrypt; | ||||||
|   if (encrypt) { |   if (encrypt) { | ||||||
| @ -156,12 +161,12 @@ function updateXFA(datasetsRef, newRefs, xref) { | |||||||
|       datasetsRef.num, |       datasetsRef.num, | ||||||
|       datasetsRef.gen |       datasetsRef.gen | ||||||
|     ); |     ); | ||||||
|     updatedXml = transform.encryptString(updatedXml); |     xfaData = transform.encryptString(xfaData); | ||||||
|   } |   } | ||||||
|   const data = |   const data = | ||||||
|     `${datasetsRef.num} ${datasetsRef.gen} obj\n` + |     `${datasetsRef.num} ${datasetsRef.gen} obj\n` + | ||||||
|     `<< /Type /EmbeddedFile /Length ${updatedXml.length}>>\nstream\n` + |     `<< /Type /EmbeddedFile /Length ${xfaData.length}>>\nstream\n` + | ||||||
|     updatedXml + |     xfaData + | ||||||
|     "\nendstream\nendobj\n"; |     "\nendstream\nendobj\n"; | ||||||
| 
 | 
 | ||||||
|   newRefs.push({ ref: datasetsRef, data }); |   newRefs.push({ ref: datasetsRef, data }); | ||||||
| @ -173,8 +178,9 @@ function incrementalUpdate({ | |||||||
|   newRefs, |   newRefs, | ||||||
|   xref = null, |   xref = null, | ||||||
|   datasetsRef = null, |   datasetsRef = null, | ||||||
|  |   xfaData = null, | ||||||
| }) { | }) { | ||||||
|   updateXFA(datasetsRef, newRefs, xref); |   updateXFA(xfaData, datasetsRef, newRefs, xref); | ||||||
| 
 | 
 | ||||||
|   const newXref = new Dict(null); |   const newXref = new Dict(null); | ||||||
|   const refForXrefTable = xrefInfo.newRef; |   const refForXrefTable = xrefInfo.newRef; | ||||||
|  | |||||||
| @ -29,6 +29,7 @@ import { | |||||||
|   $hasSettableValue, |   $hasSettableValue, | ||||||
|   $indexOf, |   $indexOf, | ||||||
|   $insertAt, |   $insertAt, | ||||||
|  |   $isBindable, | ||||||
|   $isDataValue, |   $isDataValue, | ||||||
|   $isDescendent, |   $isDescendent, | ||||||
|   $namespaceId, |   $namespaceId, | ||||||
| @ -87,12 +88,12 @@ class Binder { | |||||||
|     // data node (through $data property): we'll use it
 |     // data node (through $data property): we'll use it
 | ||||||
|     // to save form data.
 |     // to save form data.
 | ||||||
| 
 | 
 | ||||||
|  |     formNode[$data] = data; | ||||||
|     if (formNode[$hasSettableValue]()) { |     if (formNode[$hasSettableValue]()) { | ||||||
|       if (data[$isDataValue]()) { |       if (data[$isDataValue]()) { | ||||||
|         const value = data[$getDataValue](); |         const value = data[$getDataValue](); | ||||||
|         // TODO: use picture.
 |         // TODO: use picture.
 | ||||||
|         formNode[$setValue](createText(value)); |         formNode[$setValue](createText(value)); | ||||||
|         formNode[$data] = data; |  | ||||||
|       } else if ( |       } else if ( | ||||||
|         formNode instanceof Field && |         formNode instanceof Field && | ||||||
|         formNode.ui && |         formNode.ui && | ||||||
| @ -103,13 +104,11 @@ class Binder { | |||||||
|           .map(child => child[$content].trim()) |           .map(child => child[$content].trim()) | ||||||
|           .join("\n"); |           .join("\n"); | ||||||
|         formNode[$setValue](createText(value)); |         formNode[$setValue](createText(value)); | ||||||
|         formNode[$data] = data; |  | ||||||
|       } else if (this._isConsumeData()) { |       } else if (this._isConsumeData()) { | ||||||
|         warn(`XFA - Nodes haven't the same type.`); |         warn(`XFA - Nodes haven't the same type.`); | ||||||
|       } |       } | ||||||
|     } else if (!data[$isDataValue]() || this._isMatchTemplate()) { |     } else if (!data[$isDataValue]() || this._isMatchTemplate()) { | ||||||
|       this._bindElement(formNode, data); |       this._bindElement(formNode, data); | ||||||
|       formNode[$data] = data; |  | ||||||
|     } else { |     } else { | ||||||
|       warn(`XFA - Nodes haven't the same type.`); |       warn(`XFA - Nodes haven't the same type.`); | ||||||
|     } |     } | ||||||
| @ -496,6 +495,12 @@ class Binder { | |||||||
|         continue; |         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 global = false; | ||||||
|       let picture = null; |       let picture = null; | ||||||
|       let ref = 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 { $globalData, $toHTML } from "./xfa_object.js"; | ||||||
| import { Binder } from "./bind.js"; | import { Binder } from "./bind.js"; | ||||||
|  | import { DataHandler } from "./data.js"; | ||||||
| import { FontFinder } from "./fonts.js"; | import { FontFinder } from "./fonts.js"; | ||||||
| import { warn } from "../../shared/util.js"; | import { warn } from "../../shared/util.js"; | ||||||
| import { XFAParser } from "./parser.js"; | import { XFAParser } from "./parser.js"; | ||||||
| @ -23,7 +24,9 @@ class XFAFactory { | |||||||
|   constructor(data) { |   constructor(data) { | ||||||
|     try { |     try { | ||||||
|       this.root = new XFAParser().parse(XFAFactory._createDocument(data)); |       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; |       this.form[$globalData].template = this.form; | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       warn(`XFA - an error occured during parsing and binding: ${e}`); |       warn(`XFA - an error occured during parsing and binding: ${e}`); | ||||||
| @ -70,6 +73,10 @@ class XFAFactory { | |||||||
|     return pages; |     return pages; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   serializeData(storage) { | ||||||
|  |     return this.dataHandler.serialize(storage); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   static _createDocument(data) { |   static _createDocument(data) { | ||||||
|     if (!data["/xdp:xdp"]) { |     if (!data["/xdp:xdp"]) { | ||||||
|       return data["xdp:xdp"]; |       return data["xdp:xdp"]; | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ import { | |||||||
|   $clean, |   $clean, | ||||||
|   $cleanPage, |   $cleanPage, | ||||||
|   $content, |   $content, | ||||||
|  |   $data, | ||||||
|   $extra, |   $extra, | ||||||
|   $finalize, |   $finalize, | ||||||
|   $flushHTML, |   $flushHTML, | ||||||
| @ -32,9 +33,9 @@ import { | |||||||
|   $getSubformParent, |   $getSubformParent, | ||||||
|   $getTemplateRoot, |   $getTemplateRoot, | ||||||
|   $globalData, |   $globalData, | ||||||
|   $hasItem, |  | ||||||
|   $hasSettableValue, |   $hasSettableValue, | ||||||
|   $ids, |   $ids, | ||||||
|  |   $isBindable, | ||||||
|   $isCDATAXml, |   $isCDATAXml, | ||||||
|   $isSplittable, |   $isSplittable, | ||||||
|   $isTransparent, |   $isTransparent, | ||||||
| @ -572,7 +573,7 @@ class BooleanElement extends Option01 { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   [$toHTML](availableSpace) { |   [$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 type; | ||||||
|     let className; |     let className; | ||||||
|     let groupId; |     let groupId; | ||||||
|     let id; |     const field = this[$getParent]()[$getParent](); | ||||||
|     const fieldId = this[$getParent]()[$getParent]()[$uid]; |     const items = | ||||||
|     const container = this[$getParent]()[$getParent]()[$getParent](); |       (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) { |     if (container instanceof ExclGroup) { | ||||||
|       groupId = container[$uid]; |       groupId = container[$uid]; | ||||||
|       type = "radio"; |       type = "radio"; | ||||||
|       className = "xfaRadio"; |       className = "xfaRadio"; | ||||||
|       id = `${fieldId}-radio`; |       dataId = container[$data] && container[$data][$uid]; | ||||||
|     } else { |     } else { | ||||||
|       type = "checkbox"; |       type = "checkbox"; | ||||||
|       className = "xfaCheckbox"; |       className = "xfaCheckbox"; | ||||||
|  |       dataId = field[$data] && field[$data][$uid]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const input = { |     const input = { | ||||||
| @ -969,14 +984,13 @@ class CheckButton extends XFAObject { | |||||||
|         class: [className], |         class: [className], | ||||||
|         style, |         style, | ||||||
|         fieldId, |         fieldId, | ||||||
|  |         dataId, | ||||||
|         type, |         type, | ||||||
|  |         checked, | ||||||
|  |         xfaOn: exportedValue.on, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     if (id) { |  | ||||||
|       input.attributes.id = id; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (groupId) { |     if (groupId) { | ||||||
|       input.attributes.name = groupId; |       input.attributes.name = groupId; | ||||||
|     } |     } | ||||||
| @ -1022,25 +1036,36 @@ class ChoiceList extends XFAObject { | |||||||
|     const children = []; |     const children = []; | ||||||
| 
 | 
 | ||||||
|     if (field.items.children.length > 0) { |     if (field.items.children.length > 0) { | ||||||
|       const displayed = field.items.children[0][$toHTML]().html; |       const items = field.items; | ||||||
|       const values = field.items.children[1] |       let displayedIndex = 0; | ||||||
|         ? field.items.children[1][$toHTML]().html |       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++) { |       for (let i = 0, ii = displayed.length; i < ii; i++) { | ||||||
|         children.push({ |         const option = { | ||||||
|           name: "option", |           name: "option", | ||||||
|           attributes: { |           attributes: { | ||||||
|             value: values[i] || displayed[i], |             value: values[i] || displayed[i], | ||||||
|           }, |           }, | ||||||
|           value: displayed[i], |           value: displayed[i], | ||||||
|         }); |         }; | ||||||
|  |         if (values[i] === value) { | ||||||
|  |           option.attributes.selected = true; | ||||||
|  |         } | ||||||
|  |         children.push(option); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const selectAttributes = { |     const selectAttributes = { | ||||||
|       class: ["xfaSelect"], |       class: ["xfaSelect"], | ||||||
|       fieldId: this[$getParent]()[$getParent]()[$uid], |       fieldId: field[$uid], | ||||||
|  |       dataId: field[$data] && field[$data][$uid], | ||||||
|       style, |       style, | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
| @ -1272,11 +1297,13 @@ class DateTimeEdit extends XFAObject { | |||||||
|     // When the picker is host we should use type=date for the input
 |     // When the picker is host we should use type=date for the input
 | ||||||
|     // but we need to put the buttons outside the text-field.
 |     // but we need to put the buttons outside the text-field.
 | ||||||
|     const style = toStyle(this, "border", "font", "margin"); |     const style = toStyle(this, "border", "font", "margin"); | ||||||
|  |     const field = this[$getParent]()[$getParent](); | ||||||
|     const html = { |     const html = { | ||||||
|       name: "input", |       name: "input", | ||||||
|       attributes: { |       attributes: { | ||||||
|         type: "text", |         type: "text", | ||||||
|         fieldId: this[$getParent]()[$getParent]()[$uid], |         fieldId: field[$uid], | ||||||
|  |         dataId: field[$data] && field[$data][$uid], | ||||||
|         class: ["xfaTextfield"], |         class: ["xfaTextfield"], | ||||||
|         style, |         style, | ||||||
|       }, |       }, | ||||||
| @ -1976,6 +2003,10 @@ class ExclGroup extends XFAObject { | |||||||
|     this.setProperty = new XFAObjectArray(); |     this.setProperty = new XFAObjectArray(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$isBindable]() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   [$hasSettableValue]() { |   [$hasSettableValue]() { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| @ -1988,17 +2019,7 @@ class ExclGroup extends XFAObject { | |||||||
|         field.value = nodeValue; |         field.value = nodeValue; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const nodeBoolean = new BooleanElement({}); |       field.value[$setValue](value); | ||||||
|       nodeBoolean[$content] = 0; |  | ||||||
| 
 |  | ||||||
|       for (const item of field.items.children) { |  | ||||||
|         if (item[$hasItem](value)) { |  | ||||||
|           nodeBoolean[$content] = 1; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       field.value[$setValue](nodeBoolean); |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -2312,6 +2333,10 @@ class Field extends XFAObject { | |||||||
|     this.setProperty = new XFAObjectArray(); |     this.setProperty = new XFAObjectArray(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$isBindable]() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   [$setValue](value) { |   [$setValue](value) { | ||||||
|     _setValue(this, value); |     _setValue(this, value); | ||||||
|   } |   } | ||||||
| @ -2906,15 +2931,6 @@ class Items extends XFAObject { | |||||||
|     this.time = new XFAObjectArray(); |     this.time = new XFAObjectArray(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   [$hasItem](value) { |  | ||||||
|     return ( |  | ||||||
|       this.hasOwnProperty(value[$nodeName]) && |  | ||||||
|       this[value[$nodeName]].children.some( |  | ||||||
|         node => node[$content] === value[$content] |  | ||||||
|       ) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   [$toHTML]() { |   [$toHTML]() { | ||||||
|     const output = []; |     const output = []; | ||||||
|     for (const child of this[$getChildren]()) { |     for (const child of this[$getChildren]()) { | ||||||
| @ -3182,11 +3198,13 @@ class NumericEdit extends XFAObject { | |||||||
|   [$toHTML](availableSpace) { |   [$toHTML](availableSpace) { | ||||||
|     // TODO: incomplete.
 |     // TODO: incomplete.
 | ||||||
|     const style = toStyle(this, "border", "font", "margin"); |     const style = toStyle(this, "border", "font", "margin"); | ||||||
|  |     const field = this[$getParent]()[$getParent](); | ||||||
|     const html = { |     const html = { | ||||||
|       name: "input", |       name: "input", | ||||||
|       attributes: { |       attributes: { | ||||||
|         type: "text", |         type: "text", | ||||||
|         fieldId: this[$getParent]()[$getParent]()[$uid], |         fieldId: field[$uid], | ||||||
|  |         dataId: field[$data] && field[$data][$uid], | ||||||
|         class: ["xfaTextfield"], |         class: ["xfaTextfield"], | ||||||
|         style, |         style, | ||||||
|       }, |       }, | ||||||
| @ -4151,6 +4169,10 @@ class Subform extends XFAObject { | |||||||
|     this.subformSet = new XFAObjectArray(); |     this.subformSet = new XFAObjectArray(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$isBindable]() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   *[$getContainedChildren]() { |   *[$getContainedChildren]() { | ||||||
|     // This function is overriden in order to fake that subforms under
 |     // This function is overriden in order to fake that subforms under
 | ||||||
|     // this set are in fact under parent subform.
 |     // this set are in fact under parent subform.
 | ||||||
| @ -4924,11 +4946,13 @@ class TextEdit extends XFAObject { | |||||||
|     // TODO: incomplete.
 |     // TODO: incomplete.
 | ||||||
|     const style = toStyle(this, "border", "font", "margin"); |     const style = toStyle(this, "border", "font", "margin"); | ||||||
|     let html; |     let html; | ||||||
|  |     const field = this[$getParent]()[$getParent](); | ||||||
|     if (this.multiLine === 1) { |     if (this.multiLine === 1) { | ||||||
|       html = { |       html = { | ||||||
|         name: "textarea", |         name: "textarea", | ||||||
|         attributes: { |         attributes: { | ||||||
|           fieldId: this[$getParent]()[$getParent]()[$uid], |           dataId: field[$data] && field[$data][$uid], | ||||||
|  |           fieldId: field[$uid], | ||||||
|           class: ["xfaTextfield"], |           class: ["xfaTextfield"], | ||||||
|           style, |           style, | ||||||
|         }, |         }, | ||||||
| @ -4938,7 +4962,8 @@ class TextEdit extends XFAObject { | |||||||
|         name: "input", |         name: "input", | ||||||
|         attributes: { |         attributes: { | ||||||
|           type: "text", |           type: "text", | ||||||
|           fieldId: this[$getParent]()[$getParent]()[$uid], |           dataId: field[$data] && field[$data][$uid], | ||||||
|  |           fieldId: field[$uid], | ||||||
|           class: ["xfaTextfield"], |           class: ["xfaTextfield"], | ||||||
|           style, |           style, | ||||||
|         }, |         }, | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ | |||||||
| 
 | 
 | ||||||
| import { getInteger, getKeyword, HTMLResult } from "./utils.js"; | import { getInteger, getKeyword, HTMLResult } from "./utils.js"; | ||||||
| import { shadow, warn } from "../../shared/util.js"; | import { shadow, warn } from "../../shared/util.js"; | ||||||
|  | import { encodeToXmlString } from "../core_utils.js"; | ||||||
| import { NamespaceIds } from "./namespaces.js"; | import { NamespaceIds } from "./namespaces.js"; | ||||||
| import { searchNode } from "./som.js"; | import { searchNode } from "./som.js"; | ||||||
| 
 | 
 | ||||||
| @ -36,6 +37,7 @@ const $extra = Symbol("extra"); | |||||||
| const $finalize = Symbol(); | const $finalize = Symbol(); | ||||||
| const $flushHTML = Symbol(); | const $flushHTML = Symbol(); | ||||||
| const $getAttributeIt = Symbol(); | const $getAttributeIt = Symbol(); | ||||||
|  | const $getAttributes = Symbol(); | ||||||
| const $getAvailableSpace = Symbol(); | const $getAvailableSpace = Symbol(); | ||||||
| const $getChildrenByClass = Symbol(); | const $getChildrenByClass = Symbol(); | ||||||
| const $getChildrenByName = Symbol(); | const $getChildrenByName = Symbol(); | ||||||
| @ -50,12 +52,12 @@ const $getParent = Symbol(); | |||||||
| const $getTemplateRoot = Symbol(); | const $getTemplateRoot = Symbol(); | ||||||
| const $global = Symbol(); | const $global = Symbol(); | ||||||
| const $globalData = Symbol(); | const $globalData = Symbol(); | ||||||
| const $hasItem = Symbol(); |  | ||||||
| const $hasSettableValue = Symbol(); | const $hasSettableValue = Symbol(); | ||||||
| const $ids = Symbol(); | const $ids = Symbol(); | ||||||
| const $indexOf = Symbol(); | const $indexOf = Symbol(); | ||||||
| const $insertAt = Symbol(); | const $insertAt = Symbol(); | ||||||
| const $isCDATAXml = Symbol(); | const $isCDATAXml = Symbol(); | ||||||
|  | const $isBindable = Symbol(); | ||||||
| const $isDataValue = Symbol(); | const $isDataValue = Symbol(); | ||||||
| const $isDescendent = Symbol(); | const $isDescendent = Symbol(); | ||||||
| const $isSplittable = Symbol(); | const $isSplittable = Symbol(); | ||||||
| @ -78,6 +80,7 @@ const $setSetAttributes = Symbol(); | |||||||
| const $setValue = Symbol(); | const $setValue = Symbol(); | ||||||
| const $text = Symbol(); | const $text = Symbol(); | ||||||
| const $toHTML = Symbol(); | const $toHTML = Symbol(); | ||||||
|  | const $toString = Symbol(); | ||||||
| const $toStyle = Symbol(); | const $toStyle = Symbol(); | ||||||
| const $uid = Symbol("uid"); | const $uid = Symbol("uid"); | ||||||
| 
 | 
 | ||||||
| @ -101,6 +104,8 @@ const _validator = Symbol(); | |||||||
| 
 | 
 | ||||||
| let uid = 0; | let uid = 0; | ||||||
| 
 | 
 | ||||||
|  | const NS_DATASETS = NamespaceIds.datasets.id; | ||||||
|  | 
 | ||||||
| class XFAObject { | class XFAObject { | ||||||
|   constructor(nsId, name, hasChildren = false) { |   constructor(nsId, name, hasChildren = false) { | ||||||
|     this[$namespaceId] = nsId; |     this[$namespaceId] = nsId; | ||||||
| @ -161,6 +166,10 @@ class XFAObject { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$isBindable]() { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   [$setId](ids) { |   [$setId](ids) { | ||||||
|     if (this.id && this[$namespaceId] === NamespaceIds.template.id) { |     if (this.id && this[$namespaceId] === NamespaceIds.template.id) { | ||||||
|       ids.set(this.id, this); |       ids.set(this.id, this); | ||||||
| @ -207,10 +216,6 @@ class XFAObject { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   [$hasItem]() { |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   [$indexOf](child) { |   [$indexOf](child) { | ||||||
|     return this[_children].indexOf(child); |     return this[_children].indexOf(child); | ||||||
|   } |   } | ||||||
| @ -599,6 +604,7 @@ class XFAObject { | |||||||
|         shadow(clone, $symbol, this[$symbol]); |         shadow(clone, $symbol, this[$symbol]); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     clone[$uid] = `${clone[$nodeName]}${uid++}`; | ||||||
|     clone[_children] = []; |     clone[_children] = []; | ||||||
| 
 | 
 | ||||||
|     for (const name of Object.getOwnPropertyNames(this)) { |     for (const name of Object.getOwnPropertyNames(this)) { | ||||||
| @ -720,6 +726,7 @@ class XFAAttribute { | |||||||
|     this[$nodeName] = name; |     this[$nodeName] = name; | ||||||
|     this[$content] = value; |     this[$content] = value; | ||||||
|     this[$consumed] = false; |     this[$consumed] = false; | ||||||
|  |     this[$uid] = `attribute${uid++}`; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   [$getParent]() { |   [$getParent]() { | ||||||
| @ -730,6 +737,11 @@ class XFAAttribute { | |||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$setValue](value) { | ||||||
|  |     value = value.value || ""; | ||||||
|  |     this[$content] = value.toString(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   [$text]() { |   [$text]() { | ||||||
|     return this[$content]; |     return this[$content]; | ||||||
|   } |   } | ||||||
| @ -765,6 +777,44 @@ class XmlObject extends XFAObject { | |||||||
|     this[$consumed] = false; |     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) { |   [$onChild](child) { | ||||||
|     if (this[$content]) { |     if (this[$content]) { | ||||||
|       const node = new XmlObject(this[$namespaceId], "#text"); |       const node = new XmlObject(this[$namespaceId], "#text"); | ||||||
| @ -808,6 +858,10 @@ class XmlObject extends XFAObject { | |||||||
|     return this[_children].filter(c => c[$nodeName] === name); |     return this[_children].filter(c => c[$nodeName] === name); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$getAttributes]() { | ||||||
|  |     return this[_attributes]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   [$getChildrenByClass](name) { |   [$getChildrenByClass](name) { | ||||||
|     const value = this[_attributes].get(name); |     const value = this[_attributes].get(name); | ||||||
|     if (value !== undefined) { |     if (value !== undefined) { | ||||||
| @ -882,6 +936,11 @@ class XmlObject extends XFAObject { | |||||||
|     return this[$content].trim(); |     return this[$content].trim(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   [$setValue](value) { | ||||||
|  |     value = value.value || ""; | ||||||
|  |     this[$content] = value.toString(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   [$dump]() { |   [$dump]() { | ||||||
|     const dumped = Object.create(null); |     const dumped = Object.create(null); | ||||||
|     if (this[$content]) { |     if (this[$content]) { | ||||||
| @ -993,6 +1052,7 @@ export { | |||||||
|   $finalize, |   $finalize, | ||||||
|   $flushHTML, |   $flushHTML, | ||||||
|   $getAttributeIt, |   $getAttributeIt, | ||||||
|  |   $getAttributes, | ||||||
|   $getAvailableSpace, |   $getAvailableSpace, | ||||||
|   $getChildren, |   $getChildren, | ||||||
|   $getChildrenByClass, |   $getChildrenByClass, | ||||||
| @ -1007,11 +1067,11 @@ export { | |||||||
|   $getTemplateRoot, |   $getTemplateRoot, | ||||||
|   $global, |   $global, | ||||||
|   $globalData, |   $globalData, | ||||||
|   $hasItem, |  | ||||||
|   $hasSettableValue, |   $hasSettableValue, | ||||||
|   $ids, |   $ids, | ||||||
|   $indexOf, |   $indexOf, | ||||||
|   $insertAt, |   $insertAt, | ||||||
|  |   $isBindable, | ||||||
|   $isCDATAXml, |   $isCDATAXml, | ||||||
|   $isDataValue, |   $isDataValue, | ||||||
|   $isDescendent, |   $isDescendent, | ||||||
| @ -1034,6 +1094,7 @@ export { | |||||||
|   $setValue, |   $setValue, | ||||||
|   $text, |   $text, | ||||||
|   $toHTML, |   $toHTML, | ||||||
|  |   $toString, | ||||||
|   $toStyle, |   $toStyle, | ||||||
|   $uid, |   $uid, | ||||||
|   ContentObject, |   ContentObject, | ||||||
|  | |||||||
| @ -2790,6 +2790,7 @@ class WorkerTransport { | |||||||
|   saveDocument() { |   saveDocument() { | ||||||
|     return this.messageHandler |     return this.messageHandler | ||||||
|       .sendWithPromise("SaveDocument", { |       .sendWithPromise("SaveDocument", { | ||||||
|  |         isPureXfa: !!this._htmlForXfa, | ||||||
|         numPages: this._numPages, |         numPages: this._numPages, | ||||||
|         annotationStorage: this.annotationStorage.serializable, |         annotationStorage: this.annotationStorage.serializable, | ||||||
|         filename: this._fullReader?.filename ?? null, |         filename: this._fullReader?.filename ?? null, | ||||||
|  | |||||||
| @ -14,8 +14,8 @@ | |||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| class XfaLayer { | class XfaLayer { | ||||||
|   static setupStorage(html, fieldId, element, storage, intent) { |   static setupStorage(html, id, element, storage, intent) { | ||||||
|     const storedData = storage.getValue(fieldId, { value: null }); |     const storedData = storage.getValue(id, { value: null }); | ||||||
|     switch (element.name) { |     switch (element.name) { | ||||||
|       case "textarea": |       case "textarea": | ||||||
|         if (storedData.value !== null) { |         if (storedData.value !== null) { | ||||||
| @ -25,36 +25,22 @@ class XfaLayer { | |||||||
|           break; |           break; | ||||||
|         } |         } | ||||||
|         html.addEventListener("input", event => { |         html.addEventListener("input", event => { | ||||||
|           storage.setValue(fieldId, { value: event.target.value }); |           storage.setValue(id, { value: event.target.value }); | ||||||
|         }); |         }); | ||||||
|         break; |         break; | ||||||
|       case "input": |       case "input": | ||||||
|         if (element.attributes.type === "radio") { |         if ( | ||||||
|           if (storedData.value) { |           element.attributes.type === "radio" || | ||||||
|  |           element.attributes.type === "checkbox" | ||||||
|  |         ) { | ||||||
|  |           if (storedData.value === element.attributes.exportedValue) { | ||||||
|             html.setAttribute("checked", true); |             html.setAttribute("checked", true); | ||||||
|           } |           } | ||||||
|           if (intent === "print") { |           if (intent === "print") { | ||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|           html.addEventListener("change", event => { |           html.addEventListener("change", event => { | ||||||
|             const { target } = event; |             storage.setValue(id, { value: event.target.getAttribute("xfaOn") }); | ||||||
|             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 }); |  | ||||||
|           }); |           }); | ||||||
|         } else { |         } else { | ||||||
|           if (storedData.value !== null) { |           if (storedData.value !== null) { | ||||||
| @ -64,7 +50,7 @@ class XfaLayer { | |||||||
|             break; |             break; | ||||||
|           } |           } | ||||||
|           html.addEventListener("input", event => { |           html.addEventListener("input", event => { | ||||||
|             storage.setValue(fieldId, { value: event.target.value }); |             storage.setValue(id, { value: event.target.value }); | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
| @ -80,9 +66,9 @@ class XfaLayer { | |||||||
|           const options = event.target.options; |           const options = event.target.options; | ||||||
|           const value = |           const value = | ||||||
|             options.selectedIndex === -1 |             options.selectedIndex === -1 | ||||||
|               ? null |               ? "" | ||||||
|               : options[options.selectedIndex].value; |               : options[options.selectedIndex].value; | ||||||
|           storage.setValue(fieldId, { value }); |           storage.setValue(id, { value }); | ||||||
|         }); |         }); | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
| @ -96,7 +82,7 @@ class XfaLayer { | |||||||
|       attributes.name = `${attributes.name}-${intent}`; |       attributes.name = `${attributes.name}-${intent}`; | ||||||
|     } |     } | ||||||
|     for (const [key, value] of Object.entries(attributes)) { |     for (const [key, value] of Object.entries(attributes)) { | ||||||
|       if (value === null || value === undefined || key === "fieldId") { |       if (value === null || value === undefined || key === "dataId") { | ||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -115,8 +101,8 @@ class XfaLayer { | |||||||
| 
 | 
 | ||||||
|     // Set the value after the others to be sure overwrite
 |     // Set the value after the others to be sure overwrite
 | ||||||
|     // any other values.
 |     // any other values.
 | ||||||
|     if (storage && attributes.fieldId !== undefined) { |     if (storage && attributes.dataId) { | ||||||
|       this.setupStorage(html, attributes.fieldId, element, storage); |       this.setupStorage(html, attributes.dataId, element, storage); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								test/pdfs/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								test/pdfs/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -67,6 +67,7 @@ | |||||||
| !issue8229.pdf | !issue8229.pdf | ||||||
| !issue8276_reduced.pdf | !issue8276_reduced.pdf | ||||||
| !issue8372.pdf | !issue8372.pdf | ||||||
|  | !xfa_filled_imm1344e.pdf | ||||||
| !issue8424.pdf | !issue8424.pdf | ||||||
| !issue8480.pdf | !issue8480.pdf | ||||||
| !bug1650302_reduced.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, |        "link": true, | ||||||
|        "type": "load" |        "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", |     {  "id": "xfa_bug1717681", | ||||||
|        "file": "pdfs/xfa_bug1717681.pdf", |        "file": "pdfs/xfa_bug1717681.pdf", | ||||||
|        "md5": "435b1eae7e017b1a932fe204d1ba8be5", |        "md5": "435b1eae7e017b1a932fe204d1ba8be5", | ||||||
|  | |||||||
| @ -42,6 +42,7 @@ | |||||||
|     "writer_spec.js", |     "writer_spec.js", | ||||||
|     "xfa_formcalc_spec.js", |     "xfa_formcalc_spec.js", | ||||||
|     "xfa_parser_spec.js", |     "xfa_parser_spec.js", | ||||||
|  |     "xfa_serialize_data_spec.js", | ||||||
|     "xfa_tohtml_spec.js", |     "xfa_tohtml_spec.js", | ||||||
|     "xml_spec.js" |     "xml_spec.js" | ||||||
|   ] |   ] | ||||||
|  | |||||||
| @ -88,6 +88,7 @@ async function initializePDFJS(callback) { | |||||||
|       "pdfjs-test/unit/writer_spec.js", |       "pdfjs-test/unit/writer_spec.js", | ||||||
|       "pdfjs-test/unit/xfa_formcalc_spec.js", |       "pdfjs-test/unit/xfa_formcalc_spec.js", | ||||||
|       "pdfjs-test/unit/xfa_parser_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/xfa_tohtml_spec.js", | ||||||
|       "pdfjs-test/unit/xml_spec.js", |       "pdfjs-test/unit/xml_spec.js", | ||||||
|     ].map(function (moduleName) { |     ].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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user