Skip to content

Features/transparent and separation images #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 19, 2024
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ The PDF files created in this library are not fully PDF 1.7 compliant, because t
This shortcoming shall be remedied when broader font support is implemented.

### Sample program images
The sample project called *Synercoding.FileFormats.Pdf.ConsoleTester* uses multiple images. Those images were taken from [Pexels.com](https://www.pexels.com/royalty-free-images/) and are licensed under the [Pexels License](https://www.pexels.com/photo-license/).
The sample project called *Synercoding.FileFormats.Pdf.ConsoleTester* uses multiple images.
Those images were taken from:
- [Pexels.com](https://www.pexels.com/royalty-free-images/) and are licensed under the [Pexels License](https://www.pexels.com/photo-license/)
- [FreePngImg.com](https://freepngimg.com/png/59872-jaguar-panther-royalty-free-cougar-black-cheetah) and are licensed under [Creative Commons (CC BY-NC 4.0)](https://creativecommons.org/licenses/by-nc/4.0/)

## Sample usage

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 32 additions & 4 deletions samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SixLabors.ImageSharp.PixelFormats;
using Synercoding.FileFormats.Pdf.Extensions;
using Synercoding.FileFormats.Pdf.LowLevel.Colors;
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
Expand Down Expand Up @@ -63,7 +64,7 @@ public static void Main(string[] args)
});

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

Expand All @@ -81,7 +82,7 @@ public static void Main(string[] args)
page.TrimBox = trimBox;

using (var barrenStream = File.OpenRead("Pexels_com/arid-barren-desert-1975514.jpg"))
using (var barrenImage = SixLabors.ImageSharp.Image.Load(barrenStream))
using (var barrenImage = SixLabors.ImageSharp.Image.Load<Rgba32>(barrenStream))
{
var scale = (double)barrenImage.Width / barrenImage.Height;

Expand Down Expand Up @@ -175,7 +176,7 @@ public static void Main(string[] args)
page.TrimBox = trimBox;

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

Expand All @@ -187,7 +188,7 @@ public static void Main(string[] args)
});

using (var blurStream = File.OpenRead("Pexels_com/4k-wallpaper-blur-bokeh-1484253.jpg"))
using (var blurImage = SixLabors.ImageSharp.Image.Load(blurStream))
using (var blurImage = SixLabors.ImageSharp.Image.Load<Rgba32>(blurStream))
{
var reusedImage = writer.AddImage(blurImage);

Expand Down Expand Up @@ -226,6 +227,33 @@ public static void Main(string[] args)
});
});
}

