diff --git a/src/core/xfa/html_utils.js b/src/core/xfa/html_utils.js
index 644e0c0edc9a7..e788071c63393 100644
--- a/src/core/xfa/html_utils.js
+++ b/src/core/xfa/html_utils.js
@@ -213,6 +213,7 @@ function layoutText(text, xfaFont, margin, lineHeight, fontFinder, width) {
function layoutNode(node, availableSpace) {
let height = null;
let width = null;
+ let isBroken = false;
if ((!node.w || !node.h) && node.value) {
let marginH = 0;
@@ -263,6 +264,7 @@ function layoutNode(node, availableSpace) {
);
width = res.width;
height = res.height;
+ isBroken = res.isBroken;
} else {
const text = node.value[$text]();
if (text) {
@@ -276,6 +278,7 @@ function layoutNode(node, availableSpace) {
);
width = res.width;
height = res.height;
+ isBroken = res.isBroken;
}
}
@@ -287,7 +290,7 @@ function layoutNode(node, availableSpace) {
height += marginV;
}
}
- return [width, height];
+ return { w: width, h: height, isBroken };
}
function computeBbox(node, html, availableSpace) {
diff --git a/src/core/xfa/layout.js b/src/core/xfa/layout.js
index f82b42bf55a15..467f984d9fdfc 100644
--- a/src/core/xfa/layout.js
+++ b/src/core/xfa/layout.js
@@ -19,6 +19,7 @@ import {
$getSubformParent,
$getTemplateRoot,
$isSplittable,
+ $isThereMoreWidth,
} from "./xfa_object.js";
import { measureToString } from "./html_utils.js";
@@ -75,6 +76,7 @@ function flushHTML(node) {
node[$extra].children = [];
delete node[$extra].line;
+ node[$extra].numberInLine = 0;
return html;
}
@@ -83,9 +85,9 @@ function addHTML(node, html, bbox) {
const extra = node[$extra];
const availableSpace = extra.availableSpace;
+ const [x, y, w, h] = bbox;
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);
@@ -102,16 +104,17 @@ function addHTML(node, html, bbox) {
children: [],
};
extra.children.push(extra.line);
+ extra.numberInLine = 0;
}
+
+ extra.numberInLine += 1;
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.currentWidth = w;
extra.prevHeight = extra.height;
extra.height += h;
@@ -124,7 +127,6 @@ function addHTML(node, html, bbox) {
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);
@@ -134,14 +136,12 @@ function addHTML(node, html, bbox) {
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);
@@ -265,6 +265,7 @@ function checkDimensions(node, space) {
return true;
}
+ const ERROR = 2;
const parent = node[$getSubformParent]();
const attempt = (parent[$extra] && parent[$extra].attempt) || 0;
let y, w, h;
@@ -274,17 +275,19 @@ function checkDimensions(node, space) {
if (node.w !== "" || node.h !== "") {
[, , w, h] = getTransformedBBox(node);
}
+
if (attempt === 0) {
// Try to put an element in the line.
if (!node[$getTemplateRoot]()[$extra].noLayoutFailure) {
- if (node.h !== "" && Math.round(h - space.height) > 1) {
+ if (node.h !== "" && Math.round(h - space.height) > ERROR) {
// Not enough height.
return false;
}
+
if (node.w !== "") {
// True if width is enough.
- return Math.round(w - space.width) <= 1;
+ return Math.round(w - space.width) <= ERROR;
}
return space.width > 0;
@@ -295,7 +298,7 @@ function checkDimensions(node, space) {
// Put the element on the line but we can fail
// and then in the second step (next line) we'll accept.
if (node.w !== "") {
- return Math.round(w - space.width) <= 1;
+ return Math.round(w - space.width) <= ERROR;
}
return space.width > 0;
@@ -308,9 +311,16 @@ function checkDimensions(node, space) {
return true;
}
- if (node.h !== "") {
- // True if height is enough.
- return Math.round(h - space.height) <= 1;
+ if (node.h !== "" && Math.round(h - space.height) > ERROR) {
+ return false;
+ }
+
+ if (node.w === "" || Math.round(w - space.width) <= ERROR) {
+ return space.height > 0;
+ }
+
+ if (parent[$isThereMoreWidth]()) {
+ return false;
}
return space.height > 0;
@@ -325,10 +335,19 @@ function checkDimensions(node, space) {
// is breakable then we can return true.
if (node.h !== "" && !node[$isSplittable]()) {
[, , , h] = getTransformedBBox(node);
- return Math.round(h - space.height) <= 1;
+ return Math.round(h - space.height) <= ERROR;
}
// Else wait and see: this node will be layed out itself
// in the provided space and maybe a children won't fit.
+
+ if (node.w === "" || Math.round(w - space.width) <= ERROR) {
+ return space.height > 0;
+ }
+
+ if (parent[$isThereMoreWidth]()) {
+ return false;
+ }
+
return space.height > 0;
case "position":
if (node[$getTemplateRoot]()[$extra].noLayoutFailure) {
@@ -336,7 +355,7 @@ function checkDimensions(node, space) {
}
[, y, , h] = getTransformedBBox(node);
- if (node.h === "" || Math.round(h + y - space.height) <= 1) {
+ if (node.h === "" || Math.round(h + y - space.height) <= ERROR) {
return true;
}
@@ -350,7 +369,7 @@ function checkDimensions(node, space) {
if (node.h !== "") {
[, , , h] = getTransformedBBox(node);
- return Math.round(h - space.height) <= 1;
+ return Math.round(h - space.height) <= ERROR;
}
return true;
default:
diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index 71174f29b2f06..1bdbf773bb0a1 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -39,6 +39,7 @@ import {
$isBindable,
$isCDATAXml,
$isSplittable,
+ $isThereMoreWidth,
$isTransparent,
$isUsable,
$namespaceId,
@@ -948,7 +949,7 @@ class Caption extends XFAObject {
const savedReserve = this.reserve;
if (this.reserve <= 0) {
- const [w, h] = this[$getExtra](availableSpace);
+ const { w, h } = this[$getExtra](availableSpace);
switch (this.placement) {
case "left":
case "right":
@@ -1612,8 +1613,18 @@ class Draw extends XFAObject {
// then we can guess it in laying out the text.
const savedW = this.w;
const savedH = this.h;
- const [w, h] = layoutNode(this, availableSpace);
+ const { w, h, isBroken } = layoutNode(this, availableSpace);
if (w && this.w === "") {
+ // If the parent layout is lr-tb with a w=100 and we already have a child
+ // which takes 90 on the current line.
+ // If we have a text with a length (in px) equal to 100 then it'll be
+ // splitted into almost 10 chunks: so it won't be nice.
+ // So if we've potentially more width to provide in some parent containers
+ // let's increase it to give a chance to have a better rendering.
+ if (isBroken && this[$getSubformParent]()[$isThereMoreWidth]()) {
+ return HTMLResult.FAILURE;
+ }
+
this.w = w;
}
if (h && this.h === "") {
@@ -2114,10 +2125,20 @@ class ExclGroup extends XFAObject {
}
}
+ [$isThereMoreWidth]() {
+ return (
+ (this.layout.endsWith("-tb") &&
+ this[$extra].attempt === 0 &&
+ this[$extra].numberInLine > 0) ||
+ this[$getParent]()[$isThereMoreWidth]()
+ );
+ }
+
[$isSplittable]() {
// We cannot cache the result here because the contentArea
// can change.
- if (!this[$getSubformParent]()[$isSplittable]()) {
+ const parent = this[$getSubformParent]();
+ if (!parent[$isSplittable]()) {
return false;
}
@@ -2130,6 +2151,15 @@ class ExclGroup extends XFAObject {
return false;
}
+ if (
+ parent.layout &&
+ parent.layout.endsWith("-tb") &&
+ parent[$extra].numberInLine !== 0
+ ) {
+ // See comment in Subform::[$isSplittable] for an explanation.
+ return false;
+ }
+
this[$extra]._isSplittable = true;
return true;
}
@@ -2174,7 +2204,11 @@ class ExclGroup extends XFAObject {
children,
attributes,
attempt: 0,
- availableSpace,
+ numberInLine: 0,
+ availableSpace: {
+ width: Math.min(this.w || Infinity, availableSpace.width),
+ height: Math.min(this.h || Infinity, availableSpace.height),
+ },
width: 0,
height: 0,
prevHeight: 0,
@@ -2232,33 +2266,25 @@ class ExclGroup extends XFAObject {
attributes.xfaName = this.name;
}
- let failure;
- if (this.layout === "lr-tb" || this.layout === "rl-tb") {
- for (
- ;
- this[$extra].attempt < MAX_ATTEMPTS_FOR_LRTB_LAYOUT;
- this[$extra].attempt++
- ) {
- const result = this[$childrenToHTML]({
- filter,
- include: true,
- });
- if (result.success) {
- break;
- }
- if (result.isBreak()) {
- return result;
- }
+ const maxRun =
+ this.layout === "lr-tb" || this.layout === "rl-tb"
+ ? MAX_ATTEMPTS_FOR_LRTB_LAYOUT
+ : 1;
+ for (; this[$extra].attempt < maxRun; this[$extra].attempt++) {
+ if (this[$extra].attempt === MAX_ATTEMPTS_FOR_LRTB_LAYOUT - 1) {
+ // If the layout is lr-tb then having attempt equals to
+ // MAX_ATTEMPTS_FOR_LRTB_LAYOUT-1 means that we're trying to layout
+ // on the next line so this on is empty.
+ this[$extra].numberInLine = 0;
}
-
- failure = this[$extra].attempt === MAX_ATTEMPTS_FOR_LRTB_LAYOUT;
- } else {
const result = this[$childrenToHTML]({
filter,
include: true,
});
- failure = !result.success;
- if (failure && result.isBreak()) {
+ if (result.success) {
+ break;
+ }
+ if (result.isBreak()) {
return result;
}
}
@@ -2267,8 +2293,8 @@ class ExclGroup extends XFAObject {
unsetFirstUnsplittable(this);
}
- if (failure) {
- if (this[$isSplittable]()) {
+ if (this[$extra].attempt === maxRun) {
+ if (!isSplittable) {
delete this[$extra];
}
return HTMLResult.FAILURE;
@@ -2475,7 +2501,15 @@ class Field extends XFAObject {
let height = null;
if (this.caption) {
- [width, height] = this.caption[$getExtra](availableSpace);
+ const { w, h, isBroken } = this.caption[$getExtra](availableSpace);
+ // See comment in Draw::[$toHTML] to have an explanation
+ // about this line.
+ if (isBroken && this[$getSubformParent]()[$isThereMoreWidth]()) {
+ return HTMLResult.FAILURE;
+ }
+
+ width = w;
+ height = h;
if (this.ui instanceof CheckButton) {
switch (this.caption.placement) {
case "left":
@@ -4391,6 +4425,15 @@ class Subform extends XFAObject {
return true;
}
+ [$isThereMoreWidth]() {
+ return (
+ (this.layout.endsWith("-tb") &&
+ this[$extra].attempt === 0 &&
+ this[$extra].numberInLine > 0) ||
+ this[$getParent]()[$isThereMoreWidth]()
+ );
+ }
+
*[$getContainedChildren]() {
// This function is overriden in order to fake that subforms under
// this set are in fact under parent subform.
@@ -4412,7 +4455,8 @@ class Subform extends XFAObject {
[$isSplittable]() {
// We cannot cache the result here because the contentArea
// can change.
- if (!this[$getSubformParent]()[$isSplittable]()) {
+ const parent = this[$getSubformParent]();
+ if (!parent[$isSplittable]()) {
return false;
}
@@ -4436,6 +4480,20 @@ class Subform extends XFAObject {
return false;
}
+ if (
+ parent.layout &&
+ parent.layout.endsWith("-tb") &&
+ parent[$extra].numberInLine !== 0
+ ) {
+ // If parent can fit in w=100 and there's already an element which takes
+ // 90 then we've 10 for this element. Suppose this element has a tb layout
+ // and 5 elements have a width of 7 and the 6th has a width of 20:
+ // then this element (and all its content) must move on the next line.
+ // If this element is splittable then the first 5 children will stay
+ // at the end of the line: we don't want that.
+ return false;
+ }
+
this[$extra]._isSplittable = true;
return true;
@@ -4526,7 +4584,11 @@ class Subform extends XFAObject {
children,
attributes,
attempt: 0,
- availableSpace,
+ numberInLine: 0,
+ availableSpace: {
+ width: Math.min(this.w || Infinity, availableSpace.width),
+ height: Math.min(this.h || Infinity, availableSpace.height),
+ },
width: 0,
height: 0,
prevHeight: 0,
@@ -4600,6 +4662,12 @@ class Subform extends XFAObject {
? MAX_ATTEMPTS_FOR_LRTB_LAYOUT
: 1;
for (; this[$extra].attempt < maxRun; this[$extra].attempt++) {
+ if (this[$extra].attempt === MAX_ATTEMPTS_FOR_LRTB_LAYOUT - 1) {
+ // If the layout is lr-tb then having attempt equals to
+ // MAX_ATTEMPTS_FOR_LRTB_LAYOUT-1 means that we're trying to layout
+ // on the next line so this on is empty.
+ this[$extra].numberInLine = 0;
+ }
const result = this[$childrenToHTML]({
filter,
include: true,
diff --git a/src/core/xfa/text.js b/src/core/xfa/text.js
index 9f9d763619885..faed18e224d14 100644
--- a/src/core/xfa/text.js
+++ b/src/core/xfa/text.js
@@ -223,6 +223,7 @@ class TextMeasure {
height = 0,
currentLineWidth = 0,
currentLineHeight = 0;
+ let isBroken = false;
for (let i = 0, ii = this.glyphs.length; i < ii; i++) {
const [glyphWidth, glyphHeight, isSpace, isEOL] = this.glyphs[i];
@@ -245,6 +246,7 @@ class TextMeasure {
currentLineHeight = glyphHeight;
lastSpacePos = -1;
lastSpaceWidth = 0;
+ isBroken = true;
} else {
currentLineHeight = Math.max(glyphHeight, currentLineHeight);
lastSpaceWidth = currentLineWidth;
@@ -269,6 +271,8 @@ class TextMeasure {
width = Math.max(width, currentLineWidth);
currentLineWidth = glyphWidth;
}
+ isBroken = true;
+
continue;
}
@@ -279,7 +283,7 @@ class TextMeasure {
width = Math.max(width, currentLineWidth);
height += currentLineHeight + this.extraHeight;
- return { width: WIDTH_FACTOR * width, height };
+ return { width: WIDTH_FACTOR * width, height, isBroken };
}
}
diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js
index d8fbaa09a9464..ca8562971ed73 100644
--- a/src/core/xfa/xfa_object.js
+++ b/src/core/xfa/xfa_object.js
@@ -62,6 +62,7 @@ const $isBindable = Symbol();
const $isDataValue = Symbol();
const $isDescendent = Symbol();
const $isSplittable = Symbol();
+const $isThereMoreWidth = Symbol();
const $isTransparent = Symbol();
const $isUsable = Symbol();
const $lastAttribute = Symbol();
@@ -185,6 +186,16 @@ class XFAObject {
return false;
}
+ /**
+ Return true if this node (typically a container)
+ can provide more width during layout.
+ The goal is to help to know what a descendant must
+ do in case of horizontal overflow.
+ */
+ [$isThereMoreWidth]() {
+ return false;
+ }
+
[$appendChild](child) {
child[_parent] = this;
this[_children].push(child);
@@ -1074,6 +1085,7 @@ export {
$isDataValue,
$isDescendent,
$isSplittable,
+ $isThereMoreWidth,
$isTransparent,
$isUsable,
$namespaceId,
diff --git a/test/pdfs/xfa_bug1718670_1.pdf.link b/test/pdfs/xfa_bug1718670_1.pdf.link
new file mode 100644
index 0000000000000..c5ceadde41857
--- /dev/null
+++ b/test/pdfs/xfa_bug1718670_1.pdf.link
@@ -0,0 +1 @@
+https://bugzilla.mozilla.org/attachment.cgi?id=9229317
diff --git a/test/test_manifest.json b/test/test_manifest.json
index c758a7b03f90d..384e37793dd82 100644
--- a/test/test_manifest.json
+++ b/test/test_manifest.json
@@ -952,6 +952,14 @@
"enableXfa": true,
"type": "eq"
},
+ { "id": "xfa_bug1718670_1",
+ "file": "pdfs/xfa_bug1718670_1.pdf",
+ "md5": "06745be56a89acd80e5bdeddabb7cb7b",
+ "link": true,
+ "rounds": 1,
+ "enableXfa": true,
+ "type": "eq"
+ },
{ "id": "xfa_bug1718521_1",
"file": "pdfs/xfa_bug1718521_1.pdf",
"md5": "9b89dd9e6a4c6c3258ca24debd806863",