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