XFA - Add a layer to display XFA forms (#13069)

- add an option to enable XFA rendering if any;
  - for now, let the canvas layer: it could be useful to implement XFAF forms (embedded pdf in xml stream for the background and xfa form for the foreground);
  - ui elements in template DOM are pretty close to their html counterpart so we generate a fake html DOM from template one:
    - it makes easier to translate template properties to html ones;
    - it makes faster the creation of the html element in the main thread.
This commit is contained in:
calixteman 2021-03-19 10:11:40 +01:00 committed by GitHub
parent a164941351
commit 24e598a895
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 760 additions and 27 deletions

View File

@ -15,6 +15,7 @@
import { import {
assert, assert,
bytesToString,
FormatError, FormatError,
info, info,
InvalidPDFException, InvalidPDFException,
@ -28,6 +29,7 @@ import {
shadow, shadow,
stringToBytes, stringToBytes,
stringToPDFString, stringToPDFString,
stringToUTF8String,
unreachable, unreachable,
Util, Util,
warn, warn,
@ -56,6 +58,7 @@ import { calculateMD5 } from "./crypto.js";
import { Linearization } from "./parser.js"; import { Linearization } from "./parser.js";
import { OperatorList } from "./operator_list.js"; import { OperatorList } from "./operator_list.js";
import { PartialEvaluator } from "./evaluator.js"; import { PartialEvaluator } from "./evaluator.js";
import { XFAFactory } from "./xfa/factory.js";
const DEFAULT_USER_UNIT = 1.0; const DEFAULT_USER_UNIT = 1.0;
const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792]; const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
@ -79,6 +82,7 @@ class Page {
builtInCMapCache, builtInCMapCache,
globalImageCache, globalImageCache,
nonBlendModesSet, nonBlendModesSet,
xfaFactory,
}) { }) {
this.pdfManager = pdfManager; this.pdfManager = pdfManager;
this.pageIndex = pageIndex; this.pageIndex = pageIndex;
@ -91,6 +95,7 @@ class Page {
this.nonBlendModesSet = nonBlendModesSet; this.nonBlendModesSet = nonBlendModesSet;
this.evaluatorOptions = pdfManager.evaluatorOptions; this.evaluatorOptions = pdfManager.evaluatorOptions;
this.resourcesPromise = null; this.resourcesPromise = null;
this.xfaFactory = xfaFactory;
const idCounters = { const idCounters = {
obj: 0, obj: 0,
@ -137,6 +142,11 @@ class Page {
} }
_getBoundingBox(name) { _getBoundingBox(name) {
if (this.xfaData) {
const { width, height } = this.xfaData.attributes.style;
return [0, 0, parseInt(width), parseInt(height)];
}
const box = this._getInheritableProperty(name, /* getArray = */ true); const box = this._getInheritableProperty(name, /* getArray = */ true);
if (Array.isArray(box) && box.length === 4) { if (Array.isArray(box) && box.length === 4) {
@ -231,6 +241,13 @@ class Page {
return stream; return stream;
} }
get xfaData() {
if (this.xfaFactory) {
return shadow(this, "xfaData", this.xfaFactory.getPage(this.pageIndex));
}
return shadow(this, "xfaData", null);
}
save(handler, task, annotationStorage) { save(handler, task, annotationStorage) {
const partialEvaluator = new PartialEvaluator({ const partialEvaluator = new PartialEvaluator({
xref: this.xref, xref: this.xref,
@ -695,6 +712,9 @@ class PDFDocument {
} }
get numPages() { get numPages() {
if (this.xfaFactory) {
return shadow(this, "numPages", this.xfaFactory.numberPages);
}
const linearization = this.linearization; const linearization = this.linearization;
const num = linearization ? linearization.numPages : this.catalog.numPages; const num = linearization ? linearization.numPages : this.catalog.numPages;
return shadow(this, "numPages", num); return shadow(this, "numPages", num);
@ -732,6 +752,80 @@ class PDFDocument {
}); });
} }
get xfaData() {
const acroForm = this.catalog.acroForm;
if (!acroForm) {
return null;
}
const xfa = acroForm.get("XFA");
const entries = {
"xdp:xdp": "",
template: "",
datasets: "",
config: "",
connectionSet: "",
localeSet: "",
stylesheet: "",
"/xdp:xdp": "",
};
if (isStream(xfa) && !xfa.isEmpty) {
try {
entries["xdp:xdp"] = stringToUTF8String(bytesToString(xfa.getBytes()));
return entries;
} catch (_) {
warn("XFA - Invalid utf-8 string.");
return null;
}
}
if (!Array.isArray(xfa) || xfa.length === 0) {
return null;
}
for (let i = 0, ii = xfa.length; i < ii; i += 2) {
let name;
if (i === 0) {
name = "xdp:xdp";
} else if (i === ii - 2) {
name = "/xdp:xdp";
} else {
name = xfa[i];
}
if (!entries.hasOwnProperty(name)) {
continue;
}
const data = this.xref.fetchIfRef(xfa[i + 1]);
if (!isStream(data) || data.isEmpty) {
continue;
}
try {
entries[name] = stringToUTF8String(bytesToString(data.getBytes()));
} catch (_) {
warn("XFA - Invalid utf-8 string.");
return null;
}
}
return entries;
}
get xfaFactory() {
if (
this.pdfManager.enableXfa &&
this.formInfo.hasXfa &&
!this.formInfo.hasAcroForm
) {
const data = this.xfaData;
return shadow(this, "xfaFactory", data ? new XFAFactory(data) : null);
}
return shadow(this, "xfaFaxtory", null);
}
get isPureXfa() {
return this.xfaFactory !== null;
}
get formInfo() { get formInfo() {
const formInfo = { hasFields: false, hasAcroForm: false, hasXfa: false }; const formInfo = { hasFields: false, hasAcroForm: false, hasXfa: false };
const acroForm = this.catalog.acroForm; const acroForm = this.catalog.acroForm;
@ -918,6 +1012,24 @@ class PDFDocument {
} }
const { catalog, linearization } = this; const { catalog, linearization } = this;
if (this.xfaFactory) {
return Promise.resolve(
new Page({
pdfManager: this.pdfManager,
xref: this.xref,
pageIndex,
pageDict: Dict.empty,
ref: null,
globalIdFactory: this._globalIdFactory,
fontCache: catalog.fontCache,
builtInCMapCache: catalog.builtInCMapCache,
globalImageCache: catalog.globalImageCache,
nonBlendModesSet: catalog.nonBlendModesSet,
xfaFactory: this.xfaFactory,
})
);
}
const promise = const promise =
linearization && linearization.pageFirst === pageIndex linearization && linearization.pageFirst === pageIndex
? this._getLinearizationPage(pageIndex) ? this._getLinearizationPage(pageIndex)
@ -935,6 +1047,7 @@ class PDFDocument {
builtInCMapCache: catalog.builtInCMapCache, builtInCMapCache: catalog.builtInCMapCache,
globalImageCache: catalog.globalImageCache, globalImageCache: catalog.globalImageCache,
nonBlendModesSet: catalog.nonBlendModesSet, nonBlendModesSet: catalog.nonBlendModesSet,
xfaFactory: null,
}); });
})); }));
} }

