add container node support for BookmarksProvider.cs (#1133)

* add container node support for BookmarksProvider.cs

* move position

* fixed unittest error

* revert package name

* remove duplicated package info.
This commit is contained in:
Karl 2025-08-15 04:17:58 +08:00 committed by GitHub
parent a43b968ea9
commit 3650e27432
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 43 additions and 8 deletions

View File

@ -32,4 +32,18 @@ public class AccentedCharactersInBookmarksTests
}, },
nodes); 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);
}
} }

View File

@ -260,6 +260,7 @@
"UglyToad.PdfPig.Outline.DocumentBookmarkNode", "UglyToad.PdfPig.Outline.DocumentBookmarkNode",
"UglyToad.PdfPig.Outline.EmbeddedBookmarkNode", "UglyToad.PdfPig.Outline.EmbeddedBookmarkNode",
"UglyToad.PdfPig.Outline.ExternalBookmarkNode", "UglyToad.PdfPig.Outline.ExternalBookmarkNode",
"UglyToad.PdfPig.Outline.ContainerBookmarkNode",
"UglyToad.PdfPig.Outline.UriBookmarkNode", "UglyToad.PdfPig.Outline.UriBookmarkNode",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestination", "UglyToad.PdfPig.Outline.Destinations.ExplicitDestination",
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationCoordinates", "UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationCoordinates",

View File

@ -25,7 +25,7 @@
/// <summary> /// <summary>
/// Extract bookmarks, if any. /// Extract bookmarks, if any.
/// </summary> /// </summary>
public Bookmarks? GetBookmarks(Catalog catalog) public Bookmarks? GetBookmarks(Catalog catalog,bool allowContainerNode = false)
{ {
if (!catalog.CatalogDictionary.TryGet(NameToken.Outlines, pdfScanner, out DictionaryToken? outlinesDictionary)) if (!catalog.CatalogDictionary.TryGet(NameToken.Outlines, pdfScanner, out DictionaryToken? outlinesDictionary))
{ {
@ -47,7 +47,7 @@
while (next != null) 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) if (!next.TryGet(NameToken.Next, out IndirectReferenceToken nextReference)
|| !seen.Add(nextReference.Data)) || !seen.Add(nextReference.Data))
@ -65,8 +65,7 @@
/// Extract bookmarks recursively. /// Extract bookmarks recursively.
/// </summary> /// </summary>
private void ReadBookmarksRecursively(DictionaryToken nodeDictionary, int level, bool readSiblings, HashSet<IndirectReference> seen, private void ReadBookmarksRecursively(DictionaryToken nodeDictionary, int level, bool readSiblings, HashSet<IndirectReference> seen,
NamedDestinations namedDestinations, NamedDestinations namedDestinations, List<BookmarkNode> list, bool allowContainerNode = false)
List<BookmarkNode> list)
{ {
// 12.3 Document-Level Navigation // 12.3 Document-Level Navigation
@ -80,7 +79,7 @@
var children = new List<BookmarkNode>(); var children = new List<BookmarkNode>();
if (nodeDictionary.TryGet(NameToken.First, pdfScanner, out DictionaryToken? firstChild)) 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; BookmarkNode bookmark;
@ -108,6 +107,11 @@
return; 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 else
{ {
log.Error($"No /Dest(ination) or /A(ction) entry found for bookmark node: {nodeDictionary}."); log.Error($"No /Dest(ination) or /A(ction) entry found for bookmark node: {nodeDictionary}.");
@ -138,7 +142,7 @@
break; break;
} }
ReadBookmarksRecursively(current, level, false, seen, namedDestinations, list); ReadBookmarksRecursively(current, level, false, seen, namedDestinations, list, allowContainerNode);
} }
} }
} }

View File

@ -0,0 +1,16 @@
namespace UglyToad.PdfPig.Outline;
/// <summary>
/// represents a pure container bookmark node: it has a title and child nodes but no destination or action.
/// <para>This is used to handle the common "grouping" bookmarks in PDFs.</para>
/// </summary>
public class ContainerBookmarkNode : BookmarkNode
{
/// <summary>
/// create a container bookmark node.
/// </summary>
public ContainerBookmarkNode(string title, int level, IReadOnlyList<BookmarkNode> children)
: base(title, level, children)
{
}
}

View File

@ -255,14 +255,14 @@
/// Gets the bookmarks if this document contains some. /// Gets the bookmarks if this document contains some.
/// </summary> /// </summary>
/// <remarks>This will throw a <see cref="ObjectDisposedException"/> if called on a disposed <see cref="PdfDocument"/>.</remarks> /// <remarks>This will throw a <see cref="ObjectDisposedException"/> if called on a disposed <see cref="PdfDocument"/>.</remarks>
public bool TryGetBookmarks([NotNullWhen(true)] out Bookmarks? bookmarks) public bool TryGetBookmarks([NotNullWhen(true)] out Bookmarks? bookmarks, bool allowContainerNode = false)
{ {
if (isDisposed) if (isDisposed)
{ {
throw new ObjectDisposedException("Cannot access the bookmarks after the document is disposed."); 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; return bookmarks != null;
} }