mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-04-05 20:55:01 +08:00
Support adding outline bookmarks to existing pdf document (#552)
Support adding outline bookmarks to existing pdf document
This commit is contained in:
parent
a486114c8d
commit
a3a9d1a2b5
@ -208,6 +208,7 @@
|
||||
"UglyToad.PdfPig.Outline.BookmarkNode",
|
||||
"UglyToad.PdfPig.Outline.DocumentBookmarkNode",
|
||||
"UglyToad.PdfPig.Outline.ExternalBookmarkNode",
|
||||
"UglyToad.PdfPig.Outline.UriBookmarkNode",
|
||||
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestination",
|
||||
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationCoordinates",
|
||||
"UglyToad.PdfPig.Outline.Destinations.ExplicitDestinationType",
|
||||
|
@ -13,6 +13,8 @@
|
||||
using Xunit;
|
||||
using System;
|
||||
using UglyToad.PdfPig.Graphics.Operations.InlineImages;
|
||||
using UglyToad.PdfPig.Outline;
|
||||
using UglyToad.PdfPig.Outline.Destinations;
|
||||
|
||||
public class PdfDocumentBuilderTests
|
||||
{
|
||||
@ -1179,11 +1181,81 @@
|
||||
var exampleCopiedDictionary = dictCopy.FirstOrDefault();
|
||||
|
||||
Assert.NotNull(exampleCopiedDictionary);
|
||||
Assert.True(exampleCopiedDictionary.Count>0);
|
||||
Assert.True(exampleCopiedDictionary.Count > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCreateDocumentWithOutline()
|
||||
{
|
||||
var builder = new PdfDocumentBuilder();
|
||||
builder.Bookmarks = new Bookmarks(new BookmarkNode[]
|
||||
{
|
||||
new DocumentBookmarkNode(
|
||||
"1", 0, new ExplicitDestination(1, ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty),
|
||||
new[]
|
||||
{
|
||||
new DocumentBookmarkNode("1.1", 0, new ExplicitDestination(2, ExplicitDestinationType.FitPage, ExplicitDestinationCoordinates.Empty), Array.Empty<BookmarkNode>()),
|
||||
}),
|
||||
new DocumentBookmarkNode(
|
||||
"2", 0, new ExplicitDestination(3, ExplicitDestinationType.FitRectangle, ExplicitDestinationCoordinates.Empty),
|
||||
new[]
|
||||
{
|
||||
new DocumentBookmarkNode("2.1", 0, new ExplicitDestination(4, ExplicitDestinationType.FitBoundingBox, ExplicitDestinationCoordinates.Empty), Array.Empty<BookmarkNode>()),
|
||||
new DocumentBookmarkNode("2.2", 0, new ExplicitDestination(5, ExplicitDestinationType.FitBoundingBoxHorizontally, ExplicitDestinationCoordinates.Empty), Array.Empty<BookmarkNode>()),
|
||||
new DocumentBookmarkNode("2.3", 0, new ExplicitDestination(6, ExplicitDestinationType.FitBoundingBoxVertically, ExplicitDestinationCoordinates.Empty), Array.Empty<BookmarkNode>()),
|
||||
new DocumentBookmarkNode("2.4", 0, new ExplicitDestination(7, ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty), Array.Empty<BookmarkNode>()),
|
||||
new DocumentBookmarkNode("2.5", 0, new ExplicitDestination(8, ExplicitDestinationType.FitVertically, ExplicitDestinationCoordinates.Empty), Array.Empty<BookmarkNode>()),
|
||||
}),
|
||||
new UriBookmarkNode("3", 0, "https://github.com", Array.Empty<BookmarkNode>()),
|
||||
});
|
||||
|
||||
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
|
||||
|
||||
foreach (var node in builder.Bookmarks.GetNodes())
|
||||
{
|
||||
builder.AddPage(PageSize.A4).AddText(node.Title, 12, new PdfPoint(25, 800), font);
|
||||
}
|
||||
|
||||
var file = builder.Build();
|
||||
WriteFile(nameof(CanCreateDocumentWithOutline), file);
|
||||
using (var document = PdfDocument.Open(file))
|
||||
{
|
||||
Assert.True(document.TryGetBookmarks(out var bookmarks));
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "1", "1.1", "2", "2.1", "2.2", "2.3", "2.4", "2.5", "3" },
|
||||
bookmarks.GetNodes().Select(node => node.Title));
|
||||
|
||||
Assert.Equal(
|
||||
new[] { 0, 1, 0, 1, 1, 1, 1, 1, 0 },
|
||||
bookmarks.GetNodes().Select(node => node.Level));
|
||||
|
||||
Assert.Equal(
|
||||
new[] { false, true, false, true, true, true, true, true, true },
|
||||
bookmarks.GetNodes().Select(node => node.IsLeaf));
|
||||
|
||||
Assert.Equal(
|
||||
new[] { "https://github.com" },
|
||||
bookmarks.GetNodes().OfType<UriBookmarkNode>().Select(node => node.Uri));
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
ExplicitDestinationType.XyzCoordinates,
|
||||
ExplicitDestinationType.FitPage,
|
||||
ExplicitDestinationType.FitRectangle,
|
||||
ExplicitDestinationType.FitBoundingBox,
|
||||
ExplicitDestinationType.FitBoundingBoxHorizontally,
|
||||
ExplicitDestinationType.FitBoundingBoxVertically,
|
||||
ExplicitDestinationType.FitHorizontally,
|
||||
ExplicitDestinationType.FitVertically,
|
||||
},
|
||||
bookmarks.GetNodes().OfType<DocumentBookmarkNode>().Select(node => node.Destination.Type));
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
|
||||
{
|
||||
try
|
||||
|
@ -105,20 +105,9 @@
|
||||
}
|
||||
}
|
||||
else if (nodeDictionary.TryGet(NameToken.A, pdfScanner, out DictionaryToken actionDictionary)
|
||||
&& TryGetAction(actionDictionary, catalog, pdfScanner, namedDestinations, log, out var actionResult))
|
||||
&& TryGetAction(actionDictionary, catalog, pdfScanner, namedDestinations, log, title, level, children, out var actionResult))
|
||||
{
|
||||
if (actionResult.isExternal)
|
||||
{
|
||||
bookmark = new ExternalBookmarkNode(title, level, actionResult.externalFileName, children);
|
||||
}
|
||||
else if (actionResult.destination != null)
|
||||
{
|
||||
bookmark = new DocumentBookmarkNode(title, level, actionResult.destination, children);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
bookmark = actionResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -361,10 +350,9 @@
|
||||
|
||||
private static bool TryGetAction(DictionaryToken actionDictionary, Catalog catalog, IPdfTokenScanner pdfScanner,
|
||||
IReadOnlyDictionary<string, ExplicitDestination> namedDestinations,
|
||||
ILog log,
|
||||
out (bool isExternal, string externalFileName, ExplicitDestination destination) result)
|
||||
ILog log, string title, int level, List<BookmarkNode> children, out BookmarkNode result)
|
||||
{
|
||||
result = (false, null, null);
|
||||
result = null;
|
||||
|
||||
if (!actionDictionary.TryGet(NameToken.S, pdfScanner, out NameToken actionType))
|
||||
{
|
||||
@ -376,7 +364,7 @@
|
||||
if (actionDictionary.TryGet(NameToken.D, pdfScanner, out ArrayToken destinationArray)
|
||||
&& TryGetExplicitDestination(destinationArray, catalog, log, out var destination))
|
||||
{
|
||||
result = (false, null, destination);
|
||||
result = new DocumentBookmarkNode(title, level, destination, children);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -384,7 +372,7 @@
|
||||
if (actionDictionary.TryGet(NameToken.D, pdfScanner, out IDataToken<string> destinationName)
|
||||
&& namedDestinations.TryGetValue(destinationName.Data, out destination))
|
||||
{
|
||||
result = (false, null, destination);
|
||||
result = new DocumentBookmarkNode(title, level, destination, children);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -393,11 +381,22 @@
|
||||
{
|
||||
if (actionDictionary.TryGetOptionalStringDirect(NameToken.F, pdfScanner, out var filename))
|
||||
{
|
||||
result = (true, filename, null);
|
||||
result = new ExternalBookmarkNode(title, level, filename, children);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = (true, string.Empty, null);
|
||||
result = new ExternalBookmarkNode(title, level, string.Empty, children);
|
||||
return true;
|
||||
}
|
||||
else if (actionType.Equals(NameToken.Uri))
|
||||
{
|
||||
if (actionDictionary.TryGetOptionalStringDirect(NameToken.Uri, pdfScanner, out var uri))
|
||||
{
|
||||
result = new UriBookmarkNode(title, level, uri, children);
|
||||
return true;
|
||||
}
|
||||
|
||||
result = new UriBookmarkNode(title, level, string.Empty, children);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
33
src/UglyToad.PdfPig/Outline/UriBookmarkNode.cs
Normal file
33
src/UglyToad.PdfPig/Outline/UriBookmarkNode.cs
Normal file
@ -0,0 +1,33 @@
|
||||
namespace UglyToad.PdfPig.Outline
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// A node in the <see cref="Bookmarks" /> of a PDF document which corresponds
|
||||
/// to a uniform resource identifier on the Internet.
|
||||
/// </summary>
|
||||
public class UriBookmarkNode : BookmarkNode
|
||||
{
|
||||
/// <summary>
|
||||
/// The uniform resource identifier to resolve.
|
||||
/// </summary>
|
||||
public string Uri { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ExternalBookmarkNode" />.
|
||||
/// </summary>
|
||||
public UriBookmarkNode(string title, int level, string uri, IReadOnlyList<BookmarkNode> children) : base(title, level, children)
|
||||
{
|
||||
Uri = uri ?? throw new ArgumentNullException(nameof(uri));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"URI '{Uri}', {Level}, {Title}";
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ namespace UglyToad.PdfPig.Writer
|
||||
using PdfPig.Fonts.TrueType;
|
||||
using PdfPig.Fonts.Standard14Fonts;
|
||||
using PdfPig.Fonts.TrueType.Parser;
|
||||
using PdfPig.Outline;
|
||||
using PdfPig.Outline.Destinations;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Tokenization.Scanner;
|
||||
using Tokens;
|
||||
@ -52,6 +54,11 @@ namespace UglyToad.PdfPig.Writer
|
||||
/// </summary>
|
||||
public DocumentInformationBuilder DocumentInformation { get; set; } = new DocumentInformationBuilder();
|
||||
|
||||
/// <summary>
|
||||
/// The bookmark nodes to include in the document outline dictionary.
|
||||
/// </summary>
|
||||
public Bookmarks Bookmarks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current page builders in the document and the corresponding 1 indexed page numbers. Use <see cref="AddPage(double,double)"/>
|
||||
/// or <see cref="AddPage(PageSize,bool)"/> to add a new page.
|
||||
@ -103,7 +110,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
context.InitializePdf(version);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the bytes of the TrueType font file provided can be used in a PDF document.
|
||||
/// </summary>
|
||||
@ -408,7 +415,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
val = tk.Data;
|
||||
}
|
||||
|
||||
|
||||
if (!(val is ArrayToken arr))
|
||||
{
|
||||
// should be array... ignore and remove bad dict
|
||||
@ -547,6 +554,7 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
|
||||
int leafNum = 0;
|
||||
var pageReferences = new Dictionary<int, IndirectReferenceToken>(pages.Count);
|
||||
|
||||
foreach (var page in pages)
|
||||
{
|
||||
@ -589,9 +597,10 @@ namespace UglyToad.PdfPig.Writer
|
||||
}
|
||||
pageDictionary[NameToken.Contents] = new ArrayToken(streams);
|
||||
}
|
||||
context.AttemptDeduplication = prev;;
|
||||
context.AttemptDeduplication = prev;
|
||||
|
||||
leafChildren[leafNum].Add(context.WriteToken(new DictionaryToken(pageDictionary)));
|
||||
pageReferences[page.Key] = context.WriteToken(new DictionaryToken(pageDictionary));
|
||||
leafChildren[leafNum].Add(pageReferences[page.Key]);
|
||||
|
||||
if (leafChildren[leafNum].Count >= desiredLeafSize)
|
||||
{
|
||||
@ -624,6 +633,20 @@ namespace UglyToad.PdfPig.Writer
|
||||
catalogDictionary[NameToken.Pages] = rootPageInfo.Ref;
|
||||
}
|
||||
|
||||
if (Bookmarks != null && Bookmarks.Roots.Count > 0)
|
||||
{
|
||||
var bookmarks = CreateBookmarkTree(Bookmarks.Roots, pageReferences, null);
|
||||
var outline = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{NameToken.Type, NameToken.Outlines},
|
||||
{NameToken.Count, new NumericToken(Bookmarks.Roots.Count)},
|
||||
{NameToken.First, bookmarks[0]},
|
||||
{NameToken.Last, bookmarks[bookmarks.Length - 1]},
|
||||
};
|
||||
|
||||
catalogDictionary[NameToken.Outlines] = context.WriteToken(new DictionaryToken(outline));
|
||||
}
|
||||
|
||||
if (ArchiveStandard != PdfAStandard.None)
|
||||
{
|
||||
Func<IToken, IndirectReferenceToken> writerFunc = x => context.WriteToken(x);
|
||||
@ -750,6 +773,138 @@ namespace UglyToad.PdfPig.Writer
|
||||
});
|
||||
}
|
||||
|
||||
private IndirectReferenceToken[] CreateBookmarkTree(IReadOnlyList<BookmarkNode> nodes, Dictionary<int, IndirectReferenceToken> pageReferences, IndirectReferenceToken parent)
|
||||
{
|
||||
var childObjectNumbers = new IndirectReferenceToken[nodes.Count];
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
childObjectNumbers[i] = context.ReserveObjectNumber();
|
||||
}
|
||||
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
var objectNumber = childObjectNumbers[i];
|
||||
var data = new Dictionary<NameToken, IToken>
|
||||
{
|
||||
{NameToken.Title, new StringToken(node.Title)},
|
||||
{NameToken.Count, new NumericToken(node.Children.Count)}
|
||||
};
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
data[NameToken.Parent] = parent;
|
||||
}
|
||||
if (i > 0)
|
||||
{
|
||||
data[NameToken.Prev] = childObjectNumbers[i - 1];
|
||||
}
|
||||
if (i < childObjectNumbers.Length - 1)
|
||||
{
|
||||
data[NameToken.Next] = childObjectNumbers[i + 1];
|
||||
}
|
||||
|
||||
if (node.Children.Count > 0)
|
||||
{
|
||||
var children = CreateBookmarkTree(node.Children, pageReferences, objectNumber);
|
||||
data[NameToken.First] = children[0];
|
||||
data[NameToken.Last] = children[children.Length - 1];
|
||||
}
|
||||
|
||||
switch (node)
|
||||
{
|
||||
case DocumentBookmarkNode documentBookmarkNode:
|
||||
if (!pageReferences.TryGetValue(documentBookmarkNode.PageNumber, out var pageReference))
|
||||
{
|
||||
throw new KeyNotFoundException($"Page {documentBookmarkNode.PageNumber} was not found in the source document.");
|
||||
}
|
||||
|
||||
data[NameToken.Dest] = CreateExplicitDestinationToken(documentBookmarkNode.Destination, pageReference);
|
||||
break;
|
||||
|
||||
case UriBookmarkNode uriBookmarkNode:
|
||||
data[NameToken.A] = new DictionaryToken(new Dictionary<NameToken, IToken>()
|
||||
{
|
||||
[NameToken.S] = NameToken.Uri,
|
||||
[NameToken.Uri] = new StringToken(uriBookmarkNode.Uri),
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"{node.GetType().Name} is not a supported bookmark node type.");
|
||||
}
|
||||
|
||||
context.WriteToken(new DictionaryToken(data), objectNumber);
|
||||
}
|
||||
|
||||
return childObjectNumbers;
|
||||
|
||||
static ArrayToken CreateExplicitDestinationToken(ExplicitDestination destination, IndirectReferenceToken page)
|
||||
{
|
||||
return destination.Type switch
|
||||
{
|
||||
ExplicitDestinationType.XyzCoordinates => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.XYZ,
|
||||
new NumericToken(destination.Coordinates.Left ?? 0),
|
||||
new NumericToken(destination.Coordinates.Top ?? 0),
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitPage => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.Fit,
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitHorizontally => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.FitH,
|
||||
new NumericToken(destination.Coordinates.Top ?? 0),
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitVertically => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.FitV,
|
||||
new NumericToken(destination.Coordinates.Left ?? 0),
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitRectangle => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.FitR,
|
||||
new NumericToken(destination.Coordinates.Left ?? 0),
|
||||
new NumericToken(destination.Coordinates.Top ?? 0),
|
||||
new NumericToken(destination.Coordinates.Right ?? 0),
|
||||
new NumericToken(destination.Coordinates.Bottom ?? 0),
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitBoundingBox => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.FitB,
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitBoundingBoxHorizontally => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.FitBH,
|
||||
new NumericToken(destination.Coordinates.Left ?? 0),
|
||||
}),
|
||||
|
||||
ExplicitDestinationType.FitBoundingBoxVertically => new ArrayToken(new IToken[]
|
||||
{
|
||||
page,
|
||||
NameToken.FitBV,
|
||||
new NumericToken(destination.Coordinates.Left ?? 0),
|
||||
}),
|
||||
|
||||
_ => throw new NotSupportedException($"{destination.Type} is not a supported bookmark destination type."),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal class FontStored
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user