using (var pantherPngStream = File.OpenRead("FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png"))
using (var pantherImage = SixLabors.ImageSharp.Image.Load<Rgba32>(pantherPngStream))
{
var pantherImg = writer.AddImage(pantherImage);
var transparentPanther = writer.AddSeparationImage(new Separation(LowLevel.PdfName.Get("White"), PredefinedColors.Yellow), pantherImage, GrayScaleMethod.AlphaChannel);

writer.AddPage(page =>
{
page.MediaBox = mediaBox;
page.TrimBox = trimBox;

var scale = (double)transparentPanther.Width / transparentPanther.Height;
var pantherSize = new Rectangle(0, 0, 216, 216 / scale, Unit.Millimeters);

page.Content.AddImage(pantherImage, pantherSize);

page.Content.WrapInState(pantherImage, (image, content) =>
{
content.SetExtendedGraphicsState(new ExtendedGraphicsState()
{
Overprint = true
});
content.AddImage(transparentPanther, pantherSize);
});
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@
</None>
</ItemGroup>

<ItemGroup>
<None Update="FreePngImage_com\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SixLabors.ImageSharp.PixelFormats;
using Synercoding.FileFormats.Pdf.Internals;
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
using Synercoding.FileFormats.Pdf.LowLevel.Text;
Expand Down Expand Up @@ -61,7 +62,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
public static IPageContentContext AddImage(this IPageContentContext context, System.IO.Stream stream)
{
using var image = SixLabors.ImageSharp.Image.Load(stream);
using var image = SixLabors.ImageSharp.Image.Load<Rgba32>(stream);

return context.AddImage(image);
}
Expand All @@ -73,7 +74,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys
/// <param name="image">The image to place</param>
/// <param name="matrix">The placement matrix to use</param>
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Matrix matrix)
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image<Rgba32> image, Matrix matrix)
{
return context.WrapInState((image, matrix), static (tuple, context) =>
{
Expand All @@ -89,7 +90,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six
/// <param name="image">The image to place</param>
/// <param name="rectangle">The rectangle of where to place the image.</param>
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Rectangle rectangle)
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image<Rgba32> image, Rectangle rectangle)
=> context.AddImage(image, rectangle.AsPlacementMatrix());

/// <summary>
Expand All @@ -98,7 +99,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six
/// <param name="context">The context to add the image to.</param>
/// <param name="image">The image to place</param>
/// <returns>The same <paramref name="context"/> to enable chaining operations.</returns>
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image)
public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image<Rgba32> image)
{
var name = context.RawContentStream.Resources.AddImage(image);

Expand Down
28 changes: 28 additions & 0 deletions src/Synercoding.FileFormats.Pdf/GrayScaleMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Synercoding.FileFormats.Pdf;

/// <summary>
/// What method is used to generate a 1 component grayscale pixel byte array
/// </summary>
public enum GrayScaleMethod
{
/// <summary>
/// Use the red channel
/// </summary>
RedChannel,
/// <summary>
/// Use the green channel
/// </summary>
GreenChannel,
/// <summary>
/// Use the blue channel
/// </summary>
BlueChannel,
/// <summary>
/// Use the alpha channel
/// </summary>
AlphaChannel,
/// <summary>
/// Use the average of the Red, Green and Blue channels.
/// </summary>
AverageOfRGBChannels
}
128 changes: 82 additions & 46 deletions src/Synercoding.FileFormats.Pdf/Image.cs
Original file line number Diff line number Diff line change
@@ -1,64 +1,33 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Synercoding.FileFormats.Pdf.LowLevel;
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
using Synercoding.FileFormats.Pdf.LowLevel.XRef;
using System.IO.Compression;

namespace Synercoding.FileFormats.Pdf;

/// <summary>
/// Class representing an image inside a pdf
/// </summary>
public sealed class Image : IDisposable
public class Image : IDisposable
{
private bool _disposed;
private protected bool _disposed;

internal Image(PdfReference id, SixLabors.ImageSharp.Image image)
{
Reference = id;

var ms = new MemoryStream();
image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder()
{
Quality = 100,
ColorType = SixLabors.ImageSharp.Formats.Jpeg.JpegEncodingColor.YCbCrRatio444
});
Width = image.Width;
Height = image.Height;
ColorSpace = DeviceRGB.Instance.Name;
DecodeArray = new double[] { 0, 1, 0, 1, 0, 1 };
ms.Position = 0;
RawStream = ms;
}

internal Image(PdfReference id, Stream jpgStream, int width, int height, ColorSpace colorSpace)
{
Reference = id;

Width = width;
Height = height;
RawStream = jpgStream;

var (csName, decodeArray) = colorSpace switch
{
DeviceCMYK cmyk => (cmyk.Name, new double[] { 0, 1, 0, 1, 0, 1, 0, 1 }),
DeviceRGB rgb => (rgb.Name, new double[] { 0, 1, 0, 1, 0, 1 }),
_ => throw new ArgumentOutOfRangeException(nameof(colorSpace), $"The provided color space {colorSpace} is currently not supported.")
};

ColorSpace = csName;
DecodeArray = decodeArray;
}

internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName colorSpace, double[] decodeArray)
internal Image(PdfReference id, Stream jpgStream, int width, int height, ColorSpace colorSpace, Image? softMask, params StreamFilter[] filters)
{
Reference = id;

Width = width;
Height = height;
RawStream = jpgStream;
ColorSpace = colorSpace;
DecodeArray = decodeArray;
SoftMask = softMask;
Filters = filters;
}

internal Image? SoftMask { get; private set; }

internal Stream RawStream { get; private set; }

/// <summary>
Expand All @@ -79,12 +48,9 @@ internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName
/// <summary>
/// The name of the colorspace used in this <see cref="Image"/>
/// </summary>
public PdfName ColorSpace { get; }
public ColorSpace ColorSpace { get; }

/// <summary>
/// The decode array used in this <see cref="Image"/>
/// </summary>
public double[] DecodeArray { get; }
internal StreamFilter[] Filters { get; } = Array.Empty<StreamFilter>();

/// <inheritdoc />
public void Dispose()
Expand All @@ -95,4 +61,74 @@ public void Dispose()
_disposed = true;
}
}

