You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@xmlgraphics.apache.org by jg...@apache.org on 2025/08/06 22:27:11 UTC

(xmlgraphics-fop-pdf-images) branch main updated: FOP-3230: Skip paragraph type elements when merging structure trees by Luca Bellonda

This is an automated email from the ASF dual-hosted git repository.

jgoncalves pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/xmlgraphics-fop-pdf-images.git


The following commit(s) were added to refs/heads/main by this push:
     new e911433  FOP-3230: Skip paragraph type elements when merging structure trees by Luca Bellonda
e911433 is described below

commit e911433e2d87ca3c075301973f2e0b572b65c064
Author: Joao Goncalves <jg...@smartcommunications.com>
AuthorDate: Thu Mar 13 17:33:10 2025 -0500

    FOP-3230: Skip paragraph type elements when merging structure trees by Luca Bellonda
---
 .../fop/render/pdf/pdfbox/StructureTreeMerger.java |  43 +++++++--
 .../render/pdf/StructureTreeMergerTestCase.java    |  52 ++++++++---
 .../fop/render/pdf/TaggedPDFConductorTestCase.java |  97 +++++++++++++++++++++
 .../fop/render/pdf/pdfbox/to_include_pdfua.pdf     | Bin 0 -> 17455 bytes
 4 files changed, 172 insertions(+), 20 deletions(-)

diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/StructureTreeMerger.java b/src/java/org/apache/fop/render/pdf/pdfbox/StructureTreeMerger.java
index e90a7f3..72262a1 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/StructureTreeMerger.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/StructureTreeMerger.java
@@ -36,7 +36,6 @@ import org.apache.pdfbox.cos.COSObject;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.documentinterchange.taggedpdf.StandardStructureTypes;
 
-
 import org.apache.fop.pdf.PDFDictionary;
 import org.apache.fop.pdf.PDFDocument;
 import org.apache.fop.pdf.PDFNumber;
@@ -44,7 +43,10 @@ import org.apache.fop.pdf.PDFObject;
 import org.apache.fop.pdf.PDFPage;
 import org.apache.fop.pdf.PDFReference;
 import org.apache.fop.pdf.PDFStructElem;
-
+import org.apache.fop.pdf.StandardStructureTypes.Grouping;
+import org.apache.fop.pdf.StandardStructureTypes.Illustration;
+import org.apache.fop.pdf.StandardStructureTypes.Paragraphlike;
+import org.apache.fop.pdf.StructureType;
 
 import org.apache.fop.render.pdf.PDFLogicalStructureHandler;
 
@@ -126,13 +128,13 @@ public class StructureTreeMerger {
         return currentSessionElem;
     }
 
