From 912b5cbaad0967d32e5fb9b23018866c0f15e784 Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Wed, 7 Oct 2026 08:58:41 +1100 Subject: [PATCH] FOP-3354: A side float whose end falls inside a table ends after the table, not NoSuchElementException When a side float outlasts the paragraph it is anchored in and the content beside it continues into a table, the float's end falls between two rows. PageBreaker.handleFloatLayout then reads the element list again from that break, and TableLayoutManager cannot be restarted (isRestartable() is false): the table's child iterator, already exhausted, threw NoSuchElementException in LMiter.next and the document failed. PageBreakingAlgorithm.considerLegalBreak no longer takes a break between two boxes of the same non-restartable layout manager as the float's edge; the edge waits for the first legal break after it. The rows below the float keep the table's width, which the table set once for all its rows, so nothing is drawn differently from a re-layout at the row, and the block after the table is laid out at full width. Test: float_edge-inside-table.xml, which threw NoSuchElementException before. Co-Authored-By: Claude Opus 5.5 --- .../fop/layoutmgr/PageBreakingAlgorithm.java | 47 ++++++++++++++++ .../float_edge-inside-table.xml | 53 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 fop/test/layoutengine/standard-testcases/float_edge-inside-table.xml diff --git a/fop-core/src/main/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java b/fop-core/src/main/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java index 925f69ea83a..817f5712742 100644 --- a/fop-core/src/main/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java +++ b/fop-core/src/main/java/org/apache/fop/layoutmgr/PageBreakingAlgorithm.java @@ -514,10 +514,57 @@ protected void considerLegalBreak(KnuthElement element, int elementIdx) { //nop } } + // A side float's edge is a break after which the content is read again from its layout manager, + // which a table cannot do (it is not restartable): a break between two boxes of one such layout + // manager is not the edge, which waits for the first legal break after it. The rows below the + // float keep the table's width, which the table set once for all its rows, so nothing is drawn + // differently. + boolean edgeDeferred = handlingEndOfFloat && isInsideNonRestartableLM(elementIdx); + if (edgeDeferred) { + handlingEndOfFloat = false; + } super.considerLegalBreak(element, elementIdx); + if (edgeDeferred) { + handlingEndOfFloat = true; + } newFootnotes = false; } + /** Whether the boxes either side of a break both belong to one layout manager that cannot be restarted. */ + private boolean isInsideNonRestartableLM(int elementIdx) { + LayoutManager next = null; + for (int i = elementIdx; i < par.size() && next == null; i++) { + KnuthElement e = getElement(i); + if (e.isBox()) { + next = nonRestartableLM(e.getPosition()); + if (next == null) { + return false; + } + } + } + if (next == null) { + return false; + } + for (int i = elementIdx - 1; i >= 0; i--) { + KnuthElement e = getElement(i); + if (e.isBox()) { + return next == nonRestartableLM(e.getPosition()); + } + } + return false; + } + + /** The outermost layout manager in a position's chain that cannot be restarted, or null. */ + private static LayoutManager nonRestartableLM(Position position) { + for (Position p = position; p != null; p = p.getPosition()) { + LayoutManager lm = p.getLM(); + if (lm != null && !lm.isRestartable()) { + return lm; + } + } + return null; + } + /** {@inheritDoc} */ @Override protected boolean elementCanEndLine(KnuthElement element, int line, int difference) { diff --git a/fop/test/layoutengine/standard-testcases/float_edge-inside-table.xml b/fop/test/layoutengine/standard-testcases/float_edge-inside-table.xml new file mode 100644 index 00000000000..ad7c45df16a --- /dev/null +++ b/fop/test/layoutengine/standard-testcases/float_edge-inside-table.xml @@ -0,0 +1,53 @@ + + + + + + +

+ A side float outlasts the paragraph it is anchored in and its space-after, so its end falls between two + rows of the table after it. A table cannot be read again from a row, so the float's edge waits for the + first break after the table: the rows keep the table's width, and the block after the table is laid out + at full width. Before, this threw NoSuchElementException in LMiter.next. +

+
+ + + + + + + + + + BOXHeading + + + cell 1cell 2cell 3cell 4cell 5 + + after + + + + + + + + + +