View File

@ -106,13 +106,14 @@ class BasePdfManager {
} }
class LocalPdfManager extends BasePdfManager { class LocalPdfManager extends BasePdfManager {
constructor(docId, data, password, evaluatorOptions, docBaseUrl) { constructor(docId, data, password, evaluatorOptions, enableXfa, docBaseUrl) {
super(); super();
this._docId = docId; this._docId = docId;
this._password = password; this._password = password;
this._docBaseUrl = docBaseUrl; this._docBaseUrl = docBaseUrl;
this.evaluatorOptions = evaluatorOptions; this.evaluatorOptions = evaluatorOptions;
this.enableXfa = enableXfa;
const stream = new Stream(data); const stream = new Stream(data);
this.pdfDocument = new PDFDocument(this, stream); this.pdfDocument = new PDFDocument(this, stream);
@ -141,7 +142,14 @@ class LocalPdfManager extends BasePdfManager {
} }
class NetworkPdfManager extends BasePdfManager { class NetworkPdfManager extends BasePdfManager {
constructor(docId, pdfNetworkStream, args, evaluatorOptions, docBaseUrl) { constructor(
docId,
pdfNetworkStream,
args,
evaluatorOptions,
enableXfa,
docBaseUrl
) {
super(); super();
this._docId = docId; this._docId = docId;
@ -149,6 +157,7 @@ class NetworkPdfManager extends BasePdfManager {
this._docBaseUrl = docBaseUrl; this._docBaseUrl = docBaseUrl;
this.msgHandler = args.msgHandler; this.msgHandler = args.msgHandler;
this.evaluatorOptions = evaluatorOptions; this.evaluatorOptions = evaluatorOptions;
this.enableXfa = enableXfa;
this.streamManager = new ChunkedStreamManager(pdfNetworkStream, { this.streamManager = new ChunkedStreamManager(pdfNetworkStream, {
msgHandler: args.msgHandler, msgHandler: args.msgHandler,

View File

@ -188,14 +188,15 @@ class WorkerMessageHandler {
await pdfManager.ensureDoc("checkFirstPage"); await pdfManager.ensureDoc("checkFirstPage");
} }
const [numPages, fingerprint] = await Promise.all([ const [numPages, fingerprint, isPureXfa] = await Promise.all([
pdfManager.ensureDoc("numPages"), pdfManager.ensureDoc("numPages"),
pdfManager.ensureDoc("fingerprint"), pdfManager.ensureDoc("fingerprint"),
pdfManager.ensureDoc("isPureXfa"),
]); ]);
return { numPages, fingerprint }; return { numPages, fingerprint, isPureXfa };
} }
function getPdfManager(data, evaluatorOptions) { function getPdfManager(data, evaluatorOptions, enableXfa) {
var pdfManagerCapability = createPromiseCapability(); var pdfManagerCapability = createPromiseCapability();
let newPdfManager; let newPdfManager;
@ -207,6 +208,7 @@ class WorkerMessageHandler {
source.data, source.data,
source.password, source.password,
evaluatorOptions, evaluatorOptions,
enableXfa,
docBaseUrl docBaseUrl
); );
pdfManagerCapability.resolve(newPdfManager); pdfManagerCapability.resolve(newPdfManager);
@ -246,6 +248,7 @@ class WorkerMessageHandler {
rangeChunkSize: source.rangeChunkSize, rangeChunkSize: source.rangeChunkSize,
}, },
evaluatorOptions, evaluatorOptions,
enableXfa,
docBaseUrl docBaseUrl
); );
// There may be a chance that `newPdfManager` is not initialized for // There may be a chance that `newPdfManager` is not initialized for
@ -277,6 +280,7 @@ class WorkerMessageHandler {
pdfFile, pdfFile,
source.password, source.password,
evaluatorOptions, evaluatorOptions,
enableXfa,
docBaseUrl docBaseUrl
); );
pdfManagerCapability.resolve(newPdfManager); pdfManagerCapability.resolve(newPdfManager);
@ -399,7 +403,7 @@ class WorkerMessageHandler {
fontExtraProperties: data.fontExtraProperties, fontExtraProperties: data.fontExtraProperties,
}; };
getPdfManager(data, evaluatorOptions) getPdfManager(data, evaluatorOptions, data.enableXfa)
.then(function (newPdfManager) { .then(function (newPdfManager) {
if (terminated) { if (terminated) {
// We were in a process of setting up the manager, but it got // We were in a process of setting up the manager, but it got
@ -487,6 +491,16 @@ class WorkerMessageHandler {
}); });
}); });
handler.on("GetPageXfa", function wphSetupGetXfa({ pageIndex }) {
return pdfManager.getPage(pageIndex).then(function (page) {
return pdfManager.ensure(page, "xfaData");
});
});
handler.on("GetIsPureXfa", function wphSetupGetIsPureXfa(data) {
return pdfManager.ensureDoc("isPureXfa");
});
handler.on("GetOutline", function wphSetupGetOutline(data) { handler.on("GetOutline", function wphSetupGetOutline(data) {
return pdfManager.ensureCatalog("documentOutline"); return pdfManager.ensureCatalog("documentOutline");
}); });

View File

@ -13,13 +13,27 @@
* limitations under the License. * limitations under the License.
*/ */
import { $toHTML } from "./xfa_object.js";
import { Binder } from "./bind.js"; import { Binder } from "./bind.js";
import { XFAParser } from "./parser.js"; import { XFAParser } from "./parser.js";
class XFAFactory { class XFAFactory {
constructor(data) { constructor(data) {
this.root = new XFAParser().parse(XFAFactory._createDocument(data)); try {
this.form = new Binder(this.root).bind(); this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
this.pages = this.form[$toHTML]();
} catch (e) {
console.log(e);
}
}
getPage(pageIndex) {
return this.pages.children[pageIndex];
}
get numberPages() {
return this.pages.children.length;
} }
static _createDocument(data) { static _createDocument(data) {

View File

@ -0,0 +1,69 @@
/* 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.
*/
const converters = {
pt: x => x,
cm: x => Math.round((x / 2.54) * 72),
mm: x => Math.round((x / (10 * 2.54)) * 72),
in: x => Math.round(x * 72),
};
function measureToString(m) {
const conv = converters[m.unit];
if (conv) {
return `${conv(m.value)}px`;
}
return `${m.value}${m.unit}`;
}
function setWidthHeight(node, style) {
if (node.w) {
style.width = measureToString(node.w);
} else {
if (node.maxW && node.maxW.value > 0) {
style.maxWidth = measureToString(node.maxW);
}
if (node.minW && node.minW.value > 0) {
style.minWidth = measureToString(node.minW);
}
}
if (node.h) {
style.height = measureToString(node.h);
} else {
if (node.maxH && node.maxH.value > 0) {
style.maxHeight = measureToString(node.maxH);
}
if (node.minH && node.minH.value > 0) {
style.minHeight = measureToString(node.minH);
}
}
}
function setPosition(node, style) {
style.transform = "";
if (node.rotate) {
style.transform = `rotate(-${node.rotate}deg) `;
style.transformOrigin = "top left";
}
if (node.x !== "" || node.y !== "") {
style.position = "absolute";
style.left = node.x ? measureToString(node.x) : "0pt";
style.top = node.y ? measureToString(node.y) : "0pt";
}
}
export { measureToString, setPosition, setWidthHeight };

View File

@ -15,8 +15,11 @@
import { import {
$appendChild, $appendChild,
$childrenToHTML,
$content, $content,
$extra,
$finalize, $finalize,
$getParent,
$hasItem, $hasItem,
$hasSettableValue, $hasSettableValue,
$isTransparent, $isTransparent,
@ -26,6 +29,8 @@ import {
$removeChild, $removeChild,
$setSetAttributes, $setSetAttributes,
$setValue, $setValue,
$toHTML,
$uid,
ContentObject, ContentObject,
Option01, Option01,
OptionObject, OptionObject,
@ -45,6 +50,7 @@ import {
getRelevant, getRelevant,
getStringOption, getStringOption,
} from "./utils.js"; } from "./utils.js";
import { measureToString, setPosition, setWidthHeight } from "./html_utils.js";
import { warn } from "../../shared/util.js"; import { warn } from "../../shared/util.js";
const TEMPLATE_NS_ID = NamespaceIds.template.id; const TEMPLATE_NS_ID = NamespaceIds.template.id;
@ -656,6 +662,29 @@ class ContentArea extends XFAObject {
this.desc = null; this.desc = null;
this.extras = null; this.extras = null;
} }
[$toHTML]() {
// TODO: incomplete.
const left = measureToString(this.x);
const top = measureToString(this.y);
const style = {
position: "absolute",
left,
top,
width: measureToString(this.w),
height: measureToString(this.h),
};
return {
name: "div",
children: [],
attributes: {
style,
className: "xfa-contentarea",
id: this[$uid],
},
};
}
} }
class Corner extends XFAObject { class Corner extends XFAObject {
@ -1946,6 +1975,41 @@ class PageArea extends XFAObject {
this.field = new XFAObjectArray(); this.field = new XFAObjectArray();
this.subform = new XFAObjectArray(); this.subform = new XFAObjectArray();
} }
[$toHTML]() {
// TODO: incomplete.
if (this.contentArea.children.length === 0) {
return null;
}
const children = this[$childrenToHTML]({
filter: new Set(["area", "draw", "field", "subform", "contentArea"]),
include: true,
});
// TODO: handle the case where there are several content areas.
const contentArea = children.find(
node => node.attributes.className === "xfa-contentarea"
);
const style = Object.create(null);
if (this.medium && this.medium.short.value && this.medium.long.value) {
style.width = measureToString(this.medium.short);
style.height = measureToString(this.medium.long);
} else {
// TODO: compute it from contentAreas
}
return {
name: "div",
children,
attributes: {
id: this[$uid],
style,
},
contentArea,
};
}
} }
class PageSet extends XFAObject { class PageSet extends XFAObject {
@ -1970,6 +2034,20 @@ class PageSet extends XFAObject {
this.pageArea = new XFAObjectArray(); this.pageArea = new XFAObjectArray();
this.pageSet = new XFAObjectArray(); this.pageSet = new XFAObjectArray();
} }
[$toHTML]() {
// TODO: incomplete.
return {
name: "div",
children: this[$childrenToHTML]({
filter: new Set(["pageArea", "pageSet"]),
include: true,
}),
attributes: {
id: this[$uid],
},
};
}
} }
class Para extends XFAObject { class Para extends XFAObject {
@ -2465,6 +2543,64 @@ class Subform extends XFAObject {
this.subform = new XFAObjectArray(); this.subform = new XFAObjectArray();
this.subformSet = new XFAObjectArray(); this.subformSet = new XFAObjectArray();
} }
[$toHTML]() {
// TODO: incomplete.
this[$extra] = Object.create(null);
const parent = this[$getParent]();
let page = null;
if (parent[$nodeName] === "template") {
// Root subform: should have page info.
if (this.pageSet !== null) {
this[$extra].pageNumber = 0;
} else {
// TODO
warn("XFA - No pageSet in root subform");
}
} else if (parent[$extra] && parent[$extra].pageNumber !== undefined) {
// This subform is a child of root subform
// so push it in a new page.
const pageNumber = parent[$extra].pageNumber;
const pageAreas = parent.pageSet.pageArea.children;
parent[$extra].pageNumber =
(parent[$extra].pageNumber + 1) % pageAreas.length;
page = pageAreas[pageNumber][$toHTML]();
}
const style = Object.create(null);
setWidthHeight(this, style);
setPosition(this, style);
const attributes = {
style,
id: this[$uid],
};
if (this.name) {
attributes["xfa-name"] = this.name;
}
const children = this[$childrenToHTML]({
// TODO: exObject & exclGroup
filter: new Set(["area", "draw", "field", "subform", "subformSet"]),
include: true,
});
const html = {
name: "div",
attributes,
children,
};
if (page) {
page.contentArea.children.push(html);
delete page.contentArea;
return page;
}
return html;
}
} }
class SubformSet extends XFAObject { class SubformSet extends XFAObject {
@ -2580,8 +2716,32 @@ class Template extends XFAObject {
"interactiveForms", "interactiveForms",
]); ]);
this.extras = null; this.extras = null;
// Spec is unclear:
// A container element that describes a single subform capable of
// enclosing other containers.
// Can we have more than one subform ?
this.subform = new XFAObjectArray(); this.subform = new XFAObjectArray();
} }
[$finalize]() {
if (this.subform.children.length === 0) {
warn("XFA - No subforms in template node.");
}
if (this.subform.children.length >= 2) {
warn("XFA - Several subforms in template node: please file a bug.");
}
}
[$toHTML]() {
if (this.subform.children.length > 0) {
return this.subform.children[0][$toHTML]();
}
return {
name: "div",
children: [],
};
}
} }
class Text extends ContentObject { class Text extends ContentObject {

View File

@ -74,7 +74,7 @@ function getMeasurement(str, def = "0") {
} }
return { return {
value: sign === "-" ? -value : value, value: sign === "-" ? -value : value,
unit, unit: unit || "pt",
}; };
} }

