Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,25 @@ private static void _writePdf(string fileName, bool enableSubsetting = true)

page.Content.AddImage(forestImage, matrix);
}
})
// Add hyperlink
.AddPage(page =>
{
page.MediaBox = mediaBox;
page.TrimBox = trimBox;

using (var forestStream = File.OpenRead("Pexels_com/android-wallpaper-art-backlit-1114897.jpg"))
using (var forestImage = SixLabors.ImageSharp.Image.Load<Rgba32>(forestStream))
{
var scale = (double)forestImage.Width / forestImage.Height;

var matrix = Matrix.CreateScaleMatrix(new Value(scale * 303, Unit.Millimeters).AsRaw(Unit.Points), new Value(303, Unit.Millimeters).AsRaw(Unit.Points))
.Translate(new Value(-100, Unit.Millimeters).AsRaw(Unit.Points), new Value(0, Unit.Millimeters).AsRaw(Unit.Points));

page.Content.AddImage(forestImage, matrix);

page.AddHyperlink(new Uri("https://www.github.com/synercoder/fileformats.pdf"), new Rectangle(new Point(Mm(-100), Mm(0)), new Size(scale * 303, 303, Unit.Millimeters)));
}
});

using (var blurStream = File.OpenRead("Pexels_com/4k-wallpaper-blur-bokeh-1484253.jpg"))
Expand Down
106 changes: 106 additions & 0 deletions src/Synercoding.FileFormats.Pdf/Content/BlendMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace Synercoding.FileFormats.Pdf.Content;

/// <summary>
/// Specifies the blend mode to be used in the transparent imaging model.
/// </summary>
public enum BlendMode
{
/// <summary>
/// Selects the source color, ignoring the backdrop.
/// </summary>
Normal,
/// <summary>
/// Multiplies the backdrop and source color values.
/// </summary>
/// <remarks>
/// The result color is always at least as dark as either of the two constituent
/// colors. Multiplying any color with black produces black; multiplying with
/// white leaves the original color unchanged. Painting successive overlapping
/// objects with a color other than black or white produces progressively darker
/// colors.
/// </remarks>
Multiply,
/// <summary>
/// Multiplies the complements of the backdrop and source color values,
/// then complements the result
/// </summary>
/// <remarks>
/// The result color is always at least as light as either of the two constituent
/// colors. Screening any color with white produces white; screening with black
/// leaves the original color unchanged. The effect is similar to projecting
/// multiple photographic slides simultaneously onto a single screen.
/// </remarks>
Screen,
/// <summary>
/// Multiplies or screens the colors, depending on the backdrop color value.
/// Source colors overlay the backdrop while preserving its highlights and
/// shadows. The backdrop color is not replaced but is mixed with the source
/// color to reflect the lightness or darkness of the backdrop.
/// </summary>
Overlay,
/// <summary>
/// Selects the darker of the backdrop and source colors.
/// </summary>
/// <remarks>
/// The backdrop is replaced with the source where the source is darker;
/// otherwise, it is left unchanged.
/// </remarks>
Darken,
/// <summary>
/// Selects the lighter of the backdrop and source colors.
/// </summary>
/// <remarks>
/// The backdrop is replaced with the source where the source is lighter;
/// otherwise, it is left unchanged.
/// </remarks>
Lighten,
/// <summary>
/// Brightens the backdrop color to reflect the source color.
/// Painting with black produces no changes.
/// </summary>
ColorDodge,
/// <summary>
/// Darkens the backdrop color to reflect the source color.
/// Painting with white produces no change.
/// </summary>
ColorBurn,
/// <summary>
/// Multiplies or screens the colors, depending on the source color value.
/// The effect is similar to shining a harsh spotlight on the backdrop.
/// </summary>
HardLight,
/// <summary>
/// Darkens or lightens the colors, depending on the source color value.
/// The effect is similar to shining a diffused spotlight on the backdrop.
/// </summary>
SoftLight,
/// <summary>
/// Subtracts the darker of the two constituent colors from the lighter color.
/// </summary>
/// <remarks>
/// Painting with white inverts the backdrop color;
/// painting with black produces no change.
/// </remarks>
Difference,
/// <summary>
/// Produces an effect similar to that of the <see cref="Difference"/> mode but lower in contrast.
/// Painting with white inverts the backdrop color; painting with black produces no change.
/// </summary>
Exclusion,
/// <summary>
/// Creates a color with the hue of the source color and the saturation and luminosity of the backdrop color.
/// </summary>
Hue,
/// <summary>
/// Creates a color with the saturation of the source color and the hue and luminosity of the backdrop color. Painting with this mode in an area of the backdrop that is a pure gray (no saturation) produces no change.
/// </summary>
Saturation,
/// <summary>
/// Creates a color with the hue and saturation of the source color and the luminosity of the backdrop color. This preserves the gray levels of the backdrop and is useful for coloring monochrome images or tinting color images.
/// </summary>
Color,
/// <summary>
/// Creates a color with the luminosity of the source color and the hue and saturation of the backdrop color. This produces an inverse effect to that of the <see cref="Color"/> mode.
/// </summary>
Luminosity
}
45 changes: 44 additions & 1 deletion src/Synercoding.FileFormats.Pdf/Content/ExtendedGraphicsState.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Synercoding.FileFormats.Pdf.Content.Extensions;
using Synercoding.FileFormats.Pdf.Primitives;

