Add Links to Pdf Generation

This commit is contained in:
David Ochsner
2025-12-25 11:49:17 +01:00
committed by BobLd
parent 7c4f5e2424
commit 13825b9424
3 changed files with 285 additions and 1 deletions

View File

@@ -1295,6 +1295,78 @@
}
}
[Fact]
public void CanAddLinkToPage()
{
var builder = new PdfDocumentBuilder();
var page = builder.AddPage(PageSize.A4);
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
var linkArea = new PdfRectangle(25, 690, 200, 720);
page.AddLink("https://github.com", linkArea);
var bytes = builder.Build();
WriteFile(nameof(CanAddLinkToPage), bytes);
using (var document = PdfDocument.Open(bytes))
{
Assert.Equal(1, document.NumberOfPages);
var page1 = document.GetPage(1);
var annotations = page1.GetAnnotations().ToList();
Assert.Single(annotations);
var linkAnnotation = annotations[0];
Assert.Equal(Annotations.AnnotationType.Link, linkAnnotation.Type);
Assert.Equal(linkArea, linkAnnotation.Rectangle);
// Verify the URI link target
Assert.NotNull(linkAnnotation.Action);
var uriAction = Assert.IsType<Actions.UriAction>(linkAnnotation.Action);
Assert.Equal("https://github.com", uriAction.Uri);
}
}
[Fact]
public void CanAddInternalLinkToPage()
{
var builder = new PdfDocumentBuilder();
var font = builder.AddStandard14Font(Standard14Font.Helvetica);
var page1 = builder.AddPage(PageSize.A4);
var page2 = builder.AddPage(PageSize.A4);
var linkArea = new PdfRectangle(25, 690, 200, 720);
var coordinates = new ExplicitDestinationCoordinates(25, 750);
var destination = new ExplicitDestination(1, ExplicitDestinationType.XyzCoordinates, coordinates);
page2.AddLink(destination, linkArea);
var bytes = builder.Build();
WriteFile(nameof(CanAddInternalLinkToPage), bytes);
using (var document = PdfDocument.Open(bytes))
{
Assert.Equal(2, document.NumberOfPages);
var page2Doc = document.GetPage(2);
var annotations = page2Doc.GetAnnotations().ToList();
Assert.Single(annotations);
var linkAnnotation = annotations[0];
Assert.Equal(Annotations.AnnotationType.Link, linkAnnotation.Type);
Assert.Equal(linkArea, linkAnnotation.Rectangle);
// Verify the link destination
Assert.NotNull(linkAnnotation.Action);
var goToAction = Assert.IsType<Actions.GoToAction>(linkAnnotation.Action);
Assert.Equal(1, goToAction.Destination.PageNumber);
Assert.Equal(ExplicitDestinationType.XyzCoordinates, goToAction.Destination.Type);
Assert.Equal(25, goToAction.Destination.Coordinates.Left);
Assert.Equal(750, goToAction.Destination.Coordinates.Top);
}
}
private static void WriteFile(string name, byte[] bytes, string extension = "pdf")
{
try

View File

@@ -0,0 +1,178 @@
namespace UglyToad.PdfPig.Writer
{
using UglyToad.PdfPig.Actions;
using UglyToad.PdfPig.Annotations;
using UglyToad.PdfPig.Core;
using UglyToad.PdfPig.Tokens;
/// <summary>
/// Represents a link annotation that can be added to a PDF page.
/// Link annotations provide clickable areas that can trigger actions such as navigating to another page or opening a URL.
/// </summary>
public class LinkAnnotation
{
/// <summary>
/// Gets the border style for the link annotation.
/// This is overwritten by the <see cref="AnnotationBorder"/> if both are provided.
/// </summary>
public AnnotationBorder? AnnotationBorder { get; }
/// <summary>
/// Gets the border style for the link annotation.
/// </summary>
public BorderStyle? Border { get; }
/// <summary>
/// Gets the width of the border for the link annotation.
/// </summary>
public int? BorderWidth { get; }
/// <summary>
/// Gets the rectangle defining the location and size of the link annotation on the page.
/// </summary>
public PdfRectangle Rect { get; }
/// <summary>
/// Gets the quadrilaterals defining the clickable regions of the link.
/// These are typically used to define precise clickable areas that may not be rectangular.
/// </summary>
public IReadOnlyList<QuadPointsQuadrilateral> QuadPoints { get; }
/// <summary>
/// Gets the action to be performed when the link is activated.
/// </summary>
public PdfAction Action { get; }
/// <summary>
/// Specifies the border style for a link annotation.
/// </summary>
public enum BorderStyle
{
/// <summary>
/// A solid border.
/// </summary>
Solid,
/// <summary>
/// A dashed border.
/// </summary>
Dashed,
/// <summary>
/// A simulated embossed border that appears to be raised above the surface of the page.
/// </summary>
Beveled,
/// <summary>
/// A simulated engraved border that appears to be recessed below the surface of the page.
/// </summary>
Inset,
/// <summary>
/// An underline border drawn along the bottom of the annotation rectangle.
/// </summary>
Underline,
}
/// <summary>
/// Creates a new <see cref="LinkAnnotation"/> instance.
/// </summary>
/// <param name="action">The action to be performed when the link is activated.</param>
/// <param name="rect">The rectangle defining the location and size of the link on the page.</param>
/// <param name="annotationBorder">The border style for the link annotation. Optional, overwritten by <see cref="Border"/>.</param>
/// <param name="borderStyle">The border style for the link annotation. Optional.</param>
/// <param name="borderWidth">The width of the border for the link annotation. Optional.</param>
/// <param name="quadPoints">The quadrilaterals defining the clickable regions. Optional.</param>
public LinkAnnotation(
PdfAction action,
PdfRectangle rect,
AnnotationBorder? annotationBorder = null,
BorderStyle? borderStyle = null,
int? borderWidth = null,
IReadOnlyList<QuadPointsQuadrilateral>? quadPoints = null)
{
Action = action;
Rect = rect;
AnnotationBorder = annotationBorder;
Border = borderStyle;
BorderWidth = borderWidth;
QuadPoints = quadPoints ?? new List<QuadPointsQuadrilateral>();
}
/// <summary>
/// Converts this link annotation to a PDF dictionary token representation.
/// </summary>
/// <returns>A <see cref="DictionaryToken"/> representing this link annotation in PDF format.</returns>
public DictionaryToken ToToken()
{
var dict = new Dictionary<NameToken, IToken>
{
[NameToken.Type] = NameToken.Annot,
[NameToken.Subtype] = NameToken.Link,
[NameToken.Rect] = new ArrayToken([
new NumericToken(Rect.BottomLeft.X),
new NumericToken(Rect.BottomLeft.Y),
new NumericToken(Rect.TopRight.X),
new NumericToken(Rect.TopRight.Y)
]),
};
if (QuadPoints.Count > 0)
{
var quadPointsArray = new List<NumericToken>();
foreach (var quad in QuadPoints)
{
foreach (var point in quad.Points)
{
quadPointsArray.Add(new NumericToken(point.X));
quadPointsArray.Add(new NumericToken(point.Y));
}
}
dict.Add(NameToken.Quadpoints, new ArrayToken(quadPointsArray));
}
if (AnnotationBorder != null)
{
var borderArray = new List<IToken>
{
new NumericToken(AnnotationBorder.HorizontalCornerRadius),
new NumericToken(AnnotationBorder.VerticalCornerRadius),
new NumericToken(AnnotationBorder.BorderWidth),
};
if (AnnotationBorder.LineDashPattern != null && AnnotationBorder.LineDashPattern.Count > 0)
{
var dashArray = new List<NumericToken>();
foreach (var dash in AnnotationBorder.LineDashPattern)
{
dashArray.Add(new NumericToken(dash));
}
borderArray.Add(new ArrayToken(dashArray));
}
dict.Add(NameToken.Border, new ArrayToken(borderArray));
}
if (Border != null)
{
dict.Add(NameToken.Bs, new DictionaryToken(new Dictionary<NameToken, IToken>
{
[NameToken.S] = Border switch
{
BorderStyle.Solid => NameToken.S,
BorderStyle.Dashed => NameToken.D,
BorderStyle.Beveled => NameToken.B,
BorderStyle.Inset => NameToken.I,
BorderStyle.Underline => NameToken.U,
_ => NameToken.S,
},
[NameToken.W] = new NumericToken(BorderWidth ?? 1)
}));
}
return new DictionaryToken(dict);
}
}
}

View File

@@ -16,6 +16,7 @@
using Graphics.Operations.TextPositioning;
using Graphics.Operations.TextShowing;
using Graphics.Operations.TextState;
using Outline.Destinations;
using Images;
using PdfFonts;
using Tokens;
@@ -94,7 +95,7 @@
private IPageContentStream currentStream;
// links to be resolved when all page references are available
internal readonly List<(DictionaryToken token, PdfAction action)>? links;
internal readonly List<(DictionaryToken token, PdfAction action)> links = [];
// maps fonts added using PdfDocumentBuilder to page font names
private readonly Dictionary<Guid, NameToken> documentFonts = new Dictionary<Guid, NameToken>();
@@ -827,6 +828,39 @@
return new AddedImage(reference.Data, png.Width, png.Height);
}
/// <summary>
/// Adds a URL link annotation to the page at the specified rectangle area.
/// </summary>
/// <param name="url">The URL to link to</param>
/// <param name="linkArea">The rectangular area on the page that will be clickable</param>
/// <returns>This page builder for method chaining</returns>
public PdfPageBuilder AddLink(string url, PdfRectangle linkArea)
{
return AddLink(new LinkAnnotation(new UriAction(url), linkArea));
}
/// <summary>
/// Adds an internal document link annotation to the page at the specified rectangle area.
/// </summary>
/// <param name="destination">The destination within the current document to link to</param>
/// <param name="linkArea">The rectangular area on the page that will be clickable</param>
/// <returns>This page builder for method chaining</returns>
public PdfPageBuilder AddLink(ExplicitDestination destination, PdfRectangle linkArea)
{
return AddLink(new LinkAnnotation(new GoToAction(destination), linkArea));
}
/// <summary>
/// Adds a link annotation to the page.
/// </summary>
/// <param name="link">The link annotation to add</param>
/// <returns>This page builder for method chaining</returns>
public PdfPageBuilder AddLink(LinkAnnotation link)
{
links.Add((link.ToToken(), link.Action));
return this;
}
/// <summary>
/// Copy a page from unknown source to this page
/// </summary>