Merge pull request #13402 from calixteman/xfa_layout1

XFA - Fix lot of layout issues
This commit is contained in:
calixteman 2021-05-25 18:31:53 +02:00 committed by GitHub
commit 4d26623e59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2019 additions and 366 deletions

View File

@ -22,6 +22,7 @@ import {
$finalize, $finalize,
$getAttributeIt, $getAttributeIt,
$getChildren, $getChildren,
$getDataValue,
$getParent, $getParent,
$getRealChildrenByNameIt, $getRealChildrenByNameIt,
$global, $global,
@ -88,7 +89,7 @@ class Binder {
if (formNode[$hasSettableValue]()) { if (formNode[$hasSettableValue]()) {
if (data[$isDataValue]()) { if (data[$isDataValue]()) {
const value = data[$content].trim(); const value = data[$getDataValue]();
// TODO: use picture. // TODO: use picture.
formNode[$setValue](createText(value)); formNode[$setValue](createText(value));
formNode[$data] = data; formNode[$data] = data;
@ -114,7 +115,7 @@ class Binder {
} }
} }
_findDataByNameToConsume(name, dataNode, global) { _findDataByNameToConsume(name, isValue, dataNode, global) {
if (!name) { if (!name) {
return null; return null;
} }
@ -130,9 +131,16 @@ class Binder {
/* allTransparent = */ false, /* allTransparent = */ false,
/* skipConsumed = */ true /* skipConsumed = */ true
); );
match = generator.next().value; // Try to find a match of the same kind.
if (match) { while (true) {
return match; match = generator.next().value;
if (!match) {
break;
}
if (isValue === match[$isDataValue]()) {
return match;
}
} }
if ( if (
dataNode[$namespaceId] === NamespaceIds.datasets.id && dataNode[$namespaceId] === NamespaceIds.datasets.id &&
@ -149,7 +157,7 @@ class Binder {
// Secondly, if global try to find it just under the root of datasets // Secondly, if global try to find it just under the root of datasets
// (which is the location of global variables). // (which is the location of global variables).
generator = this.datasets[$getRealChildrenByNameIt]( generator = this.data[$getRealChildrenByNameIt](
name, name,
/* allTransparent = */ false, /* allTransparent = */ false,
/* skipConsumed = */ false /* skipConsumed = */ false
@ -478,6 +486,7 @@ class Binder {
if (child.bind) { if (child.bind) {
switch (child.bind.match) { switch (child.bind.match) {
case "none": case "none":
this._bindElement(child, dataNode);
continue; continue;
case "global": case "global":
global = true; global = true;
@ -485,6 +494,7 @@ class Binder {
case "dataRef": case "dataRef":
if (!child.bind.ref) { if (!child.bind.ref) {
warn(`XFA - ref is empty in node ${child[$nodeName]}.`); warn(`XFA - ref is empty in node ${child[$nodeName]}.`);
this._bindElement(child, dataNode);
continue; continue;
} }
ref = child.bind.ref; ref = child.bind.ref;
@ -545,6 +555,7 @@ class Binder {
while (matches.length < max) { while (matches.length < max) {
const found = this._findDataByNameToConsume( const found = this._findDataByNameToConsume(
child.name, child.name,
child[$hasSettableValue](),
dataNode, dataNode,
global global
); );
@ -580,6 +591,8 @@ class Binder {
} }
this._bindOccurrences(child, match, picture); this._bindOccurrences(child, match, picture);
} else if (min > 0) { } else if (min > 0) {
this._setProperties(child, dataNode);
this._bindItems(child, dataNode);
this._bindElement(child, dataNode); this._bindElement(child, dataNode);
} else { } else {
uselessNodes.push(child); uselessNodes.push(child);

View File

@ -17,6 +17,7 @@ import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { import {
$cleanup, $cleanup,
$finalize, $finalize,
$ids,
$nsAttributes, $nsAttributes,
$onChild, $onChild,
$resolvePrototypes, $resolvePrototypes,
@ -27,13 +28,11 @@ import { Template } from "./template.js";
import { UnknownNamespace } from "./unknown.js"; import { UnknownNamespace } from "./unknown.js";
import { warn } from "../../shared/util.js"; import { warn } from "../../shared/util.js";
const _ids = Symbol();
class Root extends XFAObject { class Root extends XFAObject {
constructor(ids) { constructor(ids) {
super(-1, "root", Object.create(null)); super(-1, "root", Object.create(null));
this.element = null; this.element = null;
this[_ids] = ids; this[$ids] = ids;
} }
[$onChild](child) { [$onChild](child) {
@ -44,7 +43,8 @@ class Root extends XFAObject {
[$finalize]() { [$finalize]() {
super[$finalize](); super[$finalize]();
if (this.element.template instanceof Template) { if (this.element.template instanceof Template) {
this.element.template[$resolvePrototypes](this[_ids]); this.element.template[$resolvePrototypes](this[$ids]);
this.element.template[$ids] = this[$ids];
} }
} }
} }

View File

@ -13,18 +13,38 @@
* limitations under the License. * 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"; 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) { function measureToString(m) {
if (typeof m === "string") { if (typeof m === "string") {
return "0px"; return "0px";
} }
return Number.isInteger(m) ? `${m}px` : `${m.toFixed(2)}px`; return Number.isInteger(m) ? `${m}px` : `${m.toFixed(2)}px`;
} }
const converters = { const converters = {
anchorType(node, style) { 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)) { if (!("transform" in style)) {
style.transform = ""; style.transform = "";
} }
@ -57,12 +77,28 @@ const converters = {
}, },
dimensions(node, style) { dimensions(node, style) {
const parent = node[$getParent](); const parent = node[$getParent]();
const extra = parent[$extra];
let width = node.w; let width = node.w;
if (extra && extra.columnWidths) { const height = node.h;
width = extra.columnWidths[extra.currentColumn]; if (parent.layout && parent.layout.includes("row")) {
extra.currentColumn = const extra = parent[$extra];
(extra.currentColumn + 1) % extra.columnWidths.length; 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 !== "") { if (width !== "") {
@ -72,17 +108,21 @@ const converters = {
if (node.maxW > 0) { if (node.maxW > 0) {
style.maxWidth = measureToString(node.maxW); style.maxWidth = measureToString(node.maxW);
} }
style.minWidth = measureToString(node.minW); if (parent.layout === "position") {
style.minWidth = measureToString(node.minW);
}
} }
if (node.h !== "") { if (height !== "") {
style.height = measureToString(node.h); style.height = measureToString(height);
} else { } else {
style.height = "auto"; style.height = "auto";
if (node.maxH > 0) { if (node.maxH > 0) {
style.maxHeight = measureToString(node.maxH); style.maxHeight = measureToString(node.maxH);
} }
style.minHeight = measureToString(node.minH); if (parent.layout === "position") {
style.minHeight = measureToString(node.minH);
}
} }
}, },
position(node, style) { position(node, style) {
@ -118,22 +158,31 @@ const converters = {
} }
}, },
hAlign(node, style) { hAlign(node, style) {
switch (node.hAlign) { if (node[$nodeName] === "para") {
case "justifyAll": switch (node.hAlign) {
style.textAlign = "justify-all"; case "justifyAll":
break; style.textAlign = "justify-all";
case "radix": break;
// TODO: implement this correctly ! case "radix":
style.textAlign = "left"; // TODO: implement this correctly !
break; style.textAlign = "left";
default: break;
style.textAlign = node.hAlign; default:
style.textAlign = node.hAlign;
}
} else {
switch (node.hAlign) {
case "right":
case "center":
style.justifyContent = node.hAlign;
break;
}
} }
}, },
borderMarginPadding(node, style) { borderMarginPadding(node, style) {
// Get border width in order to compute margin and padding. // Get border width in order to compute margin and padding.
const borderWidths = [0, 0, 0, 0]; const borderWidths = [0, 0, 0, 0];
const marginWidths = [0, 0, 0, 0]; const borderInsets = [0, 0, 0, 0];
const marginNode = node.margin const marginNode = node.margin
? [ ? [
node.margin.topInset, node.margin.topInset,
@ -142,30 +191,211 @@ const converters = {
node.margin.leftInset, node.margin.leftInset,
] ]
: [0, 0, 0, 0]; : [0, 0, 0, 0];
let borderMargin;
if (node.border) { 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)) { if (borderWidths.every(x => x === 0)) {
// No border: margin & padding are padding if (marginNode.every(x => x === 0)) {
if (node.margin) { return;
Object.assign(style, node.margin[$toStyle]());
} }
// No border: margin & padding are padding
Object.assign(style, node.margin[$toStyle]());
style.padding = style.margin; style.padding = style.margin;
delete style.margin; delete style.margin;
} else { delete style.outline;
style.padding = delete style.outlineOffset;
measureToString(marginNode[0] - borderWidths[0] - marginWidths[0]) + return;
" " +
measureToString(marginNode[1] - borderWidths[1] - marginWidths[1]) +
" " +
measureToString(marginNode[2] - borderWidths[2] - marginWidths[2]) +
" " +
measureToString(marginNode[3] - borderWidths[3] - marginWidths[3]);
} }
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) { function layoutClass(node) {
switch (node.layout) { switch (node.layout) {
case "position": case "position":
@ -211,26 +441,145 @@ function toStyle(node, ...names) {
return style; return style;
} }
function addExtraDivForMargin(html) { function addExtraDivForBorder(html) {
const style = html.attributes.style; const style = html.attributes.style;
if (style.margin) { const data = style.borderData;
const padding = style.margin; const children = [];
delete style.margin;
const width = style.width || "auto";
const height = style.height || "auto";
style.width = "100%"; const attributes = {
style.height = "100%"; 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", name: "div",
attributes: { 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,
};

190
src/core/xfa/layout.js Normal file
View File

@ -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 };

View File

@ -35,6 +35,7 @@ class XFAParser extends XMLParserBase {
this._current = this._builder.buildRoot(this._ids); this._current = this._builder.buildRoot(this._ids);
this._errorCode = XMLParserErrorCode.NoError; this._errorCode = XMLParserErrorCode.NoError;
this._whiteRegex = /^\s+$/; this._whiteRegex = /^\s+$/;
this._nbsps = /\xa0+/g;
} }
parse(data) { parse(data) {
@ -50,6 +51,9 @@ class XFAParser extends XMLParserBase {
} }
onText(text) { onText(text) {
// Normally by definition a &nbsp is unbreakable
// but in real life Acrobat can break strings on &nbsp.
text = text.replace(this._nbsps, match => match.slice(1) + " ");
if (this._current[$acceptWhitespace]()) { if (this._current[$acceptWhitespace]()) {
this._current[$onText](text); this._current[$onText](text);
return; return;

View File

@ -275,19 +275,32 @@ function createDataNode(root, container, expr) {
} }
for (let ii = parsed.length; i < ii; i++) { for (let ii = parsed.length; i < ii; i++) {
const { cacheName, index } = parsed[i]; const { name, operator, index } = parsed[i];
if (!isFinite(index)) { if (!isFinite(index)) {
parsed[i].index = 0; parsed[i].index = 0;
return createNodes(root, parsed.slice(i)); return createNodes(root, parsed.slice(i));
} }
const cached = somCache.get(root); let children;
if (!cached) { switch (operator) {
warn(`XFA - createDataNode must be called after searchNode.`); case operators.dot:
return null; 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) { if (children.length === 0) {
return createNodes(root, parsed.slice(i)); return createNodes(root, parsed.slice(i));
} }

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@ const dimConverters = {
cm: x => (x / 2.54) * 72, cm: x => (x / 2.54) * 72,
mm: x => (x / (10 * 2.54)) * 72, mm: x => (x / (10 * 2.54)) * 72,
in: x => x * 72, in: x => x * 72,
px: x => x,
}; };
const measurementPattern = /([+-]?[0-9]+\.?[0-9]*)(.*)/; const measurementPattern = /([+-]?[0-9]+\.?[0-9]*)(.*)/;
@ -163,6 +164,21 @@ function getBBox(data) {
return { x, y, width, height }; 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 { export {
getBBox, getBBox,
getColor, getColor,
@ -173,4 +189,5 @@ export {
getRatio, getRatio,
getRelevant, getRelevant,
getStringOption, getStringOption,
HTMLResult,
}; };

View File

@ -13,14 +13,16 @@
* limitations under the License. * 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 { shadow, warn } from "../../shared/util.js";
import { NamespaceIds } from "./namespaces.js"; import { NamespaceIds } from "./namespaces.js";
// We use these symbols to avoid name conflict between tags // We use these symbols to avoid name conflict between tags
// and properties/methods names. // and properties/methods names.
const $acceptWhitespace = Symbol(); const $acceptWhitespace = Symbol();
const $addHTML = Symbol();
const $appendChild = Symbol(); const $appendChild = Symbol();
const $break = Symbol();
const $childrenToHTML = Symbol(); const $childrenToHTML = Symbol();
const $clean = Symbol(); const $clean = Symbol();
const $cleanup = Symbol(); const $cleanup = Symbol();
@ -31,16 +33,21 @@ const $data = Symbol("data");
const $dump = Symbol(); const $dump = Symbol();
const $extra = Symbol("extra"); const $extra = Symbol("extra");
const $finalize = Symbol(); const $finalize = Symbol();
const $flushHTML = Symbol();
const $getAttributeIt = Symbol(); const $getAttributeIt = Symbol();
const $getAvailableSpace = Symbol();
const $getChildrenByClass = Symbol(); const $getChildrenByClass = Symbol();
const $getChildrenByName = Symbol(); const $getChildrenByName = Symbol();
const $getChildrenByNameIt = Symbol(); const $getChildrenByNameIt = Symbol();
const $getDataValue = Symbol();
const $getRealChildrenByNameIt = Symbol(); const $getRealChildrenByNameIt = Symbol();
const $getChildren = Symbol(); const $getChildren = Symbol();
const $getNextPage = Symbol();
const $getParent = Symbol(); const $getParent = Symbol();
const $global = Symbol(); const $global = Symbol();
const $hasItem = Symbol(); const $hasItem = Symbol();
const $hasSettableValue = Symbol(); const $hasSettableValue = Symbol();
const $ids = Symbol();
const $indexOf = Symbol(); const $indexOf = Symbol();
const $insertAt = Symbol(); const $insertAt = Symbol();
const $isDataValue = Symbol(); const $isDataValue = Symbol();
@ -55,6 +62,7 @@ const $onChildCheck = Symbol();
const $onText = Symbol(); const $onText = Symbol();
const $removeChild = Symbol(); const $removeChild = Symbol();
const $resolvePrototypes = Symbol(); const $resolvePrototypes = Symbol();
const $searchNode = Symbol();
const $setId = Symbol(); const $setId = Symbol();
const $setSetAttributes = Symbol(); const $setSetAttributes = Symbol();
const $setValue = Symbol(); const $setValue = Symbol();
@ -70,6 +78,7 @@ const _children = Symbol("_children");
const _cloneAttribute = Symbol(); const _cloneAttribute = Symbol();
const _dataValue = Symbol(); const _dataValue = Symbol();
const _defaultValue = Symbol(); const _defaultValue = Symbol();
const _filteredChildrenGenerator = Symbol();
const _getPrototype = Symbol(); const _getPrototype = Symbol();
const _getUnsetAttributes = Symbol(); const _getUnsetAttributes = Symbol();
const _hasChildren = Symbol(); const _hasChildren = Symbol();
@ -270,20 +279,67 @@ class XFAObject {
} }
[$toHTML]() { [$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; return null;
} }
[$addHTML](html, bbox) {
this[$extra].children.push(html);
}
[$getAvailableSpace]() {}
[$childrenToHTML]({ filter = null, include = true }) { [$childrenToHTML]({ filter = null, include = true }) {
const res = []; if (!this[$extra].generator) {
this[$getChildren]().forEach(node => { this[$extra].generator = this[_filteredChildrenGenerator](
if (!filter || include === filter.has(node[$nodeName])) { filter,
const html = node[$toHTML](); include
if (html) { );
res.push(html); } else {
} const availableSpace = this[$getAvailableSpace]();
const res = this[$extra].failingNode[$toHTML](availableSpace);
if (!res.success) {
return false;
} }
}); if (res.html) {
return res; 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) { [$setSetAttributes](attributes) {
@ -640,13 +696,13 @@ class XmlObject extends XFAObject {
[$toHTML]() { [$toHTML]() {
if (this[$nodeName] === "#text") { if (this[$nodeName] === "#text") {
return { return HTMLResult.success({
name: "#text", name: "#text",
value: this[$content], value: this[$content],
}; });
} }
return null; return HTMLResult.EMPTY;
} }
[$getChildren](name = null) { [$getChildren](name = null) {
@ -710,11 +766,27 @@ class XmlObject extends XFAObject {
[$isDataValue]() { [$isDataValue]() {
if (this[_dataValue] === null) { if (this[_dataValue] === null) {
return this[_children].length === 0; return (
this[_children].length === 0 ||
this[_children][0][$namespaceId] === NamespaceIds.xhtml.id
);
} }
return this[_dataValue]; 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]() { [$dump]() {
const dumped = Object.create(null); const dumped = Object.create(null);
if (this[$content]) { if (this[$content]) {
@ -811,7 +883,9 @@ class Option10 extends IntegerObject {
export { export {
$acceptWhitespace, $acceptWhitespace,
$addHTML,
$appendChild, $appendChild,
$break,
$childrenToHTML, $childrenToHTML,
$clean, $clean,
$cleanup, $cleanup,
@ -822,16 +896,21 @@ export {
$dump, $dump,
$extra, $extra,
$finalize, $finalize,
$flushHTML,
$getAttributeIt, $getAttributeIt,
$getAvailableSpace,
$getChildren, $getChildren,
$getChildrenByClass, $getChildrenByClass,
$getChildrenByName, $getChildrenByName,
$getChildrenByNameIt, $getChildrenByNameIt,
$getDataValue,
$getNextPage,
$getParent, $getParent,
$getRealChildrenByNameIt, $getRealChildrenByNameIt,
$global, $global,
$hasItem, $hasItem,
$hasSettableValue, $hasSettableValue,
$ids,
$indexOf, $indexOf,
$insertAt, $insertAt,
$isDataValue, $isDataValue,
@ -845,6 +924,7 @@ export {
$onText, $onText,
$removeChild, $removeChild,
$resolvePrototypes, $resolvePrototypes,
$searchNode,
$setId, $setId,
$setSetAttributes, $setSetAttributes,
$setValue, $setValue,

View File

@ -17,6 +17,7 @@ import {
$acceptWhitespace, $acceptWhitespace,
$childrenToHTML, $childrenToHTML,
$content, $content,
$extra,
$nodeName, $nodeName,
$onText, $onText,
$text, $text,
@ -24,8 +25,8 @@ import {
XmlObject, XmlObject,
} from "./xfa_object.js"; } from "./xfa_object.js";
import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; import { $buildXFAObject, NamespaceIds } from "./namespaces.js";
import { getMeasurement } from "./utils.js"; import { fixTextIndent, getFonts, measureToString } from "./html_utils.js";
import { measureToString } from "./html_utils.js"; import { getMeasurement, HTMLResult } from "./utils.js";
const XHTML_NS_ID = NamespaceIds.xhtml.id; const XHTML_NS_ID = NamespaceIds.xhtml.id;
@ -79,14 +80,16 @@ const StyleMapping = new Map([
], ],
["xfa-spacerun", ""], ["xfa-spacerun", ""],
["xfa-tab-stops", ""], ["xfa-tab-stops", ""],
["font-size", value => measureToString(getMeasurement(value))], ["font-size", value => measureToString(1 * getMeasurement(value))],
["letter-spacing", value => measureToString(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", value => measureToString(getMeasurement(value))],
["margin-bottom", value => measureToString(getMeasurement(value))], ["margin-bottom", value => measureToString(getMeasurement(value))],
["margin-left", value => measureToString(getMeasurement(value))], ["margin-left", value => measureToString(getMeasurement(value))],
["margin-right", value => measureToString(getMeasurement(value))], ["margin-right", value => measureToString(getMeasurement(value))],
["margin-top", 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; const spacesRegExp = /\s+/g;
@ -121,6 +124,8 @@ function mapStyle(styleStr) {
newValue; newValue;
} }
} }
fixTextIndent(style);
return style; return style;
} }
@ -162,16 +167,27 @@ class XhtmlObject extends XmlObject {
} }
} }
[$toHTML]() { [$toHTML](availableSpace) {
return { const children = [];
this[$extra] = {
children,
};
this[$childrenToHTML]({});
if (children.length === 0 && !this[$content]) {
return HTMLResult.EMPTY;
}
return HTMLResult.success({
name: this[$nodeName], name: this[$nodeName],
attributes: { attributes: {
href: this.href, href: this.href,
style: mapStyle(this.style), style: mapStyle(this.style),
}, },
children: this[$childrenToHTML]({}), children,
value: this[$content] || "", value: this[$content] || "",
}; });
} }
} }
@ -193,10 +209,15 @@ class Body extends XhtmlObject {
super(attributes, "body"); super(attributes, "body");
} }
[$toHTML]() { [$toHTML](availableSpace) {
const html = super[$toHTML](); const res = super[$toHTML](availableSpace);
const { html } = res;
if (!html) {
return HTMLResult.EMPTY;
}
html.name = "div";
html.attributes.class = "xfaRich"; html.attributes.class = "xfaRich";
return html; return res;
} }
} }
@ -209,10 +230,10 @@ class Br extends XhtmlObject {
return "\n"; return "\n";
} }
[$toHTML]() { [$toHTML](availableSpace) {
return { return HTMLResult.success({
name: "br", name: "br",
}; });
} }
} }
@ -221,34 +242,39 @@ class Html extends XhtmlObject {
super(attributes, "html"); super(attributes, "html");
} }
[$toHTML]() { [$toHTML](availableSpace) {
const children = this[$childrenToHTML]({}); const children = [];
this[$extra] = {
children,
};
this[$childrenToHTML]({});
if (children.length === 0) { if (children.length === 0) {
return { return HTMLResult.success({
name: "div", name: "div",
attributes: { attributes: {
class: "xfaRich", class: "xfaRich",
style: {}, style: {},
}, },
value: this[$content] || "", value: this[$content] || "",
}; });
} }
if (children.length === 1) { if (children.length === 1) {
const child = children[0]; const child = children[0];
if (child.attributes && child.attributes.class === "xfaRich") { if (child.attributes && child.attributes.class === "xfaRich") {
return child; return HTMLResult.success(child);
} }
} }
return { return HTMLResult.success({
name: "div", name: "div",
attributes: { attributes: {
class: "xfaRich", class: "xfaRich",
style: {}, style: {},
}, },
children, children,
}; });
} }
} }
@ -274,6 +300,10 @@ class P extends XhtmlObject {
constructor(attributes) { constructor(attributes) {
super(attributes, "p"); super(attributes, "p");
} }
[$text]() {
return super[$text]() + "\n";
}
} }
class Span extends XhtmlObject { class Span extends XhtmlObject {

View File

@ -21,7 +21,11 @@ class XfaLayer {
} }
if (key !== "style") { if (key !== "style") {
html.setAttribute(key, value); if (key === "textContent") {
html.textContent = value;
} else {
html.setAttribute(key, value);
}
} else { } else {
Object.assign(html.style, value); Object.assign(html.style, value);
} }

View File

@ -332,7 +332,7 @@ describe("XFAParser", function () {
[ [
" The first line of this paragraph is indented a half-inch.\n", " The first line of this paragraph is indented a half-inch.\n",
" Successive lines are not indented.\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("") ].join("")
); );
}); });

View File

@ -28,7 +28,7 @@ describe("XFAFactory", function () {
<contentArea x="123pt" w="456pt" h="789pt"/> <contentArea x="123pt" w="456pt" h="789pt"/>
<medium stock="default" short="456pt" long="789pt"/> <medium stock="default" short="456pt" long="789pt"/>
<draw y="1pt" w="11pt" h="22pt" rotate="90" x="2pt"> <draw y="1pt" w="11pt" h="22pt" rotate="90" x="2pt">
<font size="7pt" typeface="Arial" baselineShift="2pt"> <font size="7pt" typeface="FooBar" baselineShift="2pt">
<fill> <fill>
<color value="12,23,34"/> <color value="12,23,34"/>
<solid/> <solid/>
@ -43,6 +43,7 @@ describe("XFAFactory", function () {
<subform name="first"> <subform name="first">
</subform> </subform>
<subform name="second"> <subform name="second">
<breakBefore targetType="pageArea" startNew="1"/>
</subform> </subform>
</subform> </subform>
</template> </template>
@ -73,18 +74,23 @@ describe("XFAFactory", function () {
position: "absolute", 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.class).toEqual("xfaDraw xfaFont");
expect(draw.attributes.style).toEqual({ expect(draw.attributes.style).toEqual({
color: "#0c1722", color: "#0c1722",
fontFamily: "Arial", fontFamily: "FooBar",
fontSize: "7px", fontSize: "6.93px",
height: "22px", height: "22px",
left: "2px",
padding: "1px 4px 2px 3px", padding: "1px 4px 2px 3px",
position: "absolute",
textAlign: "left",
top: "1px",
transform: "rotate(-90deg)", transform: "rotate(-90deg)",
transformOrigin: "top left", transformOrigin: "top left",
verticalAlign: "2px", verticalAlign: "2px",
@ -93,7 +99,7 @@ describe("XFAFactory", function () {
// draw element must be on each page. // draw element must be on each page.
expect(draw.attributes.style).toEqual( expect(draw.attributes.style).toEqual(
factory.getPage(1).children[1].attributes.style factory.getPage(1).children[1].children[0].attributes.style
); );
}); });
}); });

View File

@ -24,6 +24,8 @@
.xfaLayer * { .xfaLayer * {
color: inherit; color: inherit;
font: inherit; font: inherit;
font-style: inherit;
font-weight: inherit;
font-kerning: inherit; font-kerning: inherit;
letter-spacing: inherit; letter-spacing: inherit;
text-align: inherit; text-align: inherit;
@ -33,6 +35,14 @@
background: transparent; background: transparent;
} }
.xfaLayer a {
color: blue;
}
.xfaRich li {
margin-left: 3em;
}
.xfaFont { .xfaFont {
color: black; color: black;
font-weight: normal; font-weight: normal;
@ -58,6 +68,7 @@
.xfaRich { .xfaRich {
z-index: 300; z-index: 300;
line-height: 1.2;
} }
.xfaSubform { .xfaSubform {
@ -76,23 +87,52 @@
flex: 1 1 auto; 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, .xfaTextfield,
.xfaSelect { .xfaSelect {
width: 100%; width: 100%;
height: 100%; height: 100%;
flex: 1 1 auto; flex: 100 1 0;
border: none; border: none;
resize: none; resize: none;
} }
.xfaLabel > input[type="checkbox"] { .xfaLabel > input[type="radio"] {
/* Use this trick to make the checkbox invisible but /* Use this trick to make the checkbox invisible but
but still focusable. */ but still focusable. */
position: absolute; position: absolute;
left: -99999px; 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); box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
} }
@ -133,19 +173,48 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.xfaImage, .xfaImage {
.xfaRich {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.xfaLrTb, .xfaRich {
.xfaRlTb, width: 100%;
.xfaTb, height: auto;
}
.xfaPosition { .xfaPosition {
display: block; 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 { .xfaPosition {
position: relative; position: relative;
} }