namespace Synercoding.FileFormats.Pdf.Content;
Expand All @@ -22,6 +23,43 @@ public sealed record class ExtendedGraphicsState
/// </summary>
public bool? OverprintNonStroking { get; init; }

/// <summary>
/// The current stroking alpha constant, specifying the constant shape or constant opacity value
/// to be used for stroking operations in the transparent imaging model.
/// </summary>
public double? CurrentAlphaConstantStroking
{
get;
init
{
if (value < 0 || value > 1)
throw new ArgumentOutOfRangeException(nameof(CurrentAlphaConstantStroking), "Value must be between 0 and 1.");

field = value;
}
}

/// <summary>
/// The current non-stroking alpha constant, specifying the constant shape or constant opacity value
/// to be used for non-stroking operations in the transparent imaging model.
/// </summary>
public double? CurrentAlphaConstantNonStroking
{
get;
init
{
if (value < 0 || value > 1)
throw new ArgumentOutOfRangeException(nameof(CurrentAlphaConstantNonStroking), "Value must be between 0 and 1.");

field = value;
}
}

/// <summary>
/// The current blend mode to be used in the transparent imaging model.
/// </summary>
public BlendMode? BlendMode { get; init; }

internal IPdfDictionary ToPdfDictionary()
{
var dictionary = new PdfDictionary()
Expand All @@ -33,8 +71,13 @@ internal IPdfDictionary ToPdfDictionary()
dictionary[PdfNames.OP] = new PdfBoolean(Overprint.Value);
if (OverprintNonStroking.HasValue)
dictionary[PdfNames.op] = new PdfBoolean(OverprintNonStroking.Value);
if (CurrentAlphaConstantStroking.HasValue)
dictionary[PdfNames.CA] = new PdfNumber(CurrentAlphaConstantStroking.Value);
if (CurrentAlphaConstantNonStroking.HasValue)
dictionary[PdfNames.ca] = new PdfNumber(CurrentAlphaConstantNonStroking.Value);
if (BlendMode.HasValue)
dictionary[PdfNames.BM] = BlendMode.Value.ToPdfName();

return dictionary;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Synercoding.FileFormats.Pdf.Primitives;

namespace Synercoding.FileFormats.Pdf.Content.Extensions;

internal static class BlendModeExtensions
{
public static PdfName ToPdfName(this BlendMode blendMode)
=> blendMode switch
{
BlendMode.Normal => PdfNames.Normal,
BlendMode.Multiply => PdfNames.Multiply,
BlendMode.Screen => PdfNames.Screen,
BlendMode.Overlay => PdfNames.Overlay,
BlendMode.Darken => PdfNames.Darken,
BlendMode.Lighten => PdfNames.Lighten,
BlendMode.ColorDodge => PdfNames.ColorDodge,
BlendMode.ColorBurn => PdfNames.ColorBurn,
BlendMode.HardLight => PdfNames.HardLight,
BlendMode.SoftLight => PdfNames.SoftLight,
BlendMode.Difference => PdfNames.Difference,
BlendMode.Exclusion => PdfNames.Exclusion,
BlendMode.Hue => PdfNames.Hue,
BlendMode.Saturation => PdfNames.Saturation,
BlendMode.Color => PdfNames.Color,
BlendMode.Luminosity => PdfNames.Luminosity,
_ => throw new ArgumentOutOfRangeException(nameof(blendMode), blendMode, null)
};
}
47 changes: 47 additions & 0 deletions src/Synercoding.FileFormats.Pdf/Generation/WriteablePage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Synercoding.FileFormats.Pdf.Generation;
public class WriteablePage : IDisposable
{
private readonly TableBuilder _tableBuilder;
private readonly List<PdfObject<PdfDictionary>> _annotations = [];

internal WriteablePage(TableBuilder tableBuilder, CachedResources cachedResources, int pageNumber)
{
Expand All @@ -26,6 +27,9 @@ internal WriteablePage(TableBuilder tableBuilder, CachedResources cachedResource

internal PageResources Resources { get; }

internal IReadOnlyList<PdfObject<PdfDictionary>> Annotations
=> _annotations;

/// <summary>
/// The rotation of how the page is displayed, must be in increments of 90
/// </summary>
Expand Down Expand Up @@ -77,6 +81,47 @@ public PageRotation? Rotation
/// </summary>
public IPageContentContext Content { get; }

/// <summary>
/// Adds a clickable hyperlink annotation to the page that opens an external URI.
/// </summary>
/// <param name="uri">The absolute URI to open. Must be an absolute URI.</param>
/// <param name="rectangle">
/// The clickable area, expressed in default user space. This is <b>not</b> affected by any
/// transformation applied through <see cref="Content"/>.
/// </param>
/// <returns>Returns this <see cref="WriteablePage"/> to chain calls.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="uri"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="uri"/> is not an absolute URI.</exception>
public WriteablePage AddHyperlink(Uri uri, Rectangle rectangle)
{
ArgumentNullException.ThrowIfNull(uri);
if (!uri.IsAbsoluteUri)
throw new ArgumentException("The provided uri must be an absolute uri.", nameof(uri));

var annotation = new PdfDictionary()
{
[PdfNames.Type] = PdfNames.Annot,
[PdfNames.Subtype] = PdfNames.Link,
[PdfNames.Rect] = rectangle.ToArray(),
[PdfNames.Border] = new PdfArray(new[] { 0, 0, 0 }), // no visible border
[PdfNames.F] = new PdfNumber(4), // Print flag
[PdfNames.A] = new PdfDictionary()
{
[PdfNames.S] = PdfNames.URI,
// Written as a hex string so characters like '(' ')' '\' in the uri can not corrupt the string literal.
[PdfNames.URI] = new PdfString(System.Text.Encoding.ASCII.GetBytes(uri.AbsoluteUri), isHex: true),
}
};

_annotations.Add(new PdfObject<PdfDictionary>()
{
Id = _tableBuilder.ReserveId(),
Value = annotation
});

return this;
}

internal PdfDictionary ToDictionary()
{
var dictionary = new PdfDictionary()
Expand All @@ -97,6 +142,8 @@ internal PdfDictionary ToDictionary()
dictionary[PdfNames.ArtBox] = ArtBox.Value.ToArray();
if (Rotation.HasValue)
dictionary[PdfNames.Rotate] = new PdfNumber((int)Rotation.Value);
if (_annotations.Count > 0)
dictionary[PdfNames.Annots] = new PdfArray(_annotations.Select(a => a.Id.GetReference()).Cast<IPdfPrimitive>().ToArray());

return dictionary;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Synercoding.FileFormats.Pdf/PackageDetails.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<Product>Synercoding.FileFormats.Pdf</Product>
<Title>Synercoding.FileFormats.Pdf</Title>
<Description>Contains classes which makes it easy to quickly create a pdf file.</Description>
<PackageReleaseNotes>Changed GrayScaleMethod from an enum to a delegate, giving users full control over how separation images are converted to grayscale (breaking change). Fixed issue #87 where CID text bytes containing 0x0D were incorrectly unescaped in string literals, causing parsers to misread them as 0x0A and render the wrong glyph; string literals now escape more characters and CID text in content streams is written as hex strings.</PackageReleaseNotes>
<PackageReleaseNotes>Added support for the current alpha constant and blend mode via the extended graphic state. And added basic support for clickable hyperlinks on a page.</PackageReleaseNotes>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/Synercoding.FileFormats.Pdf/PdfWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,10 @@ private void _processPage(WriteablePage page)
Value = streamObj
});

// Write annotations
foreach (var annotation in page.Annotations)
_objectWriter.Write(annotation);

_pages.Add(page.ToDictionary());
}

Expand Down
Loading
Loading