From f7d3b22480b7d7367e8130e6bb91c4fac2c496c4 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 2 Jul 2021 17:53:27 +0200 Subject: [PATCH] XFA - Improve text layout - support paragraph margins, line height, letter spacing, ... - compute missing dimensions from fields based almost on the dimensions of caption contents. --- src/core/xfa/html_utils.js | 90 ++++++++++++++++++- src/core/xfa/template.js | 177 ++++++++++++++++++++++++------------- src/core/xfa/text.js | 93 +++++++++++++++---- src/core/xfa/xhtml.js | 91 +++++++++++++++---- web/xfa_layer_builder.css | 7 +- 5 files changed, 359 insertions(+), 99 deletions(-) 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 {