diff --git a/src/core/xfa/layout.js b/src/core/xfa/layout.js index 58a0b9781..dc46ac5e5 100644 --- a/src/core/xfa/layout.js +++ b/src/core/xfa/layout.js @@ -257,70 +257,91 @@ function getTransformedBBox(node) { * in case of lr-tb or changing content area...). */ function checkDimensions(node, space) { + if (node[$getTemplateRoot]()[$extra].firstUnsplittable === null) { + return true; + } + if (node.w === 0 || node.h === 0) { return true; } - if (space.width <= 0 || space.height <= 0) { - return false; - } - const parent = node[$getParent](); - const attempt = (node[$extra] && node[$extra].attempt) || 0; + const attempt = (parent[$extra] && parent[$extra].attempt) || 0; + let y, w, h; switch (parent.layout) { case "lr-tb": case "rl-tb": - switch (attempt) { - case 0: { - let w, h; - if (node.w !== "" || node.h !== "") { - [, , w, h] = getTransformedBBox(node); - } + 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) { + // Not enough height. return false; } if (node.w !== "") { + // True if width is enough. return Math.round(w - space.width) <= 1; } - return node.minW <= space.width; + return space.width > 0; } - case 1: { - if (node.h !== "" && !node[$isSplittable]()) { - const [, , , h] = getTransformedBBox(node); - if (Math.round(h - space.height) > 1) { - return false; - } - } - return true; + + // No layout failure. + + // 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; } - default: - return true; + + return space.width > 0; } + + // Second attempt: try to put the element on the next line. + + if (node[$getTemplateRoot]()[$extra].noLayoutFailure) { + // We cannot fail. + return true; + } + + if (node.h !== "") { + // True if height is enough. + return Math.round(h - space.height) <= 1; + } + + return space.height > 0; case "table": case "tb": - if (attempt !== 1 && node.h !== "" && !node[$isSplittable]()) { - const [, , , h] = getTransformedBBox(node); - if (Math.round(h - space.height) > 1) { - return false; - } + if (node[$getTemplateRoot]()[$extra].noLayoutFailure) { + return true; } - return true; - case "position": - const [x, y, w, h] = getTransformedBBox(node); - const isWidthOk = node.w === "" || Math.round(w + x - space.width) <= 1; - const isHeightOk = node.h === "" || Math.round(h + y - space.height) <= 1; - if (isWidthOk && isHeightOk) { + // If the node has a height then check + // if it's fine with available height. If the node + // is breakable then we can return true. + if (node.h !== "" && !node[$isSplittable]()) { + [, , , h] = getTransformedBBox(node); + return Math.round(h - space.height) <= 1; + } + // Else wait and see: this node will be layed out itself + // in the provided space and maybe a children won't fit. + return space.height > 0; + case "position": + if (node[$getTemplateRoot]()[$extra].noLayoutFailure) { + return true; + } + + [, y, , h] = getTransformedBBox(node); + if (node.h === "" || Math.round(h + y - space.height) <= 1) { return true; } const area = node[$getTemplateRoot]()[$extra].currentContentArea; - if (isWidthOk) { - return h + y > area.h; - } - - return w + x > area.w; + return h + y > area.h; case "rl-row": case "row": default: diff --git a/src/core/xfa/template.js b/src/core/xfa/template.js index f9fed95d5..dc7a104df 100644 --- a/src/core/xfa/template.js +++ b/src/core/xfa/template.js @@ -28,6 +28,7 @@ import { $getAvailableSpace, $getChildren, $getContainedChildren, + $getExtra, $getNextPage, $getParent, $getSubformParent, @@ -141,6 +142,84 @@ function valueToHtml(value) { }); } +function setFirstUnsplittable(node) { + const root = node[$getTemplateRoot](); + if (root[$extra].firstUnsplittable === null) { + root[$extra].firstUnsplittable = node; + root[$extra].noLayoutFailure = true; + } +} + +function unsetFirstUnsplittable(node) { + const root = node[$getTemplateRoot](); + if (root[$extra].firstUnsplittable === node) { + root[$extra].noLayoutFailure = false; + } +} + +function handleBreak(node) { + if (node[$extra]) { + return false; + } + + node[$extra] = Object.create(null); + + if (node.targetType === "auto") { + return false; + } + + const root = node[$getTemplateRoot](); + let target = null; + if (node.target) { + target = root[$searchNode](node.target, node[$getParent]()); + target = target ? target[0] : target; + } + + const { currentPageArea, currentContentArea } = root[$extra]; + + if (node.targetType === "pageArea") { + if (!(target instanceof PageArea)) { + target = null; + } + + if (node.startNew) { + node[$extra].target = target || currentPageArea; + return true; + } else if (target && target !== currentPageArea) { + node[$extra].target = target; + return true; + } + + return false; + } + + if (!(target instanceof ContentArea)) { + target = null; + } + + const pageArea = target[$getParent](); + const contentAreas = pageArea.contentArea.children; + + let index; + if (node.startNew) { + if (target) { + index = contentAreas.findIndex(e => e === target) - 1; + } else { + index = currentPageArea.contentArea.children.findIndex( + e => e === currentContentArea + ); + } + } else if (target && target !== currentContentArea) { + index = contentAreas.findIndex(e => e === target) - 1; + } else { + return false; + } + + node[$extra].target = pageArea === currentPageArea ? null : pageArea; + node[$extra].index = index; + return true; +} + class AppearanceFilter extends StringObject { constructor(attributes) { super(TEMPLATE_NS_ID, "appearanceFilter"); @@ -718,8 +797,6 @@ class BreakAfter extends XFAObject { "auto", "contentArea", "pageArea", - "pageEven", - "pageOdd", ]); this.trailer = attributes.trailer || ""; this.use = attributes.use || ""; @@ -743,8 +820,6 @@ class BreakBefore extends XFAObject { "auto", "contentArea", "pageArea", - "pageEven", - "pageOdd", ]); this.trailer = attributes.trailer || ""; this.use = attributes.use || ""; @@ -1176,7 +1251,6 @@ class ContentArea extends XFAObject { const top = measureToString(this.y); const style = { - position: "absolute", left, top, width: measureToString(this.w), @@ -1550,9 +1624,11 @@ class Draw extends XFAObject { } } + setFirstUnsplittable(this); if (!checkDimensions(this, availableSpace)) { return HTMLResult.FAILURE; } + unsetFirstUnsplittable(this); const style = toStyle( this, @@ -2038,21 +2114,32 @@ class ExclGroup extends XFAObject { [$isSplittable]() { // We cannot cache the result here because the contentArea // can change. + if (!this[$getParent]()[$isSplittable]()) { + return false; + } + const root = this[$getTemplateRoot](); const contentArea = root[$extra].currentContentArea; if (contentArea && Math.max(this.minH, this.h || 0) >= contentArea.h) { return true; } + if (this[$extra]._isSplittable !== undefined) { + return this[$extra]._isSplittable; + } + if (this.layout === "position") { + this[$extra]._isSplittable = false; return false; } const parentLayout = this[$getParent]().layout; if (parentLayout && parentLayout.includes("row")) { + this[$extra]._isSplittable = false; return false; } + this[$extra]._isSplittable = true; return true; } @@ -2103,6 +2190,11 @@ class ExclGroup extends XFAObject { currentWidth: 0, }); + const isSplittable = this[$isSplittable](); + if (!isSplittable) { + setFirstUnsplittable(this); + } + if (!checkDimensions(this, availableSpace)) { return HTMLResult.FAILURE; } @@ -2180,6 +2272,10 @@ class ExclGroup extends XFAObject { } } + if (!isSplittable) { + unsetFirstUnsplittable(this); + } + if (failure) { if (this[$isSplittable]()) { delete this[$extra]; @@ -2366,9 +2462,11 @@ class Field extends XFAObject { fixDimensions(this); + setFirstUnsplittable(this); if (!checkDimensions(this, availableSpace)) { return HTMLResult.FAILURE; } + unsetFirstUnsplittable(this); const style = toStyle( this, @@ -3320,6 +3418,18 @@ class Overflow extends XFAObject { this.use = attributes.use || ""; this.usehref = attributes.usehref || ""; } + + [$getExtra]() { + if (!this[$extra]) { + const parent = this[$getParent](); + const root = this[$getTemplateRoot](); + const target = root[$searchNode](this.target, parent); + this[$extra] = { + target: (target && target[0]) || null, + }; + } + return this[$extra]; + } } class PageArea extends XFAObject { @@ -3457,8 +3567,10 @@ class PageArea extends XFAObject { name: "div", children, attributes: { + class: ["xfaPage"], id: this[$uid], style, + xfaName: this.name, }, }); } @@ -3875,7 +3987,7 @@ class Radial extends XFAObject { this.type === "toEdge" ? `${startColor},${endColor}` : `${endColor},${startColor}`; - return `radial-gradient(circle to center, ${colors})`; + return `radial-gradient(circle at center, ${colors})`; } } @@ -4235,32 +4347,44 @@ class Subform extends XFAObject { return getAvailableSpace(this); } - [$isSplittable](x) { + [$isSplittable]() { // We cannot cache the result here because the contentArea // can change. + if (!this[$getParent]()[$isSplittable]()) { + return false; + } + const root = this[$getTemplateRoot](); const contentArea = root[$extra].currentContentArea; if (contentArea && Math.max(this.minH, this.h || 0) >= contentArea.h) { return true; } + if (this.overflow) { + return this.overflow[$getExtra]().target !== contentArea; + } + + if (this[$extra]._isSplittable !== undefined) { + return this[$extra]._isSplittable; + } + if (this.layout === "position") { + this[$extra]._isSplittable = false; return false; } if (this.keep && this.keep.intact !== "none") { + this[$extra]._isSplittable = false; return false; } const parentLayout = this[$getParent]().layout; if (parentLayout && parentLayout.includes("row")) { + this[$extra]._isSplittable = false; return false; } - if (this.overflow && this.overflow.target) { - const target = root[$searchNode](this.overflow.target, this); - return target && target[0] === contentArea; - } + this[$extra]._isSplittable = true; return true; } @@ -4323,9 +4447,7 @@ 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; + if (handleBreak(breakBefore)) { return HTMLResult.breakNode(breakBefore); } } @@ -4359,6 +4481,24 @@ class Subform extends XFAObject { currentWidth: 0, }); + const root = this[$getTemplateRoot](); + const currentContentArea = root[$extra].currentContentArea; + const savedNoLayoutFailure = root[$extra].noLayoutFailure; + + if (this.overflow) { + // In case of overflow in the current content area, + // elements must be kept in this subform so it implies + // to have no errors on layout failures. + root[$extra].noLayoutFailure = + root[$extra].noLayoutFailure || + this.overflow[$getExtra]().target === currentContentArea; + } + + const isSplittable = this[$isSplittable](); + if (!isSplittable) { + setFirstUnsplittable(this); + } + if (!checkDimensions(this, availableSpace)) { return HTMLResult.FAILURE; } @@ -4403,16 +4543,10 @@ class Subform extends XFAObject { attributes.xfaName = this.name; } - const isSplittable = this[$isSplittable](); - - // If the container overflows into itself we add an extra - // layout step to accept finally the element which caused - // the overflow. - let maxRun = + const maxRun = this.layout === "lr-tb" || this.layout === "rl-tb" ? MAX_ATTEMPTS_FOR_LRTB_LAYOUT : 1; - maxRun += !isSplittable && this.layout !== "position" ? 1 : 0; for (; this[$extra].attempt < maxRun; this[$extra].attempt++) { const result = this[$childrenToHTML]({ filter, @@ -4426,6 +4560,11 @@ class Subform extends XFAObject { } } + if (!isSplittable) { + unsetFirstUnsplittable(this); + } + root[$extra].noLayoutFailure = savedNoLayoutFailure; + if (this[$extra].attempt === maxRun) { if (this.overflow) { this[$getTemplateRoot]()[$extra].overflowNode = this.overflow; @@ -4467,8 +4606,10 @@ class Subform extends XFAObject { if (this.breakAfter.children.length >= 1) { const breakAfter = this.breakAfter.children[0]; - this[$extra].afterBreakAfter = result; - return HTMLResult.breakNode(breakAfter); + if (handleBreak(breakAfter)) { + this[$extra].afterBreakAfter = result; + return HTMLResult.breakNode(breakAfter); + } } delete this[$extra]; @@ -4623,6 +4764,10 @@ class Template extends XFAObject { } } + [$isSplittable]() { + return true; + } + [$searchNode](expr, container) { if (expr.startsWith("#")) { // This is an id. @@ -4640,6 +4785,10 @@ class Template extends XFAObject { } this[$extra] = { overflowNode: null, + firstUnsplittable: null, + currentContentArea: null, + currentPageArea: null, + noLayoutFailure: false, pageNumber: 1, pagePosition: "first", oddOrEven: "odd", @@ -4711,9 +4860,11 @@ class Template extends XFAObject { let trailer = null; let hasSomething = true; let hasSomethingCounter = 0; + let startIndex = 0; while (true) { if (!hasSomething) { + mainHtml.children.pop(); // Nothing has been added in the previous page if (++hasSomethingCounter === MAX_EMPTY_PAGES) { warn("XFA - Something goes wrong: please file a bug."); @@ -4724,15 +4875,18 @@ class Template extends XFAObject { } targetPageArea = null; + this[$extra].currentPageArea = pageArea; const page = pageArea[$toHTML]().html; mainHtml.children.push(page); if (leader) { + this[$extra].noLayoutFailure = true; page.children.push(leader[$toHTML](pageArea[$extra].space).html); leader = null; } if (trailer) { + this[$extra].noLayoutFailure = true; page.children.push(trailer[$toHTML](pageArea[$extra].space).html); trailer = null; } @@ -4743,6 +4897,8 @@ class Template extends XFAObject { ); hasSomething = false; + this[$extra].firstUnsplittable = null; + this[$extra].noLayoutFailure = false; const flush = index => { const html = root[$flushHTML](); @@ -4753,9 +4909,10 @@ class Template extends XFAObject { } }; - for (let i = 0, ii = contentAreas.length; i < ii; i++) { + for (let i = startIndex, ii = contentAreas.length; i < ii; i++) { const contentArea = (this[$extra].currentContentArea = contentAreas[i]); const space = { width: contentArea.w, height: contentArea.h }; + startIndex = 0; if (leader) { htmlContentAreas[i].children.push(leader[$toHTML](space).html); @@ -4782,14 +4939,12 @@ class Template extends XFAObject { if (html.isBreak()) { const node = html.breakNode; + flush(i); if (node.targetType === "auto") { - flush(i); continue; } - const startNew = node.startNew === 1; - if (node.leader) { leader = this[$searchNode](node.leader, node[$getParent]()); leader = leader ? leader[0] : null; @@ -4800,41 +4955,18 @@ class Template extends XFAObject { trailer = trailer ? trailer[0] : null; } - let target = null; - if (node.target) { - target = this[$searchNode](node.target, node[$getParent]()); - target = target ? target[0] : target; - } - if (node.targetType === "pageArea") { - if (!(target instanceof PageArea)) { - target = null; - } - - if (startNew) { - targetPageArea = target || pageArea; - flush(i); - i = Infinity; - } else if (target && target !== pageArea) { - targetPageArea = target; - flush(i); - i = Infinity; - } else { - i--; - } - } else if (node.targetType === "contentArea") { - if (!(target instanceof ContentArea)) { - target = null; - } - - const index = contentAreas.findIndex(e => e === target); - if (index !== -1) { - flush(i); - i = index - 1; - } else { - i--; - } + targetPageArea = node[$extra].target; + i = Infinity; + } else if (!node[$extra].target) { + // We stay on the same page. + i = node[$extra].index; + } else { + targetPageArea = node[$extra].target; + startIndex = node[$extra].index + 1; + i = Infinity; } + continue; } @@ -4860,17 +4992,20 @@ class Template extends XFAObject { target = target ? target[0] : target; } + i = Infinity; 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--; + targetPageArea = target[$getParent](); + startIndex = + targetPageArea.contentArea.children.findIndex( + e => e === target + ) - 1; } } continue; diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js index 62f3ff13d..a890bcc05 100644 --- a/src/core/xfa/xfa_object.js +++ b/src/core/xfa/xfa_object.js @@ -43,6 +43,7 @@ const $getChildrenByClass = Symbol(); const $getChildrenByName = Symbol(); const $getChildrenByNameIt = Symbol(); const $getDataValue = Symbol(); +const $getExtra = Symbol(); const $getRealChildrenByNameIt = Symbol(); const $getChildren = Symbol(); const $getContainedChildren = Symbol(); @@ -177,11 +178,7 @@ class XFAObject { } [$getTemplateRoot]() { - let parent = this[$getParent](); - while (parent[$nodeName] !== "template") { - parent = parent[$getParent](); - } - return parent; + return this[$globalData].template; } [$isSplittable]() { @@ -1060,6 +1057,7 @@ export { $getChildrenByNameIt, $getContainedChildren, $getDataValue, + $getExtra, $getNextPage, $getParent, $getRealChildrenByNameIt, diff --git a/test/pdfs/xfa_bug1717668_1.pdf.link b/test/pdfs/xfa_bug1717668_1.pdf.link new file mode 100644 index 000000000..bb9dc54a2 --- /dev/null +++ b/test/pdfs/xfa_bug1717668_1.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9228387 diff --git a/test/pdfs/xfa_bug1717668_2.pdf.link b/test/pdfs/xfa_bug1717668_2.pdf.link new file mode 100644 index 000000000..82cd541a6 --- /dev/null +++ b/test/pdfs/xfa_bug1717668_2.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9228727 diff --git a/test/pdfs/xfa_bug1717668_3.pdf.link b/test/pdfs/xfa_bug1717668_3.pdf.link new file mode 100644 index 000000000..060751480 --- /dev/null +++ b/test/pdfs/xfa_bug1717668_3.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9228728 diff --git a/test/pdfs/xfa_bug1717805.pdf.link b/test/pdfs/xfa_bug1717805.pdf.link new file mode 100644 index 000000000..1361b6549 --- /dev/null +++ b/test/pdfs/xfa_bug1717805.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9228507 diff --git a/test/pdfs/xfa_dhl_shipment.pdf.link b/test/pdfs/xfa_dhl_shipment.pdf.link new file mode 100644 index 000000000..e68bd4c86 --- /dev/null +++ b/test/pdfs/xfa_dhl_shipment.pdf.link @@ -0,0 +1 @@ +https://web.archive.org/web/20210621073147/https://www.dhl.com/content/dam/downloads/g0/express/emailship_page/globalpage/dhl_emailship_pdfclient_my_en.pdf diff --git a/test/pdfs/xfa_issue13611.pdf.link b/test/pdfs/xfa_issue13611.pdf.link new file mode 100644 index 000000000..eba8353b6 --- /dev/null +++ b/test/pdfs/xfa_issue13611.pdf.link @@ -0,0 +1 @@ +https://github.com/mozilla/pdf.js/files/6695365/2229E.pdf diff --git a/test/test_manifest.json b/test/test_manifest.json index 50a030ef1..ab4b23497 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -946,6 +946,38 @@ "enableXfa": true, "type": "eq" }, + { "id": "xfa_bug1717805", + "file": "pdfs/xfa_bug1717805.pdf", + "md5": "c68ccebd0d92b8fd70c465660458507f", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, + { "id": "xfa_bug17176688_1", + "file": "pdfs/xfa_bug1717668_1.pdf", + "md5": "564ecff67be690b43c2a144ae5967034", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, + { "id": "xfa_bug17176688_2", + "file": "pdfs/xfa_bug1717668_2.pdf", + "md5": "08aa8bf9fec5aa7b8ff13d9ba72ca8ac", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, + { "id": "xfa_bug17176688_3", + "file": "pdfs/xfa_bug1717668_3.pdf", + "md5": "4ff2531dbefebabc3f128d4ae20552a4", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, { "id": "xfa_bug1718037", "file": "pdfs/xfa_bug1718037.pdf", "md5": "a0b53d50e9faed9d57950a5159d5da12", @@ -970,6 +1002,14 @@ "enableXfa": true, "type": "eq" }, + { "id": "xfa_dhl_shipment", + "file": "pdfs/xfa_dhl_shipment.pdf", + "md5": "503ece429d69e7d626d6932a475fbe63", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, { "id": "xfa_bug1716047", "file": "pdfs/xfa_bug1716047.pdf", "md5": "2f524163bd8397f43d195090978c3b56", @@ -1050,6 +1090,14 @@ "enableXfa": true, "type": "eq" }, + { "id": "xfa_issue13611", + "file": "pdfs/xfa_issue13611.pdf", + "md5": "e713fb46a6b4637660010f1850559ff6", + "link": true, + "rounds": 1, + "enableXfa": true, + "type": "eq" + }, { "id": "xfa_issue13634", "file": "pdfs/xfa_issue13634.pdf", "md5": "459a04045470811cbab6671772929d3d", diff --git a/test/unit/xfa_tohtml_spec.js b/test/unit/xfa_tohtml_spec.js index 0461f3075..3a6d5bd29 100644 --- a/test/unit/xfa_tohtml_spec.js +++ b/test/unit/xfa_tohtml_spec.js @@ -56,14 +56,18 @@ describe("XFAFactory", function () { - - foo + + + + bar + + @@ -92,7 +96,6 @@ describe("XFAFactory", function () { width: "456px", left: "123px", top: "0px", - position: "absolute", }); const wrapper = page1.children[0]; diff --git a/web/xfa_layer_builder.css b/web/xfa_layer_builder.css index c364289d1..a87ccacda 100644 --- a/web/xfa_layer_builder.css +++ b/web/xfa_layer_builder.css @@ -13,6 +13,15 @@ * limitations under the License. */ +.xfaPage { + overflow: hidden; + position: relative; +} + +.xfaContentarea { + position: absolute; +} + .xfaPrintOnly { display: none; } @@ -139,10 +148,6 @@ flex: 1 1 auto; } -.xfaContentArea { - overflow: hidden; -} - .xfaTextfield, .xfaSelect { background-color: rgba(0, 54, 255, 0.13);