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.
This commit is contained in:
Calixte Denizet 2021-06-13 18:57:51 +02:00
parent 246d565e3b
commit 0ea5792c86
8 changed files with 237 additions and 134 deletions

View File

@ -146,12 +146,9 @@ function addHTML(node, html, bbox) {
function getAvailableSpace(node) { function getAvailableSpace(node) {
const availableSpace = node[$extra].availableSpace; const availableSpace = node[$extra].availableSpace;
const [marginW, marginH] = node.margin const marginH = node.margin
? [ ? node.margin.topInset + node.margin.bottomInset
node.margin.leftInset + node.margin.rightInset, : 0;
node.margin.topInset + node.margin.leftInset,
]
: [0, 0];
switch (node.layout) { switch (node.layout) {
case "lr-tb": case "lr-tb":
@ -159,36 +156,49 @@ function getAvailableSpace(node) {
switch (node[$extra].attempt) { switch (node[$extra].attempt) {
case 0: case 0:
return { return {
width: availableSpace.width - marginW - node[$extra].currentWidth, width: availableSpace.width - node[$extra].currentWidth,
height: availableSpace.height - marginH - node[$extra].prevHeight, height: availableSpace.height - marginH - node[$extra].prevHeight,
}; };
case 1: case 1:
return { return {
width: availableSpace.width - marginW, width: availableSpace.width,
height: availableSpace.height - marginH - node[$extra].height, height: availableSpace.height - marginH - node[$extra].height,
}; };
default: default:
// Overflow must stay in the container.
return { return {
width: Infinity, width: Infinity,
height: availableSpace.height - marginH - node[$extra].prevHeight, height: Infinity,
}; };
} }
case "rl-row": case "rl-row":
case "row": case "row":
if (node[$extra].attempt === 0) {
const width = node[$extra].columnWidths const width = node[$extra].columnWidths
.slice(node[$extra].currentColumn) .slice(node[$extra].currentColumn)
.reduce((a, x) => a + x); .reduce((a, x) => a + x);
return { width, height: availableSpace.height - marginH }; return { width, height: availableSpace.height - marginH };
}
// Overflow must stay in the container.
return { width: Infinity, height: Infinity };
case "table": case "table":
case "tb": case "tb":
if (node[$extra].attempt === 0) {
return { return {
width: availableSpace.width - marginW, width: availableSpace.width,
height: availableSpace.height - marginH - node[$extra].height, height: availableSpace.height - marginH - node[$extra].height,
}; };
}
// Overflow must stay in the container.
return { width: Infinity, height: Infinity };
case "position": case "position":
default: default:
if (node[$extra].attempt === 0) {
return availableSpace; return availableSpace;
} }
// Overflow must stay in the container.
return { width: Infinity, height: Infinity };
}
} }
export { addHTML, flushHTML, getAvailableSpace }; export { addHTML, flushHTML, getAvailableSpace };

View File

@ -17,7 +17,6 @@ import {
$acceptWhitespace, $acceptWhitespace,
$addHTML, $addHTML,
$appendChild, $appendChild,
$break,
$childrenToHTML, $childrenToHTML,
$content, $content,
$extra, $extra,
@ -91,6 +90,12 @@ const SVG_NS = "http://www.w3.org/2000/svg";
// to handle the situation. // to handle the situation.
const MAX_ATTEMPTS_FOR_LRTB_LAYOUT = 2; 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) { function _setValue(templateNode, value) {
if (!templateNode.value) { if (!templateNode.value) {
const nodeValue = new Value({}); const nodeValue = new Value({});
@ -194,26 +199,19 @@ const NOTHING = 0;
const NOSPACE = 1; const NOSPACE = 1;
const VALID = 2; const VALID = 2;
function checkDimensions(node, space) { function checkDimensions(node, space) {
if (node[$getParent]().layout === "position") {
return VALID;
}
const [x, y, w, h] = getTransformedBBox(node); const [x, y, w, h] = getTransformedBBox(node);
if (node.w === 0 || node.h === 0) { if (node.w === 0 || node.h === 0) {
return VALID; return VALID;
} }
if (node.w !== "" && Math.round(x + w - space.width) > 1) { 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; return NOSPACE;
} }
if (node.h !== "" && Math.round(y + h - space.height) > 1) { 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; return NOSPACE;
} }
@ -393,8 +391,7 @@ class Area extends XFAObject {
availableSpace, availableSpace,
}; };
if ( const result = this[$childrenToHTML]({
!this[$childrenToHTML]({
filter: new Set([ filter: new Set([
"area", "area",
"draw", "draw",
@ -404,13 +401,16 @@ class Area extends XFAObject {
"subformSet", "subformSet",
]), ]),
include: true, include: true,
}) });
) {
if (!result.success) {
if (result.isBreak()) {
return result;
}
// Nothing to propose for the element which doesn't fit the // Nothing to propose for the element which doesn't fit the
// available space. // available space.
delete this[$extra]; delete this[$extra];
// TODO: return failure or not ? return HTMLResult.FAILURE;
return HTMLResult.empty;
} }
style.width = measureToString(this[$extra].width); 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 < MAX_ATTEMPTS_FOR_LRTB_LAYOUT;
this[$extra].attempt++ this[$extra].attempt++
) { ) {
if ( const result = this[$childrenToHTML]({
this[$childrenToHTML]({
filter, filter,
include: true, include: true,
}) });
) { if (result.success) {
break; break;
} }
if (result.isBreak()) {
return result;
}
} }
failure = this[$extra].attempt === 2; failure = this[$extra].attempt === 2;
} else { } else {
failure = !this[$childrenToHTML]({ const result = this[$childrenToHTML]({
filter, filter,
include: true, include: true,
}); });
failure = !result.success;
if (failure && result.isBreak()) {
return result;
}
} }
if (failure) { if (failure) {
@ -4131,12 +4137,6 @@ class Subform extends XFAObject {
} }
[$toHTML](availableSpace) { [$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") { if (this.presence === "hidden" || this.presence === "inactive") {
return HTMLResult.EMPTY; 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. // TODO: incomplete.
fixDimensions(this); fixDimensions(this);
const children = []; const children = [];
@ -4174,15 +4189,6 @@ class Subform extends XFAObject {
currentWidth: 0, 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)) { switch (checkDimensions(this, availableSpace)) {
case NOTHING: case NOTHING:
return HTMLResult.EMPTY; return HTMLResult.EMPTY;
@ -4192,6 +4198,14 @@ class Subform extends XFAObject {
break; 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([ const filter = new Set([
"area", "area",
"draw", "draw",
@ -4232,32 +4246,32 @@ class Subform extends XFAObject {
attributes.xfaName = this.name; attributes.xfaName = this.name;
} }
let failure; // If the container overflows into itself we add an extra
if (this.layout === "lr-tb" || this.layout === "rl-tb") { // layout step to accept finally the element which caused
for ( // the overflow.
; let maxRun =
this[$extra].attempt < MAX_ATTEMPTS_FOR_LRTB_LAYOUT; this.layout === "lr-tb" || this.layout === "rl-tb"
this[$extra].attempt++ ? MAX_ATTEMPTS_FOR_LRTB_LAYOUT
) { : 1;
if ( maxRun += noBreakOnOverflow ? 1 : 0;
this[$childrenToHTML]({ for (; this[$extra].attempt < maxRun; this[$extra].attempt++) {
filter, const result = this[$childrenToHTML]({
include: true,
})
) {
break;
}
}
failure = this[$extra].attempt === 2;
} else {
failure = !this[$childrenToHTML]({
filter, filter,
include: true, include: true,
}); });
if (result.success) {
break;
}
if (result.isBreak()) {
return result;
}
}
if (this[$extra].attempt === maxRun) {
if (this.overflow) {
getRoot(this)[$extra].overflowNode = this.overflow;
} }
if (failure) {
if (this.layout === "position") { if (this.layout === "position") {
delete this[$extra]; delete this[$extra];
} }
@ -4294,19 +4308,17 @@ class Subform extends XFAObject {
bbox = [this.x, this.y, width, height]; bbox = [this.x, this.y, width, height];
} }
const result = HTMLResult.success(createWrapper(this, html), bbox);
if (this.breakAfter.children.length >= 1) { if (this.breakAfter.children.length >= 1) {
const breakAfter = this.breakAfter.children[0]; const breakAfter = this.breakAfter.children[0];
getRoot(this)[$break](breakAfter); this[$extra].afterBreakAfter = result;
this[$extra].afterBreakAfter = HTMLResult.success( return HTMLResult.breakNode(breakAfter);
createWrapper(this, html),
bbox
);
return HTMLResult.FAILURE;
} }
delete this[$extra]; 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) { [$searchNode](expr, container) {
if (expr.startsWith("#")) { if (expr.startsWith("#")) {
// This is an id. // This is an id.
@ -4477,7 +4485,7 @@ class Template extends XFAObject {
} }
this[$extra] = { this[$extra] = {
breakingNode: null, overflowNode: null,
pageNumber: 1, pageNumber: 1,
pagePosition: "first", pagePosition: "first",
oddOrEven: "odd", oddOrEven: "odd",
@ -4540,8 +4548,20 @@ class Template extends XFAObject {
let targetPageArea; let targetPageArea;
let leader = null; let leader = null;
let trailer = null; let trailer = null;
let hasSomething = true;
let hasSomethingCounter = 0;
while (true) { 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; targetPageArea = null;
const page = pageArea[$toHTML]().html; const page = pageArea[$toHTML]().html;
mainHtml.children.push(page); mainHtml.children.push(page);
@ -4560,6 +4580,17 @@ class Template extends XFAObject {
const htmlContentAreas = page.children.filter(node => const htmlContentAreas = page.children.filter(node =>
node.attributes.class.includes("xfaContentarea") 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++) { for (let i = 0, ii = contentAreas.length; i < ii; i++) {
const contentArea = (this[$extra].currentContentArea = contentAreas[i]); const contentArea = (this[$extra].currentContentArea = contentAreas[i]);
const space = { width: contentArea.w, height: contentArea.h }; const space = { width: contentArea.w, height: contentArea.h };
@ -4574,7 +4605,7 @@ class Template extends XFAObject {
trailer = null; trailer = null;
} }
let html = root[$toHTML](space); const html = root[$toHTML](space);
if (html.success) { if (html.success) {
if (html.html) { if (html.html) {
htmlContentAreas[i].children.push(html.html); htmlContentAreas[i].children.push(html.html);
@ -4582,17 +4613,11 @@ class Template extends XFAObject {
return mainHtml; return mainHtml;
} }
// Check for breakBefore / breakAfter if (html.isBreak()) {
let mustBreak = false; const node = html.breakNode;
if (this[$extra].breakingNode) {
const node = this[$extra].breakingNode;
this[$extra].breakingNode = null;
if (node.targetType === "auto") { if (node.targetType === "auto") {
html = root[$flushHTML](); flush(i);
if (html) {
htmlContentAreas[i].children.push(html);
}
continue; continue;
} }
@ -4616,34 +4641,68 @@ class Template extends XFAObject {
if (node.targetType === "pageArea") { if (node.targetType === "pageArea") {
if (startNew) { if (startNew) {
mustBreak = true; flush(i);
i = Infinity;
} else if (target === pageArea || !(target instanceof PageArea)) { } else if (target === pageArea || !(target instanceof PageArea)) {
// Just ignore the break and do layout again. // Just ignore the break and do layout again.
i--; i--;
continue;
} else { } else {
// We must stop the contentAreas filling and go to the next page. // We must stop the contentAreas filling and go to the next page.
targetPageArea = target; targetPageArea = target;
mustBreak = true; flush(i);
i = Infinity;
} }
} else if ( } else if (node.targetType === "contentArea") {
target === "contentArea" || const index = contentAreas.findIndex(e => e === target);
!(target instanceof ContentArea) if (index !== -1) {
) { flush(i);
// Just ignore the break and do layout again. i = index - 1;
} else {
i--; i--;
}
}
continue; continue;
} }
if (this[$extra].overflowNode) {
const node = this[$extra].overflowNode;
this[$extra].overflowNode = null;
flush(i);
if (node.leader) {
leader = this[$searchNode](node.leader, node[$getParent]());
leader = leader ? leader[0] : null;
} }
html = root[$flushHTML](); if (node.trailer) {
if (html) { trailer = this[$searchNode](node.trailer, node[$getParent]());
htmlContentAreas[i].children.push(html); trailer = trailer ? trailer[0] : null;
} }
if (mustBreak) { let target = null;
break; 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; this[$extra].pageNumber += 1;

View File

@ -168,21 +168,30 @@ function getBBox(data) {
class HTMLResult { class HTMLResult {
static get FAILURE() { 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() { 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.success = success;
this.html = html; this.html = html;
this.bbox = bbox; 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) { static success(html, bbox = null) {
return new HTMLResult(true, html, bbox); return new HTMLResult(true, html, bbox, null);
} }
} }

View File

@ -23,7 +23,6 @@ import { searchNode } from "./som.js";
const $acceptWhitespace = Symbol(); const $acceptWhitespace = Symbol();
const $addHTML = Symbol(); const $addHTML = Symbol();
const $appendChild = Symbol(); const $appendChild = Symbol();
const $break = Symbol();
const $childrenToHTML = Symbol(); const $childrenToHTML = Symbol();
const $clean = Symbol(); const $clean = Symbol();
const $cleanup = Symbol(); const $cleanup = Symbol();
@ -342,7 +341,7 @@ class XFAObject {
const availableSpace = this[$getAvailableSpace](); const availableSpace = this[$getAvailableSpace]();
const res = this[$extra].failingNode[$toHTML](availableSpace); const res = this[$extra].failingNode[$toHTML](availableSpace);
if (!res.success) { if (!res.success) {
return false; return res;
} }
if (res.html) { if (res.html) {
this[$addHTML](res.html, res.bbox); this[$addHTML](res.html, res.bbox);
@ -357,7 +356,7 @@ class XFAObject {
} }
const res = gen.value; const res = gen.value;
if (!res.success) { if (!res.success) {
return false; return res;
} }
if (res.html) { if (res.html) {
this[$addHTML](res.html, res.bbox); this[$addHTML](res.html, res.bbox);
@ -366,7 +365,7 @@ class XFAObject {
this[$extra].generator = null; this[$extra].generator = null;
return true; return HTMLResult.EMPTY;
} }
[$setSetAttributes](attributes) { [$setSetAttributes](attributes) {
@ -960,7 +959,6 @@ export {
$acceptWhitespace, $acceptWhitespace,
$addHTML, $addHTML,
$appendChild, $appendChild,
$break,
$childrenToHTML, $childrenToHTML,
$clean, $clean,
$cleanup, $cleanup,

View File

@ -0,0 +1 @@
https://web.archive.org/web/20210509141350/https://www.sos.state.oh.us/globalassets/elections/directives/2020/dir2020-25_annualexpensereport.pdf

View File

@ -0,0 +1 @@
https://web.archive.org/web/20210509141345/https://www.sos.state.oh.us/globalassets/elections/directives/2020/dir2020-25_eavsform.pdf

View File

@ -0,0 +1 @@
https://bugzilla.mozilla.org/attachment.cgi?id=9226577

View File

@ -930,6 +930,30 @@
"link": true, "link": true,
"type": "load" "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", { "id": "xfa_fish_licence",
"file": "pdfs/xfa_fish_licence.pdf", "file": "pdfs/xfa_fish_licence.pdf",
"md5": "9b993128bbd7f4217098fd44116ebec2", "md5": "9b993128bbd7f4217098fd44116ebec2",