diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js
index d7cbd365c..00d275e3f 100644
--- a/src/core/xfa/html_utils.js
+++ b/src/core/xfa/html_utils.js
@@ -14,11 +14,15 @@
*/
import {
+ $content,
$extra,
$getParent,
$getSubformParent,
+ $getTemplateRoot,
+ $globalData,
$nodeName,
$pushGlyphs,
+ $text,
$toStyle,
XFAObject,
} from "./xfa_object.js";
@@ -191,8 +195,8 @@ function setMinMaxDimensions(node, style) {
}
}
-function layoutText(text, xfaFont, fontFinder, width) {
- const measure = new TextMeasure(xfaFont, fontFinder);
+function layoutText(text, xfaFont, margin, lineHeight, fontFinder, width) {
+ const measure = new TextMeasure(xfaFont, margin, lineHeight, fontFinder);
if (typeof text === "string") {
measure.addString(text);
} else {
@@ -202,6 +206,86 @@ function layoutText(text, xfaFont, fontFinder, width) {
return measure.compute(width);
}
+function layoutNode(node, availableSpace) {
+ let height = null;
+ let width = null;
+
+ if ((!node.w || !node.h) && node.value) {
+ let marginH = 0;
+ let marginV = 0;
+ if (node.margin) {
+ marginH = node.margin.leftInset + node.margin.rightInset;
+ marginV = node.margin.topInset + node.margin.bottomInset;
+ }
+
+ let lineHeight = null;
+ let margin = null;
+ if (node.para) {
+ margin = Object.create(null);
+ lineHeight = node.para.lineHeight === "" ? null : node.para.lineHeight;
+ margin.top = node.para.spaceAbove === "" ? 0 : node.para.spaceAbove;
+ margin.bottom = node.para.spaceBelow === "" ? 0 : node.para.spaceBelow;
+ margin.left = node.para.marginLeft === "" ? 0 : node.para.marginLeft;
+ margin.right = node.para.marginRight === "" ? 0 : node.para.marginRight;
+ }
+
+ let font = node.font;
+ if (!font) {
+ const root = node[$getTemplateRoot]();
+ let parent = node[$getParent]();
+ while (parent !== root) {
+ if (parent.font) {
+ font = parent.font;
+ break;
+ }
+ parent = parent[$getParent]();
+ }
+ }
+
+ const maxWidth = !node.w ? availableSpace.width : node.w;
+ const fontFinder = node[$globalData].fontFinder;
+ if (
+ node.value.exData &&
+ node.value.exData[$content] &&
+ node.value.exData.contentType === "text/html"
+ ) {
+ const res = layoutText(
+ node.value.exData[$content],
+ font,
+ margin,
+ lineHeight,
+ fontFinder,
+ maxWidth
+ );
+ width = res.width;
+ height = res.height;
+ } else {
+ const text = node.value[$text]();
+ if (text) {
+ const res = layoutText(
+ text,
+ font,
+ margin,
+ lineHeight,
+ fontFinder,
+ maxWidth
+ );
+ width = res.width;
+ height = res.height;
+ }
+ }
+
+ if (width !== null && !node.w) {
+ width += marginH;
+ }
+
+ if (height !== null && !node.h) {
+ height += marginV;
+ }
+ }
+ return [width, height];
+}
+
function computeBbox(node, html, availableSpace) {
let bbox;
if (node.w !== "" && node.h !== "") {
@@ -501,7 +585,7 @@ export {
fixTextIndent,
isPrintOnly,
layoutClass,
- layoutText,
+ layoutNode,
measureToString,
setAccess,
setFontFamily,
diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index 1e0c6c60b..dcf46704f 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -74,7 +74,7 @@ import {
fixTextIndent,
isPrintOnly,
layoutClass,
- layoutText,
+ layoutNode,
measureToString,
setAccess,
setFontFamily,
@@ -911,6 +911,26 @@ class Caption extends XFAObject {
_setValue(this, value);
}
+ [$getExtra](availableSpace) {
+ if (!this[$extra]) {
+ let { width, height } = availableSpace;
+ switch (this.placement) {
+ case "left":
+ case "right":
+ case "inline":
+ width = this.reserve <= 0 ? width : this.reserve;
+ break;
+ case "top":
+ case "bottom":
+ height = this.reserve <= 0 ? height : this.reserve;
+ break;
+ }
+
+ this[$extra] = layoutNode(this, { width, height });
+ }
+ return this[$extra];
+ }
+
[$toHTML](availableSpace) {
// TODO: incomplete.
if (!this.value) {
@@ -921,6 +941,23 @@ class Caption extends XFAObject {
if (!value) {
return HTMLResult.EMPTY;
}
+
+ const savedReserve = this.reserve;
+ if (this.reserve <= 0) {
+ const [w, h] = this[$getExtra](availableSpace);
+ switch (this.placement) {
+ case "left":
+ case "right":
+ case "inline":
+ this.reserve = w;
+ break;
+ case "top":
+ case "bottom":
+ this.reserve = h;
+ break;
+ }
+ }
+
const children = [];
if (typeof value === "string") {
children.push({
@@ -937,20 +974,18 @@ class Caption extends XFAObject {
case "right":
if (this.reserve > 0) {
style.width = measureToString(this.reserve);
- } else {
- style.minWidth = measureToString(this.reserve);
}
break;
case "top":
case "bottom":
if (this.reserve > 0) {
style.height = measureToString(this.reserve);
- } else {
- style.minHeight = measureToString(this.reserve);
}
break;
}
+ this.reserve = savedReserve;
+
return HTMLResult.success({
name: "div",
attributes: {
@@ -1569,63 +1604,22 @@ class Draw extends XFAObject {
fixDimensions(this);
- if ((this.w === "" || this.h === "") && this.value) {
- let marginH = 0;
- let marginV = 0;
- if (this.margin) {
- marginH = this.margin.leftInset + this.margin.rightInset;
- marginV = this.margin.topInset + this.margin.bottomInset;
- }
-
- const maxWidth = this.w === "" ? availableSpace.width : this.w;
- const fontFinder = this[$globalData].fontFinder;
- let font = this.font;
- if (!font) {
- let parent = this[$getParent]();
- while (!(parent instanceof Template)) {
- if (parent.font) {
- font = parent.font;
- break;
- }
- parent = parent[$getParent]();
- }
- }
-
- let height = null;
- let width = null;
- if (
- this.value.exData &&
- this.value.exData[$content] &&
- this.value.exData.contentType === "text/html"
- ) {
- const res = layoutText(
- this.value.exData[$content],
- font,
- fontFinder,
- maxWidth
- );
- width = res.width;
- height = res.height;
- } else {
- const text = this.value[$text]();
- if (text) {
- const res = layoutText(text, font, fontFinder, maxWidth);
- width = res.width;
- height = res.height;
- }
- }
-
- if (width !== null && this.w === "") {
- this.w = width + marginH;
- }
-
- if (height !== null && this.h === "") {
- this.h = height + marginV;
- }
+ // If at least one dimension is missing and we've a text
+ // then we can guess it in laying out the text.
+ const savedW = this.w;
+ const savedH = this.h;
+ const [w, h] = layoutNode(this, availableSpace);
+ if (w && this.w === "") {
+ this.w = w;
+ }
+ if (h && this.h === "") {
+ this.h = h;
}
setFirstUnsplittable(this);
if (!checkDimensions(this, availableSpace)) {
+ this.w = savedW;
+ this.h = savedH;
return HTMLResult.FAILURE;
}
unsetFirstUnsplittable(this);
@@ -1673,6 +1667,8 @@ class Draw extends XFAObject {
const value = this.value ? this.value[$toHTML](availableSpace).html : null;
if (value === null) {
+ this.w = savedW;
+ this.h = savedH;
return HTMLResult.success(createWrapper(this, html), bbox);
}
@@ -1714,6 +1710,9 @@ class Draw extends XFAObject {
}
}
+ this.w = savedW;
+ this.h = savedH;
+
return HTMLResult.success(createWrapper(this, html), bbox);
}
}
@@ -2460,10 +2459,66 @@ class Field extends XFAObject {
return HTMLResult.EMPTY;
}
+ if (this.caption) {
+ // Maybe we already tried to layout this field with
+ // another availableSpace, so to avoid to use the cached
+ // value just delete it.
+ delete this.caption[$extra];
+ }
+
+ const caption = this.caption
+ ? this.caption[$toHTML](availableSpace).html
+ : null;
+ const savedW = this.w;
+ const savedH = this.h;
+ if (this.w === "" || this.h === "") {
+ let marginH = 0;
+ let marginV = 0;
+ if (this.margin) {
+ marginH = this.margin.leftInset + this.margin.rightInset;
+ marginV = this.margin.topInset + this.margin.bottomInset;
+ }
+
+ let width = null;
+ let height = null;
+
+ if (this.caption) {
+ [width, height] = this.caption[$getExtra](availableSpace);
+ if (this.ui instanceof CheckButton) {
+ switch (this.caption.placement) {
+ case "left":
+ case "right":
+ case "inline":
+ width += this.ui.size;
+ break;
+ case "top":
+ case "bottom":
+ height += this.ui.size;
+ break;
+ }
+ }
+ }
+ if (width && this.w === "") {
+ this.w = Math.min(
+ this.maxW <= 0 ? Infinity : this.maxW,
+ Math.max(this.minW, width + marginH)
+ );
+ }
+
+ if (height && this.h === "") {
+ this.h = Math.min(
+ this.maxH <= 0 ? Infinity : this.maxH,
+ Math.max(this.minH, height + marginV)
+ );
+ }
+ }
+
fixDimensions(this);
setFirstUnsplittable(this);
if (!checkDimensions(this, availableSpace)) {
+ this.w = savedW;
+ this.h = savedH;
return HTMLResult.FAILURE;
}
unsetFirstUnsplittable(this);
@@ -2559,12 +2614,14 @@ class Field extends XFAObject {
}
}
- const caption = this.caption ? this.caption[$toHTML]().html : null;
if (!caption) {
if (ui.attributes.class) {
// Even if no caption this class will help to center the ui.
ui.attributes.class.push("xfaLeft");
}
+ this.w = savedW;
+ this.h = savedH;
+
return HTMLResult.success(createWrapper(this, html), bbox);
}
@@ -2605,6 +2662,8 @@ class Field extends XFAObject {
break;
}
+ this.w = savedW;
+ this.h = savedH;
return HTMLResult.success(createWrapper(this, html), bbox);
}
}
diff --git a/src/core/xfa/text.js b/src/core/xfa/text.js
index 064edcf8c..9f9d76361 100644
--- a/src/core/xfa/text.js
+++ b/src/core/xfa/text.js
@@ -15,17 +15,30 @@
import { selectFont } from "./fonts.js";
-const WIDTH_FACTOR = 1.2;
-const HEIGHT_FACTOR = 1.2;
+const WIDTH_FACTOR = 1.05;
class FontInfo {
- constructor(xfaFont, fontFinder) {
+ constructor(xfaFont, margin, lineHeight, fontFinder) {
+ this.lineHeight = lineHeight;
+ this.paraMargin = margin || {
+ top: 0,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ };
+
if (!xfaFont) {
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
return;
}
- this.xfaFont = xfaFont;
+ this.xfaFont = {
+ typeface: xfaFont.typeface,
+ posture: xfaFont.posture,
+ weight: xfaFont.weight,
+ size: xfaFont.size,
+ letterSpacing: xfaFont.letterSpacing,
+ };
const typeface = fontFinder.find(xfaFont.typeface);
if (!typeface) {
[this.pdfFont, this.xfaFont] = this.defaultFont(fontFinder);
@@ -54,6 +67,7 @@ class FontInfo {
posture: "normal",
weight: "normal",
size: 10,
+ letterSpacing: 0,
};
return [pdfFont, xfaFont];
}
@@ -63,29 +77,60 @@ class FontInfo {
posture: "normal",
weight: "normal",
size: 10,
+ letterSpacing: 0,
};
return [null, xfaFont];
}
}
class FontSelector {
- constructor(defaultXfaFont, fontFinder) {
+ constructor(
+ defaultXfaFont,
+ defaultParaMargin,
+ defaultLineHeight,
+ fontFinder
+ ) {
this.fontFinder = fontFinder;
- this.stack = [new FontInfo(defaultXfaFont, fontFinder)];
+ this.stack = [
+ new FontInfo(
+ defaultXfaFont,
+ defaultParaMargin,
+ defaultLineHeight,
+ fontFinder
+ ),
+ ];
}
- pushFont(xfaFont) {
+ pushData(xfaFont, margin, lineHeight) {
const lastFont = this.stack[this.stack.length - 1];
- for (const name of ["typeface", "posture", "weight", "size"]) {
+ for (const name of [
+ "typeface",
+ "posture",
+ "weight",
+ "size",
+ "letterSpacing",
+ ]) {
if (!xfaFont[name]) {
xfaFont[name] = lastFont.xfaFont[name];
}
}
- const fontInfo = new FontInfo(xfaFont, this.fontFinder);
+ for (const name of ["top", "bottom", "left", "right"]) {
+ if (isNaN(margin[name])) {
+ margin[name] = lastFont.paraMargin[name];
+ }
+ }
+
+ const fontInfo = new FontInfo(
+ xfaFont,
+ margin,
+ lineHeight || lastFont.lineHeight,
+ this.fontFinder
+ );
if (!fontInfo.pdfFont) {
fontInfo.pdfFont = lastFont.pdfFont;
}
+
this.stack.push(fontInfo);
}
@@ -102,19 +147,30 @@ class FontSelector {
* Compute a text area dimensions based on font metrics.
*/
class TextMeasure {
- constructor(defaultXfaFont, fonts) {
+ constructor(defaultXfaFont, defaultParaMargin, defaultLineHeight, fonts) {
this.glyphs = [];
- this.fontSelector = new FontSelector(defaultXfaFont, fonts);
+ this.fontSelector = new FontSelector(
+ defaultXfaFont,
+ defaultParaMargin,
+ defaultLineHeight,
+ fonts
+ );
+ this.extraHeight = 0;
}
- pushFont(xfaFont) {
- return this.fontSelector.pushFont(xfaFont);
+ pushData(xfaFont, margin, lineHeight) {
+ this.fontSelector.pushData(xfaFont, margin, lineHeight);
}
popFont(xfaFont) {
return this.fontSelector.popFont();
}
+ addPara() {
+ const lastFont = this.fontSelector.topFont();
+ this.extraHeight += lastFont.paraMargin.top + lastFont.paraMargin.bottom;
+ }
+
addString(str) {
if (!str) {
return;
@@ -123,8 +179,11 @@ class TextMeasure {
const lastFont = this.fontSelector.topFont();
const fontSize = lastFont.xfaFont.size;
if (lastFont.pdfFont) {
+ const letterSpacing = lastFont.xfaFont.letterSpacing;
const pdfFont = lastFont.pdfFont;
- const lineHeight = Math.round(Math.max(1, pdfFont.lineHeight) * fontSize);
+ const lineHeight =
+ lastFont.lineHeight ||
+ Math.round(Math.max(1, pdfFont.lineHeight) * fontSize);
const scale = fontSize / 1000;
for (const line of str.split(/[\u2029\n]/)) {
@@ -133,7 +192,7 @@ class TextMeasure {
for (const glyph of glyphs) {
this.glyphs.push([
- glyph.width * scale,
+ glyph.width * scale + letterSpacing,
lineHeight,
glyph.unicode === " ",
false,
@@ -218,9 +277,9 @@ class TextMeasure {
}
width = Math.max(width, currentLineWidth);
- height += currentLineHeight;
+ height += currentLineHeight + this.extraHeight;
- return { width: WIDTH_FACTOR * width, height: HEIGHT_FACTOR * height };
+ return { width: WIDTH_FACTOR * width, height };
}
}
diff --git a/src/core/xfa/xhtml.js b/src/core/xfa/xhtml.js
index b4532c312..0f933c692 100644
--- a/src/core/xfa/xhtml.js
+++ b/src/core/xfa/xhtml.js
@@ -193,25 +193,81 @@ class XhtmlObject extends XmlObject {
}
}
- [$pushGlyphs](measure) {
+ [$pushGlyphs](measure, mustPop = true) {
const xfaFont = Object.create(null);
+ const margin = {
+ top: NaN,
+ bottom: NaN,
+ left: NaN,
+ right: NaN,
+ };
+ let lineHeight = null;
for (const [key, value] of this.style
.split(";")
.map(s => s.split(":", 2))) {
- if (!key.startsWith("font-")) {
- continue;
- }
- if (key === "font-family") {
- xfaFont.typeface = stripQuotes(value);
- } else if (key === "font-size") {
- xfaFont.size = getMeasurement(value);
- } else if (key === "font-weight") {
- xfaFont.weight = value;
- } else if (key === "font-style") {
- xfaFont.posture = value;
+ switch (key) {
+ case "font-family":
+ xfaFont.typeface = stripQuotes(value);
+ break;
+ case "font-size":
+ xfaFont.size = getMeasurement(value);
+ break;
+ case "font-weight":
+ xfaFont.weight = value;
+ break;
+ case "font-style":
+ xfaFont.posture = value;
+ break;
+ case "letter-spacing":
+ xfaFont.letterSpacing = getMeasurement(value);
+ break;
+ case "margin":
+ const values = value.split(/ \t/).map(x => getMeasurement(x));
+ switch (values.length) {
+ case 1:
+ margin.top =
+ margin.bottom =
+ margin.left =
+ margin.right =
+ values[0];
+ break;
+ case 2:
+ margin.top = margin.bottom = values[0];
+ margin.left = margin.right = values[1];
+ break;
+ case 3:
+ margin.top = values[0];
+ margin.bottom = values[2];
+ margin.left = margin.right = values[1];
+ break;
+ case 4:
+ margin.top = values[0];
+ margin.left = values[1];
+ margin.bottom = values[2];
+ margin.right = values[3];
+ break;
+ }
+ break;
+ case "margin-top":
+ margin.top = getMeasurement(value);
+ break;
+ case "margin-bottom":
+ margin.bottom = getMeasurement(value);
+ break;
+ case "margin-left":
+ margin.left = getMeasurement(value);
+ break;
+ case "margin-right":
+ margin.right = getMeasurement(value);
+ break;
+ case "line-height":
+ lineHeight = getMeasurement(value);
+ break;
}
}
- measure.pushFont(xfaFont);
+
+ measure.pushData(xfaFont, margin, lineHeight);
+
if (this[$content]) {
measure.addString(this[$content]);
} else {
@@ -223,7 +279,10 @@ class XhtmlObject extends XmlObject {
child[$pushGlyphs](measure);
}
}
- measure.popFont();
+
+ if (mustPop) {
+ measure.popFont();
+ }
}
[$toHTML](availableSpace) {
@@ -377,8 +436,10 @@ class P extends XhtmlObject {
}
[$pushGlyphs](measure) {
- super[$pushGlyphs](measure);
+ super[$pushGlyphs](measure, /* mustPop = */ false);
measure.addString("\n");
+ measure.addPara();
+ measure.popFont();
}
[$text]() {
diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css
index a87ccacda..6e95ca33f 100644
--- a/web/xfa_layer_builder.css
+++ b/web/xfa_layer_builder.css
@@ -190,6 +190,8 @@
.xfaRich {
white-space: pre-wrap;
+ width: auto;
+ height: auto;
}
.xfaImage {
@@ -199,11 +201,6 @@
height: 100%;
}
-.xfaRich {
- width: 100%;
- height: auto;
-}
-
.xfaLrTb,
.xfaRlTb,
.xfaTb {