diff --git a/src/core/xfa/bind.js b/src/core/xfa/bind.js
index 209c837bd..e6f227ebe 100644
--- a/src/core/xfa/bind.js
+++ b/src/core/xfa/bind.js
@@ -22,6 +22,7 @@ import {
$finalize,
$getAttributeIt,
$getChildren,
+ $getDataValue,
$getParent,
$getRealChildrenByNameIt,
$global,
@@ -88,7 +89,7 @@ class Binder {
if (formNode[$hasSettableValue]()) {
if (data[$isDataValue]()) {
- const value = data[$content].trim();
+ const value = data[$getDataValue]();
// TODO: use picture.
formNode[$setValue](createText(value));
formNode[$data] = data;
@@ -114,7 +115,7 @@ class Binder {
}
}
- _findDataByNameToConsume(name, dataNode, global) {
+ _findDataByNameToConsume(name, isValue, dataNode, global) {
if (!name) {
return null;
}
@@ -130,9 +131,16 @@ class Binder {
/* allTransparent = */ false,
/* skipConsumed = */ true
);
- match = generator.next().value;
- if (match) {
- return match;
+ // Try to find a match of the same kind.
+ while (true) {
+ match = generator.next().value;
+ if (!match) {
+ break;
+ }
+
+ if (isValue === match[$isDataValue]()) {
+ return match;
+ }
}
if (
dataNode[$namespaceId] === NamespaceIds.datasets.id &&
@@ -149,7 +157,7 @@ class Binder {
// Secondly, if global try to find it just under the root of datasets
// (which is the location of global variables).
- generator = this.datasets[$getRealChildrenByNameIt](
+ generator = this.data[$getRealChildrenByNameIt](
name,
/* allTransparent = */ false,
/* skipConsumed = */ false
@@ -478,6 +486,7 @@ class Binder {
if (child.bind) {
switch (child.bind.match) {
case "none":
+ this._bindElement(child, dataNode);
continue;
case "global":
global = true;
@@ -485,6 +494,7 @@ class Binder {
case "dataRef":
if (!child.bind.ref) {
warn(`XFA - ref is empty in node ${child[$nodeName]}.`);
+ this._bindElement(child, dataNode);
continue;
}
ref = child.bind.ref;
@@ -545,6 +555,7 @@ class Binder {
while (matches.length < max) {
const found = this._findDataByNameToConsume(
child.name,
+ child[$hasSettableValue](),
dataNode,
global
);
@@ -580,6 +591,8 @@ class Binder {
}
this._bindOccurrences(child, match, picture);
} else if (min > 0) {
+ this._setProperties(child, dataNode);
+ this._bindItems(child, dataNode);
this._bindElement(child, dataNode);
} else {
uselessNodes.push(child);
diff --git a/src/core/xfa/builder.js b/src/core/xfa/builder.js
index 511a814bf..89fe62482 100644
--- a/src/core/xfa/builder.js
+++ b/src/core/xfa/builder.js
@@ -17,6 +17,7 @@ import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import {
$cleanup,
$finalize,
+ $ids,
$nsAttributes,
$onChild,
$resolvePrototypes,
@@ -27,13 +28,11 @@ import { Template } from "./template.js";
import { UnknownNamespace } from "./unknown.js";
import { warn } from "../../shared/util.js";
-const _ids = Symbol();
-
class Root extends XFAObject {
constructor(ids) {
super(-1, "root", Object.create(null));
this.element = null;
- this[_ids] = ids;
+ this[$ids] = ids;
}
[$onChild](child) {
@@ -44,7 +43,8 @@ class Root extends XFAObject {
[$finalize]() {
super[$finalize]();
if (this.element.template instanceof Template) {
- this.element.template[$resolvePrototypes](this[_ids]);
+ this.element.template[$resolvePrototypes](this[$ids]);
+ this.element.template[$ids] = this[$ids];
}
}
}
diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js
index 3aa06df08..d20ca1477 100644
--- a/src/core/xfa/html_utils.js
+++ b/src/core/xfa/html_utils.js
@@ -13,18 +13,38 @@
* limitations under the License.
*/
-import { $extra, $getParent, $toStyle, XFAObject } from "./xfa_object.js";
+import {
+ $extra,
+ $getParent,
+ $nodeName,
+ $toStyle,
+ XFAObject,
+} from "./xfa_object.js";
+import { getMeasurement } from "./utils.js";
import { warn } from "../../shared/util.js";
+const wordNonWordRegex = new RegExp(
+ "([\\p{N}\\p{L}\\p{M}]+)|([^\\p{N}\\p{L}\\p{M}]+)",
+ "gu"
+);
+const wordFirstRegex = new RegExp("^[\\p{N}\\p{L}\\p{M}]", "u");
+
function measureToString(m) {
if (typeof m === "string") {
return "0px";
}
+
return Number.isInteger(m) ? `${m}px` : `${m.toFixed(2)}px`;
}
const converters = {
anchorType(node, style) {
+ const parent = node[$getParent]();
+ if (!parent || (parent.layout && parent.layout !== "position")) {
+ // anchorType is only used in a positioned layout.
+ return;
+ }
+
if (!("transform" in style)) {
style.transform = "";
}
@@ -57,12 +77,28 @@ const converters = {
},
dimensions(node, style) {
const parent = node[$getParent]();
- const extra = parent[$extra];
let width = node.w;
- if (extra && extra.columnWidths) {
- width = extra.columnWidths[extra.currentColumn];
- extra.currentColumn =
- (extra.currentColumn + 1) % extra.columnWidths.length;
+ const height = node.h;
+ if (parent.layout && parent.layout.includes("row")) {
+ const extra = parent[$extra];
+ const colSpan = node.colSpan;
+ let w;
+ if (colSpan === -1) {
+ w = extra.columnWidths
+ .slice(extra.currentColumn)
+ .reduce((a, x) => a + x, 0);
+ extra.currentColumn = 0;
+ } else {
+ w = extra.columnWidths
+ .slice(extra.currentColumn, extra.currentColumn + colSpan)
+ .reduce((a, x) => a + x, 0);
+ extra.currentColumn =
+ (extra.currentColumn + node.colSpan) % extra.columnWidths.length;
+ }
+
+ if (!isNaN(w)) {
+ width = node.w = w;
+ }
}
if (width !== "") {
@@ -72,17 +108,21 @@ const converters = {
if (node.maxW > 0) {
style.maxWidth = measureToString(node.maxW);
}
- style.minWidth = measureToString(node.minW);
+ if (parent.layout === "position") {
+ style.minWidth = measureToString(node.minW);
+ }
}
- if (node.h !== "") {
- style.height = measureToString(node.h);
+ if (height !== "") {
+ style.height = measureToString(height);
} else {
style.height = "auto";
if (node.maxH > 0) {
style.maxHeight = measureToString(node.maxH);
}
- style.minHeight = measureToString(node.minH);
+ if (parent.layout === "position") {
+ style.minHeight = measureToString(node.minH);
+ }
}
},
position(node, style) {
@@ -118,22 +158,31 @@ const converters = {
}
},
hAlign(node, style) {
- switch (node.hAlign) {
- case "justifyAll":
- style.textAlign = "justify-all";
- break;
- case "radix":
- // TODO: implement this correctly !
- style.textAlign = "left";
- break;
- default:
- style.textAlign = node.hAlign;
+ if (node[$nodeName] === "para") {
+ switch (node.hAlign) {
+ case "justifyAll":
+ style.textAlign = "justify-all";
+ break;
+ case "radix":
+ // TODO: implement this correctly !
+ style.textAlign = "left";
+ break;
+ default:
+ style.textAlign = node.hAlign;
+ }
+ } else {
+ switch (node.hAlign) {
+ case "right":
+ case "center":
+ style.justifyContent = node.hAlign;
+ break;
+ }
}
},
borderMarginPadding(node, style) {
// Get border width in order to compute margin and padding.
const borderWidths = [0, 0, 0, 0];
- const marginWidths = [0, 0, 0, 0];
+ const borderInsets = [0, 0, 0, 0];
const marginNode = node.margin
? [
node.margin.topInset,
@@ -142,30 +191,211 @@ const converters = {
node.margin.leftInset,
]
: [0, 0, 0, 0];
+
+ let borderMargin;
if (node.border) {
- Object.assign(style, node.border[$toStyle](borderWidths, marginWidths));
+ Object.assign(style, node.border[$toStyle](borderWidths, borderInsets));
+ borderMargin = style.margin;
+ delete style.margin;
}
if (borderWidths.every(x => x === 0)) {
- // No border: margin & padding are padding
- if (node.margin) {
- Object.assign(style, node.margin[$toStyle]());
+ if (marginNode.every(x => x === 0)) {
+ return;
}
+
+ // No border: margin & padding are padding
+ Object.assign(style, node.margin[$toStyle]());
style.padding = style.margin;
delete style.margin;
- } else {
- style.padding =
- measureToString(marginNode[0] - borderWidths[0] - marginWidths[0]) +
- " " +
- measureToString(marginNode[1] - borderWidths[1] - marginWidths[1]) +
- " " +
- measureToString(marginNode[2] - borderWidths[2] - marginWidths[2]) +
- " " +
- measureToString(marginNode[3] - borderWidths[3] - marginWidths[3]);
+ delete style.outline;
+ delete style.outlineOffset;
+ return;
}
+
+ if (node.margin) {
+ Object.assign(style, node.margin[$toStyle]());
+ style.padding = style.margin;
+ delete style.margin;
+ }
+
+ if (!style.borderWidth) {
+ // We've an outline so no need to fake one.
+ return;
+ }
+
+ style.borderData = {
+ borderWidth: style.borderWidth,
+ borderColor: style.borderColor,
+ borderStyle: style.borderStyle,
+ margin: borderMargin,
+ };
+
+ delete style.borderWidth;
+ delete style.borderColor;
+ delete style.borderStyle;
},
};
+function layoutText(text, fontSize, space) {
+ // Try to guess width and height for the given text in taking into
+ // account the space where the text should fit.
+ // The computed dimensions are just an overestimation.
+ // TODO: base this estimation on real metrics.
+ let width = 0;
+ let height = 0;
+ let totalWidth = 0;
+ const lineHeight = fontSize * 1.5;
+ const averageCharSize = fontSize * 0.4;
+ const maxCharOnLine = Math.floor(space.width / averageCharSize);
+ const chunks = text.match(wordNonWordRegex);
+ let treatedChars = 0;
+
+ let i = 0;
+ let chunk = chunks[0];
+ while (chunk) {
+ const w = chunk.length * averageCharSize;
+ if (width + w <= space.width) {
+ width += w;
+ treatedChars += chunk.length;
+ chunk = chunks[i++];
+ continue;
+ }
+
+ if (!wordFirstRegex.test(chunk) || chunk.length > maxCharOnLine) {
+ const numOfCharOnLine = Math.floor(
+ (space.width - width) / averageCharSize
+ );
+ chunk = chunk.slice(numOfCharOnLine);
+ treatedChars += numOfCharOnLine;
+ if (height + lineHeight > space.height) {
+ return { width: 0, height: 0, splitPos: treatedChars };
+ }
+ totalWidth = Math.max(width, totalWidth);
+ width = 0;
+ height += lineHeight;
+ continue;
+ }
+
+ if (height + lineHeight > space.height) {
+ return { width: 0, height: 0, splitPos: treatedChars };
+ }
+
+ totalWidth = Math.max(width, totalWidth);
+ width = w;
+ height += lineHeight;
+ chunk = chunks[i++];
+ }
+
+ if (totalWidth === 0) {
+ totalWidth = width;
+ }
+
+ if (totalWidth !== 0) {
+ height += lineHeight;
+ }
+
+ return { width: totalWidth, height, splitPos: -1 };
+}
+
+function computeBbox(node, html, availableSpace) {
+ let bbox;
+ if (node.w !== "" && node.h !== "") {
+ bbox = [node.x, node.y, node.w, node.h];
+ } else {
+ if (!availableSpace) {
+ return null;
+ }
+ let width = node.w;
+ if (width === "") {
+ if (node.maxW === 0) {
+ const parent = node[$getParent]();
+ if (parent.layout === "position" && parent.w !== "") {
+ width = 0;
+ } else {
+ width = node.minW;
+ }
+ } else {
+ width = Math.min(node.maxW, availableSpace.width);
+ }
+ html.attributes.style.width = measureToString(width);
+ }
+
+ let height = node.h;
+ if (height === "") {
+ if (node.maxH === 0) {
+ const parent = node[$getParent]();
+ if (parent.layout === "position" && parent.h !== "") {
+ height = 0;
+ } else {
+ height = node.minH;
+ }
+ } else {
+ height = Math.min(node.maxH, availableSpace.height);
+ }
+ html.attributes.style.height = measureToString(height);
+ }
+
+ bbox = [node.x, node.y, width, height];
+ }
+ return bbox;
+}
+
+function fixDimensions(node) {
+ const parent = node[$getParent]();
+ if (parent.layout && parent.layout.includes("row")) {
+ const extra = parent[$extra];
+ const colSpan = node.colSpan;
+ let width;
+ if (colSpan === -1) {
+ width = extra.columnWidths
+ .slice(extra.currentColumn)
+ .reduce((a, w) => a + w, 0);
+ } else {
+ width = extra.columnWidths
+ .slice(extra.currentColumn, extra.currentColumn + colSpan)
+ .reduce((a, w) => a + w, 0);
+ }
+ if (!isNaN(width)) {
+ node.w = width;
+ }
+ }
+
+ if (parent.w && node.w) {
+ node.w = Math.min(parent.w, node.w);
+ }
+
+ if (parent.h && node.h) {
+ node.h = Math.min(parent.h, node.h);
+ }
+
+ if (parent.layout && parent.layout !== "position") {
+ // Useless in this context.
+ node.x = node.y = 0;
+ if (parent.layout === "tb") {
+ if (
+ parent.w !== "" &&
+ (node.w === "" || node.w === 0 || node.w > parent.w)
+ ) {
+ node.w = parent.w;
+ }
+ }
+ }
+
+ if (node.layout === "position") {
+ // Acrobat doesn't take into account min, max values
+ // for containers with positioned layout (which makes sense).
+ node.minW = node.minH = 0;
+ node.maxW = node.maxH = Infinity;
+ } else {
+ if (node.layout === "table") {
+ if (node.w === "" && Array.isArray(node.columnWidths)) {
+ node.w = node.columnWidths.reduce((a, x) => a + x, 0);
+ }
+ }
+ }
+}
+
function layoutClass(node) {
switch (node.layout) {
case "position":
@@ -211,26 +441,145 @@ function toStyle(node, ...names) {
return style;
}
-function addExtraDivForMargin(html) {
+function addExtraDivForBorder(html) {
const style = html.attributes.style;
- if (style.margin) {
- const padding = style.margin;
- delete style.margin;
- const width = style.width || "auto";
- const height = style.height || "auto";
+ const data = style.borderData;
+ const children = [];
- style.width = "100%";
- style.height = "100%";
+ const attributes = {
+ class: "xfaWrapper",
+ style: Object.create(null),
+ };
- return {
+ for (const key of ["top", "left"]) {
+ if (style[key] !== undefined) {
+ attributes.style[key] = style[key];
+ }
+ }
+
+ delete style.top;
+ delete style.left;
+
+ if (style.position === "absolute") {
+ attributes.style.position = "absolute";
+ } else {
+ attributes.style.position = "relative";
+ }
+ delete style.position;
+
+ if (style.justifyContent) {
+ attributes.style.justifyContent = style.justifyContent;
+ delete style.justifyContent;
+ }
+
+ if (data) {
+ delete style.borderData;
+
+ let insets;
+ if (data.margin) {
+ insets = data.margin.split(" ");
+ delete data.margin;
+ } else {
+ insets = ["0px", "0px", "0px", "0px"];
+ }
+
+ let width = "100%";
+ let height = width;
+
+ if (insets[1] !== "0px" || insets[3] !== "0px") {
+ width = `calc(100% - ${parseInt(insets[1]) + parseInt(insets[3])}px`;
+ }
+
+ if (insets[0] !== "0px" || insets[2] !== "0px") {
+ height = `calc(100% - ${parseInt(insets[0]) + parseInt(insets[2])}px`;
+ }
+
+ const borderStyle = {
+ top: insets[0],
+ left: insets[3],
+ width,
+ height,
+ };
+
+ for (const [k, v] of Object.entries(data)) {
+ borderStyle[k] = v;
+ }
+
+ if (style.transform) {
+ borderStyle.transform = style.transform;
+ }
+
+ const borderDiv = {
name: "div",
attributes: {
- style: { padding, width, height },
+ class: "xfaBorderDiv",
+ style: borderStyle,
},
- children: [html],
};
+
+ children.push(borderDiv);
}
- return html;
+
+ children.push(html);
+
+ return {
+ name: "div",
+ attributes,
+ children,
+ };
}
-export { addExtraDivForMargin, layoutClass, measureToString, toStyle };
+function fixTextIndent(styles) {
+ const indent = getMeasurement(styles.textIndent, "0px");
+ if (indent >= 0) {
+ return;
+ }
+
+ const align = styles.textAlign || "left";
+ if (align === "left" || align === "right") {
+ const name = "margin" + (align === "left" ? "Left" : "Right");
+ const margin = getMeasurement(styles[name], "0px");
+ styles[name] = `${margin - indent}pt`;
+ }
+}
+
+function getFonts(family) {
+ if (family.startsWith("'")) {
+ family = `"${family.slice(1, family.length - 1)}"`;
+ } else if (family.includes(" ") && !family.startsWith('"')) {
+ family = `"${family}"`;
+ }
+
+ // TODO in case Myriad is not available we should generate a new
+ // font based on helvetica but where glyphs have been rescaled in order
+ // to have the exact same metrics.
+ const fonts = [family];
+ switch (family) {
+ case `"Myriad Pro"`:
+ fonts.push(
+ `"Roboto Condensed"`,
+ `"Ubuntu Condensed"`,
+ `"Microsoft Sans Serif"`,
+ `"Apple Symbols"`,
+ "Helvetica",
+ `"sans serif"`
+ );
+ break;
+ case "Arial":
+ fonts.push("Helvetica", `"Liberation Sans"`, "Arimo", `"sans serif"`);
+ break;
+ }
+ return fonts.join(",");
+}
+
+export {
+ addExtraDivForBorder,
+ computeBbox,
+ fixDimensions,
+ fixTextIndent,
+ getFonts,
+ layoutClass,
+ layoutText,
+ measureToString,
+ toStyle,
+};
diff --git a/src/core/xfa/layout.js b/src/core/xfa/layout.js
new file mode 100644
index 000000000..b7eee56d8
--- /dev/null
+++ b/src/core/xfa/layout.js
@@ -0,0 +1,190 @@
+/* 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 { $extra, $flushHTML } from "./xfa_object.js";
+import { measureToString } from "./html_utils.js";
+
+// Subform and ExclGroup have a layout so they share these functions.
+
+/**
+ * How layout works ?
+ *
+ * A container has an initial space (with a width and a height) to fit in,
+ * which means that once all the children have been added then
+ * the total width/height must be lower than the given ones in
+ * the initial space.
+ * So if the container has known dimensions and these ones are ok with the
+ * space then continue else we return HTMLResult.FAILURE: it's up to the
+ * parent to deal with this failure (e.g. if parent layout is lr-tb and
+ * we fail to add a child at end of line (lr) then we try to add it on the
+ * next line).
+ * And then we run through the children, each child gets its initial space
+ * in calling its parent $getAvailableSpace method
+ * (see _filteredChildrenGenerator and $childrenToHTML in xfa_object.js)
+ * then we try to layout child in its space. If everything is ok then we add
+ * the result to its parent through $addHTML which will recompute the available
+ * space in parent according to its layout property else we return
+ * HTMLResult.Failure.
+ * Before a failure some children may have been layed out: they've been saved in
+ * [$extra].children and [$extra] has properties generator and failingNode
+ * in order to save the state where we were before a failure.
+ * This [$extra].children property is useful when a container has to be splited.
+ * So if a container is unbreakable, we must delete its [$extra] property before
+ * returning.
+ */
+
+function flushHTML(node) {
+ const attributes = node[$extra].attributes;
+ const html = {
+ name: "div",
+ attributes,
+ children: node[$extra].children,
+ };
+
+ if (node[$extra].failingNode) {
+ const htmlFromFailing = node[$extra].failingNode[$flushHTML]();
+ if (htmlFromFailing) {
+ html.children.push(htmlFromFailing);
+ }
+ }
+
+ if (html.children.length === 0) {
+ return null;
+ }
+
+ node[$extra].children = [];
+ delete node[$extra].line;
+
+ return html;
+}
+
+function addHTML(node, html, bbox) {
+ const extra = node[$extra];
+ const availableSpace = extra.availableSpace;
+
+ switch (node.layout) {
+ case "position": {
+ const [x, y, w, h] = bbox;
+ extra.width = Math.max(extra.width, x + w);
+ extra.height = Math.max(extra.height, y + h);
+ extra.children.push(html);
+ break;
+ }
+ case "lr-tb":
+ case "rl-tb":
+ if (!extra.line || extra.attempt === 1) {
+ extra.line = {
+ name: "div",
+ attributes: {
+ class: node.layout === "lr-tb" ? "xfaLr" : "xfaRl",
+ },
+ children: [],
+ };
+ extra.children.push(extra.line);
+ }
+ extra.line.children.push(html);
+
+ if (extra.attempt === 0) {
+ // Add the element on the line
+ const [, , w, h] = bbox;
+ extra.currentWidth += w;
+ extra.height = Math.max(extra.height, extra.prevHeight + h);
+ } else {
+ const [, , w, h] = bbox;
+ extra.width = Math.max(extra.width, extra.currentWidth);
+ extra.currentWidth = w;
+ extra.prevHeight = extra.height;
+ extra.height += h;
+
+ // The element has been added on a new line so switch to line mode now.
+ extra.attempt = 0;
+ }
+ break;
+ case "rl-row":
+ case "row": {
+ extra.children.push(html);
+ const [, , w, h] = bbox;
+ extra.width += w;
+ extra.height = Math.max(extra.height, h);
+ const height = measureToString(extra.height);
+ for (const child of extra.children) {
+ if (child.attributes.class === "xfaWrapper") {
+ child.children[child.children.length - 1].attributes.style.height =
+ height;
+ } else {
+ child.attributes.style.height = height;
+ }
+ }
+ break;
+ }
+ case "table": {
+ const [, , w, h] = bbox;
+ extra.width = Math.min(availableSpace.width, Math.max(extra.width, w));
+ extra.height += h;
+ extra.children.push(html);
+ break;
+ }
+ case "tb": {
+ const [, , , h] = bbox;
+ extra.width = availableSpace.width;
+ extra.height += h;
+ extra.children.push(html);
+ break;
+ }
+ }
+}
+
+function getAvailableSpace(node) {
+ const availableSpace = node[$extra].availableSpace;
+
+ switch (node.layout) {
+ case "lr-tb":
+ case "rl-tb":
+ switch (node[$extra].attempt) {
+ case 0:
+ return {
+ width: availableSpace.width - node[$extra].currentWidth,
+ height: availableSpace.height - node[$extra].prevHeight,
+ };
+ case 1:
+ return {
+ width: availableSpace.width,
+ height: availableSpace.height - node[$extra].height,
+ };
+ default:
+ return {
+ width: Infinity,
+ height: availableSpace.height - node[$extra].prevHeight,
+ };
+ }
+ case "rl-row":
+ case "row":
+ const width = node[$extra].columnWidths
+ .slice(node[$extra].currentColumn)
+ .reduce((a, x) => a + x);
+ return { width, height: availableSpace.height };
+ case "table":
+ case "tb":
+ return {
+ width: availableSpace.width,
+ height: availableSpace.height - node[$extra].height,
+ };
+ case "position":
+ default:
+ return availableSpace;
+ }
+}
+
+export { addHTML, flushHTML, getAvailableSpace };
diff --git a/src/core/xfa/parser.js b/src/core/xfa/parser.js
index 5bb7c0010..326cca0a6 100644
--- a/src/core/xfa/parser.js
+++ b/src/core/xfa/parser.js
@@ -35,6 +35,7 @@ class XFAParser extends XMLParserBase {
this._current = this._builder.buildRoot(this._ids);
this._errorCode = XMLParserErrorCode.NoError;
this._whiteRegex = /^\s+$/;
+ this._nbsps = /\xa0+/g;
}
parse(data) {
@@ -50,6 +51,9 @@ class XFAParser extends XMLParserBase {
}
onText(text) {
+ // Normally by definition a   is unbreakable
+ // but in real life Acrobat can break strings on  .
+ text = text.replace(this._nbsps, match => match.slice(1) + " ");
if (this._current[$acceptWhitespace]()) {
this._current[$onText](text);
return;
diff --git a/src/core/xfa/som.js b/src/core/xfa/som.js
index 3fb2d04f5..9c8a8829c 100644
--- a/src/core/xfa/som.js
+++ b/src/core/xfa/som.js
@@ -275,19 +275,32 @@ function createDataNode(root, container, expr) {
}
for (let ii = parsed.length; i < ii; i++) {
- const { cacheName, index } = parsed[i];
+ const { name, operator, index } = parsed[i];
if (!isFinite(index)) {
parsed[i].index = 0;
return createNodes(root, parsed.slice(i));
}
- const cached = somCache.get(root);
- if (!cached) {
- warn(`XFA - createDataNode must be called after searchNode.`);
- return null;
+ let children;
+ switch (operator) {
+ case operators.dot:
+ children = root[$getChildrenByName](name, false);
+ break;
+ case operators.dotDot:
+ children = root[$getChildrenByName](name, true);
+ break;
+ case operators.dotHash:
+ children = root[$getChildrenByClass](name);
+ if (children instanceof XFAObjectArray) {
+ children = children.children;
+ } else {
+ children = [children];
+ }
+ break;
+ default:
+ break;
}
- const children = cached.get(cacheName);
if (children.length === 0) {
return createNodes(root, parsed.slice(i));
}
diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index e1c836ca5..ef7e89e9f 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -14,21 +14,30 @@
*/
import {
+ $addHTML,
$appendChild,
+ $break,
$childrenToHTML,
$content,
$extra,
$finalize,
+ $flushHTML,
+ $getAvailableSpace,
+ $getChildren,
+ $getNextPage,
$getParent,
$hasItem,
$hasSettableValue,
+ $ids,
$isTransparent,
$namespaceId,
$nodeName,
$onChild,
$removeChild,
+ $searchNode,
$setSetAttributes,
$setValue,
+ $text,
$toHTML,
$toStyle,
$uid,
@@ -41,11 +50,17 @@ import {
} from "./xfa_object.js";
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import {
- addExtraDivForMargin,
+ addExtraDivForBorder,
+ computeBbox,
+ fixDimensions,
+ fixTextIndent,
+ getFonts,
layoutClass,
+ layoutText,
measureToString,
toStyle,
} from "./html_utils.js";
+import { addHTML, flushHTML, getAvailableSpace } from "./layout.js";
import {
getBBox,
getColor,
@@ -56,11 +71,20 @@ import {
getRatio,
getRelevant,
getStringOption,
+ HTMLResult,
} from "./utils.js";
import { stringToBytes, Util, warn } from "../../shared/util.js";
+import { searchNode } from "./som.js";
const TEMPLATE_NS_ID = NamespaceIds.template.id;
+// In case of lr-tb (and rl-tb) layouts, we try:
+// - to put the container at the end of a line
+// - and if it fails we try on the next line.
+// If both tries failed then it's up to the parent
+// to handle the situation.
+const MAX_ATTEMPTS_FOR_LRTB_LAYOUT = 2;
+
function _setValue(templateNode, value) {
if (!templateNode.value) {
const nodeValue = new Value({});
@@ -70,6 +94,37 @@ function _setValue(templateNode, value) {
templateNode.value[$setValue](value);
}
+function getRoot(node) {
+ let parent = node[$getParent]();
+ while (!(parent instanceof Template)) {
+ parent = parent[$getParent]();
+ }
+ return parent;
+}
+
+const NOTHING = 0;
+const NOSPACE = 1;
+const VALID = 2;
+function checkDimensions(node, space) {
+ if (node.w !== "" && Math.round(node.w + node.x - space.width) > 1) {
+ const area = getRoot(node)[$extra].currentContentArea;
+ if (node.w + node.x > area.w) {
+ return NOTHING;
+ }
+ return NOSPACE;
+ }
+
+ if (node.h !== "" && Math.round(node.h + node.y - space.height) > 1) {
+ const area = getRoot(node)[$extra].currentContentArea;
+ if (node.h + node.y > area.h) {
+ return NOTHING;
+ }
+
+ return NOSPACE;
+ }
+ return VALID;
+}
+
class AppearanceFilter extends StringObject {
constructor(attributes) {
super(TEMPLATE_NS_ID, "appearanceFilter");
@@ -113,7 +168,7 @@ class Area extends XFAObject {
this.colSpan = getInteger({
data: attributes.colSpan,
defaultValue: 1,
- validate: n => n >= 1,
+ validate: n => n >= 1 || n === -1,
});
this.id = attributes.id || "";
this.name = attributes.name || "";
@@ -137,10 +192,20 @@ class Area extends XFAObject {
return true;
}
- [$toHTML]() {
- // TODO: incomplete.
- this[$extra] = Object.create(null);
+ [$addHTML](html, bbox) {
+ const [x, y, w, h] = bbox;
+ this[$extra].width = Math.max(this[$extra].width, x + w);
+ this[$extra].height = Math.max(this[$extra].height, y + h);
+ this[$extra].children.push(html);
+ }
+
+ [$getAvailableSpace]() {
+ return this[$extra].availableSpace;
+ }
+
+ [$toHTML](availableSpace) {
+ // TODO: incomplete.
const style = toStyle(this, "position");
const attributes = {
style,
@@ -152,11 +217,36 @@ class Area extends XFAObject {
attributes.xfaName = this.name;
}
- const children = this[$childrenToHTML]({
- // TODO: exObject & exclGroup
- filter: new Set(["area", "draw", "field", "subform", "subformSet"]),
- include: true,
- });
+ const children = [];
+ this[$extra] = {
+ children,
+ width: 0,
+ height: 0,
+ availableSpace,
+ };
+
+ if (
+ !this[$childrenToHTML]({
+ filter: new Set([
+ "area",
+ "draw",
+ "field",
+ "exclGroup",
+ "subform",
+ "subformSet",
+ ]),
+ include: true,
+ })
+ ) {
+ // Nothing to propose for the element which doesn't fit the
+ // available space.
+ delete this[$extra];
+ // TODO: return failure or not ?
+ return HTMLResult.empty;
+ }
+
+ style.width = measureToString(this[$extra].width);
+ style.height = measureToString(this[$extra].height);
const html = {
name: "div",
@@ -164,7 +254,10 @@ class Area extends XFAObject {
children,
};
- return html;
+ const bbox = [this.x, this.y, this[$extra].width, this[$extra].height];
+ delete this[$extra];
+
+ return HTMLResult.success(html, bbox);
}
}
@@ -382,8 +475,8 @@ class BooleanElement extends Option01 {
this.usehref = attributes.usehref || "";
}
- [$toHTML]() {
- return this[$content] === 1;
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(this[$content] === 1);
}
}
@@ -419,36 +512,43 @@ class Border extends XFAObject {
}
}
- if (widths) {
- for (let i = 0; i < 4; i++) {
- widths[i] = edges[i].thickness;
- }
+ widths = widths || [0, 0, 0, 0];
+ for (let i = 0; i < 4; i++) {
+ widths[i] = edges[i].thickness;
}
- const edgeStyles = edges.map(node => node[$toStyle]());
- const cornerStyles = this.corner.children.map(node => node[$toStyle]());
+ margins = margins || [0, 0, 0, 0];
+
+ const edgeStyles = edges.map(node => {
+ const style = node[$toStyle]();
+ style.color = style.color || "#000000";
+ return style;
+ });
let style;
if (this.margin) {
style = this.margin[$toStyle]();
- if (margins) {
- margins[0] = this.margin.topInset;
- margins[1] = this.margin.rightInset;
- margins[2] = this.margin.bottomInset;
- margins[3] = this.margin.leftInset;
- }
+ margins[0] = this.margin.topInset;
+ margins[1] = this.margin.rightInset;
+ margins[2] = this.margin.bottomInset;
+ margins[3] = this.margin.leftInset;
} else {
style = Object.create(null);
}
+ let isForUi = false;
+ const parent = this[$getParent]();
+ const grandParent = parent ? parent[$getParent]() : null;
+ if (grandParent instanceof Ui) {
+ isForUi = true;
+ }
+
if (this.fill) {
Object.assign(style, this.fill[$toStyle]());
}
- style.borderWidth = edgeStyles.map(s => s.width).join(" ");
- style.borderColor = edgeStyles.map(s => s.color).join(" ");
- style.borderStyle = edgeStyles.map(s => s.style).join(" ");
-
- if (cornerStyles.length > 0) {
+ let hasRadius = false;
+ if (this.corner.children.some(node => node.radius !== 0)) {
+ const cornerStyles = this.corner.children.map(node => node[$toStyle]());
if (cornerStyles.length === 2 || cornerStyles.length === 3) {
const last = cornerStyles[cornerStyles.length - 1];
for (let i = cornerStyles.length; i < 4; i++) {
@@ -457,16 +557,60 @@ class Border extends XFAObject {
}
style.borderRadius = cornerStyles.map(s => s.radius).join(" ");
+ hasRadius = true;
}
- switch (this.presence) {
- case "invisible":
- case "hidden":
- style.borderStyle = "";
- break;
- case "inactive":
- style.borderStyle = "none";
- break;
+ const firstEdge = edgeStyles[0];
+ if (
+ !hasRadius &&
+ (this.edge.children.length <= 1 ||
+ (edgeStyles.every(
+ x =>
+ x.style === firstEdge.style &&
+ x.width === firstEdge.width &&
+ x.color === firstEdge.color
+ ) &&
+ margins.every(x => x === margins[0])))
+ ) {
+ // Rectangular border and same values for each edge then we've an outline
+ // so no need to emulate it.
+
+ let borderStyle;
+ switch (this.presence) {
+ case "invisible":
+ case "hidden":
+ borderStyle = "";
+ break;
+ case "inactive":
+ borderStyle = "none";
+ break;
+ default:
+ borderStyle = firstEdge.style;
+ break;
+ }
+
+ style.outline = `${firstEdge.width} ${firstEdge.color} ${borderStyle}`;
+ const offset = edges[0].thickness + margins[0];
+ style.outlineOffset = `-${measureToString(offset)}`;
+ if (isForUi) {
+ style.padding = `${measureToString(offset + 1)}`;
+ }
+ } else {
+ switch (this.presence) {
+ case "invisible":
+ case "hidden":
+ style.borderStyle = "";
+ break;
+ case "inactive":
+ style.borderStyle = "none";
+ break;
+ default:
+ style.borderStyle = edgeStyles.map(s => s.style).join(" ");
+ break;
+ }
+
+ style.borderWidth = edgeStyles.map(s => s.width).join(" ");
+ style.borderColor = edgeStyles.map(s => s.color).join(" ");
}
return style;
@@ -557,6 +701,11 @@ class BreakBefore extends XFAObject {
this.usehref = attributes.usehref || "";
this.script = null;
}
+
+ [$toHTML](availableSpace) {
+ this[$extra] = {};
+ return HTMLResult.FAILURE;
+ }
}
class Button extends XFAObject {
@@ -574,15 +723,16 @@ class Button extends XFAObject {
this.extras = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: highlight.
- return {
+ return HTMLResult.success({
name: "button",
attributes: {
class: "xfaButton",
style: {},
},
- };
+ children: [],
+ });
}
}
@@ -635,15 +785,15 @@ class Caption extends XFAObject {
_setValue(this, value);
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: incomplete.
if (!this.value) {
- return null;
+ return HTMLResult.EMPTY;
}
- const value = this.value[$toHTML]();
+ const value = this.value[$toHTML](availableSpace).html;
if (!value) {
- return null;
+ return HTMLResult.EMPTY;
}
const children = [];
if (typeof value === "string") {
@@ -659,21 +809,30 @@ class Caption extends XFAObject {
switch (this.placement) {
case "left":
case "right":
- style.minWidth = measureToString(this.reserve);
+ if (this.reserve > 0) {
+ style.width = measureToString(this.reserve);
+ } else {
+ style.minWidth = measureToString(this.reserve);
+ }
break;
case "top":
case "bottom":
- style.minHeight = measureToString(this.reserve);
+ if (this.reserve > 0) {
+ style.height = measureToString(this.reserve);
+ } else {
+ style.minHeight = measureToString(this.reserve);
+ }
break;
}
- return {
+ return HTMLResult.success({
name: "div",
attributes: {
style,
+ class: "xfaCaption",
},
children,
- };
+ });
}
}
@@ -730,7 +889,7 @@ class CheckButton extends XFAObject {
this.margin = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: shape and mark == default.
const style = toStyle(this, "border", "margin");
const size = measureToString(this.size);
@@ -782,19 +941,26 @@ class CheckButton extends XFAObject {
style.height = size;
}
- return {
+ const input = {
+ name: "input",
+ attributes: {
+ class: "xfaCheckbox",
+ type: "radio",
+ },
+ };
+
+ const container = this[$getParent]()[$getParent]()[$getParent]();
+ if (container instanceof ExclGroup) {
+ input.attributes.name = container[$uid];
+ }
+
+ return HTMLResult.success({
name: "label",
attributes: {
class: "xfaLabel",
},
children: [
- {
- name: "input",
- attributes: {
- class: "xfaCheckbox",
- type: "checkbox",
- },
- },
+ input,
{
name: "span",
attributes: {
@@ -804,7 +970,7 @@ class CheckButton extends XFAObject {
},
},
],
- };
+ });
}
}
@@ -831,10 +997,40 @@ class ChoiceList extends XFAObject {
this.margin = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: incomplete.
const style = toStyle(this, "border", "margin");
- return {
+ const ui = this[$getParent]();
+ const field = ui[$getParent]();
+ const children = [];
+
+ if (field.items.children.length > 0) {
+ const displayed = field.items.children[0][$toHTML]().html;
+ const values = field.items.children[1]
+ ? field.items.children[1][$toHTML]().html
+ : [];
+
+ for (let i = 0, ii = displayed.length; i < ii; i++) {
+ children.push({
+ name: "option",
+ attributes: {
+ value: values[i] || displayed[i],
+ },
+ value: displayed[i],
+ });
+ }
+ }
+
+ const selectAttributes = {
+ class: "xfaSelect",
+ style,
+ };
+
+ if (this.open === "multiSelect") {
+ selectAttributes.multiple = true;
+ }
+
+ return HTMLResult.success({
name: "label",
attributes: {
class: "xfaLabel",
@@ -842,14 +1038,11 @@ class ChoiceList extends XFAObject {
children: [
{
name: "select",
- attributes: {
- class: "xfaSelect",
- multiple: this.open === "multiSelect",
- style,
- },
+ children,
+ attributes: selectAttributes,
},
],
- };
+ });
}
}
@@ -860,7 +1053,7 @@ class Color extends XFAObject {
this.id = attributes.id || "";
this.use = attributes.use || "";
this.usehref = attributes.usehref || "";
- this.value = getColor(attributes.value);
+ this.value = attributes.value ? getColor(attributes.value) : "";
this.extras = null;
}
@@ -869,7 +1062,9 @@ class Color extends XFAObject {
}
[$toStyle]() {
- return Util.makeHexColor(this.value.r, this.value.g, this.value.b);
+ return this.value
+ ? Util.makeHexColor(this.value.r, this.value.g, this.value.b)
+ : null;
}
}
@@ -920,7 +1115,7 @@ class ContentArea extends XFAObject {
this.extras = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: incomplete.
const left = measureToString(this.x);
const top = measureToString(this.y);
@@ -932,7 +1127,7 @@ class ContentArea extends XFAObject {
width: measureToString(this.w),
height: measureToString(this.h),
};
- return {
+ return HTMLResult.success({
name: "div",
children: [],
attributes: {
@@ -940,7 +1135,7 @@ class ContentArea extends XFAObject {
class: "xfaContentarea",
id: this[$uid],
},
- };
+ });
}
}
@@ -1004,8 +1199,8 @@ class DateElement extends ContentObject {
this[$content] = new Date(this[$content].trim());
}
- [$toHTML]() {
- return this[$content].toString();
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(this[$content].toString());
}
}
@@ -1022,8 +1217,8 @@ class DateTime extends ContentObject {
this[$content] = new Date(this[$content].trim());
}
- [$toHTML]() {
- return this[$content].toString();
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(this[$content].toString());
}
}
@@ -1045,7 +1240,7 @@ class DateTimeEdit extends XFAObject {
this.margin = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: incomplete.
// When the picker is host we should use type=date for the input
// but we need to put the buttons outside the text-field.
@@ -1059,13 +1254,13 @@ class DateTimeEdit extends XFAObject {
},
};
- return {
+ return HTMLResult.success({
name: "label",
attributes: {
class: "xfaLabel",
},
children: [html],
- };
+ });
}
}
@@ -1093,8 +1288,10 @@ class Decimal extends ContentObject {
this[$content] = isNaN(number) ? null : number;
}
- [$toHTML]() {
- return this[$content] !== null ? this[$content].toString() : "";
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(
+ this[$content] !== null ? this[$content].toString() : ""
+ );
}
}
@@ -1170,7 +1367,7 @@ class Draw extends XFAObject {
this.colSpan = getInteger({
data: attributes.colSpan,
defaultValue: 1,
- validate: x => x >= 1,
+ validate: n => n >= 1 || n === -1,
});
this.h = attributes.h ? getMeasurement(attributes.h) : "";
this.hAlign = getStringOption(attributes.hAlign, [
@@ -1224,9 +1421,36 @@ class Draw extends XFAObject {
_setValue(this, value);
}
- [$toHTML]() {
- if (!this.value) {
- return null;
+ [$toHTML](availableSpace) {
+ if (
+ this.presence === "hidden" ||
+ this.presence === "inactive" ||
+ this.h === 0 ||
+ this.w === 0
+ ) {
+ return HTMLResult.EMPTY;
+ }
+
+ fixDimensions(this);
+
+ if (this.w !== "" && this.h === "" && this.value) {
+ const text = this.value[$text]();
+ if (text) {
+ const { height } = layoutText(text, this.font.size, {
+ width: this.w,
+ height: Infinity,
+ });
+ this.h = height || "";
+ }
+ }
+
+ switch (checkDimensions(this, availableSpace)) {
+ case NOTHING:
+ return HTMLResult.EMPTY;
+ case NOSPACE:
+ return HTMLResult.FAILURE;
+ default:
+ break;
}
const style = toStyle(
@@ -1256,35 +1480,59 @@ class Draw extends XFAObject {
attributes.xfaName = this.name;
}
- let html = {
+ const html = {
name: "div",
attributes,
children: [],
};
- const value = this.value ? this.value[$toHTML]() : null;
+ const extra = addExtraDivForBorder(html);
+ const bbox = computeBbox(this, html, availableSpace);
+
+ const value = this.value ? this.value[$toHTML](availableSpace).html : null;
if (value === null) {
- return html;
+ return HTMLResult.success(extra, bbox);
}
html.children.push(value);
+ if (value.attributes.class === "xfaRich") {
+ if (this.h === "") {
+ style.height = "auto";
+ }
+ if (this.w === "") {
+ style.width = "auto";
+ }
+ if (this.para) {
+ // By definition exData are external data so para
+ // has no effect on it.
+ attributes.style.display = "flex";
+ attributes.style.flexDirection = "column";
+ switch (this.para.vAlign) {
+ case "top":
+ attributes.style.justifyContent = "start";
+ break;
+ case "bottom":
+ attributes.style.justifyContent = "end";
+ break;
+ case "middle":
+ attributes.style.justifyContent = "center";
+ break;
+ }
- if (this.para && value.attributes.class === "xfaRich") {
- const paraStyle = this.para[$toStyle]();
- if (!value.attributes.style) {
- value.attributes.style = paraStyle;
- } else {
- for (const [key, val] of Object.entries(paraStyle)) {
- if (!(key in value.attributes.style)) {
- value.attributes.style[key] = val;
+ const paraStyle = this.para[$toStyle]();
+ if (!value.attributes.style) {
+ value.attributes.style = paraStyle;
+ } else {
+ for (const [key, val] of Object.entries(paraStyle)) {
+ if (!(key in value.attributes.style)) {
+ value.attributes.style[key] = val;
+ }
}
}
}
}
- html = addExtraDivForMargin(html);
-
- return html;
+ return HTMLResult.success(extra, bbox);
}
}
@@ -1310,7 +1558,11 @@ class Edge extends XFAObject {
"lowered",
"raised",
]);
- this.thickness = getMeasurement(attributes.thickness, "0.5pt");
+ // Cheat the thickness to have something nice at the end
+ this.thickness = Math.max(
+ 1,
+ Math.round(getMeasurement(attributes.thickness, "0.5pt"))
+ );
this.use = attributes.use || "";
this.usehref = attributes.usehref || "";
this.color = null;
@@ -1322,8 +1574,8 @@ class Edge extends XFAObject {
const style = toStyle(this, "visibility");
Object.assign(style, {
linecap: this.cap,
- width: measureToString(this.thickness),
- color: this.color ? this.color[$toHTML]() : "#000000",
+ width: measureToString(Math.max(1, Math.round(this.thickness))),
+ color: this.color ? this.color[$toStyle]() : "#000000",
style: "",
});
@@ -1542,13 +1794,13 @@ class ExData extends ContentObject {
return false;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
if (this.contentType !== "text/html" || !this[$content]) {
// TODO: fix other cases.
- return null;
+ return HTMLResult.EMPTY;
}
- return this[$content][$toHTML]();
+ return this[$content][$toHTML](availableSpace);
}
}
@@ -1602,7 +1854,7 @@ class ExclGroup extends XFAObject {
this.colSpan = getInteger({
data: attributes.colSpan,
defaultValue: 1,
- validate: x => x >= 1,
+ validate: n => n >= 1 || n === -1,
});
this.h = attributes.h ? getMeasurement(attributes.h) : "";
this.hAlign = getStringOption(attributes.hAlign, [
@@ -1683,29 +1935,159 @@ class ExclGroup extends XFAObject {
}
}
- [$toHTML]() {
- if (!this.value) {
- return null;
+ [$flushHTML]() {
+ return flushHTML(this);
+ }
+
+ [$addHTML](html, bbox) {
+ addHTML(this, html, bbox);
+ }
+
+ [$getAvailableSpace]() {
+ return getAvailableSpace(this);
+ }
+
+ [$toHTML](availableSpace) {
+ if (
+ this.presence === "hidden" ||
+ this.presence === "inactive" ||
+ this.h === 0 ||
+ this.w === 0
+ ) {
+ return HTMLResult.EMPTY;
}
- const style = toStyle(this, "dimensions", "position", "anchorType");
+ fixDimensions(this);
+
+ const children = [];
const attributes = {
- style,
id: this[$uid],
- class: "xfaExclgroup",
};
- const children = this[$childrenToHTML]({
- // TODO: exObject & exclGroup
- filter: new Set(["field"]),
- include: true,
+ if (!this[$extra]) {
+ this[$extra] = Object.create(null);
+ }
+
+ Object.assign(this[$extra], {
+ children,
+ attributes,
+ attempt: 0,
+ availableSpace,
+ width: 0,
+ height: 0,
+ prevHeight: 0,
+ currentWidth: 0,
});
- return {
+ switch (checkDimensions(this, availableSpace)) {
+ case NOTHING:
+ return HTMLResult.EMPTY;
+ case NOSPACE:
+ return HTMLResult.FAILURE;
+ default:
+ break;
+ }
+
+ availableSpace = {
+ width: this.w === "" ? availableSpace.width : this.w,
+ height: this.h === "" ? availableSpace.height : this.h,
+ };
+
+ const filter = new Set(["field"]);
+
+ if (this.layout === "row") {
+ const columnWidths = this[$getParent]().columnWidths;
+ if (Array.isArray(columnWidths) && columnWidths.length > 0) {
+ this[$extra].columnWidths = columnWidths;
+ this[$extra].currentColumn = 0;
+ }
+ }
+
+ const style = toStyle(
+ this,
+ "anchorType",
+ "dimensions",
+ "position",
+ "presence",
+ "borderMarginPadding",
+ "hAlign"
+ );
+ const clazz = ["xfaExclgroup"];
+ const cl = layoutClass(this);
+ if (cl) {
+ clazz.push(cl);
+ }
+
+ attributes.style = style;
+ attributes.class = clazz.join(" ");
+
+ if (this.name) {
+ attributes.xfaName = this.name;
+ }
+
+ let failure;
+ if (this.layout === "lr-tb" || this.layout === "rl-tb") {
+ for (
+ ;
+ this[$extra].attempt < MAX_ATTEMPTS_FOR_LRTB_LAYOUT;
+ this[$extra].attempt++
+ ) {
+ if (
+ this[$childrenToHTML]({
+ filter,
+ include: true,
+ })
+ ) {
+ break;
+ }
+ }
+
+ failure = this[$extra].attempt === 2;
+ } else {
+ failure = !this[$childrenToHTML]({
+ filter,
+ include: true,
+ });
+ }
+
+ if (failure) {
+ return HTMLResult.FAILURE;
+ }
+
+ let marginH = 0;
+ let marginV = 0;
+ if (this.margin) {
+ marginH = this.margin.leftInset + this.margin.rightInset;
+ marginV = this.margin.topInset + this.margin.bottomInset;
+ }
+
+ if (this.w === "") {
+ style.width = measureToString(this[$extra].width + marginH);
+ }
+ if (this.h === "") {
+ style.height = measureToString(this[$extra].height + marginV);
+ }
+
+ let html = {
name: "div",
attributes,
children,
};
+
+ html = addExtraDivForBorder(html);
+ let bbox;
+ if (this.w !== "" && this.h !== "") {
+ bbox = [this.x, this.y, this.w, this.h];
+ } else {
+ const width = this.w === "" ? marginH + this[$extra].width : this.w;
+ const height = this.h === "" ? marginV + this[$extra].height : this.h;
+
+ bbox = [this.x, this.y, width, height];
+ }
+
+ delete this[$extra];
+
+ return HTMLResult.success(html, bbox);
}
}
@@ -1747,6 +2129,10 @@ class Extras extends XFAObject {
this.text = new XFAObjectArray();
this.time = new XFAObjectArray();
}
+
+ // (Spec) The XFA template grammar defines the extras and desc elements,
+ // which can be used to add human-readable or machine-readable
+ // data to a template.
}
class Field extends XFAObject {
@@ -1773,7 +2159,7 @@ class Field extends XFAObject {
this.colSpan = getInteger({
data: attributes.colSpan,
defaultValue: 1,
- validate: x => x >= 1,
+ validate: n => n >= 1 || n === -1,
});
this.h = attributes.h ? getMeasurement(attributes.h) : "";
this.hAlign = getStringOption(attributes.hAlign, [
@@ -1837,9 +2223,26 @@ class Field extends XFAObject {
_setValue(this, value);
}
- [$toHTML]() {
- if (!this.ui) {
- return null;
+ [$toHTML](availableSpace) {
+ if (
+ !this.ui ||
+ this.presence === "hidden" ||
+ this.presence === "inactive" ||
+ this.h === 0 ||
+ this.w === 0
+ ) {
+ return HTMLResult.EMPTY;
+ }
+
+ fixDimensions(this);
+
+ switch (checkDimensions(this, availableSpace)) {
+ case NOTHING:
+ return HTMLResult.EMPTY;
+ case NOSPACE:
+ return HTMLResult.FAILURE;
+ default:
+ break;
}
const style = toStyle(
@@ -1850,7 +2253,8 @@ class Field extends XFAObject {
"rotate",
"anchorType",
"presence",
- "borderMarginPadding"
+ "borderMarginPadding",
+ "hAlign"
);
const clazz = ["xfaField"];
@@ -1876,9 +2280,12 @@ class Field extends XFAObject {
children,
};
- const ui = this.ui ? this.ui[$toHTML]() : null;
+ const bbox = computeBbox(this, html, availableSpace);
+ html = addExtraDivForBorder(html);
+
+ const ui = this.ui ? this.ui[$toHTML]().html : null;
if (!ui) {
- return html;
+ return HTMLResult.success(html, bbox);
}
if (!ui.attributes.style) {
@@ -1886,13 +2293,24 @@ class Field extends XFAObject {
}
children.push(ui);
- if (this.value && ui.name !== "button") {
- ui.children[0].attributes.value = this.value[$toHTML]().value;
+ if (this.value) {
+ if (this.ui.imageEdit) {
+ ui.children.push(this.value[$toHTML]().html);
+ } else if (ui.name !== "button") {
+ const value = this.value[$toHTML]().html;
+ if (value) {
+ if (ui.children[0].name === "textarea") {
+ ui.children[0].attributes.textContent = value.value;
+ } else {
+ ui.children[0].attributes.value = value.value;
+ }
+ }
+ }
}
- const caption = this.caption ? this.caption[$toHTML]() : null;
+ const caption = this.caption ? this.caption[$toHTML]().html : null;
if (!caption) {
- return html;
+ return HTMLResult.success(html, bbox);
}
if (ui.name === "button") {
@@ -1901,8 +2319,9 @@ class Field extends XFAObject {
if (caption.name === "div") {
caption.name = "span";
}
- ui.children = [caption];
- return html;
+ ui.children.push(caption);
+
+ return HTMLResult.success(html, bbox);
}
ui.children.splice(0, 0, caption);
@@ -1914,9 +2333,11 @@ class Field extends XFAObject {
ui.attributes.style.flexDirection = "row-reverse";
break;
case "top":
+ ui.attributes.style.alignItems = "start";
ui.attributes.style.flexDirection = "column";
break;
case "bottom":
+ ui.attributes.style.alignItems = "start";
ui.attributes.style.flexDirection = "column-reverse";
break;
case "inline":
@@ -1925,9 +2346,7 @@ class Field extends XFAObject {
break;
}
- html = addExtraDivForMargin(html);
-
- return html;
+ return HTMLResult.success(html, bbox);
}
}
@@ -1955,6 +2374,12 @@ class Fill extends XFAObject {
}
[$toStyle]() {
+ const parent = this[$getParent]();
+ let propName = "color";
+ if (parent instanceof Border) {
+ propName = "background";
+ }
+ const style = Object.create(null);
for (const name of Object.getOwnPropertyNames(this)) {
if (name === "extras" || name === "color") {
continue;
@@ -1964,18 +2389,15 @@ class Fill extends XFAObject {
continue;
}
- return {
- color: obj[$toStyle](this.color),
- };
+ style[propName] = obj[$toStyle](this.color);
+ return style;
}
if (this.color) {
- return {
- background: this.color[$toStyle](),
- };
+ style[propName] = this.color[$toStyle]();
}
- return {};
+ return style;
}
}
@@ -2024,8 +2446,10 @@ class Float extends ContentObject {
this[$content] = isNaN(number) ? null : number;
}
- [$toHTML]() {
- return this[$content] !== null ? this[$content].toString() : "";
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(
+ this[$content] !== null ? this[$content].toString() : ""
+ );
}
}
@@ -2088,18 +2512,17 @@ class Font extends XFAObject {
[$toStyle]() {
const style = toStyle(this, "fill");
- const color = style.background;
+ const color = style.color;
if (color) {
if (color === "#000000") {
- delete style.background;
+ // Default font color.
+ delete style.color;
} else if (!color.startsWith("#")) {
// We've a gradient which is not possible for a font color
// so use a workaround.
+ style.background = color;
style.backgroundClip = "text";
style.color = "transparent";
- } else {
- style.color = color;
- delete style.background;
}
}
@@ -2140,12 +2563,12 @@ class Font extends XFAObject {
style.fontStyle = this.posture;
}
- const fontSize = measureToString(this.size);
+ const fontSize = measureToString(0.99 * this.size);
if (fontSize !== "10px") {
style.fontSize = fontSize;
}
- style.fontFamily = this.typeface;
+ style.fontFamily = getFonts(this.typeface);
if (this.underline !== 0) {
style.textDecoration = "underline";
@@ -2253,23 +2676,24 @@ class Image extends StringObject {
// containing a picture.
// In general, we don't get remote data and use what we have
// in the pdf itself, so no picture for non null href.
- return null;
+ return HTMLResult.EMPTY;
}
+ // TODO: Firefox doesn't support natively tiff (and tif) format.
if (this.transferEncoding === "base64") {
const buffer = stringToBytes(atob(this[$content]));
const blob = new Blob([buffer], { type: this.contentType });
- return {
+ return HTMLResult.success({
name: "img",
attributes: {
class: "xfaImage",
style: {},
src: URL.createObjectURL(blob),
},
- };
+ });
}
- return null;
+ return HTMLResult.EMPTY;
}
}
@@ -2284,6 +2708,18 @@ class ImageEdit extends XFAObject {
this.extras = null;
this.margin = null;
}
+
+ [$toHTML](availableSpace) {
+ if (this.data === "embed") {
+ return HTMLResult.success({
+ name: "div",
+ children: [],
+ attributes: {},
+ });
+ }
+
+ return HTMLResult.EMPTY;
+ }
}
class Integer extends ContentObject {
@@ -2300,8 +2736,10 @@ class Integer extends ContentObject {
this[$content] = isNaN(number) ? null : number;
}
- [$toHTML]() {
- return this[$content] !== null ? this[$content].toString() : "";
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(
+ this[$content] !== null ? this[$content].toString() : ""
+ );
}
}
@@ -2355,6 +2793,14 @@ class Items extends XFAObject {
)
);
}
+
+ [$toHTML]() {
+ const output = [];
+ for (const child of this[$getChildren]()) {
+ output.push(child[$text]());
+ }
+ return HTMLResult.success(output);
+ }
}
class Keep extends XFAObject {
@@ -2559,7 +3005,7 @@ class NumericEdit extends XFAObject {
this.margin = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: incomplete.
const style = toStyle(this, "border", "font", "margin");
const html = {
@@ -2571,13 +3017,13 @@ class NumericEdit extends XFAObject {
},
};
- return {
+ return HTMLResult.success({
name: "label",
attributes: {
class: "xfaLabel",
},
children: [html],
- };
+ });
}
}
@@ -2686,39 +3132,68 @@ class PageArea extends XFAObject {
this.subform = new XFAObjectArray();
}
- [$toHTML]() {
- // TODO: incomplete.
- if (this.contentArea.children.length === 0) {
- return null;
+ [$getNextPage]() {
+ if (!this[$extra]) {
+ this[$extra] = {
+ numberOfUse: 1,
+ };
+ }
+ const parent = this[$getParent]();
+ if (parent.relation === "orderedOccurrence") {
+ if (
+ this.occur &&
+ (this.occur.max === -1 || this[$extra].numberOfUse < this.occur.max)
+ ) {
+ this[$extra].numberOfUse += 1;
+ return this;
+ }
}
- const children = this[$childrenToHTML]({
- filter: new Set(["area", "draw", "field", "subform", "contentArea"]),
- include: true,
- });
+ delete this[$extra];
+ return parent[$getNextPage]();
+ }
- // TODO: handle the case where there are several content areas.
- const contentArea = children.find(
- node => node.attributes.class === "xfaContentarea"
- );
+ [$getAvailableSpace]() {
+ return { width: Infinity, height: Infinity };
+ }
+
+ [$toHTML]() {
+ // TODO: incomplete.
+ if (!this[$extra]) {
+ this[$extra] = {
+ numberOfUse: 1,
+ };
+ }
+
+ const children = [];
+ this[$extra].children = children;
const style = Object.create(null);
if (this.medium && this.medium.short && this.medium.long) {
style.width = measureToString(this.medium.short);
style.height = measureToString(this.medium.long);
+ if (this.medium.orientation === "landscape") {
+ const x = style.width;
+ style.width = style.height;
+ style.height = x;
+ }
} else {
- // TODO: compute it from contentAreas
+ warn("XFA - No medium specified in pageArea: please file a bug.");
}
- return {
+ this[$childrenToHTML]({
+ filter: new Set(["area", "draw", "field", "subform", "contentArea"]),
+ include: true,
+ });
+
+ return HTMLResult.success({
name: "div",
children,
attributes: {
id: this[$uid],
style,
},
- contentArea,
- };
+ });
}
}
@@ -2745,18 +3220,71 @@ class PageSet extends XFAObject {
this.pageSet = new XFAObjectArray();
}
- [$toHTML]() {
- // TODO: incomplete.
- return {
- name: "div",
- children: this[$childrenToHTML]({
- filter: new Set(["pageArea", "pageSet"]),
- include: true,
- }),
- attributes: {
- id: this[$uid],
- },
- };
+ [$getNextPage]() {
+ if (!this[$extra]) {
+ this[$extra] = {
+ numberOfUse: 1,
+ currentIndex: -1,
+ };
+ }
+
+ if (this.relation === "orderedOccurrence") {
+ if (this[$extra].currentIndex + 1 < this.pageArea.children.length) {
+ this[$extra].currentIndex += 1;
+ return this.pageArea.children[this[$extra].currentIndex];
+ }
+
+ if (this[$extra].currentIndex + 1 < this.pageSet.children.length) {
+ this[$extra].currentIndex += 1;
+ return this.pageSet.children[this[$extra].currentIndex];
+ }
+
+ if (
+ this.occur &&
+ (this.occur.max === -1 || this[$extra].numberOfUse < this.occur.max)
+ ) {
+ this[$extra].numberOfUse += 1;
+ this[$extra].currentIndex = 0;
+ if (this.pageArea.children.length > 0) {
+ return this.pageArea.children[0];
+ }
+ return this.pageSet.children[0][$getNextPage]();
+ }
+
+ delete this[$extra];
+ const parent = this[$getParent]();
+ if (parent instanceof PageSet) {
+ return parent[$getNextPage]();
+ }
+
+ return this[$getNextPage]();
+ }
+ const pageNumber = getRoot(this)[$extra].pageNumber;
+ const parity = pageNumber % 2 === 0 ? "even" : "odd";
+ const position = pageNumber === 0 ? "first" : "rest";
+
+ let page = this.pageArea.children.find(
+ p => p.oddOrEven === parity && p.pagePosition === position
+ );
+ if (page) {
+ return page;
+ }
+
+ page = this.pageArea.children.find(
+ p => p.oddOrEven === "any" && p.pagePosition === position
+ );
+ if (page) {
+ return page;
+ }
+
+ page = this.pageArea.children.find(
+ p => p.oddOrEven === "any" && p.pagePosition === "any"
+ );
+ if (page) {
+ return page;
+ }
+
+ return this.pageArea.children[0];
}
}
@@ -2837,11 +3365,10 @@ class Para extends XFAObject {
}
if (this.textIndent !== "") {
style.textIndent = measureToString(this.textIndent);
+ fixTextIndent(style);
}
- // TODO: vAlign
-
- if (this.lineHeight !== "") {
+ if (this.lineHeight > 0) {
style.lineHeight = measureToString(this.lineHeight);
}
@@ -3272,7 +3799,7 @@ class Subform extends XFAObject {
this.colSpan = getInteger({
data: attributes.colSpan,
defaultValue: 1,
- validate: x => x >= 1,
+ validate: n => n >= 1 || n === -1,
});
this.columnWidths = (attributes.columnWidths || "")
.trim()
@@ -3356,11 +3883,95 @@ class Subform extends XFAObject {
this.subformSet = new XFAObjectArray();
}
- [$toHTML]() {
- // TODO: incomplete.
- this[$extra] = Object.create(null);
+ [$flushHTML]() {
+ return flushHTML(this);
+ }
- if (this.layout === "row") {
+ [$addHTML](html, bbox) {
+ addHTML(this, html, bbox);
+ }
+
+ [$getAvailableSpace]() {
+ return getAvailableSpace(this);
+ }
+
+ [$toHTML](availableSpace) {
+ if (this.name === "helpText") {
+ return HTMLResult.EMPTY;
+ }
+ if (this[$extra] && this[$extra].afterBreakAfter) {
+ const ret = this[$extra].afterBreakAfter;
+ delete this[$extra];
+ return ret;
+ }
+
+ if (this.presence === "hidden" || this.presence === "inactive") {
+ return HTMLResult.EMPTY;
+ }
+
+ if (
+ this.breakBefore.children.length > 1 ||
+ this.breakAfter.children.length > 1
+ ) {
+ // Specs are always talking about the breakBefore element
+ // and it doesn't really make sense to have several ones.
+ warn(
+ "XFA - Several breakBefore or breakAfter in subforms: please file a bug."
+ );
+ }
+
+ // TODO: implement usehref (probably in bind.js).
+
+ // TODO: incomplete.
+ fixDimensions(this);
+ const children = [];
+ const attributes = {
+ id: this[$uid],
+ };
+
+ if (!this[$extra]) {
+ this[$extra] = Object.create(null);
+ }
+
+ Object.assign(this[$extra], {
+ children,
+ attributes,
+ attempt: 0,
+ availableSpace,
+ width: 0,
+ height: 0,
+ prevHeight: 0,
+ currentWidth: 0,
+ });
+
+ if (this.breakBefore.children.length >= 1) {
+ const breakBefore = this.breakBefore.children[0];
+ if (!breakBefore[$extra]) {
+ breakBefore[$extra] = true;
+ getRoot(this)[$break](breakBefore);
+ return HTMLResult.FAILURE;
+ }
+ }
+
+ switch (checkDimensions(this, availableSpace)) {
+ case NOTHING:
+ return HTMLResult.EMPTY;
+ case NOSPACE:
+ return HTMLResult.FAILURE;
+ default:
+ break;
+ }
+
+ const filter = new Set([
+ "area",
+ "draw",
+ "exclGroup",
+ "field",
+ "subform",
+ "subformSet",
+ ]);
+
+ if (this.layout.includes("row")) {
const columnWidths = this[$getParent]().columnWidths;
if (Array.isArray(columnWidths) && columnWidths.length > 0) {
this[$extra].columnWidths = columnWidths;
@@ -3368,62 +3979,98 @@ class Subform extends XFAObject {
}
}
- 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 = toStyle(this, "dimensions", "position", "presence");
+ const style = toStyle(
+ this,
+ "anchorType",
+ "dimensions",
+ "position",
+ "presence",
+ "borderMarginPadding",
+ "hAlign"
+ );
const clazz = ["xfaSubform"];
const cl = layoutClass(this);
if (cl) {
clazz.push(cl);
}
- const attributes = {
- style,
- id: this[$uid],
- class: clazz.join(" "),
- };
+ attributes.style = style;
+ attributes.class = clazz.join(" ");
if (this.name) {
attributes.xfaName = this.name;
}
- const children = this[$childrenToHTML]({
- // TODO: exObject & exclGroup
- filter: new Set(["area", "draw", "field", "subform", "subformSet"]),
- include: true,
- });
+ let failure;
+ if (this.layout === "lr-tb" || this.layout === "rl-tb") {
+ for (
+ ;
+ this[$extra].attempt < MAX_ATTEMPTS_FOR_LRTB_LAYOUT;
+ this[$extra].attempt++
+ ) {
+ if (
+ this[$childrenToHTML]({
+ filter,
+ include: true,
+ })
+ ) {
+ break;
+ }
+ }
- const html = {
+ failure = this[$extra].attempt === 2;
+ } else {
+ failure = !this[$childrenToHTML]({
+ filter,
+ include: true,
+ });
+ }
+
+ if (failure) {
+ return HTMLResult.FAILURE;
+ }
+
+ let marginH = 0;
+ let marginV = 0;
+ if (this.margin) {
+ marginH = this.margin.leftInset + this.margin.rightInset;
+ marginV = this.margin.topInset + this.margin.bottomInset;
+ }
+
+ if (this.w === "") {
+ style.width = measureToString(this[$extra].width + marginH);
+ }
+ if (this.h === "") {
+ style.height = measureToString(this[$extra].height + marginV);
+ }
+
+ let html = {
name: "div",
attributes,
children,
};
- if (page) {
- page.contentArea.children.push(html);
- delete page.contentArea;
- return page;
+ html = addExtraDivForBorder(html);
+ let bbox;
+ if (this.w !== "" && this.h !== "") {
+ bbox = [this.x, this.y, this.w, this.h];
+ } else {
+ const width = this.w === "" ? marginH + this[$extra].width : this.w;
+ const height = this.h === "" ? marginV + this[$extra].height : this.h;
+
+ bbox = [this.x, this.y, width, height];
}
- return html;
+ if (this.breakAfter.children.length >= 1) {
+ const breakAfter = this.breakAfter.children[0];
+ getRoot(this)[$break](breakAfter);
+ this[$extra].afterBreakAfter = HTMLResult.success(html, bbox);
+ return HTMLResult.FAILURE;
+ }
+
+ delete this[$extra];
+
+ return HTMLResult.success(html, bbox);
}
}
@@ -3451,6 +4098,27 @@ class SubformSet extends XFAObject {
this.subform = new XFAObjectArray();
this.subformSet = new XFAObjectArray();
}
+
+ [$toHTML]() {
+ const children = [];
+ if (!this[$extra]) {
+ this[$extra] = Object.create(null);
+ }
+ this[$extra].children = children;
+
+ this[$childrenToHTML]({
+ filter: new Set(["subform", "subformSet"]),
+ include: true,
+ });
+
+ return HTMLResult.success({
+ name: "div",
+ children,
+ attributes: {
+ id: this[$uid],
+ },
+ });
+ }
}
class SubjectDN extends ContentObject {
@@ -3557,14 +4225,197 @@ class Template extends XFAObject {
}
}
- [$toHTML]() {
- if (this.subform.children.length > 0) {
- return this.subform.children[0][$toHTML]();
+ [$break](node) {
+ this[$extra].breakingNode = node;
+ }
+
+ [$searchNode](expr, container) {
+ if (expr.startsWith("#")) {
+ // This is an id.
+ return [this[$ids].get(expr.slice(1))];
}
- return {
+ return searchNode(this, container, expr, true, true);
+ }
+
+ [$toHTML]() {
+ if (!this.subform.children.length) {
+ return HTMLResult.success({
+ name: "div",
+ children: [],
+ });
+ }
+
+ this[$extra] = {
+ breakingNode: null,
+ pageNumber: 1,
+ pagePosition: "first",
+ oddOrEven: "odd",
+ blankOrNotBlank: "nonBlank",
+ };
+
+ const root = this.subform.children[0];
+ const pageAreas = root.pageSet.pageArea.children;
+ const mainHtml = {
name: "div",
children: [],
};
+
+ let pageArea = null;
+ let breakBefore = null;
+ let breakBeforeTarget = null;
+ if (root.breakBefore.children.length >= 1) {
+ breakBefore = root.breakBefore.children[0];
+ breakBeforeTarget = breakBefore.target;
+ } else if (
+ root.subform.children.length >= 1 &&
+ root.subform.children[0].breakBefore.children.length >= 1
+ ) {
+ breakBefore = root.subform.children[0].breakBefore.children[0];
+ breakBeforeTarget = breakBefore.target;
+ } else if (root.break && root.break.beforeTarget) {
+ breakBefore = root.break;
+ breakBeforeTarget = breakBefore.beforeTarget;
+ } else if (
+ root.subform.children.length >= 1 &&
+ root.subform.children[0].break &&
+ root.subform.children[0].break.beforeTarget
+ ) {
+ breakBefore = root.subform.children[0].break;
+ breakBeforeTarget = breakBefore.beforeTarget;
+ }
+
+ if (breakBefore) {
+ const target = this[$searchNode](
+ breakBeforeTarget,
+ breakBefore[$getParent]()
+ );
+ if (target instanceof PageArea) {
+ pageArea = target;
+ // Consume breakBefore.
+ breakBefore[$extra] = {};
+ }
+ }
+
+ if (!pageArea) {
+ pageArea = pageAreas[0];
+ }
+
+ const pageAreaParent = pageArea[$getParent]();
+ pageAreaParent[$extra] = {
+ numberOfUse: 1,
+ currentIndex: pageAreaParent.pageArea.children.indexOf(pageArea),
+ };
+
+ let targetPageArea;
+ let leader = null;
+ let trailer = null;
+
+ while (true) {
+ targetPageArea = null;
+ const page = pageArea[$toHTML]().html;
+ mainHtml.children.push(page);
+
+ if (leader) {
+ page.children.push(leader[$toHTML](page[$extra].space).html);
+ leader = null;
+ }
+
+ if (trailer) {
+ page.children.push(trailer[$toHTML](page[$extra].space).html);
+ trailer = null;
+ }
+
+ const contentAreas = pageArea.contentArea.children;
+ const htmlContentAreas = page.children.filter(
+ node => node.attributes.class === "xfaContentarea"
+ );
+ for (let i = 0, ii = contentAreas.length; i < ii; i++) {
+ const contentArea = (this[$extra].currentContentArea = contentAreas[i]);
+ const space = { width: contentArea.w, height: contentArea.h };
+
+ if (leader) {
+ htmlContentAreas[i].children.push(leader[$toHTML](space).html);
+ leader = null;
+ }
+
+ if (trailer) {
+ htmlContentAreas[i].children.push(trailer[$toHTML](space).html);
+ trailer = null;
+ }
+
+ let html = root[$toHTML](space);
+ if (html.success) {
+ if (html.html) {
+ htmlContentAreas[i].children.push(html.html);
+ }
+ return mainHtml;
+ }
+
+ // Check for breakBefore / breakAfter
+ let mustBreak = false;
+ if (this[$extra].breakingNode) {
+ const node = this[$extra].breakingNode;
+ this[$extra].breakingNode = null;
+
+ if (node.targetType === "auto") {
+ // Just ignore the break and do layout again.
+ i--;
+ continue;
+ }
+
+ const startNew = node.startNew === 1;
+
+ if (node.leader) {
+ leader = this[$searchNode](node.leader, node[$getParent]());
+ leader = leader ? leader[0] : null;
+ }
+
+ if (node.trailer) {
+ trailer = this[$searchNode](node.trailer, node[$getParent]());
+ trailer = trailer ? trailer[0] : null;
+ }
+
+ let target = null;
+ if (node.target) {
+ target = this[$searchNode](node.target, node[$getParent]());
+ target = target ? target[0] : target;
+ }
+
+ if (node.targetType === "pageArea") {
+ if (startNew) {
+ mustBreak = true;
+ } else if (target === pageArea || !(target instanceof PageArea)) {
+ // Just ignore the break and do layout again.
+ i--;
+ continue;
+ } else {
+ // We must stop the contentAreas filling and go to the next page.
+ targetPageArea = target;
+ mustBreak = true;
+ }
+ } else if (
+ target === "contentArea" ||
+ !(target instanceof ContentArea)
+ ) {
+ // Just ignore the break and do layout again.
+ i--;
+ continue;
+ }
+ }
+
+ html = root[$flushHTML]();
+ if (html) {
+ htmlContentAreas[i].children.push(html);
+ }
+
+ if (mustBreak) {
+ break;
+ }
+ }
+
+ this[$extra].pageNumber += 1;
+ pageArea = targetPageArea || pageArea[$getNextPage]();
+ }
}
}
@@ -3592,7 +4443,7 @@ class Text extends ContentObject {
return false;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
if (typeof this[$content] === "string") {
// \u2028 is a line separator.
// \u2029 is a paragraph separator.
@@ -3652,10 +4503,10 @@ class Text extends ContentObject {
});
}
- return html;
+ return HTMLResult.success(html);
}
- return this[$content][$toHTML]();
+ return this[$content][$toHTML](availableSpace);
}
}
@@ -3691,7 +4542,7 @@ class TextEdit extends XFAObject {
this.margin = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: incomplete.
const style = toStyle(this, "border", "font", "margin");
let html;
@@ -3714,13 +4565,13 @@ class TextEdit extends XFAObject {
};
}
- return {
+ return HTMLResult.success({
name: "label",
attributes: {
class: "xfaLabel",
},
children: [html],
- };
+ });
}
}
@@ -3738,8 +4589,8 @@ class Time extends StringObject {
this[$content] = new Date(this[$content]);
}
- [$toHTML]() {
- return this[$content].toString();
+ [$toHTML](availableSpace) {
+ return HTMLResult.success(this[$content].toString());
}
}
@@ -3828,7 +4679,7 @@ class Ui extends XFAObject {
this.textEdit = null;
}
- [$toHTML]() {
+ [$toHTML](availableSpace) {
// TODO: picture.
for (const name of Object.getOwnPropertyNames(this)) {
if (name === "extras" || name === "picture") {
@@ -3839,9 +4690,9 @@ class Ui extends XFAObject {
continue;
}
- return obj[$toHTML]();
+ return obj[$toHTML](availableSpace);
}
- return null;
+ return HTMLResult.EMPTY;
}
}
@@ -3903,6 +4754,17 @@ class Value extends XFAObject {
}
[$setValue](value) {
+ const parent = this[$getParent]();
+ if (parent instanceof Field) {
+ if (parent.ui && parent.ui.imageEdit) {
+ if (!this.image) {
+ this.image = new Image({});
+ }
+ this.image[$content] = value[$content];
+ return;
+ }
+ }
+
const valueName = value[$nodeName];
if (this[valueName] !== null) {
this[valueName][$content] = value[$content];
@@ -3922,17 +4784,33 @@ class Value extends XFAObject {
this[$appendChild](value);
}
- [$toHTML]() {
+ [$text]() {
+ if (this.exData) {
+ return this.exData[$content][$text]().trim();
+ }
+ for (const name of Object.getOwnPropertyNames(this)) {
+ if (name === "image") {
+ continue;
+ }
+ const obj = this[name];
+ if (obj instanceof XFAObject) {
+ return (obj[$content] || "").toString().trim();
+ }
+ }
+ return null;
+ }
+
+ [$toHTML](availableSpace) {
for (const name of Object.getOwnPropertyNames(this)) {
const obj = this[name];
if (!(obj instanceof XFAObject)) {
continue;
}
- return obj[$toHTML]();
+ return obj[$toHTML](availableSpace);
}
- return null;
+ return HTMLResult.EMPTY;
}
}
diff --git a/src/core/xfa/utils.js b/src/core/xfa/utils.js
index 872fff44f..585ed5464 100644
--- a/src/core/xfa/utils.js
+++ b/src/core/xfa/utils.js
@@ -18,6 +18,7 @@ const dimConverters = {
cm: x => (x / 2.54) * 72,
mm: x => (x / (10 * 2.54)) * 72,
in: x => x * 72,
+ px: x => x,
};
const measurementPattern = /([+-]?[0-9]+\.?[0-9]*)(.*)/;
@@ -163,6 +164,21 @@ function getBBox(data) {
return { x, y, width, height };
}
+class HTMLResult {
+ constructor(success, html, bbox) {
+ this.success = success;
+ this.html = html;
+ this.bbox = bbox;
+ }
+
+ static success(html, bbox = null) {
+ return new HTMLResult(true, html, bbox);
+ }
+}
+
+HTMLResult.FAILURE = new HTMLResult(false, null, null);
+HTMLResult.EMPTY = new HTMLResult(true, null, null);
+
export {
getBBox,
getColor,
@@ -173,4 +189,5 @@ export {
getRatio,
getRelevant,
getStringOption,
+ HTMLResult,
};
diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js
index 8c167a679..4d62c20ea 100644
--- a/src/core/xfa/xfa_object.js
+++ b/src/core/xfa/xfa_object.js
@@ -13,14 +13,16 @@
* limitations under the License.
*/
-import { getInteger, getKeyword } from "./utils.js";
+import { getInteger, getKeyword, HTMLResult } from "./utils.js";
import { shadow, warn } from "../../shared/util.js";
import { NamespaceIds } from "./namespaces.js";
// We use these symbols to avoid name conflict between tags
// and properties/methods names.
const $acceptWhitespace = Symbol();
+const $addHTML = Symbol();
const $appendChild = Symbol();
+const $break = Symbol();
const $childrenToHTML = Symbol();
const $clean = Symbol();
const $cleanup = Symbol();
@@ -31,16 +33,21 @@ const $data = Symbol("data");
const $dump = Symbol();
const $extra = Symbol("extra");
const $finalize = Symbol();
+const $flushHTML = Symbol();
const $getAttributeIt = Symbol();
+const $getAvailableSpace = Symbol();
const $getChildrenByClass = Symbol();
const $getChildrenByName = Symbol();
const $getChildrenByNameIt = Symbol();
+const $getDataValue = Symbol();
const $getRealChildrenByNameIt = Symbol();
const $getChildren = Symbol();
+const $getNextPage = Symbol();
const $getParent = Symbol();
const $global = Symbol();
const $hasItem = Symbol();
const $hasSettableValue = Symbol();
+const $ids = Symbol();
const $indexOf = Symbol();
const $insertAt = Symbol();
const $isDataValue = Symbol();
@@ -55,6 +62,7 @@ const $onChildCheck = Symbol();
const $onText = Symbol();
const $removeChild = Symbol();
const $resolvePrototypes = Symbol();
+const $searchNode = Symbol();
const $setId = Symbol();
const $setSetAttributes = Symbol();
const $setValue = Symbol();
@@ -70,6 +78,7 @@ const _children = Symbol("_children");
const _cloneAttribute = Symbol();
const _dataValue = Symbol();
const _defaultValue = Symbol();
+const _filteredChildrenGenerator = Symbol();
const _getPrototype = Symbol();
const _getUnsetAttributes = Symbol();
const _hasChildren = Symbol();
@@ -270,20 +279,67 @@ class XFAObject {
}
[$toHTML]() {
+ return HTMLResult.EMPTY;
+ }
+
+ *[_filteredChildrenGenerator](filter, include) {
+ for (const node of this[$getChildren]()) {
+ if (!filter || include === filter.has(node[$nodeName])) {
+ const availableSpace = this[$getAvailableSpace]();
+ const res = node[$toHTML](availableSpace);
+ if (!res.success) {
+ this[$extra].failingNode = node;
+ }
+ yield res;
+ }
+ }
+ }
+
+ [$flushHTML]() {
return null;
}
+ [$addHTML](html, bbox) {
+ this[$extra].children.push(html);
+ }
+
+ [$getAvailableSpace]() {}
+
[$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);
- }
+ if (!this[$extra].generator) {
+ this[$extra].generator = this[_filteredChildrenGenerator](
+ filter,
+ include
+ );
+ } else {
+ const availableSpace = this[$getAvailableSpace]();
+ const res = this[$extra].failingNode[$toHTML](availableSpace);
+ if (!res.success) {
+ return false;
}
- });
- return res;
+ if (res.html) {
+ this[$addHTML](res.html, res.bbox);
+ }
+ delete this[$extra].failingNode;
+ }
+
+ while (true) {
+ const gen = this[$extra].generator.next();
+ if (gen.done) {
+ break;
+ }
+ const res = gen.value;
+ if (!res.success) {
+ return false;
+ }
+ if (res.html) {
+ this[$addHTML](res.html, res.bbox);
+ }
+ }
+
+ this[$extra].generator = null;
+
+ return true;
}
[$setSetAttributes](attributes) {
@@ -640,13 +696,13 @@ class XmlObject extends XFAObject {
[$toHTML]() {
if (this[$nodeName] === "#text") {
- return {
+ return HTMLResult.success({
name: "#text",
value: this[$content],
- };
+ });
}
- return null;
+ return HTMLResult.EMPTY;
}
[$getChildren](name = null) {
@@ -710,11 +766,27 @@ class XmlObject extends XFAObject {
[$isDataValue]() {
if (this[_dataValue] === null) {
- return this[_children].length === 0;
+ return (
+ this[_children].length === 0 ||
+ this[_children][0][$namespaceId] === NamespaceIds.xhtml.id
+ );
}
return this[_dataValue];
}
+ [$getDataValue]() {
+ if (this[_dataValue] === null) {
+ if (this[_children].length === 0) {
+ return this[$content].trim();
+ }
+ if (this[_children][0][$namespaceId] === NamespaceIds.xhtml.id) {
+ return this[_children][0][$text]().trim();
+ }
+ return null;
+ }
+ return this[$content].trim();
+ }
+
[$dump]() {
const dumped = Object.create(null);
if (this[$content]) {
@@ -811,7 +883,9 @@ class Option10 extends IntegerObject {
export {
$acceptWhitespace,
+ $addHTML,
$appendChild,
+ $break,
$childrenToHTML,
$clean,
$cleanup,
@@ -822,16 +896,21 @@ export {
$dump,
$extra,
$finalize,
+ $flushHTML,
$getAttributeIt,
+ $getAvailableSpace,
$getChildren,
$getChildrenByClass,
$getChildrenByName,
$getChildrenByNameIt,
+ $getDataValue,
+ $getNextPage,
$getParent,
$getRealChildrenByNameIt,
$global,
$hasItem,
$hasSettableValue,
+ $ids,
$indexOf,
$insertAt,
$isDataValue,
@@ -845,6 +924,7 @@ export {
$onText,
$removeChild,
$resolvePrototypes,
+ $searchNode,
$setId,
$setSetAttributes,
$setValue,
diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js
index 3dfdfa997..efa1b8ecd 100644
--- a/src/core/xfa/xhtml.js
+++ b/src/core/xfa/xhtml.js
@@ -17,6 +17,7 @@ import {
$acceptWhitespace,
$childrenToHTML,
$content,
+ $extra,
$nodeName,
$onText,
$text,
@@ -24,8 +25,8 @@ import {
XmlObject,
} from "./xfa_object.js";
import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
-import { getMeasurement } from "./utils.js";
-import { measureToString } from "./html_utils.js";
+import { fixTextIndent, getFonts, measureToString } from "./html_utils.js";
+import { getMeasurement, HTMLResult } from "./utils.js";
const XHTML_NS_ID = NamespaceIds.xhtml.id;
@@ -79,14 +80,16 @@ const StyleMapping = new Map([
],
["xfa-spacerun", ""],
["xfa-tab-stops", ""],
- ["font-size", value => measureToString(getMeasurement(value))],
+ ["font-size", value => measureToString(1 * getMeasurement(value))],
["letter-spacing", value => measureToString(getMeasurement(value))],
- ["line-height", value => measureToString(getMeasurement(value))],
+ ["line-height", value => measureToString(0.99 * getMeasurement(value))],
["margin", value => measureToString(getMeasurement(value))],
["margin-bottom", value => measureToString(getMeasurement(value))],
["margin-left", value => measureToString(getMeasurement(value))],
["margin-right", value => measureToString(getMeasurement(value))],
["margin-top", value => measureToString(getMeasurement(value))],
+ ["text-indent", value => measureToString(getMeasurement(value))],
+ ["font-family", value => getFonts(value)],
]);
const spacesRegExp = /\s+/g;
@@ -121,6 +124,8 @@ function mapStyle(styleStr) {
newValue;
}
}
+
+ fixTextIndent(style);
return style;
}
@@ -162,16 +167,27 @@ class XhtmlObject extends XmlObject {
}
}
- [$toHTML]() {
- return {
+ [$toHTML](availableSpace) {
+ const children = [];
+ this[$extra] = {
+ children,
+ };
+
+ this[$childrenToHTML]({});
+
+ if (children.length === 0 && !this[$content]) {
+ return HTMLResult.EMPTY;
+ }
+
+ return HTMLResult.success({
name: this[$nodeName],
attributes: {
href: this.href,
style: mapStyle(this.style),
},
- children: this[$childrenToHTML]({}),
+ children,
value: this[$content] || "",
- };
+ });
}
}
@@ -193,10 +209,15 @@ class Body extends XhtmlObject {
super(attributes, "body");
}
- [$toHTML]() {
- const html = super[$toHTML]();
+ [$toHTML](availableSpace) {
+ const res = super[$toHTML](availableSpace);
+ const { html } = res;
+ if (!html) {
+ return HTMLResult.EMPTY;
+ }
+ html.name = "div";
html.attributes.class = "xfaRich";
- return html;
+ return res;
}
}
@@ -209,10 +230,10 @@ class Br extends XhtmlObject {
return "\n";
}
- [$toHTML]() {
- return {
+ [$toHTML](availableSpace) {
+ return HTMLResult.success({
name: "br",
- };
+ });
}
}
@@ -221,34 +242,39 @@ class Html extends XhtmlObject {
super(attributes, "html");
}
- [$toHTML]() {
- const children = this[$childrenToHTML]({});
+ [$toHTML](availableSpace) {
+ const children = [];
+ this[$extra] = {
+ children,
+ };
+
+ this[$childrenToHTML]({});
if (children.length === 0) {
- return {
+ return HTMLResult.success({
name: "div",
attributes: {
class: "xfaRich",
style: {},
},
value: this[$content] || "",
- };
+ });
}
if (children.length === 1) {
const child = children[0];
if (child.attributes && child.attributes.class === "xfaRich") {
- return child;
+ return HTMLResult.success(child);
}
}
- return {
+ return HTMLResult.success({
name: "div",
attributes: {
class: "xfaRich",
style: {},
},
children,
- };
+ });
}
}
@@ -274,6 +300,10 @@ class P extends XhtmlObject {
constructor(attributes) {
super(attributes, "p");
}
+
+ [$text]() {
+ return super[$text]() + "\n";
+ }
}
class Span extends XhtmlObject {
diff --git a/src/display/xfa_layer.js b/src/display/xfa_layer.js
index 7bd7d3620..5949d5661 100644
--- a/src/display/xfa_layer.js
+++ b/src/display/xfa_layer.js
@@ -21,7 +21,11 @@ class XfaLayer {
}
if (key !== "style") {
- html.setAttribute(key, value);
+ if (key === "textContent") {
+ html.textContent = value;
+ } else {
+ html.setAttribute(key, value);
+ }
} else {
Object.assign(html.style, value);
}
diff --git a/test/unit/xfa_parser_spec.js b/test/unit/xfa_parser_spec.js
index a83f661e5..603933d7d 100644
--- a/test/unit/xfa_parser_spec.js
+++ b/test/unit/xfa_parser_spec.js
@@ -332,7 +332,7 @@ describe("XFAParser", function () {
[
" The first line of this paragraph is indented a half-inch.\n",
" Successive lines are not indented.\n",
- " This is the last line of the paragraph.\n ",
+ " This is the last line of the paragraph.\n \n",
].join("")
);
});
diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js
index ee408a064..7176fe0df 100644
--- a/test/unit/xfa_tohtml_spec.js
+++ b/test/unit/xfa_tohtml_spec.js
@@ -28,7 +28,7 @@ describe("XFAFactory", function () {
-
+
@@ -43,6 +43,7 @@ describe("XFAFactory", function () {
+
@@ -73,18 +74,23 @@ describe("XFAFactory", function () {
position: "absolute",
});
- const draw = page1.children[1];
+ const wrapper = page1.children[1];
+ const draw = wrapper.children[0];
+
+ expect(wrapper.attributes.class).toEqual("xfaWrapper");
+ expect(wrapper.attributes.style).toEqual({
+ left: "2px",
+ position: "absolute",
+ top: "1px",
+ });
+
expect(draw.attributes.class).toEqual("xfaDraw xfaFont");
expect(draw.attributes.style).toEqual({
color: "#0c1722",
- fontFamily: "Arial",
- fontSize: "7px",
+ fontFamily: "FooBar",
+ fontSize: "6.93px",
height: "22px",
- left: "2px",
padding: "1px 4px 2px 3px",
- position: "absolute",
- textAlign: "left",
- top: "1px",
transform: "rotate(-90deg)",
transformOrigin: "top left",
verticalAlign: "2px",
@@ -93,7 +99,7 @@ describe("XFAFactory", function () {
// draw element must be on each page.
expect(draw.attributes.style).toEqual(
- factory.getPage(1).children[1].attributes.style
+ factory.getPage(1).children[1].children[0].attributes.style
);
});
});
diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css
index ab1deb375..a5e01bdf7 100644
--- a/web/xfa_layer_builder.css
+++ b/web/xfa_layer_builder.css
@@ -24,6 +24,8 @@
.xfaLayer * {
color: inherit;
font: inherit;
+ font-style: inherit;
+ font-weight: inherit;
font-kerning: inherit;
letter-spacing: inherit;
text-align: inherit;
@@ -33,6 +35,14 @@
background: transparent;
}
+.xfaLayer a {
+ color: blue;
+}
+
+.xfaRich li {
+ margin-left: 3em;
+}
+
.xfaFont {
color: black;
font-weight: normal;
@@ -58,6 +68,7 @@
.xfaRich {
z-index: 300;
+ line-height: 1.2;
}
.xfaSubform {
@@ -76,23 +87,52 @@
flex: 1 1 auto;
}
+.xfaBorderDiv {
+ background: transparent;
+ position: absolute;
+ pointer-events: none;
+}
+
+.xfaWrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: auto;
+ height: auto;
+}
+
+.xfaContentArea {
+ overflow: hidden;
+}
+
+.xfaTextfield,
+.xfaSelect {
+ background-color: rgba(0, 54, 255, 0.13);
+}
+
+.xfaTextfield:focus,
+.xfaSelect:focus {
+ background-color: transparent;
+}
+
.xfaTextfield,
.xfaSelect {
width: 100%;
height: 100%;
- flex: 1 1 auto;
+ flex: 100 1 0;
border: none;
resize: none;
}
-.xfaLabel > input[type="checkbox"] {
+.xfaLabel > input[type="radio"] {
/* Use this trick to make the checkbox invisible but
but still focusable. */
position: absolute;
left: -99999px;
}
-.xfaLabel > input[type="checkbox"]:focus + .xfaCheckboxMark {
+.xfaLabel > input[type="radio"]:focus + .xfaCheckboxMark {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
}
@@ -133,19 +173,48 @@
white-space: pre-wrap;
}
-.xfaImage,
-.xfaRich {
+.xfaImage {
width: 100%;
height: 100%;
}
-.xfaLrTb,
-.xfaRlTb,
-.xfaTb,
+.xfaRich {
+ width: 100%;
+ height: auto;
+}
+
.xfaPosition {
display: block;
}
+.xfaLrTb,
+.xfaRlTb,
+.xfaTb {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.xfaLr,
+.xfaRl,
+.xfaTb > div {
+ flex: 1 1 auto;
+}
+
+.xfaTb > div {
+ justify-content: left;
+}
+
+.xfaLr > div {
+ display: inline;
+ float: left;
+}
+
+.xfaRl > div {
+ display: inline;
+ float: right;
+}
+
.xfaPosition {
position: relative;
}