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) {
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 };
}
}

View File

@ -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;
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;
}
if (mustBreak) {
break;
}
flush(i);
}
this[$extra].pageNumber += 1;

View File

@ -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);
}
}

View File

@ -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,

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,
"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",