diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/PdfContentByte.java b/openpdf/src/main/java/com/lowagie/text/pdf/PdfContentByte.java
index 9c2f401ab..28c871726 100644
--- a/openpdf/src/main/java/com/lowagie/text/pdf/PdfContentByte.java
+++ b/openpdf/src/main/java/com/lowagie/text/pdf/PdfContentByte.java
@@ -3419,9 +3419,9 @@ public void beginMarkedContentSequence(PdfStructureElement struc, PdfDictionary
dic.put(PdfName.PG, writer.getCurrentPage());
dic.put(PdfName.MCID, new PdfNumber(mark));
ar.add(dic);
- struc.setPageMark(writer.getPageNumber() - 1, -1);
+ struc.setPageMark(pdf.getCurrentPageStructParentIndex(), -1);
} else {
- struc.setPageMark(writer.getPageNumber() - 1, mark);
+ struc.setPageMark(pdf.getCurrentPageStructParentIndex(), mark);
struc.put(PdfName.PG, writer.getCurrentPage());
}
pdf.incMarkPoint();
diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/PdfDocument.java b/openpdf/src/main/java/com/lowagie/text/pdf/PdfDocument.java
index 5b517ea63..f1c92de8b 100644
--- a/openpdf/src/main/java/com/lowagie/text/pdf/PdfDocument.java
+++ b/openpdf/src/main/java/com/lowagie/text/pdf/PdfDocument.java
@@ -246,6 +246,9 @@ public class PdfDocument extends Document {
// [L10] DocListener interface
protected int markPoint;
+ // structure parent tree key reserved for the current page (tagged PDF)
+ protected int currentPageStructParentIndex;
+
// [L11] DocListener interface
/**
* This is the size of the next page.
@@ -977,7 +980,7 @@ public boolean newPage() {
// [F12] we add tag info
if (writer.isTagged()) {
- page.put(PdfName.STRUCTPARENTS, new PdfNumber(writer.getCurrentPageNumber() - 1));
+ page.put(PdfName.STRUCTPARENTS, new PdfNumber(getCurrentPageStructParentIndex()));
}
if (text.size() > textEmptySize) {
@@ -1144,6 +1147,11 @@ protected void initPage() throws DocumentException {
textEmptySize = text.size();
markPoint = 0;
+ // reserve a structure parent tree key for this page so that page content and any tagged objects
+ // (annotations) on it share the same, collision-free key space (see PdfStructureTreeRoot)
+ if (writer.isTagged()) {
+ currentPageStructParentIndex = writer.getStructureTreeRoot().obtainStructureParentIndex();
+ }
setNewPageSizeAndMargins();
imageEnd = -1;
indentation.imageIndentRight = 0;
@@ -2244,6 +2252,16 @@ void incMarkPoint() {
++markPoint;
}
+ /**
+ * Returns the structure parent tree key reserved for the current page. Marked content on the page
+ * and this value (written as the page's StructParents entry) must agree.
+ *
+ * @return the current page's structure parent tree key
+ */
+ int getCurrentPageStructParentIndex() {
+ return currentPageStructParentIndex;
+ }
+
// [U3] page actions
void setCropBoxSize(Rectangle crop) {
diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureElement.java b/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureElement.java
index 14fda9cea..7c23799aa 100644
--- a/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureElement.java
+++ b/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureElement.java
@@ -139,4 +139,47 @@ void setPageMark(int page, int mark) {
public PdfIndirectReference getReference() {
return this.reference;
}
+
+ /**
+ * References a whole annotation from this structure element, as required to tag annotations
+ * (typically links) in a tagged or PDF/UA document. This
+ *
+ * - adds an OBJR entry pointing at the annotation to this element's kids,
+ * - stores a unique {@code StructParent} key on the annotation, and
+ * - registers that key in the structure parent tree, mapping it directly to this element.
+ *
+ * The annotation must already have been added to the document (for example through
+ * {@link PdfWriter#addAnnotation(PdfAnnotation)}) and must live on the current page. Any visible
+ * content of the annotation (such as the link text) should be marked before calling this method so
+ * that it becomes a preceding kid of this element.
+ *
+ * @param annotation the annotation to reference; its indirect reference is used for the OBJR entry
+ */
+ public void addAnnotation(PdfAnnotation annotation) {
+ PdfWriter writer = top.getWriter();
+ int structParent = top.obtainStructureParentIndex();
+ annotation.put(PdfName.STRUCTPARENT, new PdfNumber(structParent));
+ top.setObjectParent(structParent, reference);
+
+ PdfDictionary objr = new PdfDictionary(PdfName.OBJR);
+ objr.put(PdfName.OBJ, annotation.getIndirectReference());
+ objr.put(PdfName.PG, writer.getCurrentPage());
+ addKid(objr);
+ }
+
+ private void addKid(PdfObject kid) {
+ PdfObject k = get(PdfName.K);
+ PdfArray kids;
+ if (k == null) {
+ kids = new PdfArray();
+ put(PdfName.K, kids);
+ } else if (k.isArray()) {
+ kids = (PdfArray) k;
+ } else {
+ kids = new PdfArray();
+ kids.add(k);
+ put(PdfName.K, kids);
+ }
+ kids.add(kid);
+ }
}
diff --git a/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureTreeRoot.java b/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureTreeRoot.java
index 631c27030..e20776df2 100644
--- a/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureTreeRoot.java
+++ b/openpdf/src/main/java/com/lowagie/text/pdf/PdfStructureTreeRoot.java
@@ -60,6 +60,18 @@
public class PdfStructureTreeRoot extends PdfDictionary {
private final Map parentTree = new HashMap<>();
+ /**
+ * Parent tree entries that map a single StructParent key directly to a structure element. This is
+ * used for whole objects (such as annotations) referenced from the structure tree through an OBJR
+ * entry, which - unlike marked-content sequences - must resolve to the structure element itself and
+ * not to an array (see ISO 32000-1, 14.7.4.4 and ISO 14289-1).
+ */
+ private final Map objectParents = new HashMap<>();
+ /**
+ * A monotonically increasing counter used to hand out unique StructParent / StructParents keys for
+ * both pages and objects, so that their entries in the structure parent tree never collide.
+ */
+ private int parentTreeNextKey = 0;
private final PdfIndirectReference reference;
/**
@@ -121,16 +133,44 @@ void setPageMark(int page, PdfIndirectReference reference) {
ar.add(reference);
}
+ /**
+ * Returns the next available StructParent / StructParents key and advances the internal counter.
+ * Pages and objects (annotations) share this key space so that their structure parent tree entries
+ * remain unique.
+ *
+ * @return the next parent tree key
+ */
+ public int obtainStructureParentIndex() {
+ return parentTreeNextKey++;
+ }
+
+ /**
+ * Registers a structure parent tree entry that maps a StructParent key directly to a structure
+ * element. Used for whole objects (annotations) referenced through an OBJR entry.
+ *
+ * @param structParent the StructParent key stored on the object
+ * @param structureElement the reference of the structure element that is the object's parent
+ */
+ void setObjectParent(int structParent, PdfIndirectReference structureElement) {
+ objectParents.put(structParent, structureElement);
+ }
+
private void nodeProcess(PdfDictionary dictionary, PdfIndirectReference reference)
throws IOException {
PdfObject obj = dictionary.get(PdfName.K);
- if (obj != null && obj.isArray() && !((PdfArray) obj).getElements().isEmpty() && !((PdfArray) obj).getElements()
- .get(0).isNumber()) {
+ if (obj != null && obj.isArray()) {
PdfArray ar = (PdfArray) obj;
for (int k = 0; k < ar.size(); ++k) {
- PdfStructureElement e = (PdfStructureElement) ar.getDirectObject(k);
- ar.set(k, e.getReference());
- nodeProcess(e, e.getReference());
+ // Only structure elements are recursed into and replaced by their reference. Other kids
+ // (marked-content identifiers, MCR dictionaries and OBJR references to annotations) are
+ // left untouched so that mixed kids - e.g. a Link element containing both link text and
+ // an OBJR - are handled correctly.
+ PdfObject direct = ar.getDirectObject(k);
+ if (direct instanceof PdfStructureElement) {
+ PdfStructureElement e = (PdfStructureElement) direct;
+ ar.set(k, e.getReference());
+ nodeProcess(e, e.getReference());
+ }
}
}
if (reference != null) {
@@ -144,9 +184,12 @@ void buildTree() throws IOException {
PdfArray ar = parentTree.get(i);
numTree.put(i, writer.addToBody(ar).getIndirectReference());
}
+ // Object (annotation) entries map their key directly to the parent structure element.
+ numTree.putAll(objectParents);
PdfDictionary dicTree = PdfNumberTree.writeTree(numTree, writer);
if (dicTree != null) {
put(PdfName.PARENTTREE, writer.addToBody(dicTree).getIndirectReference());
+ put(PdfName.PARENTTREENEXTKEY, new PdfNumber(parentTreeNextKey));
}
nodeProcess(this, reference);