From 0ea5792c862fe006f75d9dfcc925967f27755356 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 13 Jun 2021 18:57:51 +0200 Subject: [PATCH] XFA - Add support for overflow element - and fix few bugs: - avoid infinite loop when layout the document; - avoid confusion between break and layout failure; - don't add margin width in tb layout when getting available space. --- src/core/xfa/layout.js | 46 ++-- src/core/xfa/template.js | 273 +++++++++++-------- src/core/xfa/utils.js | 17 +- src/core/xfa/xfa_object.js | 8 +- test/pdfs/xfa_annual_expense_report.pdf.link | 1 + test/pdfs/xfa_annual_voting_survey.pdf.link | 1 + test/pdfs/xfa_bug1716047.pdf.link | 1 + test/test_manifest.json | 24 ++ 8 files changed, 237 insertions(+), 134 deletions(-) create mode 100644 test/pdfs/xfa_annual_expense_report.pdf.link create mode 100644 test/pdfs/xfa_annual_voting_survey.pdf.link create mode 100644 test/pdfs/xfa_bug1716047.pdf.link diff --git a/src/core/xfa/layout.js b/src/core/xfa/layout.js index d56b0d7743e63..79b9cbecc6d52 100644 --- a/src/core/xfa/layout.js +++ b/src/core/xfa/layout.js @@ -146,12 +146,9 @@ function addHTML(node, html, bbox) { function getAvailableSpace(node) { const availableSpace = node[$extra].availableSpace; - const [marginW, marginH] = node.margin - ? [ - node.margin.leftInset + node.margin.rightInset, - node.margin.topInset + node.margin.leftInset, - ] - : [0, 0]; + const marginH = node.margin + ? node.margin.topInset + node.margin.bottomInset + : 0; switch (node.layout) { case "lr-tb": @@ -159,35 +156,48 @@ function getAvailableSpace(node) { switch (node[$extra].attempt) { case 0: return { - width: availableSpace.width - marginW - node[$extra].currentWidth, + width: availableSpace.width - node[$extra].currentWidth, height: availableSpace.height - marginH - node[$extra].prevHeight, }; case 1: return { - width: availableSpace.width - marginW, + width: availableSpace.width, height: availableSpace.height - marginH - node[$extra].height, }; default: + // Overflow must stay in the container. return { width: Infinity, - height: availableSpace.height - marginH - node[$extra].prevHeight, + height: Infinity, }; } case "rl-row": case "row": - const width = node[$extra].columnWidths - .slice(node[$extra].currentColumn) - .reduce((a, x) => a + x); - return { width, height: availableSpace.height - marginH }; + if (node[$extra].attempt === 0) { + const width = node[$extra].columnWidths + .slice(node[$extra].currentColumn) + .reduce((a, x) => a + x); + return { width, height: availableSpace.height - marginH }; + } + // Overflow must stay in the container. + return { width: Infinity, height: Infinity }; case "table": case "tb": - return { - width: availableSpace.width - marginW, - height: availableSpace.height - marginH - node[$extra].height, - }; + if (node[$extra].attempt === 0) { + return { + width: availableSpace.width, + height: availableSpace.height - marginH - node[$extra].height, + }; + } + // Overflow must stay in the container. + return { width: Infinity, height: Infinity }; case "position": default: - return availableSpace; + if (node[$extra].attempt === 0) { + return availableSpace; + } + // Overflow must stay in the container. + return { width: Infinity, height: Infinity }; } } diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index 31f43537f69a4..04428554e0249 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -17,7 +17,6 @@ import { $acceptWhitespace, $addHTML, $appendChild, - $break, $childrenToHTML, $content, $extra, @@ -91,6 +90,12 @@ const SVG_NS = "http://www.w3.org/2000/svg"; // to handle the situation. const MAX_ATTEMPTS_FOR_LRTB_LAYOUT = 2; +// It's possible to have a bug in the layout and so as +// a consequence we could loop for ever in Template::toHTML() +// so in order to avoid that (and avoid a OOM crash) we break +// the loop after having MAX_EMPTY_PAGES empty pages. +const MAX_EMPTY_PAGES = 3; + function _setValue(templateNode, value) { if (!templateNode.value) { const nodeValue = new Value({}); @@ -194,26 +199,19 @@ const NOTHING = 0; const NOSPACE = 1; const VALID = 2; function checkDimensions(node, space) { + if (node[$getParent]().layout === "position") { + return VALID; + } const [x, y, w, h] = getTransformedBBox(node); if (node.w === 0 || node.h === 0) { return VALID; } if (node.w !== "" && Math.round(x + w - space.width) > 1) { - const area = getRoot(node)[$extra].currentContentArea; - if (x + w > area.w) { - return NOTHING; - } - return NOSPACE; } if (node.h !== "" && Math.round(y + h - space.height) > 1) { - const area = getRoot(node)[$extra].currentContentArea; - if (y + h > area.h) { - return NOTHING; - } - return NOSPACE; } @@ -393,24 +391,26 @@ class Area extends XFAObject { availableSpace, }; - if ( - !this[$childrenToHTML]({ - filter: new Set([ - "area", - "draw", - "field", - "exclGroup", - "subform", - "subformSet", - ]), - include: true, - }) - ) { + const result = this[$childrenToHTML]({ + filter: new Set([ + "area", + "draw", + "field", + "exclGroup", + "subform", + "subformSet", + ]), + include: true, + }); + + if (!result.success) { + if (result.isBreak()) { + return result; + } // Nothing to propose for the element which doesn't fit the // available space. delete this[$extra]; - // TODO: return failure or not ? - return HTMLResult.empty; + return HTMLResult.FAILURE; } style.width = measureToString(this[$extra].width); @@ -2121,22 +2121,28 @@ class ExclGroup extends XFAObject { this[$extra].attempt < MAX_ATTEMPTS_FOR_LRTB_LAYOUT; this[$extra].attempt++ ) { - if ( - this[$childrenToHTML]({ - filter, - include: true, - }) - ) { + const result = this[$childrenToHTML]({ + filter, + include: true, + }); + if (result.success) { break; } + if (result.isBreak()) { + return result; + } } failure = this[$extra].attempt === 2; } else { - failure = !this[$childrenToHTML]({ + const result = this[$childrenToHTML]({ filter, include: true, }); + failure = !result.success; + if (failure && result.isBreak()) { + return result; + } } if (failure) { @@ -4131,12 +4137,6 @@ class Subform extends XFAObject { } [$toHTML](availableSpace) { - if (this[$extra] && this[$extra].afterBreakAfter) { - const ret = this[$extra].afterBreakAfter; - delete this[$extra]; - return ret; - } - if (this.presence === "hidden" || this.presence === "inactive") { return HTMLResult.EMPTY; } @@ -4152,6 +4152,21 @@ class Subform extends XFAObject { ); } + if (this.breakBefore.children.length >= 1) { + const breakBefore = this.breakBefore.children[0]; + if (!breakBefore[$extra]) { + // Set $extra to true to consume it. + breakBefore[$extra] = true; + return HTMLResult.breakNode(breakBefore); + } + } + + if (this[$extra] && this[$extra].afterBreakAfter) { + const result = this[$extra].afterBreakAfter; + delete this[$extra]; + return result; + } + // TODO: incomplete. fixDimensions(this); const children = []; @@ -4174,15 +4189,6 @@ class Subform extends XFAObject { currentWidth: 0, }); - if (this.breakBefore.children.length >= 1) { - const breakBefore = this.breakBefore.children[0]; - if (!breakBefore[$extra]) { - breakBefore[$extra] = true; - getRoot(this)[$break](breakBefore); - return HTMLResult.FAILURE; - } - } - switch (checkDimensions(this, availableSpace)) { case NOTHING: return HTMLResult.EMPTY; @@ -4192,6 +4198,14 @@ class Subform extends XFAObject { break; } + let noBreakOnOverflow = false; + if (this.overflow && this.overflow.target) { + const root = getRoot(this); + const target = root[$searchNode](this.overflow.target, this); + noBreakOnOverflow = + target && target[0] === root[$extra].currentContentArea; + } + const filter = new Set([ "area", "draw", @@ -4232,32 +4246,32 @@ class Subform 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++ - ) { - if ( - this[$childrenToHTML]({ - filter, - include: true, - }) - ) { - break; - } - } - - failure = this[$extra].attempt === 2; - } else { - failure = !this[$childrenToHTML]({ + // If the container overflows into itself we add an extra + // layout step to accept finally the element which caused + // the overflow. + let maxRun = + this.layout === "lr-tb" || this.layout === "rl-tb" + ? MAX_ATTEMPTS_FOR_LRTB_LAYOUT + : 1; + maxRun += noBreakOnOverflow ? 1 : 0; + for (; this[$extra].attempt < maxRun; this[$extra].attempt++) { + const result = this[$childrenToHTML]({ filter, include: true, }); + if (result.success) { + break; + } + if (result.isBreak()) { + return result; + } } - if (failure) { + if (this[$extra].attempt === maxRun) { + if (this.overflow) { + getRoot(this)[$extra].overflowNode = this.overflow; + } + if (this.layout === "position") { delete this[$extra]; } @@ -4294,19 +4308,17 @@ class Subform extends XFAObject { bbox = [this.x, this.y, width, height]; } + const result = HTMLResult.success(createWrapper(this, html), bbox); + if (this.breakAfter.children.length >= 1) { const breakAfter = this.breakAfter.children[0]; - getRoot(this)[$break](breakAfter); - this[$extra].afterBreakAfter = HTMLResult.success( - createWrapper(this, html), - bbox - ); - return HTMLResult.FAILURE; + this[$extra].afterBreakAfter = result; + return HTMLResult.breakNode(breakAfter); } delete this[$extra]; - return HTMLResult.success(createWrapper(this, html), bbox); + return result; } } @@ -4456,10 +4468,6 @@ class Template extends XFAObject { } } - [$break](node) { - this[$extra].breakingNode = node; - } - [$searchNode](expr, container) { if (expr.startsWith("#")) { // This is an id. @@ -4477,7 +4485,7 @@ class Template extends XFAObject { } this[$extra] = { - breakingNode: null, + overflowNode: null, pageNumber: 1, pagePosition: "first", oddOrEven: "odd", @@ -4540,8 +4548,20 @@ class Template extends XFAObject { let targetPageArea; let leader = null; let trailer = null; + let hasSomething = true; + let hasSomethingCounter = 0; while (true) { + if (!hasSomething) { + // Nothing has been added in the previous page + if (++hasSomethingCounter === MAX_EMPTY_PAGES) { + warn("XFA - Something goes wrong: please file a bug."); + return mainHtml; + } + } else { + hasSomethingCounter = 0; + } + targetPageArea = null; const page = pageArea[$toHTML]().html; mainHtml.children.push(page); @@ -4560,6 +4580,17 @@ class Template extends XFAObject { const htmlContentAreas = page.children.filter(node => node.attributes.class.includes("xfaContentarea") ); + + hasSomething = false; + + const flush = index => { + const html = root[$flushHTML](); + if (html) { + hasSomething = true; + htmlContentAreas[index].children.push(html); + } + }; + for (let i = 0, ii = contentAreas.length; i < ii; i++) { const contentArea = (this[$extra].currentContentArea = contentAreas[i]); const space = { width: contentArea.w, height: contentArea.h }; @@ -4574,7 +4605,7 @@ class Template extends XFAObject { trailer = null; } - let html = root[$toHTML](space); + const html = root[$toHTML](space); if (html.success) { if (html.html) { htmlContentAreas[i].children.push(html.html); @@ -4582,17 +4613,11 @@ class Template extends XFAObject { return mainHtml; } - // Check for breakBefore / breakAfter - let mustBreak = false; - if (this[$extra].breakingNode) { - const node = this[$extra].breakingNode; - this[$extra].breakingNode = null; + if (html.isBreak()) { + const node = html.breakNode; if (node.targetType === "auto") { - html = root[$flushHTML](); - if (html) { - htmlContentAreas[i].children.push(html); - } + flush(i); continue; } @@ -4616,34 +4641,68 @@ class Template extends XFAObject { if (node.targetType === "pageArea") { if (startNew) { - mustBreak = true; + flush(i); + i = Infinity; } else if (target === pageArea || !(target instanceof PageArea)) { // Just ignore the break and do layout again. i--; - continue; } else { // We must stop the contentAreas filling and go to the next page. targetPageArea = target; - mustBreak = true; + flush(i); + i = Infinity; + } + } else if (node.targetType === "contentArea") { + const index = contentAreas.findIndex(e => e === target); + if (index !== -1) { + flush(i); + i = index - 1; + } else { + i--; } - } else if ( - target === "contentArea" || - !(target instanceof ContentArea) - ) { - // Just ignore the break and do layout again. - i--; - continue; } + continue; } - html = root[$flushHTML](); - if (html) { - htmlContentAreas[i].children.push(html); - } + if (this[$extra].overflowNode) { + const node = this[$extra].overflowNode; + this[$extra].overflowNode = null; - if (mustBreak) { - break; + flush(i); + + if (node.leader) { + leader = this[$searchNode](node.leader, node[$getParent]()); + leader = leader ? leader[0] : null; + } + + if (node.trailer) { + trailer = this[$searchNode](node.trailer, node[$getParent]()); + trailer = trailer ? trailer[0] : null; + } + + let target = null; + if (node.target) { + target = this[$searchNode](node.target, node[$getParent]()); + target = target ? target[0] : target; + } + + if (target instanceof PageArea) { + // We must stop the contentAreas filling and go to the next page. + targetPageArea = target; + i = Infinity; + continue; + } else if (target instanceof ContentArea) { + const index = contentAreas.findIndex(e => e === target); + if (index !== -1) { + i = index - 1; + } else { + i--; + } + } + continue; } + + flush(i); } this[$extra].pageNumber += 1; diff --git a/src/core/xfa/utils.js b/src/core/xfa/utils.js index ab88aa88a438c..28aa1eaa2016e 100644 --- a/src/core/xfa/utils.js +++ b/src/core/xfa/utils.js @@ -168,21 +168,30 @@ function getBBox(data) { class HTMLResult { static get FAILURE() { - return shadow(this, "FAILURE", new HTMLResult(false, null, null)); + return shadow(this, "FAILURE", new HTMLResult(false, null, null, null)); } static get EMPTY() { - return shadow(this, "EMPTY", new HTMLResult(true, null, null)); + return shadow(this, "EMPTY", new HTMLResult(true, null, null, null)); } - constructor(success, html, bbox) { + constructor(success, html, bbox, breakNode) { this.success = success; this.html = html; this.bbox = bbox; + this.breakNode = breakNode; + } + + isBreak() { + return !!this.breakNode; + } + + static breakNode(node) { + return new HTMLResult(false, null, null, node); } static success(html, bbox = null) { - return new HTMLResult(true, html, bbox); + return new HTMLResult(true, html, bbox, null); } } diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js index 07047619a3a44..a6cd94121dfbe 100644 --- a/src/core/xfa/xfa_object.js +++ b/src/core/xfa/xfa_object.js @@ -23,7 +23,6 @@ import { searchNode } from "./som.js"; const $acceptWhitespace = Symbol(); const $addHTML = Symbol(); const $appendChild = Symbol(); -const $break = Symbol(); const $childrenToHTML = Symbol(); const $clean = Symbol(); const $cleanup = Symbol(); @@ -342,7 +341,7 @@ class XFAObject { 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); @@ -357,7 +356,7 @@ class XFAObject { } const res = gen.value; if (!res.success) { - return false; + return res; } if (res.html) { this[$addHTML](res.html, res.bbox); @@ -366,7 +365,7 @@ class XFAObject { this[$extra].generator = null; - return true; + return HTMLResult.EMPTY; } [$setSetAttributes](attributes) { @@ -960,7 +959,6 @@ export { $acceptWhitespace, $addHTML, $appendChild, - $break, $childrenToHTML, $clean, $cleanup, diff --git a/test/pdfs/xfa_annual_expense_report.pdf.link b/test/pdfs/xfa_annual_expense_report.pdf.link new file mode 100644 index 0000000000000..83cb8a80e82ad --- /dev/null +++ b/test/pdfs/xfa_annual_expense_report.pdf.link @@ -0,0 +1 @@ +https://web.archive.org/web/20210509141350/https://www.sos.state.oh.us/globalassets/elections/directives/2020/dir2020-25_annualexpensereport.pdf diff --git a/test/pdfs/xfa_annual_voting_survey.pdf.link b/test/pdfs/xfa_annual_voting_survey.pdf.link new file mode 100644 index 0000000000000..6bd2e5794f490 --- /dev/null +++ b/test/pdfs/xfa_annual_voting_survey.pdf.link @@ -0,0 +1 @@ +https://web.archive.org/web/20210509141345/https://www.sos.state.oh.us/globalassets/elections/directives/2020/dir2020-25_eavsform.pdf diff --git a/test/pdfs/xfa_bug1716047.pdf.link b/test/pdfs/xfa_bug1716047.pdf.link new file mode 100644 index 0000000000000..ecf4050ed705c --- /dev/null +++ b/test/pdfs/xfa_bug1716047.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9226577 diff --git a/test/test_manifest.json b/test/test_manifest.json index 957837e9e7d7f..cbcc0874368bc 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -930,6 +930,30 @@ "link": true, "type": "load" }, + { "id": "xfa_bug1716047", + "file": "pdfs/xfa_bug1716047.pdf", + "md5": "2f524163bd8397f43d195090978c3b56", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, + { "id": "xfa_annual_expense_report", + "file": "pdfs/xfa_annual_expense_report.pdf", + "md5": "06866e7a6bbc0346789208ef5f6e885c", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, + { "id": "xfa_annual_voting_survey", + "file": "pdfs/xfa_annual_voting_survey.pdf", + "md5": "92239648ea1bf189435c927e498c4ec9", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, { "id": "xfa_fish_licence", "file": "pdfs/xfa_fish_licence.pdf", "md5": "9b993128bbd7f4217098fd44116ebec2",