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 + * + * 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);