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 {
assert,
bytesToString,
FormatError,
info,
InvalidPDFException,
@ -28,6 +29,7 @@ import {
shadow,
stringToBytes,
stringToPDFString,
stringToUTF8String,
unreachable,
Util,
warn,
@ -56,6 +58,7 @@ import { calculateMD5 } from "./crypto.js";
import { Linearization } from "./parser.js";
import { OperatorList } from "./operator_list.js";
import { PartialEvaluator } from "./evaluator.js";
import { XFAFactory } from "./xfa/factory.js";
const DEFAULT_USER_UNIT = 1.0;
const LETTER_SIZE_MEDIABOX = [0, 0, 612, 792];
@ -79,6 +82,7 @@ class Page {
builtInCMapCache,
globalImageCache,
nonBlendModesSet,
xfaFactory,
}) {
this.pdfManager = pdfManager;
this.pageIndex = pageIndex;
@ -91,6 +95,7 @@ class Page {
this.nonBlendModesSet = nonBlendModesSet;
this.evaluatorOptions = pdfManager.evaluatorOptions;
this.resourcesPromise = null;
this.xfaFactory = xfaFactory;
const idCounters = {
obj: 0,
@ -137,6 +142,11 @@ class Page {
}
_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);
if (Array.isArray(box) && box.length === 4) {
@ -231,6 +241,13 @@ class Page {
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) {
const partialEvaluator = new PartialEvaluator({
xref: this.xref,
@ -695,6 +712,9 @@ class PDFDocument {
}
get numPages() {
if (this.xfaFactory) {
return shadow(this, "numPages", this.xfaFactory.numberPages);
}
const linearization = this.linearization;
const num = linearization ? linearization.numPages : this.catalog.numPages;
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() {
const formInfo = { hasFields: false, hasAcroForm: false, hasXfa: false };
const acroForm = this.catalog.acroForm;
@ -918,6 +1012,24 @@ class PDFDocument {
}
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 =
linearization && linearization.pageFirst === pageIndex
? this._getLinearizationPage(pageIndex)
@ -935,6 +1047,7 @@ class PDFDocument {
builtInCMapCache: catalog.builtInCMapCache,
globalImageCache: catalog.globalImageCache,
nonBlendModesSet: catalog.nonBlendModesSet,
xfaFactory: null,
});
}));
}

View File

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

View File

@ -188,14 +188,15 @@ class WorkerMessageHandler {
await pdfManager.ensureDoc("checkFirstPage");
}
const [numPages, fingerprint] = await Promise.all([
const [numPages, fingerprint, isPureXfa] = await Promise.all([
pdfManager.ensureDoc("numPages"),
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();
let newPdfManager;
@ -207,6 +208,7 @@ class WorkerMessageHandler {
source.data,
source.password,
evaluatorOptions,
enableXfa,
docBaseUrl
);
pdfManagerCapability.resolve(newPdfManager);
@ -246,6 +248,7 @@ class WorkerMessageHandler {
rangeChunkSize: source.rangeChunkSize,
},
evaluatorOptions,
enableXfa,
docBaseUrl
);
// There may be a chance that `newPdfManager` is not initialized for
@ -277,6 +280,7 @@ class WorkerMessageHandler {
pdfFile,
source.password,
evaluatorOptions,
enableXfa,
docBaseUrl
);
pdfManagerCapability.resolve(newPdfManager);
@ -399,7 +403,7 @@ class WorkerMessageHandler {
fontExtraProperties: data.fontExtraProperties,
};
getPdfManager(data, evaluatorOptions)
getPdfManager(data, evaluatorOptions, data.enableXfa)
.then(function (newPdfManager) {
if (terminated) {
// 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) {
return pdfManager.ensureCatalog("documentOutline");
});

View File

@ -13,13 +13,27 @@
* limitations under the License.
*/
import { $toHTML } from "./xfa_object.js";
import { Binder } from "./bind.js";
import { XFAParser } from "./parser.js";
class XFAFactory {
constructor(data) {
this.root = new XFAParser().parse(XFAFactory._createDocument(data));
this.form = new Binder(this.root).bind();
try {
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) {

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 {
$appendChild,
$childrenToHTML,
$content,
$extra,
$finalize,
$getParent,
$hasItem,
$hasSettableValue,
$isTransparent,
@ -26,6 +29,8 @@ import {
$removeChild,
$setSetAttributes,
$setValue,
$toHTML,
$uid,
ContentObject,
Option01,
OptionObject,
@ -45,6 +50,7 @@ import {
getRelevant,
getStringOption,
} from "./utils.js";
import { measureToString, setPosition, setWidthHeight } from "./html_utils.js";
import { warn } from "../../shared/util.js";
const TEMPLATE_NS_ID = NamespaceIds.template.id;
@ -656,6 +662,29 @@ class ContentArea extends XFAObject {
this.desc = 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 {
@ -1946,6 +1975,41 @@ class PageArea extends XFAObject {
this.field = 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 {
@ -1970,6 +2034,20 @@ class PageSet extends XFAObject {
this.pageArea = 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 {
@ -2465,6 +2543,64 @@ class Subform extends XFAObject {
this.subform = 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 {
@ -2580,8 +2716,32 @@ class Template extends XFAObject {
"interactiveForms",
]);
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();
}
[$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 {

View File

@ -74,7 +74,7 @@ function getMeasurement(str, def = "0") {
}
return {
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
// and properties/methods names.
const $appendChild = Symbol();
const $childrenToHTML = Symbol();
const $clean = Symbol();
const $cleanup = Symbol();
const $clone = Symbol();
@ -27,6 +28,7 @@ const $consumed = Symbol();
const $content = Symbol("content");
const $data = Symbol("data");
const $dump = Symbol();
const $extra = Symbol("extra");
const $finalize = Symbol();
const $getAttributeIt = Symbol();
const $getChildrenByClass = Symbol();
@ -56,6 +58,8 @@ const $setId = Symbol();
const $setSetAttributes = Symbol();
const $setValue = Symbol();
const $text = Symbol();
const $toHTML = Symbol();
const $uid = Symbol("uid");
const _applyPrototype = Symbol();
const _attributes = Symbol();
@ -73,6 +77,8 @@ const _parent = Symbol("parent");
const _setAttributes = Symbol();
const _validator = Symbol();
let uid = 0;
class XFAObject {
constructor(nsId, name, hasChildren = false) {
this[$namespaceId] = nsId;
@ -80,6 +86,7 @@ class XFAObject {
this[_hasChildren] = hasChildren;
this[_parent] = null;
this[_children] = [];
this[$uid] = `${name}${uid++}`;
}
[$onChild](child) {
@ -252,6 +259,23 @@ class XFAObject {
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) {
if (attributes.use || attributes.id) {
// 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) {
if (!name) {
return this[_children];
@ -766,6 +801,7 @@ class Option10 extends IntegerObject {
export {
$appendChild,
$childrenToHTML,
$clean,
$cleanup,
$clone,
@ -773,6 +809,7 @@ export {
$content,
$data,
$dump,
$extra,
$finalize,
$getAttributeIt,
$getChildren,
@ -801,6 +838,8 @@ export {
$setSetAttributes,
$setValue,
$text,
$toHTML,
$uid,
ContentObject,
IntegerObject,
Option01,

View File

@ -162,6 +162,8 @@ function setPDFNetworkStreamFactory(pdfNetworkStreamFactory) {
* parsed font data from the worker-thread. This may be useful for debugging
* purposes (and backwards compatibility), but note that it will lead to
* 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
* context to create elements with and to load resources, such as fonts,
* into. Defaults to the current document.
@ -284,6 +286,7 @@ function getDocument(src) {
params.ignoreErrors = params.stopAtErrors !== true;
params.fontExtraProperties = params.fontExtraProperties === true;
params.pdfBug = params.pdfBug === true;
params.enableXfa = params.enableXfa === true;
if (!Number.isInteger(params.maxImageSize)) {
params.maxImageSize = -1;
@ -438,6 +441,7 @@ function _fetchDocument(worker, source, pdfDataRangeTransport, docId) {
ignoreErrors: source.ignoreErrors,
isEvalSupported: source.isEvalSupported,
fontExtraProperties: source.fontExtraProperties,
enableXfa: source.enableXfa,
})
.then(function (workerId) {
if (worker.destroyed) {
@ -674,6 +678,13 @@ class PDFDocumentProxy {
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.
* @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.
*
@ -2709,6 +2730,12 @@ class WorkerTransport {
});
}
getPageXfa(pageIndex) {
return this.messageHandler.sendWithPromise("GetPageXfa", {
pageIndex,
});
}
getOutline() {
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 { renderTextLayer } from "./display/text_layer.js";
import { SVGGraphics } from "./display/svg.js";
import { XfaLayer } from "./display/xfa_layer.js";
/* eslint-disable-next-line no-unused-vars */
const pdfjsVersion =
@ -167,4 +168,6 @@ export {
renderTextLayer,
// From "./display/svg.js":
SVGGraphics,
// From "./display/xfa_layer.js":
XfaLayer,
};

View File

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

View File

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

View File

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

View File

@ -42,6 +42,7 @@ import { NullL10n } from "./l10n_utils.js";
import { PDFPageView } from "./pdf_page_view.js";
import { SimpleLinkService } from "./pdf_link_service.js";
import { TextLayerBuilder } from "./text_layer_builder.js";
import { XfaLayerBuilder } from "./xfa_layer_builder.js";
const DEFAULT_CACHE_SIZE = 10;
@ -478,6 +479,7 @@ class BaseViewer {
if (!pdfDocument) {
return;
}
const isPureXfa = pdfDocument.isPureXfa;
const pagesCount = pdfDocument.numPages;
const firstPagePromise = pdfDocument.getPage(1);
// Rendering (potentially) depends on this, hence fetching it immediately.
@ -523,6 +525,7 @@ class BaseViewer {
const viewport = firstPdfPage.getViewport({ scale: scale * CSS_UNITS });
const textLayerFactory =
this.textLayerMode !== TextLayerMode.DISABLE ? this : null;
const xfaLayerFactory = isPureXfa ? this : null;
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
const pageView = new PDFPageView({
@ -536,6 +539,7 @@ class BaseViewer {
textLayerFactory,
textLayerMode: this.textLayerMode,
annotationLayerFactory: this,
xfaLayerFactory,
imageResourcesPath: this.imageResourcesPath,
renderInteractiveForms: this.renderInteractiveForms,
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
* 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
*/
@ -243,5 +255,6 @@ export {
IPDFHistory,
IPDFLinkService,
IPDFTextLayerFactory,
IPDFXfaLayerFactory,
IRenderableView,
};

View File

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

View File

@ -14,6 +14,7 @@
*/
@import url(text_layer_builder.css);
@import url(annotation_layer_builder.css);
@import url(xfa_layer_builder.css);
.pdfViewer .canvasWrapper {
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 };