From dd55e76f5d00a2ebf9db0bf93620401a2eb87fd3 Mon Sep 17 00:00:00 2001
From: Calixte Denizet <calixte.denizet@gmail.com>
Date: Sun, 11 Jul 2021 19:12:50 +0200
Subject: [PATCH] XFA - Avoid to have containers not pushed in the html   - it
 aims to fix issue #13668.

---
 src/core/xfa/layout.js            | 39 ++++++++++++++---------
 src/core/xfa/template.js          | 52 +++++++++++++++++++++++++------
 test/pdfs/xfa_issue13668.pdf.link |  1 +
 test/test_manifest.json           |  8 +++++
 4 files changed, 76 insertions(+), 24 deletions(-)
 create mode 100644 test/pdfs/xfa_issue13668.pdf.link

diff --git a/src/core/xfa/layout.js b/src/core/xfa/layout.js
index 4f3dcc230..231c676c2 100644
--- a/src/core/xfa/layout.js
+++ b/src/core/xfa/layout.js
@@ -52,10 +52,21 @@ import { measureToString } from "./html_utils.js";
  * returning.
  */
 
+function createLine(node, children) {
+  return {
+    name: "div",
+    attributes: {
+      class: [node.layout === "lr-tb" ? "xfaLr" : "xfaRl"],
+    },
+    children,
+  };
+}
+
 function flushHTML(node) {
   if (!node[$extra]) {
     return null;
   }
+
   const attributes = node[$extra].attributes;
   const html = {
     name: "div",
@@ -66,7 +77,11 @@ function flushHTML(node) {
   if (node[$extra].failingNode) {
     const htmlFromFailing = node[$extra].failingNode[$flushHTML]();
     if (htmlFromFailing) {
-      html.children.push(htmlFromFailing);
+      if (node.layout.endsWith("-tb")) {
+        html.children.push(createLine(node, [htmlFromFailing]));
+      } else {
+        html.children.push(htmlFromFailing);
+      }
     }
   }
 
@@ -74,10 +89,6 @@ function flushHTML(node) {
     return null;
   }
 
-  node[$extra].children = [];
-  delete node[$extra].line;
-  node[$extra].numberInLine = 0;
-
   return html;
 }
 
@@ -96,13 +107,7 @@ function addHTML(node, html, bbox) {
     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.line = createLine(node, []);
         extra.children.push(extra.line);
         extra.numberInLine = 0;
       }
@@ -281,8 +286,14 @@ function checkDimensions(node, space) {
           }
 
           if (node.w !== "") {
-            // True if width is enough.
-            return Math.round(w - space.width) <= ERROR;
+            if (Math.round(w - space.width) <= ERROR) {
+              return true;
+            }
+            if (parent[$extra].numberInLine === 0) {
+              return space.height > 0;
+            }
+
+            return false;
           }
 
           return space.width > 0;
diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js
index f9e89565d..03d61db6b 100644
--- a/src/core/xfa/template.js
+++ b/src/core/xfa/template.js
@@ -268,10 +268,17 @@ function handleBreak(node) {
 function handleOverflow(node, extraNode, space) {
   const root = node[$getTemplateRoot]();
   const saved = root[$extra].noLayoutFailure;
+  const savedMethod = extraNode[$getSubformParent];
+
+  // Replace $getSubformParent to emulate that extraNode is just
+  // under node.
+  extraNode[$getSubformParent] = () => node;
+
   root[$extra].noLayoutFailure = true;
   const res = extraNode[$toHTML](space);
   node[$addHTML](res.html, res.bbox);
   root[$extra].noLayoutFailure = saved;
+  extraNode[$getSubformParent] = savedMethod;
 }
 
 class AppearanceFilter extends StringObject {
@@ -2236,6 +2243,7 @@ class ExclGroup extends XFAObject {
       children,
       attributes,
       attempt: 0,
+      line: null,
       numberInLine: 0,
       availableSpace: {
         width: Math.min(this.w || Infinity, availableSpace.width),
@@ -2298,12 +2306,10 @@ class ExclGroup extends XFAObject {
       attributes.xfaName = this.name;
     }
 
-    const maxRun =
-      this.layout === "lr-tb" || this.layout === "rl-tb"
-        ? MAX_ATTEMPTS_FOR_LRTB_LAYOUT
-        : 1;
+    const isLrTb = this.layout === "lr-tb" || this.layout === "rl-tb";
+    const maxRun = isLrTb ? 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 (isLrTb && 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.
@@ -2319,6 +2325,16 @@ class ExclGroup extends XFAObject {
       if (result.isBreak()) {
         return result;
       }
+      if (
+        isLrTb &&
+        this[$extra].attempt === 0 &&
+        this[$extra].numberInLine === 0 &&
+        !this[$getTemplateRoot]()[$extra].noLayoutFailure
+      ) {
+        // See comment in Subform::[$toHTML].
+        this[$extra].attempt = maxRun;
+        break;
+      }
     }
 
     if (!isSplittable) {
@@ -4646,6 +4662,7 @@ class Subform extends XFAObject {
 
     Object.assign(this[$extra], {
       children,
+      line: null,
       attributes,
       attempt: 0,
       numberInLine: 0,
@@ -4729,12 +4746,10 @@ class Subform extends XFAObject {
       }
     }
 
-    const maxRun =
-      this.layout === "lr-tb" || this.layout === "rl-tb"
-        ? MAX_ATTEMPTS_FOR_LRTB_LAYOUT
-        : 1;
+    const isLrTb = this.layout === "lr-tb" || this.layout === "rl-tb";
+    const maxRun = isLrTb ? 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 (isLrTb && 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.
@@ -4750,6 +4765,23 @@ class Subform extends XFAObject {
       if (result.isBreak()) {
         return result;
       }
+      if (
+        isLrTb &&
+        this[$extra].attempt === 0 &&
+        this[$extra].numberInLine === 0 &&
+        !root[$extra].noLayoutFailure
+      ) {
+        // We're failing to put the first element on the line so no
+        // need to test on the next line.
+        // The goal is not only to avoid some useless checks but to avoid
+        // bugs too: if a descendant managed to put a node and failed
+        // on the next one, going to the next step here will imply to
+        // visit the descendant again, clear [$extra].children and restart
+        // on the failing node, consequently the first node just disappears
+        // because it has never been flushed.
+        this[$extra].attempt = maxRun;
+        break;
+      }
     }
 
     if (!isSplittable) {
diff --git a/test/pdfs/xfa_issue13668.pdf.link b/test/pdfs/xfa_issue13668.pdf.link
new file mode 100644
index 000000000..7f8e7f47b
--- /dev/null
+++ b/test/pdfs/xfa_issue13668.pdf.link
@@ -0,0 +1 @@
+https://web.archive.org/web/20210711170923/https://www.placementsmondiauxsunlife.com/content/dam/sunlife/regional/canada/documents/slgi/4839-I-F.pdf
diff --git a/test/test_manifest.json b/test/test_manifest.json
index 397db806a..4be2035a0 100644
--- a/test/test_manifest.json
+++ b/test/test_manifest.json
@@ -1192,6 +1192,14 @@
        "enableXfa": true,
        "type": "eq"
     },
+    {  "id": "xfa_issue13668",
+       "file": "pdfs/xfa_issue13668.pdf",
+       "md5": "8a5ed3c8a58b425b1ec53329334a0f5b",
+       "link": true,
+       "rounds": 1,
+       "enableXfa": true,
+       "type": "eq"
+    },
     {  "id": "xfa_issue13633",
        "file": "pdfs/xfa_issue13633.pdf",
        "md5": "e5b0d09285ca6a140eba08d740be0ea0",