private static Stream _encodeToJpg(SixLabors.ImageSharp.Image image)
{
var ms = new MemoryStream();
image.SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder()
{
Quality = 100,
ColorType = SixLabors.ImageSharp.Formats.Jpeg.JpegEncodingColor.YCbCrRatio444
});
ms.Position = 0;

return ms;
}

internal static Image Get(TableBuilder tableBuilder, Image<Rgba32> image)
{
return new Image(tableBuilder.ReserveId(), _encodeToJpg(image), image.Width, image.Height, DeviceRGB.Instance, GetMask(tableBuilder, image), StreamFilter.DCTDecode);
}

internal static Image? GetMask(TableBuilder tableBuilder, Image<Rgba32> image)
{
var hasTrans = image.Metadata.TryGetPngMetadata(out var pngMeta)
&&
(
pngMeta.ColorType == SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha
|| pngMeta.ColorType == SixLabors.ImageSharp.Formats.Png.PngColorType.GrayscaleWithAlpha
);

return hasTrans
? new Image(tableBuilder.ReserveId(), AsImageByteStream(image, GrayScaleMethod.AlphaChannel), image.Width, image.Height, DeviceGray.Instance, null, StreamFilter.FlateDecode)
: null;
}

internal static Stream AsImageByteStream(Image<Rgba32> image, GrayScaleMethod grayScaleMethod)
{
using (var byteStream = new MemoryStream())
{
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y++)
{
var pixelRow = accessor.GetRowSpan(y);

// pixelRow.Length has the same value as accessor.Width,
// but using pixelRow.Length allows the JIT to optimize away bounds checks:
for (int x = 0; x < pixelRow.Length; x++)
{
// Get a reference to the pixel at position x
ref Rgba32 pixel = ref pixelRow[x];

var pixelValue = grayScaleMethod switch
{
GrayScaleMethod.AlphaChannel => pixel.A,
GrayScaleMethod.RedChannel => pixel.R,
GrayScaleMethod.GreenChannel => pixel.G,
GrayScaleMethod.BlueChannel => pixel.B,
GrayScaleMethod.AverageOfRGBChannels => (byte)( ( pixel.R + pixel.G + pixel.B ) / 3 ),
_ => throw new NotImplementedException()
};

byteStream.WriteByte(pixelValue);
}
}
});

byteStream.Position = 0;

return PdfWriter.FlateEncode(byteStream);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public static PdfName ToPdfName(this StreamFilter streamFilter)
return streamFilter switch
{
StreamFilter.DCTDecode => PdfName.Get("DCTDecode"),
StreamFilter.FlateDecode => PdfName.Get("FlateDecode"),
_ => throw new NotImplementedException(),
};
}
Expand Down
20 changes: 5 additions & 15 deletions src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SixLabors.ImageSharp.PixelFormats;
using Synercoding.FileFormats.Pdf.Internals;
using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces;
using Synercoding.FileFormats.Pdf.LowLevel.Text;
Expand Down Expand Up @@ -56,25 +57,14 @@ public PdfName AddJpgUnsafe(System.IO.Stream jpgStream, int originalWidth, int o
{
var id = _tableBuilder.ReserveId();

var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace);
var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, null, StreamFilter.DCTDecode);

return AddImage(pdfImage);
}

public PdfName AddJpgUnsafe(System.IO.Stream jpgStream, int originalWidth, int originalHeight, PdfName colorSpace, double[] decodeArray)
public PdfName AddImage(SixLabors.ImageSharp.Image<Rgba32> image)
{
var id = _tableBuilder.ReserveId();

var pdfImage = new Image(id, jpgStream, originalWidth, originalHeight, colorSpace, decodeArray);

return AddImage(pdfImage);
}

public PdfName AddImage(SixLabors.ImageSharp.Image image)
{
var id = _tableBuilder.ReserveId();

var pdfImage = new Image(id, image);
var pdfImage = Image.Get(_tableBuilder, image);

return AddImage(pdfImage);
}
Expand Down Expand Up @@ -108,7 +98,7 @@ internal PdfName AddSeparation(Separation separation)

var key = PREFIX_SEPARATION + System.Threading.Interlocked.Increment(ref _separationCounter).ToString().PadLeft(6, '0');
var name = PdfName.Get(key);
_separations[separation] = (name, _tableBuilder.ReserveId());
_separations[separation] = (name, _tableBuilder.GetSeparationId(separation));

return name;
}
Expand Down
Loading
Loading