Compare commits

...

3 Commits

Author SHA1 Message Date
BobLd
c27f1b6553 Only throw if ArrayToken length is less than 4 in ToRectangle() and fix #1238
Some checks failed
Build, test and publish draft / build (push) Has been cancelled
Build and test [MacOS] / build (push) Has been cancelled
Run Common Crawl Tests / build (0000-0001) (push) Has been cancelled
Run Common Crawl Tests / build (0002-0003) (push) Has been cancelled
Run Common Crawl Tests / build (0004-0005) (push) Has been cancelled
Run Common Crawl Tests / build (0006-0007) (push) Has been cancelled
Run Integration Tests / build (push) Has been cancelled
Nightly Release / Check if this commit has already been published (push) Has been cancelled
Nightly Release / tests (push) Has been cancelled
Nightly Release / build_and_publish_nightly (push) Has been cancelled
2026-02-01 16:10:24 +00:00
BobLd
629891e80e Make LinkAnnotation internal to fix unit tests 2026-02-01 15:50:21 +00:00
David Ochsner
13825b9424 Add Links to Pdf Generation 2026-02-01 15:13:55 +00:00
6 changed files with 301 additions and 2 deletions

View File

@@ -11,6 +11,20 @@
public class GithubIssuesTests
{
[Fact]
public void Issues1238()
{
var path = IntegrationHelpers.GetDocumentPath("6.Secrets.to.Startup.Success.PDFDrive.pdf");
using (var document = PdfDocument.Open(path, new ParsingOptions() { UseLenientParsing = true }))
{
var page = document.GetPage(159);
Assert.NotNull(page);
Assert.StartsWith("uct. At the longer-cycle, broader end of the spectrum are identity-level", page.Text);
Assert.Equal(0, page.Rotation.Value);
}
}
[Fact]
public void Issue1217()
{

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

@@ -250,8 +250,9 @@
throw new ArgumentNullException(nameof(array));
}
if (array.Data.Count != 4)
if (array.Data.Count < 4)
{
// Should be exactly 4, but can be more (see issues 1238). We ignore the rest.
throw new PdfDocumentFormatException($"Cannot convert array to rectangle, expected 4 values instead got: {array.Data.Count}.");
}

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>
internal sealed 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>
private PdfPageBuilder AddLink(LinkAnnotation link)
{
links.Add((link.ToToken(), link.Action));
return this;
}
/// <summary>
/// Copy a page from unknown source to this page
/// </summary>