Skip to content

Commit 6ef7dd7

Browse files
committed
Place caption tag in tables in correct location
DEVSIX-7951
1 parent e42df99 commit 6ef7dd7

File tree

11 files changed

+223
-15
lines changed

11 files changed

+223
-15
lines changed

layout/src/main/java/com/itextpdf/layout/tagging/ITaggingRule.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.layout.tagging;
2424

25+
/**
26+
* Implementation of the interface is used to create required children
27+
* structure for the specified role. E.g. table must have TRs as children.
28+
*/
2529
interface ITaggingRule {
30+
/**
31+
* Action which creates required children structure for the role.
32+
*
33+
* @param taggingHelper tagging helper.
34+
* @param taggingHintKey element for which children structure will be created.
35+
*
36+
* @return {@code true} if the structure was created successfully, {@code false} otherwise.
37+
*/
2638
boolean onTagFinish(LayoutTaggingHelper taggingHelper, TaggingHintKey taggingHintKey);
2739
}

layout/src/main/java/com/itextpdf/layout/tagging/LayoutTaggingHelper.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,26 @@ This file is part of the iText (R) project.
5050
import org.slf4j.Logger;
5151
import org.slf4j.LoggerFactory;
5252

53+
/**
54+
* The class is a helper which is used to correctly create structure
55+
* tree for layout element (with keeping right order for tags).
56+
*/
5357
public class LayoutTaggingHelper {
5458
private TagStructureContext context;
5559
private PdfDocument document;
5660
private boolean immediateFlush;
5761

62+
// kidsHints and parentHints fields represent tree of TaggingHintKey, where parentHints
63+
// stores a parent for the key, and kidsHints stores kids for key.
5864
private Map<TaggingHintKey, List<TaggingHintKey>> kidsHints;
5965
private Map<TaggingHintKey, TaggingHintKey> parentHints;
6066

6167
private Map<IRenderer, TagTreePointer> autoTaggingPointerSavedPosition;
6268

6369
private Map<String, List<ITaggingRule>> taggingRules;
6470

65-
private Map<PdfObject, TaggingDummyElement> existingTagsDummies;
71+
// dummiesForPreExistingTags is used to process TaggingDummyElement
72+
private Map<PdfObject, TaggingDummyElement> dummiesForPreExistingTags;
6673

6774
private final int RETVAL_NO_PARENT = -1;
6875
private final int RETVAL_PARENT_AND_KID_FINISHED = -2;
@@ -79,7 +86,7 @@ public LayoutTaggingHelper(PdfDocument document, boolean immediateFlush) {
7986
this.taggingRules = new HashMap<>();
8087
registerRules(context.getTagStructureTargetVersion());
8188

82-
existingTagsDummies = new LinkedHashMap<>();
89+
dummiesForPreExistingTags = new LinkedHashMap<>();
8390
}
8491

8592
public static void addTreeHints(LayoutTaggingHelper taggingHelper, IRenderer rootRenderer) {
@@ -103,10 +110,10 @@ public static TaggingHintKey getOrCreateHintKey(IPropertyContainer container) {
103110

104111
public void addKidsHint(TagTreePointer parentPointer, Iterable<? extends IPropertyContainer> newKids) {
105112
PdfDictionary pointerStructElem = context.getPointerStructElem(parentPointer).getPdfObject();
106-
TaggingDummyElement dummy = existingTagsDummies.get(pointerStructElem);
113+
TaggingDummyElement dummy = dummiesForPreExistingTags.get(pointerStructElem);
107114
if (dummy == null) {
108115
dummy = new TaggingDummyElement(parentPointer.getRole());
109-
existingTagsDummies.put(pointerStructElem, dummy);
116+
dummiesForPreExistingTags.put(pointerStructElem, dummy);
110117
}
111118
context.getWaitingTagsManager().assignWaitingState(parentPointer, getOrCreateHintKey(dummy));
112119
addKidsHint(dummy, newKids);
@@ -295,11 +302,11 @@ public void releaseFinishedHints() {
295302
}
296303

297304
public void releaseAllHints() {
298-
for (TaggingDummyElement dummy : existingTagsDummies.values()) {
305+
for (TaggingDummyElement dummy : dummiesForPreExistingTags.values()) {
299306
finishTaggingHint(dummy);
300307
finishDummyKids(getKidsHint(getHintKey(dummy)));
301308
}
302-
existingTagsDummies.clear();
309+
dummiesForPreExistingTags.clear();
303310

304311
releaseFinishedHints();
305312

@@ -524,6 +531,7 @@ private void addKidsHint(TaggingHintKey parentKey, Collection<TaggingHintKey> ne
524531
} else {
525532
kidsHint.add(kidKey);
526533
}
534+
kidsHints.put(parentKey, kidsHint);
527535
parentHints.put(kidKey, parentKey);
528536

529537
if (parentTagAlreadyCreated) {
@@ -542,10 +550,6 @@ private void addKidsHint(TaggingHintKey parentKey, Collection<TaggingHintKey> ne
542550
}
543551
}
544552
}
545-
546-
if (!kidsHint.isEmpty()) {
547-
kidsHints.put(parentKey, kidsHint);
548-
}
549553
}
550554

551555
private boolean createSingleTag(TaggingHintKey hintKey, TagTreePointer tagPointer) {

layout/src/main/java/com/itextpdf/layout/tagging/TableTaggingRule.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ This file is part of the iText (R) project.
2424

2525
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
2626
import com.itextpdf.layout.element.Cell;
27+
import com.itextpdf.layout.element.Div;
2728
import com.itextpdf.layout.element.Table;
29+
import com.itextpdf.layout.properties.CaptionSide;
30+
import com.itextpdf.layout.properties.Property;
31+
2832
import java.util.ArrayList;
2933
import java.util.Collections;
3034
import java.util.List;
@@ -70,9 +74,12 @@ public boolean onTagFinish(LayoutTaggingHelper taggingHelper, TaggingHintKey tab
7074
TaggingDummyElement tbodyTag = null;
7175
tbodyTag = new TaggingDummyElement(createTBody ? StandardRoles.TBODY : null);
7276

77+
7378
for (TaggingHintKey nonCellKid : nonCellKids) {
7479
String kidRole = nonCellKid.getAccessibleElement().getAccessibilityProperties().getRole();
75-
if (!StandardRoles.THEAD.equals(kidRole) && !StandardRoles.TFOOT.equals(kidRole)) {
80+
if (!StandardRoles.THEAD.equals(kidRole) && !StandardRoles.TFOOT.equals(kidRole) && !StandardRoles.CAPTION.equals(kidRole)) {
81+
// In usual cases it isn't expected that this for loop will work, but it is possible to
82+
// create custom tag hierarchy by specifying role, and put any child to tableHintKey
7683
taggingHelper.moveKidHint(nonCellKid, tableHintKey);
7784
}
7885
}
@@ -89,7 +96,6 @@ public boolean onTagFinish(LayoutTaggingHelper taggingHelper, TaggingHintKey tab
8996
taggingHelper.moveKidHint(nonCellKid, tableHintKey);
9097
}
9198
}
92-
9399
for (TreeMap<Integer, TaggingHintKey> rowTags : tableTags.values()) {
94100
TaggingDummyElement row = new TaggingDummyElement(StandardRoles.TR);
95101
TaggingHintKey rowTagHint = LayoutTaggingHelper.getOrCreateHintKey(row);
@@ -105,6 +111,35 @@ public boolean onTagFinish(LayoutTaggingHelper taggingHelper, TaggingHintKey tab
105111
taggingHelper.addKidsHint(tbodyTag, Collections.<TaggingDummyElement>singletonList(row), -1);
106112
}
107113

114+
for (TaggingHintKey nonCellKid : nonCellKids) {
115+
String kidRole = nonCellKid.getAccessibleElement().getAccessibilityProperties().getRole();
116+
if (StandardRoles.CAPTION.equals(kidRole)) {
117+
moveCaption(taggingHelper, nonCellKid, tableHintKey);
118+
}
119+
}
120+
108121
return true;
109122
}
123+
124+
private static void moveCaption(LayoutTaggingHelper taggingHelper, TaggingHintKey caption, TaggingHintKey tableHintKey) {
125+
if (!(tableHintKey.getAccessibleElement() instanceof Table)) {
126+
return;
127+
}
128+
Table tableElem = (Table) tableHintKey.getAccessibleElement();
129+
Div captionDiv = tableElem.getCaption();
130+
if (captionDiv == null) {
131+
return;
132+
}
133+
CaptionSide captionSide;
134+
if (captionDiv.<CaptionSide>getProperty(Property.CAPTION_SIDE) == null) {
135+
captionSide = CaptionSide.TOP;
136+
} else {
137+
captionSide = (CaptionSide) captionDiv.<CaptionSide>getProperty(Property.CAPTION_SIDE);
138+
}
139+
if (CaptionSide.TOP.equals(captionSide)) {
140+
taggingHelper.moveKidHint(caption, tableHintKey, 0);
141+
} else {
142+
taggingHelper.moveKidHint(caption, tableHintKey);
143+
}
144+
}
110145
}

layout/src/main/java/com/itextpdf/layout/tagging/TaggingDummyElement.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,41 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.layout.tagging;
2424

25-
import com.itextpdf.kernel.pdf.tagutils.DefaultAccessibilityProperties;
2625
import com.itextpdf.kernel.pdf.tagutils.AccessibilityProperties;
26+
import com.itextpdf.kernel.pdf.tagutils.DefaultAccessibilityProperties;
2727
import com.itextpdf.layout.IPropertyContainer;
2828
import com.itextpdf.layout.properties.Property;
2929

30+
/**
31+
* Instances of the class are used for {@link TaggingHintKey} which don't have model element
32+
* e.g. TR or THEAD in the table. Nobody will call {@link LayoutTaggingHelper#finishTaggingHint(IPropertyContainer)}
33+
* for them, it is why they should be handled separately.
34+
*/
3035
public class TaggingDummyElement implements IAccessibleElement, IPropertyContainer {
3136
private DefaultAccessibilityProperties properties;
3237

3338
private Object id;
3439

40+
/**
41+
* Instantiate a new {@link TaggingDummyElement} instance.
42+
*
43+
* @param role the role.
44+
*/
3545
public TaggingDummyElement(String role) {
3646
this.properties = new DefaultAccessibilityProperties(role);
3747
}
3848

49+
/**
50+
* {@inheritDoc}
51+
*/
3952
@Override
4053
public AccessibilityProperties getAccessibilityProperties() {
4154
return properties;
4255
}
4356

57+
/**
58+
* {@inheritDoc}
59+
*/
4460
@Override
4561
public <T1> T1 getProperty(int property) {
4662
if (property == Property.TAGGING_HINT_KEY) {
@@ -49,33 +65,51 @@ public <T1> T1 getProperty(int property) {
4965
return (T1) (Object) null;
5066
}
5167

68+
/**
69+
* {@inheritDoc}
70+
*/
5271
@Override
5372
public void setProperty(int property, Object value) {
5473
if (property == Property.TAGGING_HINT_KEY) {
5574
this.id = value;
5675
}
5776
}
5877

78+
/**
79+
* {@inheritDoc}
80+
*/
5981
@Override
6082
public boolean hasProperty(int property) {
6183
throw new UnsupportedOperationException();
6284
}
6385

86+
/**
87+
* {@inheritDoc}
88+
*/
6489
@Override
6590
public boolean hasOwnProperty(int property) {
6691
throw new UnsupportedOperationException();
6792
}
6893

94+
/**
95+
* {@inheritDoc}
96+
*/
6997
@Override
7098
public <T1> T1 getOwnProperty(int property) {
7199
throw new UnsupportedOperationException();
72100
}
73101

102+
/**
103+
* {@inheritDoc}
104+
*/
74105
@Override
75106
public <T1> T1 getDefaultProperty(int property) {
76107
throw new UnsupportedOperationException();
77108
}
78109

110+
/**
111+
* {@inheritDoc}
112+
*/
79113
@Override
80114
public void deleteOwnProperty(int property) {
81115
throw new UnsupportedOperationException();

layout/src/main/java/com/itextpdf/layout/tagging/TaggingHintKey.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,96 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.layout.tagging;
2424

25+
import com.itextpdf.layout.element.IElement;
26+
import com.itextpdf.layout.renderer.IRenderer;
27+
import com.itextpdf.layout.renderer.RootRenderer;
28+
29+
/**
30+
* TaggingHintKey instances are created in the scope of {@link RootRenderer#addChild(IRenderer)}
31+
* to preserve logical order of layout elements from model elements.
32+
*/
2533
public final class TaggingHintKey {
2634
private IAccessibleElement elem;
2735
private boolean isArtifact;
2836
private boolean isFinished;
2937
private String overriddenRole;
3038
private boolean elementBasedFinishingOnly;
3139

40+
/**
41+
* Instantiate a new {@link TaggingHintKey} instance.
42+
*
43+
* @param elem element this hint key will be created for.
44+
* @param createdElementBased {@code true} if element implements {@link IElement}.
45+
*/
3246
TaggingHintKey(IAccessibleElement elem, boolean createdElementBased) {
3347
this.elem = elem;
3448
this.elementBasedFinishingOnly = createdElementBased;
3549
}
3650

51+
/**
52+
* Get accessible element.
53+
*
54+
* @return the accessible element.
55+
*/
3756
public IAccessibleElement getAccessibleElement() {
3857
return elem;
3958
}
4059

60+
/**
61+
* Retrieve hint key finished flag.
62+
*
63+
* @return {@code true} if hint key is finished, {@code false} otherwise.
64+
*/
4165
boolean isFinished() {
4266
return isFinished;
4367
}
4468

69+
/**
70+
* Set finished flag for hint key instance.
71+
*/
4572
void setFinished() {
4673
this.isFinished = true;
4774
}
4875

76+
/**
77+
* Retrieve information whether this hint key is artifact or not.
78+
*
79+
* @return {@code true} if hint key corresponds to artifact, {@code false} otherwise.
80+
*/
4981
boolean isArtifact() {
5082
return isArtifact;
5183
}
5284

85+
/**
86+
* Specify that hint key instance corresponds to artifact.
87+
*/
5388
void setArtifact() {
5489
this.isArtifact = true;
5590
}
5691

92+
/**
93+
* Get overridden role.
94+
*
95+
* @return the overridden role.
96+
*/
5797
String getOverriddenRole() {
5898
return overriddenRole;
5999
}
60100

101+
/**
102+
* Set the overridden role.
103+
*
104+
* @param overriddenRole overridden role.
105+
*/
61106
void setOverriddenRole(String overriddenRole) {
62107
this.overriddenRole = overriddenRole;
63108
}
64109

110+
/**
111+
* Retrieve information whether the element backed by this hint key implements {@link IElement}.
112+
*
113+
* @return {@code} true if element implements {@link IElement}.
114+
*/
65115
boolean isElementBasedFinishingOnly() {
66116
return elementBasedFinishingOnly;
67117
}

layout/src/test/java/com/itextpdf/layout/LayoutTaggingTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,31 @@ public void neutralRoleTaggingTest() throws Exception {
11281128
compareResult(outFile, "cmp_" + outFile);
11291129
}
11301130

1131+
@Test
1132+
public void unexpectedTableHintChildTest()
1133+
throws IOException, InterruptedException, ParserConfigurationException, SAXException {
1134+
String outFile = "unexpectedTableHintChildTest.pdf";
1135+
1136+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(destinationFolder + outFile))){
1137+
Document document = new Document(pdfDocument);
1138+
pdfDocument.setTagged();
1139+
1140+
Div div = new Div();
1141+
div.getAccessibilityProperties().setRole(StandardRoles.TABLE);
1142+
1143+
final Paragraph c1 = new Paragraph("c1");
1144+
c1.getAccessibilityProperties().setRole(StandardRoles.LINK);
1145+
div.add(c1);
1146+
1147+
Paragraph p1 = new Paragraph("c");
1148+
p1.getAccessibilityProperties().setRole(StandardRoles.TD);
1149+
div.add(p1);
1150+
1151+
document.add(div);
1152+
}
1153+
compareResult(outFile, "cmp_" + outFile);
1154+
}
1155+
11311156
private Paragraph createParagraph1() throws IOException {
11321157
PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD);
11331158
Paragraph p = new Paragraph().add("text chunk. ").add("explicitly added separate text chunk");

0 commit comments

Comments
 (0)