From b3b92bb14f96624199ae6ceaa0dcb7766f5f40e4 Mon Sep 17 00:00:00 2001 From: Joao Goncalves Date: Fri, 15 May 2026 13:39:34 -0500 Subject: [PATCH] FOP-3316 Fix reading order when using the static-region-per-page conf --- .../table/TableContentLayoutManager.java | 5 +- .../render/pdf/PDFStructureTreeBuilder.java | 11 +- .../render/pdf/PDFStructureTreeTestCase.java | 206 +++++++++++------- fop/test/fo/reading_order.fo | 30 +++ .../reading_order_block_spanned_over_page.fo | 30 +++ fop/test/fo/reading_order_table_in_body.fo | 63 ++++++ fop/test/fo/reading_order_table_in_header.fo | 53 +++++ 7 files changed, 305 insertions(+), 93 deletions(-) create mode 100644 fop/test/fo/reading_order.fo create mode 100644 fop/test/fo/reading_order_block_spanned_over_page.fo create mode 100644 fop/test/fo/reading_order_table_in_body.fo create mode 100644 fop/test/fo/reading_order_table_in_header.fo diff --git a/fop-core/src/main/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java b/fop-core/src/main/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java index 053ce737a11..2b58d96d80e 100644 --- a/fop-core/src/main/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java +++ b/fop-core/src/main/java/org/apache/fop/layoutmgr/table/TableContentLayoutManager.java @@ -465,7 +465,7 @@ void addAreas(PositionIterator parentIter, LayoutContext layoutContext) { addHeaderFooterAreas(headerElements, tableLM.getTable().getTableHeader(), painter, false); if (!ancestorTreatAsArtifact) { - headerIsBeingRepeated = true; + headerIsBeingRepeated = !tableLM.getFObj().getUserAgent().isStaticRegionsPerPageForAccessibility(); } layoutContext.setTreatAsArtifact(ancestorTreatAsArtifact); } @@ -487,7 +487,8 @@ void addAreas(PositionIterator parentIter, LayoutContext layoutContext) { if (footerElements != null && !footerElements.isEmpty()) { boolean ancestorTreatAsArtifact = layoutContext.treatAsArtifact(); - layoutContext.setTreatAsArtifact(treatFooterAsArtifact); + layoutContext.setTreatAsArtifact(treatFooterAsArtifact + && !tableLM.getFObj().getUserAgent().isStaticRegionsPerPageForAccessibility()); //Positions for footers are simply added at the end addHeaderFooterAreas(footerElements, tableLM.getTable().getTableFooter(), painter, true); if (lastPos instanceof TableHFPenaltyPosition && !tableLM.getFooterFootnotes().isEmpty()) { diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java index b755cfe29d1..7b673a2f8b6 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFStructureTreeBuilder.java @@ -355,8 +355,6 @@ public PDFStructElem build(StructureHierarchyMember parent, Attributes attribute private PDFStructElem rootStructureElement; - private boolean staticContent; - void setPdfFactory(PDFFactory pdfFactory) { this.pdfFactory = pdfFactory; } @@ -403,9 +401,6 @@ public void endPageSequence() { } public StructureTreeElement startNode(String name, Attributes attributes, StructureTreeElement parent) { - if ("static-content".equals(name)) { - staticContent = true; - } if (!isPDFA1Safe(name)) { return null; } @@ -416,7 +411,7 @@ public StructureTreeElement startNode(String name, Attributes attributes, Struct parentElem = parent; } StructureTreeElement structElem; - if (staticContent && pdfFactory.getDocument().isStaticRegionsPerPageForAccessibility()) { + if (pdfFactory.getDocument().isStaticRegionsPerPageForAccessibility()) { structElem = new Factory(name, parentElem, attributes); } else { structElem = createStructureElement( @@ -464,10 +459,6 @@ public PDFStructElem createStructureElement(int pageNumber) { } public void endNode(String name) { - if ("static-content".equals(name)) { - staticContent = false; - } - if (isPDFA1Safe(name)) { ancestors.removeFirst(); } diff --git a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFStructureTreeTestCase.java b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFStructureTreeTestCase.java index 938c81379ec..351ea0faafc 100644 --- a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFStructureTreeTestCase.java +++ b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFStructureTreeTestCase.java @@ -21,6 +21,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -35,12 +38,27 @@ import org.xml.sax.helpers.AttributesImpl; import static org.junit.Assert.assertEquals; +import org.apache.fop.accessibility.StructureTreeElement; import org.apache.fop.apps.FOUserAgent; import org.apache.fop.apps.Fop; import org.apache.fop.apps.FopFactory; import org.apache.fop.pdf.PDFLinearizationTestCase; import org.apache.fop.pdf.PDFStructElem; import org.apache.fop.pdf.StandardStructureTypes; +import org.apache.fop.pdf.StructureType; + +import static org.apache.fop.pdf.StandardStructureTypes.Grouping.DIV; +import static org.apache.fop.pdf.StandardStructureTypes.Grouping.DOCUMENT; +import static org.apache.fop.pdf.StandardStructureTypes.Grouping.PART; +import static org.apache.fop.pdf.StandardStructureTypes.Grouping.SECT; +import static org.apache.fop.pdf.StandardStructureTypes.Paragraphlike.P; +import static org.apache.fop.pdf.StandardStructureTypes.Table.TABLE; +import static org.apache.fop.pdf.StandardStructureTypes.Table.TBODY; +import static org.apache.fop.pdf.StandardStructureTypes.Table.TD; +import static org.apache.fop.pdf.StandardStructureTypes.Table.TFOOT; +import static org.apache.fop.pdf.StandardStructureTypes.Table.TH; +import static org.apache.fop.pdf.StandardStructureTypes.Table.THEAD; +import static org.apache.fop.pdf.StandardStructureTypes.Table.TR; public class PDFStructureTreeTestCase { @Test @@ -77,111 +95,137 @@ public void testStaticRegionPerPage() throws Exception { } @Test - public void testTableHeaderDuplicatedIfStaticRegionsPerPageTrue() throws Exception { - List elems = getPDFStructElems("\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Table overflow\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Table Title \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Row 1 Column A\n" - + " \n" - + " \n" - + " Row 1 Column B\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Row 2 Column A\n" - + " \n" - + " \n" - + " Row 2 Column B\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Row 2 Column A\n" - + " \n" - + " \n" - + " Row 2 Column B\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " Row 2 Column A\n" - + " \n" - + " \n" - + " Row 2 Column B\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + ""); + public void testReadingOrder() throws Exception { + checkReadingOrder(Arrays.asList(DOCUMENT, PART, SECT, DIV, P, P, DIV, P, DIV, P, P, DIV, P), + "test/fo/reading_order.fo"); + } + @Test + public void testReadingOrderWithTableInHeader() throws Exception { + checkReadingOrder(Arrays.asList(DOCUMENT, PART, SECT, DIV, P, TABLE, THEAD, TR, TH, P, TBODY, TR, TD, P, TFOOT, + TR, TD, P, P, DIV, P, DIV, P, TABLE, THEAD, TR, TH, P, TBODY, TR, TD, P, TFOOT, + TR, TD, P, P, DIV, P), + "test/fo/reading_order_table_in_header.fo"); + } + + @Test + public void testReadingOrderBlockSpannedOverPage() throws Exception { + checkReadingOrder(Arrays.asList(DOCUMENT, PART, SECT, DIV, P, P, DIV, P, DIV, P, P, DIV, P, DIV, P, P, DIV, P), + "test/fo/reading_order_block_spanned_over_page.fo"); + } + + @Test + public void testReadingOrderTableInBody() throws Exception { + checkReadingOrder(Arrays.asList(DOCUMENT, PART, SECT, DIV, P, P, TABLE, THEAD, TR, TH, P, TBODY, TR, TD, P, TD, + P, TFOOT, TR, TD, P, DIV, P, DIV, P, P, TABLE, THEAD, TR, TH, P, TBODY, TR, TD, P, TD, P, TFOOT, + TR, TD, P, DIV, P), + "test/fo/reading_order_table_in_body.fo"); + } + + private int countTableElements(List elems, StructureType elementType) { int count = 0; for (PDFStructElem elem : elems) { - if (elem.getStructureType().equals(StandardStructureTypes.Table.THEAD)) { + if (elem.getStructureType().equals(elementType)) { count++; } } - assertEquals("The static region per page conf must apply to static regions only", 1, count); + return count; } - private List getPDFStructElems(String fo) throws Exception { - FopFactory fopFactory = getFopFactory(); - FOUserAgent userAgent = fopFactory.newFOUserAgent(); - foToOutput(fo, fopFactory, userAgent); + private void checkReadingOrder(List orderedTypes, String filePath) throws Exception { + List elems = getPDFStructElems(filePath, true); - PDFStructElem block = (PDFStructElem) userAgent - .getStructureTreeEventHandler().startNode("block", new AttributesImpl(), null); + int index = 0; + for (PDFStructElem elem : elems) { + assertEquals("Reading order must be preserved when static-region-per-page is true", + orderedTypes.get(index), elem.getStructureType()); + index++; + } - return block.getDocument().getStructureTreeElements(); + assertEquals("Must verify all the PDFStructElements", orderedTypes.size(), elems.size()); + } + + @Test + public void testTableDuplicatedIfStaticRegionsPerPageTrue() throws Exception { + checkTableBodyCount(true, "test/fo/reading_order_table_in_body.fo", + "A table element should only have one respective structure element", 2); + } + + @Test + public void testTableBodyNotDuplicatedIfStaticRegionsPerPageFalse() throws Exception { + checkTableBodyCount(false, "test/fo/reading_order_table_in_body.fo", + "A table element should only have one respective structure element", 1); + } + + @Test + public void testTableBodyDuplicatedIfInsideStaticContent() throws Exception { + checkTableBodyCount(true, "test/fo/reading_order_table_in_header.fo", + "If the conf is set to true, a table element must be duplicated like any other fo element", 2); + } + + private void checkTableBodyCount(boolean staticRegionPerPage, String filePath, String assertionMessage, + int expectedCount) throws Exception { + List elems = getPDFStructElems(filePath, staticRegionPerPage); + + int count = countTableElements(elems, TABLE); + assertEquals(assertionMessage, expectedCount, count); + + count = countTableElements(elems, StandardStructureTypes.Table.TBODY); + assertEquals(assertionMessage, expectedCount, count); + + count = countTableElements(elems, THEAD); + assertEquals(assertionMessage, expectedCount, count); + + count = countTableElements(elems, StandardStructureTypes.Table.TFOOT); + assertEquals(assertionMessage, expectedCount, count); + } + + private List getPDFStructElems(String foFileName, boolean staticRegionPerPage) throws Exception { + FopFactory fopFactory = getFopFactory(staticRegionPerPage, true); + FOUserAgent userAgent = fopFactory.newFOUserAgent(); + foToOutput(new FileInputStream(foFileName), fopFactory, userAgent); + + StructureTreeElement block = userAgent.getStructureTreeEventHandler() + .startNode("#PCDATA", new AttributesImpl(), null); + + PDFStructElem blockElem; + if (block instanceof PDFStructElem) { + blockElem = (PDFStructElem) block; + } else { + blockElem = ((PDFStructureTreeBuilder.Factory) block).createStructureElement(1); + } + + return blockElem.getDocument().getStructureTreeElements(); } private ByteArrayOutputStream foToOutput(String fo) throws Exception { - FopFactory fopFactory = getFopFactory(); - return foToOutput(fo, fopFactory, fopFactory.newFOUserAgent()); + FopFactory fopFactory = getFopFactory(true, false); + return foToOutput(new ByteArrayInputStream(fo.getBytes()), fopFactory, fopFactory.newFOUserAgent()); } - private ByteArrayOutputStream foToOutput(String fo, FopFactory fopFactory, FOUserAgent userAgent) throws Exception { + + private ByteArrayOutputStream foToOutput(InputStream inputStream, FopFactory fopFactory, FOUserAgent userAgent) + throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); Fop fop = fopFactory.newFop("application/pdf", userAgent, bos); Transformer transformer = TransformerFactory.newInstance().newTransformer(); - Source src = new StreamSource(new ByteArrayInputStream(fo.getBytes())); + + Source src = new StreamSource(inputStream); Result res = new SAXResult(fop.getDefaultHandler()); transformer.transform(src, res); return bos; } - private FopFactory getFopFactory() throws Exception { - String fopxconf = - "true"; + private FopFactory getFopFactory(boolean staticRegionPerPage, boolean useObjectsStreams) throws Exception { + String fopxconf = "" + + " true" + + " \n" + + " \n" + + " " + useObjectsStreams + "" + + " \n" + + " \n" + + ""; return FopFactory.newInstance(new File(".").toURI(), new ByteArrayInputStream(fopxconf.getBytes())); } } diff --git a/fop/test/fo/reading_order.fo b/fop/test/fo/reading_order.fo new file mode 100644 index 00000000000..aa04d0798fa --- /dev/null +++ b/fop/test/fo/reading_order.fo @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + rest header + + + + + + + rest footer + + + + + test + test1 + + + \ No newline at end of file diff --git a/fop/test/fo/reading_order_block_spanned_over_page.fo b/fop/test/fo/reading_order_block_spanned_over_page.fo new file mode 100644 index 00000000000..88abe1909c0 --- /dev/null +++ b/fop/test/fo/reading_order_block_spanned_over_page.fo @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + rest header + + + + + + + rest footer + + + + + test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test + test1 + + + \ No newline at end of file diff --git a/fop/test/fo/reading_order_table_in_body.fo b/fop/test/fo/reading_order_table_in_body.fo new file mode 100644 index 00000000000..65b82b80d53 --- /dev/null +++ b/fop/test/fo/reading_order_table_in_body.fo @@ -0,0 +1,63 @@ + + + + + + + + + + + + + rest header + + + + + + + rest footer + + + + + + + + + + Table Title + + + + + + + Table footer + + + + + + + Row 1 Column A + + + Row 1 Column B + + + + + Row 2 Column A + + + Row 2 Column B + + + + + + + + \ No newline at end of file diff --git a/fop/test/fo/reading_order_table_in_header.fo b/fop/test/fo/reading_order_table_in_header.fo new file mode 100644 index 00000000000..694b127ab4c --- /dev/null +++ b/fop/test/fo/reading_order_table_in_header.fo @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + rest header + + + + + table header + + + + + + + table footer + + + + + + + table body + + + + + + + + + + + rest footer + + + + + test + test1 + + + \ No newline at end of file