diff --git a/src/core/document.js b/src/core/document.js index d5cec7c94efbf..38f6a0404c17e 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -146,8 +146,7 @@ class Page { _getBoundingBox(name) { if (this.xfaData) { - const { width, height } = this.xfaData.attributes.style; - return [0, 0, parseInt(width), parseInt(height)]; + return this.xfaData.bbox; } const box = this._getInheritableProperty(name, /* getArray = */ true); @@ -241,7 +240,9 @@ class Page { get xfaData() { if (this.xfaFactory) { - return shadow(this, "xfaData", this.xfaFactory.getPage(this.pageIndex)); + return shadow(this, "xfaData", { + bbox: this.xfaFactory.getBoundingBox(this.pageIndex), + }); } return shadow(this, "xfaData", null); } @@ -851,8 +852,11 @@ class PDFDocument { return shadow(this, "xfaFaxtory", null); } - get isPureXfa() { - return this.xfaFactory !== null; + get htmlForXfa() { + if (this.xfaFactory) { + return this.xfaFactory.getPages(); + } + return null; } async loadXfaFonts(handler, task) { diff --git a/src/core/worker.js b/src/core/worker.js index f88691622bc47..4c121485c509d 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -187,13 +187,13 @@ class WorkerMessageHandler { await pdfManager.ensureDoc("checkFirstPage"); } - const [numPages, fingerprint, isPureXfa] = await Promise.all([ + const [numPages, fingerprint, htmlForXfa] = await Promise.all([ pdfManager.ensureDoc("numPages"), pdfManager.ensureDoc("fingerprint"), - pdfManager.ensureDoc("isPureXfa"), + pdfManager.ensureDoc("htmlForXfa"), ]); - if (isPureXfa) { + if (htmlForXfa) { const task = new WorkerTask("loadXfaFonts"); startWorkerTask(task); await pdfManager @@ -203,7 +203,7 @@ class WorkerMessageHandler { }) .then(() => finishWorkerTask(task)); } - return { numPages, fingerprint, isPureXfa }; + return { numPages, fingerprint, htmlForXfa }; } function getPdfManager(data, evaluatorOptions, enableXfa) { @@ -501,12 +501,6 @@ class WorkerMessageHandler { }); }); - handler.on("GetPageXfa", function wphSetupGetXfa({ pageIndex }) { - return pdfManager.getPage(pageIndex).then(function (page) { - return pdfManager.ensure(page, "xfaData"); - }); - }); - handler.on("GetOutline", function wphSetupGetOutline(data) { return pdfManager.ensureCatalog("documentOutline"); }); diff --git a/src/core/xfa/factory.js b/src/core/xfa/factory.js index 27e7c19c45129..cea1a911096aa 100644 --- a/src/core/xfa/factory.js +++ b/src/core/xfa/factory.js @@ -22,18 +22,35 @@ class XFAFactory { try { this.root = new XFAParser().parse(XFAFactory._createDocument(data)); this.form = new Binder(this.root).bind(); - this.pages = this.form[$toHTML](); + this._createPages(); } catch (e) { console.log(e); } } - getPage(pageIndex) { - return this.pages.children[pageIndex]; + _createPages() { + this.pages = this.form[$toHTML](); + this.dims = this.pages.children.map(c => { + const { width, height } = c.attributes.style; + return [0, 0, parseInt(width), parseInt(height)]; + }); + } + + getBoundingBox(pageIndex) { + return this.dims[pageIndex]; } get numberPages() { - return this.pages.children.length; + return this.dims.length; + } + + getPages() { + if (!this.pages) { + this._createPages(); + } + const pages = this.pages; + this.pages = null; + return pages; } static _createDocument(data) { diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index ef7e89e9f7127..b29dc3e0aae57 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -941,11 +941,14 @@ class CheckButton extends XFAObject { style.height = size; } + const fieldId = this[$getParent]()[$getParent]()[$uid]; const input = { name: "input", attributes: { class: "xfaCheckbox", + fieldId, type: "radio", + id: `${fieldId}-radio`, }, }; @@ -1023,6 +1026,7 @@ class ChoiceList extends XFAObject { const selectAttributes = { class: "xfaSelect", + fieldId: this[$getParent]()[$getParent]()[$uid], style, }; @@ -1249,6 +1253,7 @@ class DateTimeEdit extends XFAObject { name: "input", attributes: { type: "text", + fieldId: this[$getParent]()[$getParent]()[$uid], class: "xfaTextfield", style, }, @@ -3012,6 +3017,7 @@ class NumericEdit extends XFAObject { name: "input", attributes: { type: "text", + fieldId: this[$getParent]()[$getParent]()[$uid], class: "xfaTextfield", style, }, @@ -4550,6 +4556,7 @@ class TextEdit extends XFAObject { html = { name: "textarea", attributes: { + fieldId: this[$getParent]()[$getParent]()[$uid], class: "xfaTextfield", style, }, @@ -4559,6 +4566,7 @@ class TextEdit extends XFAObject { name: "input", attributes: { type: "text", + fieldId: this[$getParent]()[$getParent]()[$uid], class: "xfaTextfield", style, }, diff --git a/src/display/api.js b/src/display/api.js index ce7bac9d15da0..5cd2229c89c11 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -695,7 +695,15 @@ class PDFDocumentProxy { * @type {boolean} True if only XFA form. */ get isPureXfa() { - return this._pdfInfo.isPureXfa; + return !!this._transport._htmlForXfa; + } + + /** + * @type {Object | null} An object representing a HTML tree structure + * to render the XFA, or `null` when no XFA form exists. + */ + get allXfaHtml() { + return this._transport._htmlForXfa; } /** @@ -1255,8 +1263,8 @@ class PDFPageProxy { * are {Object} with a name, attributes (class, style, ...), value and * children, very similar to a HTML DOM tree), or `null` if no XFA exists. */ - getXfa() { - return (this._xfaPromise ||= this._transport.getPageXfa(this._pageIndex)); + async getXfa() { + return this._transport._htmlForXfa?.children[this._pageIndex] || null; } /** @@ -1552,7 +1560,6 @@ class PDFPageProxy { this.objs.clear(); this._annotationsPromise = null; this._jsActionsPromise = null; - this._xfaPromise = null; this._structTreePromise = null; this.pendingCleanup = false; return Promise.all(waitOn); @@ -1588,7 +1595,6 @@ class PDFPageProxy { this.objs.clear(); this._annotationsPromise = null; this._jsActionsPromise = null; - this._xfaPromise = null; this._structTreePromise = null; if (resetStats && this._stats) { this._stats = new StatTimer(); @@ -2456,6 +2462,8 @@ class WorkerTransport { messageHandler.on("GetDoc", ({ pdfInfo }) => { this._numPages = pdfInfo.numPages; + this._htmlForXfa = pdfInfo.htmlForXfa; + delete pdfInfo.htmlForXfa; loadingTask._capability.resolve(new PDFDocumentProxy(pdfInfo, this)); }); @@ -2812,12 +2820,6 @@ class WorkerTransport { }); } - getPageXfa(pageIndex) { - return this.messageHandler.sendWithPromise("GetPageXfa", { - pageIndex, - }); - } - getStructTree(pageIndex) { return this.messageHandler.sendWithPromise("GetStructTree", { pageIndex, diff --git a/src/display/xfa_layer.js b/src/display/xfa_layer.js index 5949d5661fc93..85d94f860e432 100644 --- a/src/display/xfa_layer.js +++ b/src/display/xfa_layer.js @@ -14,9 +14,60 @@ */ class XfaLayer { - static setAttributes(html, attrs) { - for (const [key, value] of Object.entries(attrs)) { - if (value === null || value === undefined) { + static setupStorage(html, fieldId, element, storage) { + const storedData = storage.getValue(fieldId, { value: null }); + switch (element.name) { + case "textarea": + html.textContent = storedData.value !== null ? storedData.value : ""; + html.addEventListener("input", event => { + storage.setValue(fieldId, { value: event.target.value }); + }); + break; + case "input": + if (storedData.value !== null) { + html.setAttribute("value", storedData.value); + } + if (element.attributes.type === "radio") { + 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 { + html.addEventListener("input", event => { + storage.setValue(fieldId, { value: event.target.value }); + }); + } + break; + case "select": + if (storedData.value !== null) { + for (const option of element.children) { + if (option.attributes.value === storedData.value) { + option.attributes.selected = true; + } + } + } + html.addEventListener("input", event => { + const options = event.target.options; + const value = + options.selectedIndex === -1 + ? null + : options[options.selectedIndex].value; + storage.setValue(fieldId, { value }); + }); + break; + } + } + + static setAttributes(html, element, storage) { + const { attributes } = element; + for (const [key, value] of Object.entries(attributes)) { + if (value === null || value === undefined || key === "fieldId") { continue; } @@ -30,13 +81,20 @@ class XfaLayer { Object.assign(html.style, value); } } + + // 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); + } } static render(parameters) { + const storage = parameters.annotationStorage; const root = parameters.xfa; const rootHtml = document.createElement(root.name); if (root.attributes) { - XfaLayer.setAttributes(rootHtml, root.attributes); + this.setAttributes(rootHtml, root); } const stack = [[root, -1, rootHtml]]; @@ -69,7 +127,7 @@ class XfaLayer { const childHtml = document.createElement(name); html.appendChild(childHtml); if (child.attributes) { - XfaLayer.setAttributes(childHtml, child.attributes); + this.setAttributes(childHtml, child, storage); } if (child.children && child.children.length > 0) { diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js index 7176fe0dfbb2e..c116c44404c3e 100644 --- a/test/unit/xfa_tohtml_spec.js +++ b/test/unit/xfa_tohtml_spec.js @@ -57,7 +57,8 @@ describe("XFAFactory", function () { expect(factory.numberPages).toEqual(2); - const page1 = factory.getPage(0); + const pages = factory.getPages(); + const page1 = pages.children[0]; expect(page1.attributes.style).toEqual({ height: "789px", width: "456px", @@ -99,7 +100,7 @@ describe("XFAFactory", function () { // draw element must be on each page. expect(draw.attributes.style).toEqual( - factory.getPage(1).children[1].children[0].attributes.style + pages.children[1].children[1].children[0].attributes.style ); }); }); diff --git a/web/base_viewer.js b/web/base_viewer.js index 2468e1cfc84ab..ec36ad9c519bf 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -1317,12 +1317,16 @@ class BaseViewer { /** * @param {HTMLDivElement} pageDiv * @param {PDFPage} pdfPage + * @param {AnnotationStorage} [annotationStorage] - Storage for annotation + * data in forms. * @returns {XfaLayerBuilder} */ - createXfaLayerBuilder(pageDiv, pdfPage) { + createXfaLayerBuilder(pageDiv, pdfPage, annotationStorage = null) { return new XfaLayerBuilder({ pageDiv, pdfPage, + annotationStorage: + annotationStorage || this.pdfDocument?.annotationStorage, }); } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index ee85d48093bda..4cf10b0fcc2bb 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -603,7 +603,8 @@ class PDFPageView { if (!this.xfaLayer) { this.xfaLayer = this.xfaLayerFactory.createXfaLayerBuilder( div, - pdfPage + pdfPage, + /* annotationStorage = */ null ); } this._renderXfaLayer(); diff --git a/web/xfa_layer_builder.js b/web/xfa_layer_builder.js index 1b13a13cb10f3..e7d2ae2ea5c0d 100644 --- a/web/xfa_layer_builder.js +++ b/web/xfa_layer_builder.js @@ -19,15 +19,17 @@ import { XfaLayer } from "pdfjs-lib"; * @typedef {Object} XfaLayerBuilderOptions * @property {HTMLDivElement} pageDiv * @property {PDFPage} pdfPage + * @property {AnnotationStorage} [annotationStorage] */ class XfaLayerBuilder { /** * @param {XfaLayerBuilderOptions} options */ - constructor({ pageDiv, pdfPage }) { + constructor({ pageDiv, pdfPage, annotationStorage }) { this.pageDiv = pageDiv; this.pdfPage = pdfPage; + this.annotationStorage = annotationStorage; this.div = null; this._cancelled = false; @@ -40,29 +42,34 @@ class XfaLayerBuilder { * annotations is complete. */ render(viewport, intent = "display") { - return this.pdfPage.getXfa().then(xfa => { - if (this._cancelled) { - return; - } + return this.pdfPage + .getXfa() + .then(xfa => { + if (this._cancelled) { + return; + } + const parameters = { + viewport: viewport.clone({ dontFlip: true }), + div: this.div, + xfa, + page: this.pdfPage, + annotationStorage: this.annotationStorage, + }; - const parameters = { - viewport: viewport.clone({ dontFlip: true }), - div: this.div, - xfa, - page: this.pdfPage, - }; + if (this.div) { + XfaLayer.update(parameters); + } else { + // Create an xfa layer div and render the form + this.div = document.createElement("div"); + this.pageDiv.appendChild(this.div); + parameters.div = this.div; - if (this.div) { - XfaLayer.update(parameters); - } else { - // Create an xfa layer div and render the form - this.div = document.createElement("div"); - this.pageDiv.appendChild(this.div); - parameters.div = this.div; - - XfaLayer.render(parameters); - } - }); + XfaLayer.render(parameters); + } + }) + .catch(error => { + console.error(error); + }); } cancel() { @@ -84,11 +91,13 @@ class DefaultXfaLayerFactory { /** * @param {HTMLDivElement} pageDiv * @param {PDFPage} pdfPage + * @param {AnnotationStorage} [annotationStorage] */ - createXfaLayerBuilder(pageDiv, pdfPage) { + createXfaLayerBuilder(pageDiv, pdfPage, annotationStorage = null) { return new XfaLayerBuilder({ pageDiv, pdfPage, + annotationStorage, }); } }