diff --git a/README.md b/README.md index cde46aa..93bae7d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png new file mode 100644 index 0000000..a1ffe71 Binary files /dev/null and b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/FreePngImage_com/59872-jaguar-panther-royalty-free-cougar-black-cheetah.png differ diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs index 41d56b5..298ee5a 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Program.cs @@ -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; @@ -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(forestStream)) { var scale = (double)forestImage.Width / forestImage.Height; @@ -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(barrenStream)) { var scale = (double)barrenImage.Width / barrenImage.Height; @@ -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(forestStream)) { var scale = (double)forestImage.Width / forestImage.Height; @@ -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(blurStream)) { var reusedImage = writer.AddImage(blurImage); @@ -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(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); + }); + }); + } } } } diff --git a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj index 0a3a1cc..049d071 100644 --- a/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj +++ b/samples/Synercoding.FileFormats.Pdf.ConsoleTester/Synercoding.FileFormats.Pdf.ConsoleTester.csproj @@ -15,4 +15,10 @@ + + + PreserveNewest + + + diff --git a/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs b/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs index 9d7e0d6..e04ad6e 100644 --- a/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs +++ b/src/Synercoding.FileFormats.Pdf/Extensions/IPageContentContextExtensions.cs @@ -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; @@ -61,7 +62,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys /// The same to enable chaining operations. 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(stream); return context.AddImage(image); } @@ -73,7 +74,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Sys /// The image to place /// The placement matrix to use /// The same to enable chaining operations. - public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Matrix matrix) + public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Matrix matrix) { return context.WrapInState((image, matrix), static (tuple, context) => { @@ -89,7 +90,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six /// The image to place /// The rectangle of where to place the image. /// The same to enable chaining operations. - public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Rectangle rectangle) + public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image, Rectangle rectangle) => context.AddImage(image, rectangle.AsPlacementMatrix()); /// @@ -98,7 +99,7 @@ public static IPageContentContext AddImage(this IPageContentContext context, Six /// The context to add the image to. /// The image to place /// The same to enable chaining operations. - public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image) + public static IPageContentContext AddImage(this IPageContentContext context, SixLabors.ImageSharp.Image image) { var name = context.RawContentStream.Resources.AddImage(image); diff --git a/src/Synercoding.FileFormats.Pdf/GrayScaleMethod.cs b/src/Synercoding.FileFormats.Pdf/GrayScaleMethod.cs new file mode 100644 index 0000000..96cdc55 --- /dev/null +++ b/src/Synercoding.FileFormats.Pdf/GrayScaleMethod.cs @@ -0,0 +1,28 @@ +namespace Synercoding.FileFormats.Pdf; + +/// +/// What method is used to generate a 1 component grayscale pixel byte array +/// +public enum GrayScaleMethod +{ + /// + /// Use the red channel + /// + RedChannel, + /// + /// Use the green channel + /// + GreenChannel, + /// + /// Use the blue channel + /// + BlueChannel, + /// + /// Use the alpha channel + /// + AlphaChannel, + /// + /// Use the average of the Red, Green and Blue channels. + /// + AverageOfRGBChannels +} diff --git a/src/Synercoding.FileFormats.Pdf/Image.cs b/src/Synercoding.FileFormats.Pdf/Image.cs index 9500f2f..019ffeb 100644 --- a/src/Synercoding.FileFormats.Pdf/Image.cs +++ b/src/Synercoding.FileFormats.Pdf/Image.cs @@ -1,54 +1,20 @@ 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; /// /// Class representing an image inside a pdf /// -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; @@ -56,9 +22,12 @@ internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName Height = height; RawStream = jpgStream; ColorSpace = colorSpace; - DecodeArray = decodeArray; + SoftMask = softMask; + Filters = filters; } + internal Image? SoftMask { get; private set; } + internal Stream RawStream { get; private set; } /// @@ -79,12 +48,9 @@ internal Image(PdfReference id, Stream jpgStream, int width, int height, PdfName /// /// The name of the colorspace used in this /// - public PdfName ColorSpace { get; } + public ColorSpace ColorSpace { get; } - /// - /// The decode array used in this - /// - public double[] DecodeArray { get; } + internal StreamFilter[] Filters { get; } = Array.Empty(); /// public void Dispose() @@ -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 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 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 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); + } + } } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs index fd39a5f..54d44ec 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Extensions/StreamFilterExtensions.cs @@ -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(), }; } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs index b73d780..97e1028 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/Internal/PageResources.cs @@ -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; @@ -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 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); } @@ -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; } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs index dc6727f..d6ec4d5 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/ObjectStream.cs @@ -3,6 +3,7 @@ using Synercoding.FileFormats.Pdf.LowLevel.Internal; using Synercoding.FileFormats.Pdf.LowLevel.Text; using Synercoding.FileFormats.Pdf.LowLevel.XRef; +using System.IO.Compression; namespace Synercoding.FileFormats.Pdf.LowLevel; @@ -26,7 +27,8 @@ public ObjectStream Write(ContentStream contentStream) if (!_tableBuilder.TrySetPosition(contentStream.Reference, InnerStream.Position)) return this; - _indirectStream(contentStream.Reference, contentStream.InnerStream.InnerStream); + using (var flateStream = PdfWriter.FlateEncode(contentStream.InnerStream.InnerStream)) + _indirectStream(contentStream.Reference, flateStream, StreamFilter.FlateDecode); return this; } @@ -92,19 +94,43 @@ public ObjectStream Write(Image image) if (!_tableBuilder.TrySetPosition(image.Reference, InnerStream.Position)) return this; - _indirectStream(image.Reference, image.RawStream, image, static (image, dictionary) => + _indirectStream(image.Reference, image.RawStream, (image, _tableBuilder), static (tuple, dictionary) => { + var (image, tableBuilder) = tuple; dictionary .Write(PdfName.Get("Type"), PdfName.Get("XObject")) .Write(PdfName.Get("Subtype"), PdfName.Get("Image")) .Write(PdfName.Get("Width"), image.Width) .Write(PdfName.Get("Height"), image.Height) - .Write(PdfName.Get("ColorSpace"), image.ColorSpace) .Write(PdfName.Get("BitsPerComponent"), 8) - .Write(PdfName.Get("Decode"), image.DecodeArray); - }, StreamFilter.DCTDecode); + .Write(PdfName.Get("Decode"), _decodeArray(image.ColorSpace)) + .WriteIfNotNull(PdfName.Get("SMask"), image.SoftMask?.Reference); + + + if (image.ColorSpace is Separation separation) + { + var sepId = tableBuilder.GetSeparationId(separation); + dictionary.Write(PdfName.Get("ColorSpace"), sepId); + } + else + { + dictionary.Write(PdfName.Get("ColorSpace"), image.ColorSpace.Name); + } + }, image.Filters); + + if (image.SoftMask != null) + Write(image.SoftMask); + + if(image.ColorSpace is Separation separation) + Write(separation); return this; + + static double[] _decodeArray(ColorSpace colorSpace) + => Enumerable.Range(0, colorSpace.Components) + .Select(_ => new double[] { 0, 1 }) + .SelectMany(x => x) + .ToArray(); } public ObjectStream Write(PdfPage page) @@ -196,13 +222,15 @@ public ObjectStream Write(PdfReference reference, Type1StandardFont font) return this; } - public ObjectStream Write(PdfReference reference, Separation separation) + public ObjectStream Write(Separation separation) { - if (!_tableBuilder.TrySetPosition(reference, InnerStream.Position)) + var id = _tableBuilder.GetSeparationId(separation); + + if (!_tableBuilder.TrySetPosition(id, InnerStream.Position)) return this; InnerStream - .StartObject(reference) + .StartObject(id) .WriteByte(BRACKET_OPEN) .Write(PdfName.Get("Separation")) .Write(separation.Name) diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs index 1c16185..2b50ae8 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfDictionary.cs @@ -315,7 +315,18 @@ public PdfDictionary WriteIfNotNull(PdfName key, Rectangle? rectangle) : this; /// - /// Write a number to the stream if it is not null + /// Writes a pdf reference to the dictionary if it is not null + /// + /// The key of the item in the dictionary + /// The reference to add. + /// The to support chaining operations. + public PdfDictionary WriteIfNotNull(PdfName key, PdfReference? value) + => value.HasValue + ? Write(key, value.Value) + : this; + + /// + /// Write a number to the dictionary if it is not null /// /// The key of the item in the dictionary /// The number to write. @@ -326,7 +337,7 @@ public PdfDictionary WriteIfNotNull(PdfName key, int? value) : this; /// - /// Write a number to the stream if it is not null + /// Write a number to the dictionary if it is not null /// /// The key of the item in the dictionary /// The number to write. diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs index f57ea69..9a3f10d 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/PdfReference.cs @@ -1,9 +1,11 @@ +using System.Diagnostics.CodeAnalysis; + namespace Synercoding.FileFormats.Pdf.LowLevel; /// /// A struct representing a reference /// -public readonly struct PdfReference +public readonly struct PdfReference : IEquatable { /// /// Constructor for that uses generation 0 @@ -34,9 +36,26 @@ internal PdfReference(int objectId, int generation) /// public int Generation { get; } + /// + public bool Equals(PdfReference other) + => ObjectId == other.ObjectId && Generation == other.Generation; + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is PdfReference pdfRef && Equals(pdfRef); + + /// + public override int GetHashCode() + => HashCode.Combine(ObjectId, Generation); + /// public override string ToString() - { - return $"{ObjectId} {Generation}"; - } + => $"{ObjectId} {Generation}"; + + /// + public static bool operator ==(PdfReference left, PdfReference right) + => left.Equals(right); + + /// + public static bool operator !=(PdfReference left, PdfReference right) => !( left == right ); } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs index f546b1b..5aede75 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/StreamFilter.cs @@ -9,5 +9,12 @@ internal enum StreamFilter /// Decompress data encoded using a DCT (discrete cosine transform) technique based on the JPEG standard, /// reproducing image sample data that approximates the original data. /// - DCTDecode + DCTDecode, + + /// + /// The Flate method is based on the public-domain zlib/deflate compression method, + /// which is a variable-length Lempel-Ziv adaptive compression method cascaded + /// with adaptive Huffman coding. It is fully defined in Internet RFC 1950, and Internet RFC 1951. + /// + FlateDecode, } diff --git a/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs index f6b104b..ab01ca1 100644 --- a/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs +++ b/src/Synercoding.FileFormats.Pdf/LowLevel/XRef/TableBuilder.cs @@ -1,3 +1,4 @@ +using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; using Synercoding.FileFormats.Pdf.LowLevel.Internal; namespace Synercoding.FileFormats.Pdf.LowLevel.XRef; @@ -6,6 +7,18 @@ internal class TableBuilder { private readonly IdGenerator _idGen = new IdGenerator(); private readonly Dictionary _positions = new Dictionary(); + private readonly Dictionary _addedSeparations = new Dictionary(); + + public PdfReference GetSeparationId(Separation separation) + { + if (_addedSeparations.TryGetValue(separation, out var id)) + return id; + + id = ReserveId(); + _addedSeparations.Add(separation, id); + return id; + + } public PdfReference ReserveId() { diff --git a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs index 174b1a1..8851695 100644 --- a/src/Synercoding.FileFormats.Pdf/PdfWriter.cs +++ b/src/Synercoding.FileFormats.Pdf/PdfWriter.cs @@ -1,8 +1,11 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using Synercoding.FileFormats.Pdf.LowLevel; using Synercoding.FileFormats.Pdf.LowLevel.Colors.ColorSpaces; using Synercoding.FileFormats.Pdf.LowLevel.Extensions; using Synercoding.FileFormats.Pdf.LowLevel.Internal; using Synercoding.FileFormats.Pdf.LowLevel.XRef; +using System.IO.Compression; using System.Reflection; namespace Synercoding.FileFormats.Pdf; @@ -147,13 +150,35 @@ public async Task AddPageAsync(T data, Func page /// /// The image that needs to be added. /// The image reference that can be used in pages - public Image AddImage(SixLabors.ImageSharp.Image image) + public Image AddImage(Image image) + { + _throwWhenEndingWritten(); + + var pdfImage = Image.Get(_tableBuilder, image); + + _objectStream.Write(pdfImage); + + return pdfImage; + } + + /// + /// Add a separation image to the . + /// + /// The to use. + /// The image to use. + /// The to use. + /// The SeparationImage reference that can be used in pages + public Image AddSeparationImage(Separation separation, Image image, GrayScaleMethod grayScaleMethod) { _throwWhenEndingWritten(); var id = _tableBuilder.ReserveId(); - var pdfImage = new Image(id, image); + var mask = Image.GetMask(_tableBuilder, image); + + var imageStream = Image.AsImageByteStream(image, grayScaleMethod); + + var pdfImage = new Image(id, imageStream, image.Width, image.Height, separation, mask, StreamFilter.FlateDecode); _objectStream.Write(pdfImage); @@ -177,7 +202,7 @@ public Image AddJpgUnsafe(Stream jpgStream, int originalWidth, int originalHeigh 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); _objectStream.Write(pdfImage); @@ -230,8 +255,8 @@ private void _writePageAndResourcesToObjectStream(PdfPage page) foreach (var (font, refId) in page.Resources.FontReferences) _objectStream.Write(refId, font); - foreach (var (separation, (_, refId)) in page.Resources.SeparationReferences) - _objectStream.Write(refId, separation); + foreach (var (separation, _) in page.Resources.SeparationReferences) + _objectStream.Write(separation); foreach (var (state, (_, refId)) in page.Resources.ExtendedGraphicsStates) _objectStream.Write(refId, state); @@ -239,6 +264,23 @@ private void _writePageAndResourcesToObjectStream(PdfPage page) _objectStream.Write(page.Content.RawContentStream); } + internal static Stream FlateEncode(Stream inputStream) + { + var outputStream = new MemoryStream(); + + outputStream.WriteByte(0x78); + outputStream.WriteByte(0xDA); + + inputStream.Position = 0; + using (var flateStream = new DeflateStream(outputStream, CompressionLevel.SmallestSize, true)) + { + inputStream.CopyTo(flateStream); + } + + outputStream.Position = 0; + return outputStream; + } + private void _throwWhenEndingWritten() { if (_endingWritten) throw new InvalidOperationException("Can't change document information when PDF trailer is written to the stream.");