-    public void setCurrentSessionElem() {
-        if (isCurrentSessionElemEmpty(currentSessionElem)) {
+    private void searchAndsetCurrentSessionElem() {
+        if (isCurrentSessionElemInvalid(currentSessionElem)) {
             List<PDFStructElem> structureTreeElements = pdfDoc.getStructureTreeElements();
             for (int i = structureTreeElements.size() - 1; i >= 0; i--) {
                 PDFStructElem elem = structureTreeElements.get(i);
                 // We need to make sure it's not an empty element (like an empty block)
-                if (!isCurrentSessionElemEmpty(elem)) {
+                if (!isCurrentSessionElemInvalid(elem)) {
                     currentSessionElem = elem;
                     break;
                 }
@@ -140,8 +142,35 @@ public class StructureTreeMerger {
         }
     }
 
-    private boolean isCurrentSessionElemEmpty(PDFStructElem elem) {
-        return elem == null || elem.get("P") == null;
+    public void setCurrentSessionElem() {
+        if (isElementFigureChildOfBlockLike(currentSessionElem)) {
+            // this will become a Div at the end of the merging
+            currentSessionElem = currentSessionElem.getParentStructElem();
+        } else {
+            searchAndsetCurrentSessionElem();
+        }
+    }
+
+    private static boolean isElementFigureChildOfBlockLike(PDFStructElem elem) {
+        if (elem.getStructureType() == Illustration.FIGURE) {
+            PDFStructElem parent = elem.getParentStructElem();
+            if (null != parent) {
+                final StructureType parentType = parent.getStructureType();
+                if ((parentType == Paragraphlike.P) || (parentType == Grouping.DIV)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isCurrentSessionElemInvalid(PDFStructElem elem) {
+        return elem == null || elem.get("P") == null || isStructureTypeParagraph(elem.getStructureType());
+    }
+
+    private boolean isStructureTypeParagraph(StructureType type) {
+        return Arrays.asList(Paragraphlike.H, Paragraphlike.H1, Paragraphlike.H2, Paragraphlike.H3, Paragraphlike.H4,
+                Paragraphlike.H5, Paragraphlike.H6, Paragraphlike.P).contains(type);
     }
 
     private void createParents(COSArray markedContentParents) throws IOException {
diff --git a/test/java/org/apache/fop/render/pdf/StructureTreeMergerTestCase.java b/test/java/org/apache/fop/render/pdf/StructureTreeMergerTestCase.java
index d1c4ddd..155d674 100644
--- a/test/java/org/apache/fop/render/pdf/StructureTreeMergerTestCase.java
+++ b/test/java/org/apache/fop/render/pdf/StructureTreeMergerTestCase.java
@@ -20,7 +20,9 @@ package org.apache.fop.render.pdf;
 import java.awt.Rectangle;
 import java.awt.geom.Rectangle2D;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
@@ -258,12 +260,12 @@ public class StructureTreeMergerTestCase {
     public void testNonEmptyElement() {
         PDFStructElem sectElem = createPDFStructElem(StandardStructureTypes.Grouping.SECT, CONTENT);
         PDFStructElem partElem = createPDFStructElem(StandardStructureTypes.Paragraphlike.P, CONTENT);
-        defaultTestSetCurrentSessionElem(Arrays.asList(sectElem, partElem), null, partElem,
-                "If element is not empty, it must be chosen");
+        defaultTestSetCurrentSessionElem(Arrays.asList(partElem, sectElem), null, sectElem,
+                "If element is not empty and not of type Paragraph, it must be chosen");
     }
 
     @Test
-    public void testEmptyElement() {
+    public void testEmptyParagraphElement() {
         PDFStructElem sectElem = createPDFStructElem(StandardStructureTypes.Grouping.SECT, CONTENT);
         PDFStructElem partElem = createPDFStructElem(StandardStructureTypes.Paragraphlike.P, null);
         defaultTestSetCurrentSessionElem(Arrays.asList(sectElem, partElem), null, sectElem,
@@ -280,26 +282,46 @@ public class StructureTreeMergerTestCase {
 
     @Test
     public void testEmptySectElement() {
-        PDFStructElem partElem = createPDFStructElem(StandardStructureTypes.Paragraphlike.P, CONTENT);
+        PDFStructElem divElem = createPDFStructElem(StandardStructureTypes.Grouping.DIV, CONTENT);
         PDFStructElem sectElem = createPDFStructElem(StandardStructureTypes.Grouping.SECT, null);
-        defaultTestSetCurrentSessionElem(Arrays.asList(partElem, sectElem), null, partElem,
+        defaultTestSetCurrentSessionElem(Arrays.asList(divElem, sectElem), null, divElem,
                 "Elements can't be empty");
     }
 
     @Test
     public void testSetterReplacesPreviousValueIfEmpty() {
-        PDFStructElem partElem = createPDFStructElem(StandardStructureTypes.Paragraphlike.P, CONTENT);
+        PDFStructElem divElem = createPDFStructElem(StandardStructureTypes.Grouping.DIV, CONTENT);
         PDFStructElem sectElem = createPDFStructElem(StandardStructureTypes.Grouping.SECT, null);
-        defaultTestSetCurrentSessionElem(Arrays.asList(partElem, sectElem), Arrays.asList(sectElem, partElem),
-                partElem, "If the current session elem is empty, it must be replaced");
+        defaultTestSetCurrentSessionElem(Arrays.asList(sectElem, divElem), null,
+                divElem, "Must replace current session elem if empty");
     }
 
     @Test
     public void testSetterDoesntReplacePreviousValueIfNotEmpty() {
-        PDFStructElem partElem = createPDFStructElem(StandardStructureTypes.Paragraphlike.P, CONTENT);
+        PDFStructElem divElem = createPDFStructElem(StandardStructureTypes.Grouping.DIV, CONTENT);
+        PDFStructElem emptySectElem = createPDFStructElem(StandardStructureTypes.Grouping.SECT, null);
         PDFStructElem sectElem = createPDFStructElem(StandardStructureTypes.Grouping.SECT, CONTENT);
-        defaultTestSetCurrentSessionElem(Arrays.asList(partElem, sectElem), Arrays.asList(sectElem, partElem),
-                sectElem, "Elements can't be empty");
+        defaultTestSetCurrentSessionElem(Arrays.asList(divElem, emptySectElem), Collections.singletonList(sectElem),
+                divElem, "Should not replace if current element is valid");
+    }
+
+    @Test
+    public void testIgnoredParagraphTypeElements() {
+        PDFStructElem divElem = createPDFStructElem(StandardStructureTypes.Grouping.DIV, CONTENT);
+
+        List<PDFStructElem> elems = new ArrayList<>();
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H, CONTENT));
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H1, CONTENT));
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H2, CONTENT));
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H3, CONTENT));
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H4, CONTENT));
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H5, CONTENT));
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.H6, CONTENT));
+        elems.add(divElem);
+        elems.add(createPDFStructElem(StandardStructureTypes.Paragraphlike.P, CONTENT));
+
+        defaultTestSetCurrentSessionElem(elems, null, divElem,
+                "Paragraph type elements must be ignored and replaced");
     }
 
     private void defaultTestSetCurrentSessionElem(List<PDFStructElem> elems, List<PDFStructElem> secondElems,
@@ -311,12 +333,16 @@ public class StructureTreeMergerTestCase {
         page.setDocument(mockPdfDoc);
         PDFBoxAdapter mockAdapter = mock(PDFBoxAdapter.class);
         when(mockAdapter.getTargetPage()).thenReturn(page);
-        StructureTreeMerger structureTreeMerger = new StructureTreeMerger(null, null,
+
+        StructureTreeMerger structureTreeMerger = new StructureTreeMerger(elems.get(0), null,
                 mockAdapter, null);
+        assertEquals("Constructor sets the current session element without restrictions",
+                elems.get(0), structureTreeMerger.getCurrentSessionElem());
+
         structureTreeMerger.setCurrentSessionElem();
         assertEquals(assertionMessage, expected, structureTreeMerger.getCurrentSessionElem());
         if (secondElems != null) {
-            when(mockPdfDoc.getStructureTreeElements()).thenReturn(elems);
+            when(mockPdfDoc.getStructureTreeElements()).thenReturn(secondElems);
             structureTreeMerger.setCurrentSessionElem();
             assertEquals(assertionMessage, expected, structureTreeMerger.getCurrentSessionElem());
         }
diff --git a/test/java/org/apache/fop/render/pdf/TaggedPDFConductorTestCase.java b/test/java/org/apache/fop/render/pdf/TaggedPDFConductorTestCase.java
index affa973..381eee2 100644
--- a/test/java/org/apache/fop/render/pdf/TaggedPDFConductorTestCase.java
+++ b/test/java/org/apache/fop/render/pdf/TaggedPDFConductorTestCase.java
@@ -29,6 +29,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 
 import org.apache.fop.events.DefaultEventBroadcaster;
+import org.apache.fop.pdf.PDFArray;
 import org.apache.fop.pdf.PDFDictionary;
 import org.apache.fop.pdf.PDFDocument;
 import org.apache.fop.pdf.PDFName;
@@ -37,6 +38,9 @@ import org.apache.fop.pdf.PDFObject;
 import org.apache.fop.pdf.PDFPage;
 import org.apache.fop.pdf.PDFResources;
 import org.apache.fop.pdf.PDFStructElem;
+import org.apache.fop.pdf.StandardStructureTypes.Grouping;
+import org.apache.fop.pdf.StandardStructureTypes.Illustration;
+import org.apache.fop.pdf.StandardStructureTypes.Paragraphlike;
 import org.apache.fop.render.pdf.pdfbox.PDFBoxAdapter;
 import org.apache.fop.render.pdf.pdfbox.PDFBoxAdapterTestCase;
 import org.apache.fop.render.pdf.pdfbox.TaggedPDFConductor;
@@ -48,6 +52,8 @@ public class TaggedPDFConductorTestCase {
     private static final String OTF = "otf.pdf";
     private static final String IMAGE = "hello2.pdf";
     private static final String NOPARENTTREE = "NoParentTree.pdf";
+    private static final String logicalStructImageSingleChild = "to_include_pdfua.pdf";
+
     private PDFPage pdfPage;
     private PDFDocument pdfDoc;
 
@@ -158,4 +164,95 @@ public class TaggedPDFConductorTestCase {
         runConductor(NOPARENTTREE, elem);
         Assert.assertEquals(print(elem), "/Div/Document");
     }
+
+    private String printTree(PDFStructElem elem) {
+        Object selfName = elem.get("S");
+        if (null == selfName) {
+            return "*";
+        }
+        StringBuilder sb = new StringBuilder(selfName.toString());
+        if ((elem.getKids() != null) && !elem.getKids().isEmpty()) {
+            int realSize = 0;
+            for (PDFObject kid : elem.getKids()) {
+                if (kid instanceof PDFStructElem) {
+                    realSize++;
+                }
+            }
+            if (realSize > 0) {
+                sb.append("[[(" + realSize + ")");
+                for (PDFObject kid : elem.getKids()) {
+                    if (kid instanceof PDFStructElem) {
+                        sb.append(printTree((PDFStructElem) kid));
+                    }
+                }
+                sb.append("]]");
+            }
+        }
+        return sb.toString();
+    }
+
+    private PDFLogicalStructureHandler runConductorForHandler(String pdf, PDFStructElem elem,
+            PDFStructElem[] textElements) throws IOException {
+        setUp();
+        PDDocument doc = PDFBoxAdapterTestCase.load(pdf);
+        PDPage srcPage = doc.getPage(0);
+        elem.setObjectNumber(2);
+        PDFBoxAdapter adapter = new PDFBoxAdapter(pdfPage, new HashMap<>(), new HashMap<>(), new HashMap<>(),
+                new HashMap<>(), new DefaultEventBroadcaster());
+        PDFLogicalStructureHandler handler = setUpPDFLogicalStructureHandler();
+        for (PDFStructElem textElement : textElements) {
+            handler.addTextContentItem(textElement);
+        }
+        new TaggedPDFConductor(elem, handler, srcPage, adapter).handleLogicalStructure(doc);
+        doc.close();
+        return handler;
+    }
+
+    private static String getParentTreeContentsAsString(PDFLogicalStructureHandler logicalStructureHandler) {
+        StringBuilder result = new StringBuilder();
+        PDFArray pageParentTree = logicalStructureHandler.getPageParentTree();
+        final int maxIndex = pageParentTree.length();
+        for (int index = 0; index < maxIndex; index++) {
+            Object item = pageParentTree.get(index);
+            if (item instanceof PDFStructElem) {
+                PDFStructElem elem = (PDFStructElem) item;
+                result.append(elem.get("S").toString());
+            } else {
+                result.append("|");
+            }
+        }
+        return result.toString();
+    }
+
+    @Test
+    public void testImageChild() throws IOException {
+        PDFStructElem rootElement = new PDFStructElem(null, Grouping.PART);
+        PDFStructElem pElementHeader = new PDFStructElem(rootElement, Paragraphlike.H1);
+        PDFStructElem pElementSibling = new PDFStructElem(rootElement, Paragraphlike.P);
+        PDFStructElem pElement = new PDFStructElem(rootElement, Paragraphlike.P);
+        PDFStructElem placeHolderElement = new PDFStructElem.Placeholder(rootElement);
+        rootElement.addKid(pElementHeader);
+        rootElement.addKid(pElementSibling);
+        rootElement.addKid(pElement);
+        rootElement.addKid(placeHolderElement);
+        PDFStructElem figureElement = new PDFStructElem(pElement, Illustration.FIGURE);
+        pElement.addKid(figureElement);
+        PDFStructElem[] texts = { pElementHeader, pElementSibling };
+        PDFLogicalStructureHandler logicalStructureHandler = runConductorForHandler(logicalStructImageSingleChild,
+                figureElement, texts);
+
+        // check structure
+        Assert.assertEquals("/Part[[(4)/H1/P/Div[[(5)/Text#20body/Text#20body/Text#20body/Text#20body/Figure]]*]]",
+                printTree(rootElement));
+        Assert.assertEquals("/H1", printTree(pElementHeader));
+        Assert.assertEquals("/P", printTree(pElementSibling));
+        Assert.assertEquals("/Div[[(5)/Text#20body/Text#20body/Text#20body/Text#20body/Figure]]", printTree(pElement));
+        Assert.assertEquals("/Figure", printTree(figureElement));
+
+        // check parent tree
+        Assert.assertEquals("/H1/P/Text#20body/Text#20body/Text#20body/Text#20body",
+                getParentTreeContentsAsString(logicalStructureHandler));
+
+    }
+
 }
diff --git a/test/resources/org/apache/fop/render/pdf/pdfbox/to_include_pdfua.pdf b/test/resources/org/apache/fop/render/pdf/pdfbox/to_include_pdfua.pdf
new file mode 100644
index 0000000..81bf4fe
Binary files /dev/null and b/test/resources/org/apache/fop/render/pdf/pdfbox/to_include_pdfua.pdf differ


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@xmlgraphics.apache.org
For additional commands, e-mail: commits-help@xmlgraphics.apache.org