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,
$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);

View File

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

View File

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

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._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 &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]()) {
this._current[$onText](text);
return;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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,

View File

@ -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 {

View File

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

View File

@ -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("")
);
});

View File

@ -28,7 +28,7 @@ describe("XFAFactory", function () {
<contentArea x="123pt" w="456pt" h="789pt"/>
<medium stock="default" short="456pt" long="789pt"/>
<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>
<color value="12,23,34"/>
<solid/>
@ -43,6 +43,7 @@ describe("XFAFactory", function () {
<subform name="first">
</subform>
<subform name="second">
<breakBefore targetType="pageArea" startNew="1"/>
</subform>
</subform>
</template>
@ -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
);
});
});

View File

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