View File

@ -20,6 +20,7 @@ import { NamespaceIds } from "./namespaces.js";
// We use these symbols to avoid name conflict between tags // We use these symbols to avoid name conflict between tags
// and properties/methods names. // and properties/methods names.
const $appendChild = Symbol(); const $appendChild = Symbol();
const $childrenToHTML = Symbol();
const $clean = Symbol(); const $clean = Symbol();
const $cleanup = Symbol(); const $cleanup = Symbol();
const $clone = Symbol(); const $clone = Symbol();
@ -27,6 +28,7 @@ const $consumed = Symbol();
const $content = Symbol("content"); const $content = Symbol("content");
const $data = Symbol("data"); const $data = Symbol("data");
const $dump = Symbol(); const $dump = Symbol();
const $extra = Symbol("extra");
const $finalize = Symbol(); const $finalize = Symbol();
const $getAttributeIt = Symbol(); const $getAttributeIt = Symbol();
const $getChildrenByClass = Symbol(); const $getChildrenByClass = Symbol();
@ -56,6 +58,8 @@ const $setId = Symbol();
const $setSetAttributes = Symbol(); const $setSetAttributes = Symbol();
const $setValue = Symbol(); const $setValue = Symbol();
const $text = Symbol(); const $text = Symbol();
const $toHTML = Symbol();
const $uid = Symbol("uid");
const _applyPrototype = Symbol(); const _applyPrototype = Symbol();
const _attributes = Symbol(); const _attributes = Symbol();
@ -73,6 +77,8 @@ const _parent = Symbol("parent");
const _setAttributes = Symbol(); const _setAttributes = Symbol();
const _validator = Symbol(); const _validator = Symbol();
let uid = 0;
class XFAObject { class XFAObject {
constructor(nsId, name, hasChildren = false) { constructor(nsId, name, hasChildren = false) {
this[$namespaceId] = nsId; this[$namespaceId] = nsId;
@ -80,6 +86,7 @@ class XFAObject {
this[_hasChildren] = hasChildren; this[_hasChildren] = hasChildren;
this[_parent] = null; this[_parent] = null;
this[_children] = []; this[_children] = [];
this[$uid] = `${name}${uid++}`;
} }
[$onChild](child) { [$onChild](child) {
@ -252,6 +259,23 @@ class XFAObject {
return dumped; return dumped;
} }
[$toHTML]() {
return null;
}
[$childrenToHTML]({ filter = null, include = true }) {
const res = [];
this[$getChildren]().forEach(node => {
if (!filter || include === filter.has(node[$nodeName])) {
const html = node[$toHTML]();
if (html) {
res.push(html);
}
}
});
return res;
}
[$setSetAttributes](attributes) { [$setSetAttributes](attributes) {
if (attributes.use || attributes.id) { if (attributes.use || attributes.id) {
// Just keep set attributes because this node uses a proto or is a proto. // Just keep set attributes because this node uses a proto or is a proto.
@ -604,6 +628,17 @@ class XmlObject extends XFAObject {
} }
} }
[$toHTML]() {
if (this[$nodeName] === "#text") {
return {
name: "#text",
value: this[$content],
};
}
return null;
}
[$getChildren](name = null) { [$getChildren](name = null) {
if (!name) { if (!name) {
return this[_children]; return this[_children];
@ -766,6 +801,7 @@ class Option10 extends IntegerObject {
export { export {
$appendChild, $appendChild,
$childrenToHTML,
$clean, $clean,
$cleanup, $cleanup,
$clone, $clone,
@ -773,6 +809,7 @@ export {
$content, $content,
$data, $data,
$dump, $dump,
$extra,
$finalize, $finalize,
$getAttributeIt, $getAttributeIt,
$getChildren, $getChildren,
@ -801,6 +838,8 @@ export {
$setSetAttributes, $setSetAttributes,
$setValue, $setValue,
$text, $text,
$toHTML,
$uid,
ContentObject, ContentObject,
IntegerObject, IntegerObject,
Option01, Option01,

View File

@ -162,6 +162,8 @@ function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) {
* parsed font data from the worker-thread. This may be useful for debugging * parsed font data from the worker-thread. This may be useful for debugging
* purposes (and backwards compatibility), but note that it will lead to * purposes (and backwards compatibility), but note that it will lead to
* increased memory usage. The default value is `false`. * increased memory usage. The default value is `false`.
* @property {boolean} [enableXfa] - Render Xfa forms if any.
* The default value is `false`.
* @property {HTMLDocument} [ownerDocument] - Specify an explicit document * @property {HTMLDocument} [ownerDocument] - Specify an explicit document
* context to create elements with and to load resources, such as fonts, * context to create elements with and to load resources, such as fonts,
* into. Defaults to the current document. * into. Defaults to the current document.
@ -284,6 +286,7 @@ function getDocument(src) {
params.ignoreErrors = params.stopAtErrors !== true; params.ignoreErrors = params.stopAtErrors !== true;
params.fontExtraProperties = params.fontExtraProperties === true; params.fontExtraProperties = params.fontExtraProperties === true;
params.pdfBug = params.pdfBug === true; params.pdfBug = params.pdfBug === true;
params.enableXfa = params.enableXfa === true;
if (!Number.isInteger(params.maxImageSize)) { if (!Number.isInteger(params.maxImageSize)) {
params.maxImageSize = -1; params.maxImageSize = -1;
@ -438,6 +441,7 @@ function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
ignoreErrors: source.ignoreErrors, ignoreErrors: source.ignoreErrors,
isEvalSupported: source.isEvalSupported, isEvalSupported: source.isEvalSupported,
fontExtraProperties: source.fontExtraProperties, fontExtraProperties: source.fontExtraProperties,
enableXfa: source.enableXfa,
}) })
.then(function (workerId) { .then(function (workerId) {
if (worker.destroyed) { if (worker.destroyed) {
@ -674,6 +678,13 @@ class PDFDocumentProxy {
return this._pdfInfo.fingerprint; return this._pdfInfo.fingerprint;
} }
/**
* @type {boolean} True if only XFA form.
*/
get isPureXfa() {
return this._pdfInfo.isPureXfa;
}
/** /**
* @param {number} pageNumber - The page number to get. The first page is 1. * @param {number} pageNumber - The page number to get. The first page is 1.
* @returns {Promise<PDFPageProxy>} A promise that is resolved with * @returns {Promise<PDFPageProxy>} A promise that is resolved with
@ -1165,6 +1176,16 @@ class PDFPageProxy {
)); ));
} }
/**
* @returns {Promise<Object | null>} A promise that is resolved with
* an {Object} with a fake DOM object (a tree structure where elements
* 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));
}
/** /**
* Begins the process of rendering a page to the desired context. * Begins the process of rendering a page to the desired context.
* *
@ -2709,6 +2730,12 @@ class WorkerTransport {
}); });
} }
getPageXfa(pageIndex) {
return this.messageHandler.sendWithPromise("GetPageXfa", {
pageIndex,
});
}
getOutline() { getOutline() {
return this.messageHandler.sendWithPromise("GetOutline", null); return this.messageHandler.sendWithPromise("GetOutline", null);
} }

89
src/display/xfa_layer.js Normal file
View File

@ -0,0 +1,89 @@
/* 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.
*/
class XfaLayer {
static setAttributes(html, attrs) {
for (const [key, value] of Object.entries(attrs)) {
if (value === null || value === undefined) {
continue;
}
if (key !== "style") {
html.setAttribute(key, value);
} else {
Object.assign(html.style, value);
}
}
}
static render(parameters) {
const root = parameters.xfa;
const rootHtml = document.createElement(root.name);
if (root.attributes) {
XfaLayer.setAttributes(rootHtml, root.attributes);
}
const stack = [[root, -1, rootHtml]];
parameters.div.appendChild(rootHtml);
const coeffs = parameters.viewport.transform.join(",");
parameters.div.style.transform = `matrix(${coeffs})`;
while (stack.length > 0) {
const [parent, i, html] = stack[stack.length - 1];
if (i + 1 === parent.children.length) {
stack.pop();
continue;
}
const child = parent.children[++stack[stack.length - 1][1]];
if (child === null) {
continue;
}
const { name } = child;
if (name === "#text") {
html.appendChild(document.createTextNode(child.value));
continue;
}
const childHtml = document.createElement(name);
html.appendChild(childHtml);
if (child.attributes) {
XfaLayer.setAttributes(childHtml, child.attributes);
}
if (child.children && child.children.length > 0) {
stack.push([child, -1, childHtml]);
} else if (child.value) {
childHtml.appendChild(document.createTextNode(child.value));
}
}
}
/**
* Update the xfa layer.
*
* @public
* @param {XfaLayerParameters} parameters
* @memberof XfaLayer
*/
static update(parameters) {
const transform = `matrix(${parameters.viewport.transform.join(",")})`;
parameters.div.style.transform = transform;
parameters.div.hidden = false;
}
}
export { XfaLayer };

View File

@ -56,6 +56,7 @@ import { apiCompatibilityParams } from "./display/api_compatibility.js";
import { GlobalWorkerOptions } from "./display/worker_options.js"; import { GlobalWorkerOptions } from "./display/worker_options.js";
import { renderTextLayer } from "./display/text_layer.js"; import { renderTextLayer } from "./display/text_layer.js";
import { SVGGraphics } from "./display/svg.js"; import { SVGGraphics } from "./display/svg.js";
import { XfaLayer } from "./display/xfa_layer.js";
/* eslint-disable-next-line no-unused-vars */ /* eslint-disable-next-line no-unused-vars */
const pdfjsVersion = const pdfjsVersion =
@ -167,4 +168,6 @@ export {
renderTextLayer, renderTextLayer,
// From "./display/svg.js": // From "./display/svg.js":
SVGGraphics, SVGGraphics,
// From "./display/xfa_layer.js":
XfaLayer,
}; };

View File

@ -81,9 +81,9 @@ describe("XFAParser", function () {
}; };
const mediumAttributes = { const mediumAttributes = {
id: "", id: "",
long: { value: 0, unit: "" }, long: { value: 0, unit: "pt" },
orientation: "portrait", orientation: "portrait",
short: { value: 0, unit: "" }, short: { value: 0, unit: "pt" },
stock: "", stock: "",
trayIn: "auto", trayIn: "auto",
trayOut: "auto", trayOut: "auto",
@ -116,17 +116,17 @@ describe("XFAParser", function () {
allowMacro: 0, allowMacro: 0,
anchorType: "topLeft", anchorType: "topLeft",
colSpan: 1, colSpan: 1,
columnWidths: [{ value: 0, unit: "" }], columnWidths: [{ value: 0, unit: "pt" }],
h: { value: 0, unit: "" }, h: { value: 0, unit: "pt" },
hAlign: "left", hAlign: "left",
id: "", id: "",
layout: "position", layout: "position",
locale: "", locale: "",
maxH: { value: 0, unit: "" }, maxH: { value: 0, unit: "pt" },
maxW: { value: 0, unit: "" }, maxW: { value: 0, unit: "pt" },
mergeMode: "consumeData", mergeMode: "consumeData",
minH: { value: 0, unit: "" }, minH: { value: 0, unit: "pt" },
minW: { value: 0, unit: "" }, minW: { value: 0, unit: "pt" },
name: "", name: "",
presence: "visible", presence: "visible",
relevant: [], relevant: [],
@ -134,14 +134,14 @@ describe("XFAParser", function () {
scope: "name", scope: "name",
use: "", use: "",
usehref: "", usehref: "",
w: { value: 0, unit: "" }, w: { value: 0, unit: "pt" },
x: { value: 0, unit: "" }, x: { value: 0, unit: "pt" },
y: { value: 0, unit: "" }, y: { value: 0, unit: "pt" },
proto: { proto: {
area: { area: {
...attributes, ...attributes,
colSpan: 1, colSpan: 1,
x: { value: 0, unit: "" }, x: { value: 0, unit: "pt" },
y: { value: -3.14, unit: "in" }, y: { value: -3.14, unit: "in" },
relevant: [ relevant: [
{ excluded: true, viewname: "foo" }, { excluded: true, viewname: "foo" },
@ -162,7 +162,7 @@ describe("XFAParser", function () {
{ {
...mediumAttributes, ...mediumAttributes,
imagingBBox: { imagingBBox: {
x: { value: 1, unit: "" }, x: { value: 1, unit: "pt" },
y: { value: 2, unit: "in" }, y: { value: 2, unit: "in" },
width: { value: 3.4, unit: "cm" }, width: { value: 3.4, unit: "cm" },
height: { value: 5.67, unit: "px" }, height: { value: 5.67, unit: "px" },
@ -171,10 +171,10 @@ describe("XFAParser", function () {
{ {
...mediumAttributes, ...mediumAttributes,
imagingBBox: { imagingBBox: {
x: { value: -1, unit: "" }, x: { value: -1, unit: "pt" },
y: { value: -1, unit: "" }, y: { value: -1, unit: "pt" },
width: { value: -1, unit: "" }, width: { value: -1, unit: "pt" },
height: { value: -1, unit: "" }, height: { value: -1, unit: "pt" },
}, },
}, },
], ],

View File

@ -518,6 +518,7 @@ const PDFViewerApplication = {
useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
enableScripting: AppOptions.get("enableScripting"), enableScripting: AppOptions.get("enableScripting"),
enableXfa: AppOptions.get("enableXfa"),
}); });
pdfRenderingQueue.setViewer(this.pdfViewer); pdfRenderingQueue.setViewer(this.pdfViewer);
pdfLinkService.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer);

View File

@ -205,6 +205,11 @@ const defaultOptions = {
value: "", value: "",
kind: OptionKind.API, kind: OptionKind.API,
}, },
enableXfa: {
/** @type {boolean} */
value: false,
kind: OptionKind.API,
},
fontExtraProperties: { fontExtraProperties: {
/** @type {boolean} */ /** @type {boolean} */
value: false, value: false,

View File

@ -42,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
import { PDFPageView } from "./pdf_page_view.js"; import { PDFPageView } from "./pdf_page_view.js";
import { SimpleLinkService } from "./pdf_link_service.js"; import { SimpleLinkService } from "./pdf_link_service.js";
import { TextLayerBuilder } from "./text_layer_builder.js"; import { TextLayerBuilder } from "./text_layer_builder.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
const DEFAULT_CACHE_SIZE = 10; const DEFAULT_CACHE_SIZE = 10;
@ -478,6 +479,7 @@ class BaseViewer {
if (!pdfDocument) { if (!pdfDocument) {
return; return;
} }
const isPureXfa = pdfDocument.isPureXfa;
const pagesCount = pdfDocument.numPages; const pagesCount = pdfDocument.numPages;
const firstPagePromise = pdfDocument.getPage(1); const firstPagePromise = pdfDocument.getPage(1);
// Rendering (potentially) depends on this, hence fetching it immediately. // Rendering (potentially) depends on this, hence fetching it immediately.
@ -523,6 +525,7 @@ class BaseViewer {
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS }); const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
const textLayerFactory = const textLayerFactory =
this.textLayerMode !== TextLayerMode.DISABLE ? this : null; this.textLayerMode !== TextLayerMode.DISABLE ? this : null;
const xfaLayerFactory = isPureXfa ? this : null;
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) { for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
const pageView = new PDFPageView({ const pageView = new PDFPageView({
@ -536,6 +539,7 @@ class BaseViewer {
textLayerFactory, textLayerFactory,
textLayerMode: this.textLayerMode, textLayerMode: this.textLayerMode,
annotationLayerFactory: this, annotationLayerFactory: this,
xfaLayerFactory,
imageResourcesPath: this.imageResourcesPath, imageResourcesPath: this.imageResourcesPath,
renderInteractiveForms: this.renderInteractiveForms, renderInteractiveForms: this.renderInteractiveForms,
renderer: this.renderer, renderer: this.renderer,
@ -1308,6 +1312,18 @@ class BaseViewer {
}); });
} }
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPage} pdfPage
* @returns {XfaLayerBuilder}
*/
createXfaLayerBuilder(pageDiv, pdfPage) {
return new XfaLayerBuilder({
pageDiv,
pdfPage,
});
}
/** /**
* @type {boolean} Whether all pages of the PDF document have identical * @type {boolean} Whether all pages of the PDF document have identical
* widths and heights. * widths and heights.

View File

@ -204,6 +204,18 @@ class IPDFAnnotationLayerFactory {
) {} ) {}
} }
/**
* @interface
*/
class IPDFXfaLayerFactory {
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPage} pdfPage
* @returns {XfaLayerBuilder}
*/
createXfaLayerBuilder(pageDiv, pdfPage) {}
}
/** /**
* @interface * @interface
*/ */
@ -243,5 +255,6 @@ export {
IPDFHistory, IPDFHistory,
IPDFLinkService, IPDFLinkService,
IPDFTextLayerFactory, IPDFTextLayerFactory,
IPDFXfaLayerFactory,
IRenderableView, IRenderableView,
}; };

View File

@ -48,6 +48,7 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js";
* behaviour is enabled. The constants from {TextLayerMode} should be used. * behaviour is enabled. The constants from {TextLayerMode} should be used.
* The default value is `TextLayerMode.ENABLE`. * The default value is `TextLayerMode.ENABLE`.
* @property {IPDFAnnotationLayerFactory} annotationLayerFactory * @property {IPDFAnnotationLayerFactory} annotationLayerFactory
* @property {IPDFXfaLayerFactory} xfaLayerFactory
* @property {string} [imageResourcesPath] - Path for image resources, mainly * @property {string} [imageResourcesPath] - Path for image resources, mainly
* for annotation icons. Include trailing slash. * for annotation icons. Include trailing slash.
* @property {boolean} renderInteractiveForms - Turns on rendering of * @property {boolean} renderInteractiveForms - Turns on rendering of
@ -102,6 +103,7 @@ class PDFPageView {
this.renderingQueue = options.renderingQueue; this.renderingQueue = options.renderingQueue;
this.textLayerFactory = options.textLayerFactory; this.textLayerFactory = options.textLayerFactory;
this.annotationLayerFactory = options.annotationLayerFactory; this.annotationLayerFactory = options.annotationLayerFactory;
this.xfaLayerFactory = options.xfaLayerFactory;
this.renderer = options.renderer || RendererType.CANVAS; this.renderer = options.renderer || RendererType.CANVAS;
this.enableWebGL = options.enableWebGL || false; this.enableWebGL = options.enableWebGL || false;
this.l10n = options.l10n || NullL10n; this.l10n = options.l10n || NullL10n;
@ -116,6 +118,7 @@ class PDFPageView {
this.annotationLayer = null; this.annotationLayer = null;
this.textLayer = null; this.textLayer = null;
this.zoomLayer = null; this.zoomLayer = null;
this.xfaLayer = null;
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "page"; div.className = "page";
@ -164,6 +167,24 @@ class PDFPageView {
} }
} }
/**
* @private
*/
async _renderXfaLayer() {
let error = null;
try {
await this.xfaLayer.render(this.viewport, "display");
} catch (ex) {
error = ex;
} finally {
this.eventBus.dispatch("xfalayerrendered", {
source: this,
pageNumber: this.id,
error,
});
}
}
/** /**
* @private * @private
*/ */
@ -197,9 +218,14 @@ class PDFPageView {
const currentZoomLayerNode = (keepZoomLayer && this.zoomLayer) || null; const currentZoomLayerNode = (keepZoomLayer && this.zoomLayer) || null;
const currentAnnotationNode = const currentAnnotationNode =
(keepAnnotations && this.annotationLayer?.div) || null; (keepAnnotations && this.annotationLayer?.div) || null;
const currentXfaLayerNode = this.xfaLayer?.div || null;
for (let i = childNodes.length - 1; i >= 0; i--) { for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i]; const node = childNodes[i];
if (currentZoomLayerNode === node || currentAnnotationNode === node) { if (
currentZoomLayerNode === node ||
currentAnnotationNode === node ||
currentXfaLayerNode === node
) {
continue; continue;
} }
div.removeChild(node); div.removeChild(node);
@ -393,6 +419,10 @@ class PDFPageView {
if (redrawAnnotations && this.annotationLayer) { if (redrawAnnotations && this.annotationLayer) {
this._renderAnnotationLayer(); this._renderAnnotationLayer();
} }
if (this.xfaLayer) {
this._renderXfaLayer();
}
} }
get width() { get width() {
@ -553,6 +583,17 @@ class PDFPageView {
} }
this._renderAnnotationLayer(); this._renderAnnotationLayer();
} }
if (this.xfaLayerFactory) {
if (!this.xfaLayer) {
this.xfaLayer = this.xfaLayerFactory.createXfaLayerBuilder(
div,
pdfPage
);
}
this._renderXfaLayer();
}
div.setAttribute("data-loaded", true); div.setAttribute("data-loaded", true);
this.eventBus.dispatch("pagerender", { this.eventBus.dispatch("pagerender", {

View File

@ -14,6 +14,7 @@
*/ */
@import url(text_layer_builder.css); @import url(text_layer_builder.css);
@import url(annotation_layer_builder.css); @import url(annotation_layer_builder.css);
@import url(xfa_layer_builder.css);
.pdfViewer .canvasWrapper { .pdfViewer .canvasWrapper {
overflow: hidden; overflow: hidden;

22
web/xfa_layer_builder.css Normal file
View File

@ -0,0 +1,22 @@
*/* 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.
*/
.xfaLayer {
position: absolute;
top: 0;
left: 0;
z-index: 200;
transform-origin: 0 0;
}

97
web/xfa_layer_builder.js Normal file
View File

@ -0,0 +1,97 @@
/* 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 { XfaLayer } from "pdfjs-lib";
/**
* @typedef {Object} XfaLayerBuilderOptions
* @property {HTMLDivElement} pageDiv
* @property {PDFPage} pdfPage
*/
class XfaLayerBuilder {
/**
* @param {XfaLayerBuilderOptions} options
*/
constructor({ pageDiv, pdfPage }) {
this.pageDiv = pageDiv;
this.pdfPage = pdfPage;
this.div = null;
this._cancelled = false;
}
/**
* @param {PageViewport} viewport
* @param {string} intent (default value is 'display')
* @returns {Promise<void>} A promise that is resolved when rendering of the
* annotations is complete.
*/
render(viewport, intent = "display") {
return this.pdfPage.getXfa().then(xfa => {
if (this._cancelled) {
return;
}
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.div.className = "xfaLayer";
this.pageDiv.appendChild(this.div);
parameters.div = this.div;
XfaLayer.render(parameters);
}
});
}
cancel() {
this._cancelled = true;
}
hide() {
if (!this.div) {
return;
}
this.div.hidden = true;
}
}
/**
* @implements IPDFXfaLayerFactory
*/
class DefaultXfaLayerFactory {
/**
* @param {HTMLDivElement} pageDiv
* @param {PDFPage} pdfPage
*/
createXfaLayerBuilder(pageDiv, pdfPage) {
return new XfaLayerBuilder({
pageDiv,
pdfPage,
});
}
}
export { DefaultXfaLayerFactory, XfaLayerBuilder };