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; }