diff --git a/src/core/document.js b/src/core/document.js index bbe3fc836..4ac9c2788 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -787,7 +787,8 @@ class PDFDocument { get numPages() { let num = 0; if (this.xfaFactory) { - num = this.xfaFactory.numPages; + // num is a Promise. + num = this.xfaFactory.getNumPages(); } else if (this.linearization) { num = this.linearization.numPages; } else { diff --git a/src/core/xfa/factory.js b/src/core/xfa/factory.js index 6547ab1d8..e2f49c243 100644 --- a/src/core/xfa/factory.js +++ b/src/core/xfa/factory.js @@ -19,6 +19,7 @@ import { $nodeName, $text, $toHTML, + $toPages, } from "./xfa_object.js"; import { Binder } from "./bind.js"; import { DataHandler } from "./data.js"; @@ -45,9 +46,32 @@ class XFAFactory { return this.root && this.form; } - _createPages() { + /** + * In order to avoid to block the event loop, the conversion + * into pages is made asynchronously. + */ + _createPagesHelper() { + const iterator = this.form[$toPages](); + return new Promise((resolve, reject) => { + const nextIteration = () => { + try { + const value = iterator.next(); + if (value.done) { + resolve(value.value); + } else { + setTimeout(nextIteration, 0); + } + } catch (e) { + reject(e); + } + }; + setTimeout(nextIteration, 0); + }); + } + + async _createPages() { try { - this.pages = this.form[$toHTML](); + this.pages = await this._createPagesHelper(); this.dims = this.pages.children.map(c => { const { width, height } = c.attributes.style; return [0, 0, parseInt(width), parseInt(height)]; @@ -61,9 +85,9 @@ class XFAFactory { return this.dims[pageIndex]; } - get numPages() { + async getNumPages() { if (!this.pages) { - this._createPages(); + await this._createPages(); } return this.dims.length; } @@ -94,9 +118,9 @@ class XFAFactory { this.form[$globalData].fontFinder.add(fonts, reallyMissingFonts); } - getPages() { + async getPages() { if (!this.pages) { - this._createPages(); + await this._createPages(); } const pages = this.pages; this.pages = null; diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index c616c54d6..cefb9b8af 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -55,6 +55,7 @@ import { $tabIndex, $text, $toHTML, + $toPages, $toStyle, $uid, ContentObject, @@ -5395,7 +5396,12 @@ class Template extends XFAObject { return searchNode(this, container, expr, true, true); } - [$toHTML]() { + /** + * This function is a generator because the conversion into + * pages is done asynchronously and we want to save the state + * of the function where we were in the previous iteration. + */ + *[$toPages]() { if (!this.subform.children.length) { return HTMLResult.success({ name: "div", @@ -5641,6 +5647,7 @@ class Template extends XFAObject { } } pageArea = targetPageArea || pageArea[$getNextPage](); + yield null; } } } diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js index 785950e8c..b1808fb0f 100644 --- a/src/core/xfa/xfa_object.js +++ b/src/core/xfa/xfa_object.js @@ -84,6 +84,7 @@ const $setSetAttributes = Symbol(); const $setValue = Symbol(); const $tabIndex = Symbol(); const $text = Symbol(); +const $toPages = Symbol(); const $toHTML = Symbol(); const $toString = Symbol(); const $toStyle = Symbol(); @@ -1137,6 +1138,7 @@ export { $tabIndex, $text, $toHTML, + $toPages, $toString, $toStyle, $uid, diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js index da16e4bce..2d9c1bb77 100644 --- a/test/unit/xfa_tohtml_spec.js +++ b/test/unit/xfa_tohtml_spec.js @@ -39,7 +39,7 @@ describe("XFAFactory", function () { } describe("toHTML", function () { - it("should convert some basic properties to CSS", function () { + it("should convert some basic properties to CSS", async () => { const xml = ` @@ -86,9 +86,9 @@ describe("XFAFactory", function () { const factory = new XFAFactory({ "xdp:xdp": xml }); factory.setFonts([]); - expect(factory.numPages).toEqual(2); + expect(await factory.getNumPages()).toEqual(2); - const pages = factory.getPages(); + const pages = await factory.getPages(); const page1 = pages.children[0]; expect(page1.attributes.style).toEqual({ height: "789px", @@ -144,7 +144,7 @@ describe("XFAFactory", function () { ); }); - it("should have an alt attribute from toolTip", function () { + it("should have an alt attribute from toolTip", async () => { if (isNodeJS) { pending("Image is not supported in Node.js."); } @@ -174,15 +174,15 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const field = searchHtmlNode(pages, "name", "img"); expect(field.attributes.alt).toEqual("alt text"); }); - it("should have a aria heading role and level", function () { + it("should have a aria heading role and level", async () => { const xml = ` @@ -208,9 +208,9 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const page1 = pages.children[0]; const wrapper = page1.children[0]; const draw = wrapper.children[0]; @@ -219,7 +219,7 @@ describe("XFAFactory", function () { expect(draw.attributes["aria-level"]).toEqual("2"); }); - it("should have aria table role", function () { + it("should have aria table role", async () => { const xml = ` @@ -263,9 +263,9 @@ describe("XFAFactory", function () { const factory = new XFAFactory({ "xdp:xdp": xml }); factory.setFonts([]); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const table = searchHtmlNode( pages, "xfaName", @@ -303,7 +303,7 @@ describe("XFAFactory", function () { expect(cell.attributes.role).toEqual("cell"); }); - it("should have a maxLength property", function () { + it("should have a maxLength property", async () => { const xml = ` @@ -336,15 +336,15 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const field = searchHtmlNode(pages, "name", "input"); expect(field.attributes.maxLength).toEqual(123); }); - it("should have an aria-label property from speak", function () { + it("should have an aria-label property from speak", async () => { const xml = ` @@ -378,15 +378,15 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const field = searchHtmlNode(pages, "name", "input"); expect(field.attributes["aria-label"]).toEqual("Screen Reader"); }); - it("should have an aria-label property from toolTip", function () { + it("should have an aria-label property from toolTip", async () => { const xml = ` @@ -420,15 +420,15 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const field = searchHtmlNode(pages, "name", "input"); expect(field.attributes["aria-label"]).toEqual("Screen Reader"); }); - it("should have an input or textarea", function () { + it("should have an input or textarea", async () => { const xml = ` @@ -463,9 +463,9 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const field1 = searchHtmlNode(pages, "name", "input"); expect(field1).not.toEqual(null); @@ -474,7 +474,7 @@ describe("XFAFactory", function () { }); }); - it("should have an input or textarea", function () { + it("should have an input or textarea", async () => { const xml = ` @@ -517,15 +517,15 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); const field1 = searchHtmlNode(pages, "name", "input"); expect(field1).not.toEqual(null); expect(field1.attributes.value).toEqual("123"); }); - it("should parse URLs correctly", function () { + it("should parse URLs correctly", async () => { function getXml(href) { return ` @@ -560,38 +560,38 @@ describe("XFAFactory", function () { // A valid, and complete, URL. factory = new XFAFactory({ "xdp:xdp": getXml("https://www.example.com/") }); - expect(factory.numPages).toEqual(1); - pages = factory.getPages(); + expect(await factory.getNumPages()).toEqual(1); + pages = await factory.getPages(); a = searchHtmlNode(pages, "name", "a"); expect(a.value).toEqual("https://www.example.com/"); expect(a.attributes.href).toEqual("https://www.example.com/"); // A valid, but incomplete, URL. factory = new XFAFactory({ "xdp:xdp": getXml("www.example.com/") }); - expect(factory.numPages).toEqual(1); - pages = factory.getPages(); + expect(await factory.getNumPages()).toEqual(1); + pages = await factory.getPages(); a = searchHtmlNode(pages, "name", "a"); expect(a.value).toEqual("www.example.com/"); expect(a.attributes.href).toEqual("http://www.example.com/"); // A valid email-address. factory = new XFAFactory({ "xdp:xdp": getXml("mailto:test@example.com") }); - expect(factory.numPages).toEqual(1); - pages = factory.getPages(); + expect(await factory.getNumPages()).toEqual(1); + pages = await factory.getPages(); a = searchHtmlNode(pages, "name", "a"); expect(a.value).toEqual("mailto:test@example.com"); expect(a.attributes.href).toEqual("mailto:test@example.com"); // Not a valid URL. factory = new XFAFactory({ "xdp:xdp": getXml("qwerty/") }); - expect(factory.numPages).toEqual(1); - pages = factory.getPages(); + expect(await factory.getNumPages()).toEqual(1); + pages = await factory.getPages(); a = searchHtmlNode(pages, "name", "a"); expect(a.value).toEqual("qwerty/"); expect(a.attributes.href).toEqual(""); }); - it("should replace button with an URL by a link", function () { + it("should replace button with an URL by a link", async () => { const xml = ` @@ -635,9 +635,9 @@ describe("XFAFactory", function () { `; const factory = new XFAFactory({ "xdp:xdp": xml }); - expect(factory.numPages).toEqual(1); + expect(await factory.getNumPages()).toEqual(1); - const pages = factory.getPages(); + const pages = await factory.getPages(); let a = searchHtmlNode(pages, "name", "a"); expect(a.attributes.href).toEqual("https://github.com/mozilla/pdf.js"); expect(a.attributes.newWindow).toEqual(true);