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 2026/03/02 15:53:35 UTC
(xmlgraphics-fop-pdf-images) branch main updated: FOP-3122 Copy bookmarks
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 02e5306 FOP-3122 Copy bookmarks
02e5306 is described below
commit 02e530600f61c3050b674045b6dac9ebb558ec9d
Author: Joao Goncalves <an...@gmail.com>
AuthorDate: Fri Jan 16 15:43:15 2026 -0600
FOP-3122 Copy bookmarks
---
.../fop/render/pdf/pdfbox/CloneAnnotations.java | 3 +-
.../fop/render/pdf/pdfbox/PDFBoxAdapter.java | 163 ++++++++++++++++++---
.../fop/render/pdf/pdfbox/PDFBoxAdapterUtil.java | 61 ++++++++
.../render/pdf/pdfbox/PDFBoxAdapterTestCase.java | 17 ++-
.../org/apache/fop/render/pdf/pdfbox/bookmarks.pdf | Bin 0 -> 6974 bytes
5 files changed, 218 insertions(+), 26 deletions(-)
diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/CloneAnnotations.java b/src/java/org/apache/fop/render/pdf/pdfbox/CloneAnnotations.java
index 7beaeba..5d09cc9 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/CloneAnnotations.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/CloneAnnotations.java
@@ -82,7 +82,8 @@ public class CloneAnnotations implements HandleAnnotations<COSObject> {
COSDictionary dictionary = (COSDictionary) ((COSObject) annot).getObject();
COSBase parent = dictionary.getItem(COSName.PARENT);
if (parent != null) {
- clonedAnnot.put(COSName.PARENT.getName(), pdfBoxAdapter.cloneForNewDocument(parent, parent, exclude));
+ clonedAnnot.put(COSName.PARENT.getName(), new PDFCloner(this.pdfBoxAdapter, true)
+ .cloneForNewDocument(parent, parent, exclude));
}
}
}
diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java
index fe8727c..a89e7ac 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java
@@ -25,6 +25,7 @@ import java.awt.geom.Rectangle2D;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -34,6 +35,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeMap;
import org.apache.commons.io.IOUtils;
@@ -49,6 +51,7 @@ import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
+import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.fop.events.EventBroadcaster;
@@ -61,6 +64,7 @@ import org.apache.fop.pdf.PDFFormXObject;
import org.apache.fop.pdf.PDFNumber;
import org.apache.fop.pdf.PDFObject;
import org.apache.fop.pdf.PDFPage;
+import org.apache.fop.pdf.PDFReference;
import org.apache.fop.pdf.PDFRoot;
import org.apache.fop.pdf.PDFStream;
@@ -476,12 +480,129 @@ public class PDFBoxAdapter {
}
private void bindOptionalContent(PDDocument sourceDoc) throws IOException {
- /*
- * PDOptionalContentProperties ocProperties =
- * sourceDoc.getDocumentCatalog().getOCProperties(); PDFDictionary ocDictionary =
- * (PDFDictionary) cloneForNewDocument(ocProperties); if (ocDictionary != null) {
- * this.pdfDoc.getRoot().put(COSName.OCPROPERTIES.getName(), ocDictionary); }
- */
+ PDOptionalContentProperties ocProperties = sourceDoc.getDocumentCatalog().getOCProperties();
+ PDFDictionary ocDictionary = (PDFDictionary) cloneForNewDocument(ocProperties);
+ if (ocDictionary != null) {
+ this.pdfDoc.getRoot().put(COSName.OCPROPERTIES.getName(), ocDictionary);
+ }
+ COSDictionary catalog = sourceDoc.getDocumentCatalog().getCOSObject();
+ copyToRoot(catalog.getDictionaryObject(COSName.VIEWER_PREFERENCES), COSName.VIEWER_PREFERENCES.getName());
+ copyToRoot(catalog.getDictionaryObject("Requirements"), "Requirements");
+ copyOutlineToRoot(catalog.getDictionaryObject(COSName.NAMES), COSName.NAMES.getName());
+ copyToRoot(catalog.getDictionaryObject(COSName.COLLECTION), COSName.COLLECTION.getName());
+ copyOutlineToRoot(catalog.getItem(COSName.OUTLINES), COSName.OUTLINES.getName());
+ }
+
+ private void copyToRoot(COSBase input, String type) throws IOException {
+ if (input != null) {
+ PDFDictionary output = (PDFDictionary) pdfDoc.getRoot().get(type);
+ PDFDictionary cloned = (PDFDictionary) cloneForNewDocument(input);
+ if (output != null) {
+ copyDict(output, cloned);
+ }
+ pdfDoc.getRoot().put(type, cloned);
+ }
+ }
+
+ private void copyOutlineToRoot(COSBase input, String type) throws IOException {
+ if (input != null) {
+ PDFDictionary output = (PDFDictionary) pdfDoc.getRoot().get(type);
+ PDFDictionary cloned = (PDFDictionary) new PDFCloner(this, true).cloneForNewDocument(input);
+ if (output != null) {
+ copyDict(output, cloned);
+ }
+ movePage(cloned, new ArrayList<>());
+ pdfDoc.getRoot().put(type, cloned);
+ }
+ }
+
+ private void movePage(PDFDictionary cloned, List<Object> visited) {
+ for (String k : cloned.keySet()) {
+ Object obj = cloned.get(k);
+ if (obj instanceof PDFDictionary && !visited.contains(obj)) {
+ visited.add(obj);
+ movePage((PDFDictionary) obj, visited);
+ }
+ if (obj instanceof PDFArray && !visited.contains(obj)) {
+ visited.add(obj);
+ movePage((PDFArray) obj, visited, k);
+ }
+ }
+ }
+
+ private void movePage(PDFArray array, List<Object> visited, String k) {
+ for (int i = 0; i < array.length(); i++) {
+ Object obj = array.get(i);
+ if (obj instanceof PDFDictionary && !visited.contains(obj)) {
+ visited.add(obj);
+ movePage((PDFDictionary) obj, visited);
+ }
+ if (obj instanceof PDFArray && !visited.contains(obj)) {
+ if (("Dest".equals(k) || "Names".equals(k)) && ((PDFArray) obj).get(0) instanceof PDFReference) {
+ PDFReference ref = (PDFReference) ((PDFArray) obj).get(0);
+ array.set(i, ref.getObject());
+ }
+ visited.add(obj);
+ movePage((PDFArray) obj, visited, k);
+ }
+ }
+ }
+
+ private void copyDict(PDFDictionary output, PDFDictionary cloned) {
+ for (String k : output.keySet()) {
+ Object existingObj = output.get(k);
+ if (existingObj instanceof PDFDictionary) {
+ PDFDictionary c = (PDFDictionary) cloned.get(k);
+ if (existingObj == c) {
+ continue;
+ }
+ if (c == null) {
+ cloned.put(k, existingObj);
+ } else {
+ copyDict((PDFDictionary) existingObj, c);
+ }
+ } else if (existingObj instanceof PDFArray) {
+ PDFArray existing = (PDFArray) existingObj;
+ PDFArray clonedArray = (PDFArray) cloned.get(k);
+ if (k.equals("Names") && existing.length() > 0 && clonedArray != null) {
+ Map<String, Object[]> sortData = new TreeMap<>();
+ readNamesData(sortData, existing);
+ readNamesData(sortData, clonedArray);
+ clonedArray.clear();
+ for (Object[] sortedData : sortData.values()) {
+ clonedArray.add(sortedData[0]);
+ clonedArray.add(sortedData[1]);
+ }
+ } else if (clonedArray != null) {
+ for (int i = 0; i < existing.length(); i++) {
+ clonedArray.add(existing.get(i));
+ }
+ }
+ } else {
+ cloned.put(k, existingObj);
+ }
+ }
+ }
+
+ private void readNamesData(Map<String, Object[]> sortData, PDFArray array) {
+ Object[] data = new Object[2];
+ for (int i = 0; i < array.length(); i++) {
+ Object o = array.get(i);
+ if (o instanceof PDFDictionary) {
+ data[1] = o;
+ Object name = ((PDFDictionary)o).get("F");
+ if (name == null) {
+ name = data[0];
+ }
+ if (name instanceof byte[]) {
+ name = new String((byte[])name, StandardCharsets.UTF_8);
+ }
+ sortData.put((String) name, data);
+ data = new Object[2];
+ } else {
+ data[0] = o;
+ }
+ }
}
private void handleAnnotations(PDDocument sourceDoc, PDPage page, AffineTransform pageAdjust, Rectangle pos)
@@ -496,28 +617,22 @@ public class PDFBoxAdapter {
PDFBoxAdapterUtil.moveAnnotations(page, pageAnnotations, pageAdjust, pos);
//Pseudo-cache the target page in place of the original source page.
- //This essentially replaces the original page reference with the target page.
- COSObject cosPage = null;
- COSDictionary parentDic = (COSDictionary) page.getCOSObject().getDictionaryObject(COSName.PARENT, COSName.P);
- COSArray kids = (COSArray) parentDic.getDictionaryObject(COSName.KIDS);
+ //This essentially replaces the original page reference with the target page
+ List kids = PDFBoxAdapterUtil.getKids(sourceDoc);
for (int i = 0; i < kids.size(); i++) {
//Hopefully safe to cast, as kids need to be indirect objects
- COSObject kid = (COSObject) kids.get(i);
- if (!pageNumbers.containsKey(i)) {
- PDFArray a = new PDFArray();
- a.add(null);
- pdfDoc.assignObjectNumber(a);
- pdfDoc.addTrailerObject(a);
- pageNumbers.put(i, a);
- }
- cacheClonedObject(kid, pageNumbers.get(i));
- if (kid.getObject() == page.getCOSObject()) {
- cosPage = kid;
+ Object kid = kids.get(i);
+ if (getCachedClone(kid) == null) {
+ if (!pageNumbers.containsKey(i)) {
+ PDFArray a = new PDFArray();
+ a.add(null);
+ pdfDoc.assignObjectNumber(a);
+ pdfDoc.addTrailerObject(a);
+ pageNumbers.put(i, a);
+ }
+ cacheClonedObject(kid, pageNumbers.get(i));
}
}
- if (cosPage == null) {
- throw new IOException("Illegal PDF. Page not part of parent page node.");
- }
Set<?> fields = copyAnnotations(page, srcAcroForm);
boolean formAlreadyCopied = getCachedClone(srcAcroForm) != null;
diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterUtil.java b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterUtil.java
index 151f23d..9d2fc0b 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterUtil.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterUtil.java
@@ -23,10 +23,14 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.TreeMap;
import org.apache.commons.io.IOUtils;
@@ -36,6 +40,7 @@ import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
+import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
@@ -229,4 +234,60 @@ public final class PDFBoxAdapterUtil {
(float)rectangleT.getWidth(), (float)rectangleT.getHeight());
return rect;
}
+
+ public static List getKids(PDDocument sourceDoc) {
+ List kids = new ArrayList();
+ List<PDPage> pages = getAllKids(sourceDoc.getDocumentCatalog().getPages().getCOSObject(), new HashSet<>());
+
+ for (PDPage page : pages) {
+ PDPage parent = getParent(page);
+ COSArray kidsArray = (COSArray) parent.getCOSObject().getDictionaryObject(COSName.KIDS);
+ for (Object kid : kidsArray.toList()) {
+ if (!kids.contains(kid)) {
+ kids.add(kid);
+ }
+ }
+ }
+
+ return kids;
+ }
+
+ private static List<PDPage> getAllKids(COSDictionary page, Set<COSBase> seen) {
+ if (page == null) {
+ return Collections.emptyList();
+ }
+
+ COSArray kids = (COSArray)page.getDictionaryObject(COSName.KIDS);
+ if (kids == null) {
+ return Collections.emptyList();
+ }
+
+ List<PDPage> result = new ArrayList<>();
+ for (int i = 0; i < kids.size(); ++i) {
+ if (!seen.contains(kids.get(i))) {
+ COSBase obj = kids.getObject(i);
+ if (obj instanceof COSDictionary) {
+ COSDictionary kid = (COSDictionary)obj;
+ if (COSName.PAGE.equals(kid.getDictionaryObject(COSName.TYPE))) {
+ result.add(new PDPage(kid));
+ } else {
+ result.addAll(getAllKids(kid, seen));
+ }
+ }
+
+ seen.add(kids.get(i));
+ }
+ }
+
+ return result;
+ }
+
+ private static PDPage getParent(PDPage page) {
+ COSDictionary parentDic = page.getCOSObject().getCOSDictionary(COSName.PARENT, COSName.P);
+ if (parentDic != null) {
+ return new PDPage(parentDic);
+ }
+
+ return page;
+ }
}
diff --git a/test/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterTestCase.java b/test/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterTestCase.java
index a79e53a..8a93cc3 100644
--- a/test/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterTestCase.java
+++ b/test/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapterTestCase.java
@@ -39,6 +39,7 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
@@ -101,6 +102,7 @@ public class PDFBoxAdapterTestCase {
protected static final String SMASK = "smask.pdf";
protected static final String ACCESSIBLERADIOBUTTONS = "accessibleradiobuttons.pdf";
protected static final String PATTERN = "pattern.pdf";
+ protected static final String BOOKMARKS = "bookmarks.pdf";
protected static final String FORMROTATED = "formrotated.pdf";
protected static final String SOFTMASK = "softmask.pdf";
protected static final String DCT_ARRAY = "dctArray.pdf";
@@ -201,6 +203,9 @@ public class PDFBoxAdapterTestCase {
pdfdoc.output(os);
String out = os.toString(StandardCharsets.UTF_8.name());
assertTrue(out.contains("/Parent "));
+ assertFalse(out.contains("/Kids "));
+ pdfdoc.outputTrailer(os);
+ out = os.toString(StandardCharsets.UTF_8.name());
assertTrue(out.contains("/Kids "));
}
}
@@ -625,7 +630,7 @@ public class PDFBoxAdapterTestCase {
loadPage(pdfdoc, FORMROTATED);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
pdfdoc.output(bos);
- Assert.assertFalse(bos.toString(StandardCharsets.UTF_8.name()).contains("/R 90"));
+ assertFalse(bos.toString(StandardCharsets.UTF_8.name()).contains("/R 90"));
}
private String getAnnotationsID(PDFPage page) throws IOException {
@@ -783,4 +788,14 @@ public class PDFBoxAdapterTestCase {
assertTrue(output.contains("/ImageMatrix [148 0 0 78 0 0]"));
assertTrue(output.contains("/ImageMatrix [192 0 0 192 0 0]"));
}
+
+ @Test
+ public void testBookmarks() throws IOException {
+ PDFDocument pdfdoc = new PDFDocument("");
+ loadPage(pdfdoc, BOOKMARKS);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ PDFDictionary outlines = (PDFDictionary) pdfdoc.getRoot().get("Outlines");
+ outlines.output(bos);
+ assertEquals("<< /First 1 0 R /Last 1 0 R >>", bos.toString("UTF-8"));
+ }
}
diff --git a/test/resources/org/apache/fop/render/pdf/pdfbox/bookmarks.pdf b/test/resources/org/apache/fop/render/pdf/pdfbox/bookmarks.pdf
new file mode 100644
index 0000000..775b55e
Binary files /dev/null and b/test/resources/org/apache/fop/render/pdf/pdfbox/bookmarks.pdf differ
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@xmlgraphics.apache.org
For additional commands, e-mail: commits-help@xmlgraphics.apache.org