mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-26 10:08:06 +02:00
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:
parent
a164941351
commit
24e598a895
20 changed files with 760 additions and 27 deletions
|
@ -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) {
|
||||
|
|
69
src/core/xfa/html_utils.js
Normal file
69
src/core/xfa/html_utils.js
Normal 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 };
|
|
@ -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 {
|
||||
|
|
|
@ -74,7 +74,7 @@ function getMeasurement(str, def = "0") {
|
|||
}
|
||||
return {
|
||||
value: sign === "-" ? -value : value,
|
||||
unit,
|
||||
unit: unit || "pt",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue