Skip to content

XFA - Get each page asynchronously in order to avoid blocking the event loop (#14014) #14240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/core/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
36 changes: 30 additions & 6 deletions src/core/xfa/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
$nodeName,
$text,
$toHTML,
$toPages,
} from "./xfa_object.js";
import { Binder } from "./bind.js";
import { DataHandler } from "./data.js";
Expand All @@ -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)];
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 8 additions & 1 deletion src/core/xfa/template.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
$tabIndex,
$text,
$toHTML,
$toPages,
$toStyle,
$uid,
ContentObject,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -5641,6 +5647,7 @@ class Template extends XFAObject {
}
}
pageArea = targetPageArea || pageArea[$getNextPage]();
yield null;
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/xfa/xfa_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1137,6 +1138,7 @@ export {
$tabIndex,
$text,
$toHTML,
$toPages,
$toString,
$toStyle,
$uid,
Expand Down
78 changes: 39 additions & 39 deletions test/unit/xfa_tohtml_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.");
}
Expand Down Expand Up @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand All @@ -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];
Expand All @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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);

Expand All @@ -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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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 `
<?xml version="1.0"?>
Expand Down Expand Up @@ -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:[email protected]") });
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:[email protected]");
expect(a.attributes.href).toEqual("mailto:[email protected]");

// 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 = `
<?xml version="1.0"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
Expand Down Expand Up @@ -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);
Expand Down