diff --git a/src/UglyToad.PdfPig.Tests/Integration/AccentedCharactersInBookmarksTests.cs b/src/UglyToad.PdfPig.Tests/Integration/AccentedCharactersInBookmarksTests.cs
index a9eb5ffa..1000cf4c 100644
--- a/src/UglyToad.PdfPig.Tests/Integration/AccentedCharactersInBookmarksTests.cs
+++ b/src/UglyToad.PdfPig.Tests/Integration/AccentedCharactersInBookmarksTests.cs
@@ -32,4 +32,18 @@ public class AccentedCharactersInBookmarksTests
},
nodes);
}
+
+ [Fact]
+ public void CanReadContainerBookmarksCorrectly()
+ {
+ var path = IntegrationHelpers.GetDocumentPath("dotnet-ai.pdf");
+
+ using var document = PdfDocument.Open(path);
+ var isFound = document.TryGetBookmarks(out var bookmarks, false);
+ Assert.True(isFound);
+ Assert.True(bookmarks.Roots.Count == 3);
+ isFound = document.TryGetBookmarks(out bookmarks, true);
+ Assert.True(isFound);
+ Assert.True(bookmarks.Roots.Count > 3);
+ }
}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig.Tests/Integration/Documents/dotnet-ai.pdf b/src/UglyToad.PdfPig.Tests/Integration/Documents/dotnet-ai.pdf
new file mode 100644
index 00000000..5601723f
Binary files /dev/null and b/src/UglyToad.PdfPig.Tests/Integration/Documents/dotnet-ai.pdf differ
diff --git a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
index 11b77b17..2049cb45 100644
--- a/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
+++ b/src/UglyToad.PdfPig.Tests/PublicApiScannerTests.cs
@@ -260,6 +260,7 @@
"UglyToad.PdfPig.Outline.DocumentBookmarkNode",
"UglyToad.PdfPig.Outline.EmbeddedBookmarkNode",
"UglyToad.PdfPig.Outline.ExternalBookmarkNode",
+ "UglyToad.PdfPig.Outline.ContainerBookmarkNode",
"UglyToad.PdfPig.Outline.UriBookmarkNode",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestination",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationCoordinates",
diff --git a/src/UglyToad.PdfPig/Outline/BookmarksProvider.cs b/src/UglyToad.PdfPig/Outline/BookmarksProvider.cs
index 9eb96c4e..6e1b5235 100644
--- a/src/UglyToad.PdfPig/Outline/BookmarksProvider.cs
+++ b/src/UglyToad.PdfPig/Outline/BookmarksProvider.cs
@@ -25,7 +25,7 @@
///
/// Extract bookmarks, if any.
///
- public Bookmarks? GetBookmarks(Catalog catalog)
+ public Bookmarks? GetBookmarks(Catalog catalog,bool allowContainerNode = false)
{
if (!catalog.CatalogDictionary.TryGet(NameToken.Outlines, pdfScanner, out DictionaryToken? outlinesDictionary))
{
@@ -47,7 +47,7 @@
while (next != null)
{
- ReadBookmarksRecursively(next, 0, false, seen, catalog.NamedDestinations, roots);
+ ReadBookmarksRecursively(next, 0, false, seen, catalog.NamedDestinations, roots, allowContainerNode);
if (!next.TryGet(NameToken.Next, out IndirectReferenceToken nextReference)
|| !seen.Add(nextReference.Data))
@@ -65,8 +65,7 @@
/// Extract bookmarks recursively.
///
private void ReadBookmarksRecursively(DictionaryToken nodeDictionary, int level, bool readSiblings, HashSet seen,
- NamedDestinations namedDestinations,
- List list)
+ NamedDestinations namedDestinations, List list, bool allowContainerNode = false)
{
// 12.3 Document-Level Navigation
@@ -80,7 +79,7 @@
var children = new List();
if (nodeDictionary.TryGet(NameToken.First, pdfScanner, out DictionaryToken? firstChild))
{
- ReadBookmarksRecursively(firstChild, level + 1, true, seen, namedDestinations, children);
+ ReadBookmarksRecursively(firstChild, level + 1, true, seen, namedDestinations, children, allowContainerNode);
}
BookmarkNode bookmark;
@@ -108,6 +107,11 @@
return;
}
}
+ else if(allowContainerNode)
+ {
+ bookmark = new ContainerBookmarkNode(title, level, children);
+ log.Warn($"No /Dest(ination) or /A(ction) entry found for bookmark node: {nodeDictionary}.");
+ }
else
{
log.Error($"No /Dest(ination) or /A(ction) entry found for bookmark node: {nodeDictionary}.");
@@ -138,7 +142,7 @@
break;
}
- ReadBookmarksRecursively(current, level, false, seen, namedDestinations, list);
+ ReadBookmarksRecursively(current, level, false, seen, namedDestinations, list, allowContainerNode);
}
}
}
diff --git a/src/UglyToad.PdfPig/Outline/ContainerBookmarkNode.cs b/src/UglyToad.PdfPig/Outline/ContainerBookmarkNode.cs
new file mode 100644
index 00000000..32de327b
--- /dev/null
+++ b/src/UglyToad.PdfPig/Outline/ContainerBookmarkNode.cs
@@ -0,0 +1,16 @@
+namespace UglyToad.PdfPig.Outline;
+
+///
+/// represents a pure container bookmark node: it has a title and child nodes but no destination or action.
+/// This is used to handle the common "grouping" bookmarks in PDFs.
+///
+public class ContainerBookmarkNode : BookmarkNode
+{
+ ///
+ /// create a container bookmark node.
+ ///
+ public ContainerBookmarkNode(string title, int level, IReadOnlyList children)
+ : base(title, level, children)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/UglyToad.PdfPig/PdfDocument.cs b/src/UglyToad.PdfPig/PdfDocument.cs
index 1dec1afd..1b00405a 100644
--- a/src/UglyToad.PdfPig/PdfDocument.cs
+++ b/src/UglyToad.PdfPig/PdfDocument.cs
@@ -255,14 +255,14 @@
/// Gets the bookmarks if this document contains some.
///
/// This will throw a if called on a disposed .
- public bool TryGetBookmarks([NotNullWhen(true)] out Bookmarks? bookmarks)
+ public bool TryGetBookmarks([NotNullWhen(true)] out Bookmarks? bookmarks, bool allowContainerNode = false)
{
if (isDisposed)
{
throw new ObjectDisposedException("Cannot access the bookmarks after the document is disposed.");
}
- bookmarks = bookmarksProvider.GetBookmarks(Structure.Catalog);
+ bookmarks = bookmarksProvider.GetBookmarks(Structure.Catalog, allowContainerNode);
return bookmarks != null;
}