diff --git a/BlastPDF/Builder/Graphics/Drawing/PdfImage.cs b/BlastPDF/Builder/Graphics/Drawing/PdfImage.cs new file mode 100644 index 0000000..69a9f7d --- /dev/null +++ b/BlastPDF/Builder/Graphics/Drawing/PdfImage.cs @@ -0,0 +1,8 @@ +namespace BlastPDF.Builder.Graphics.Drawing; + +public static class PdfImageExtensions { + public static PdfGraphicsObject Image(this PdfGraphicsObject graphics, string resource) { + graphics.SubObjects.Add(new PdfXObject { Resource = resource }); + return graphics; + } +} \ No newline at end of file diff --git a/BlastPDF/Builder/Graphics/Drawing/PdfInlineImage.cs b/BlastPDF/Builder/Graphics/Drawing/PdfInlineImage.cs index f416763..3ccbe96 100644 --- a/BlastPDF/Builder/Graphics/Drawing/PdfInlineImage.cs +++ b/BlastPDF/Builder/Graphics/Drawing/PdfInlineImage.cs @@ -20,6 +20,11 @@ public class PdfInlineImage : PdfGraphicsObject public static PdfInlineImage FromFile(string filename, FileFormat format, PdfColorSpace colorSpace, PdfFilter[] filters) { var image = IImage.Decode(filename, format); + return FromImage(image, colorSpace, filters); + } + + public static PdfInlineImage FromImage(IImage image, PdfColorSpace colorSpace, PdfFilter[] filters) + { var result = new PdfInlineImage { Width = image.Width(), Height = image.Height(), @@ -38,7 +43,7 @@ public static PdfInlineImage FromFile(string filename, FileFormat format, PdfCol } } -public static class PdfImageExtensions +public static class PdfInlineImageExtensions { public static PdfGraphicsObject InlineImage(this PdfGraphicsObject graphics, string filename, FileFormat format, PdfColorSpace colorSpace = PdfColorSpace.DeviceRGB, PdfFilter[] filters = null) { diff --git a/BlastPDF/Builder/PdfDocument.cs b/BlastPDF/Builder/PdfDocument.cs index 0f3b4b9..725bc97 100644 --- a/BlastPDF/Builder/PdfDocument.cs +++ b/BlastPDF/Builder/PdfDocument.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using BlastPDF.Builder.Resources; +using BlastPDF.Builder.Resources.Font; namespace BlastPDF.Builder; diff --git a/BlastPDF/Builder/PdfPage.cs b/BlastPDF/Builder/PdfPage.cs index cb94819..f298a4b 100644 --- a/BlastPDF/Builder/PdfPage.cs +++ b/BlastPDF/Builder/PdfPage.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using BlastPDF.Builder.Graphics; using BlastPDF.Builder.Resources; +using BlastPDF.Builder.Resources.Font; namespace BlastPDF.Builder; diff --git a/BlastPDF/Builder/Resources/PdfFontResource.cs b/BlastPDF/Builder/Resources/Font/PdfFontResource.cs similarity index 99% rename from BlastPDF/Builder/Resources/PdfFontResource.cs rename to BlastPDF/Builder/Resources/Font/PdfFontResource.cs index 6151f0a..5a65346 100644 --- a/BlastPDF/Builder/Resources/PdfFontResource.cs +++ b/BlastPDF/Builder/Resources/Font/PdfFontResource.cs @@ -1,6 +1,6 @@ using System; -namespace BlastPDF.Builder.Resources; +namespace BlastPDF.Builder.Resources.Font; public enum PdfFontType { diff --git a/BlastPDF/Builder/Resources/PdfFontType1.cs b/BlastPDF/Builder/Resources/Font/PdfFontType1.cs similarity index 96% rename from BlastPDF/Builder/Resources/PdfFontType1.cs rename to BlastPDF/Builder/Resources/Font/PdfFontType1.cs index 7b41bd1..e7696b4 100644 --- a/BlastPDF/Builder/Resources/PdfFontType1.cs +++ b/BlastPDF/Builder/Resources/Font/PdfFontType1.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace BlastPDF.Builder.Resources; +namespace BlastPDF.Builder.Resources.Font; public class PdfFontType1 : PdfFontResource { diff --git a/BlastPDF/Builder/Resources/Image/PdfImageResource.cs b/BlastPDF/Builder/Resources/Image/PdfImageResource.cs new file mode 100644 index 0000000..310beec --- /dev/null +++ b/BlastPDF/Builder/Resources/Image/PdfImageResource.cs @@ -0,0 +1,78 @@ +using System; +using BlastPDF.Builder.Graphics; +using BlastPDF.Filter; +using SharperImage; +using SharperImage.Formats; + +namespace BlastPDF.Builder.Resources.Image; + +public class PdfImageResource : PdfObject +{ + public uint Width { get; set; } + public uint Height { get; set; } + public PdfColorSpace ColorSpace { get; set; } + public int BitsPerComponent { get; set; } + public PdfFilter[] Filters { get; set; } + + public IImage ImageData { get; set; } + + public static PdfImageResource FromFile(string filename, FileFormat format, PdfColorSpace colorSpace, PdfFilter[] filters) + { + var image = IImage.Decode(filename, format); + return FromImage(image, colorSpace, filters); + } + + public static PdfImageResource FromImage(IImage image, PdfColorSpace colorSpace, PdfFilter[] filters) + { + var result = new PdfImageResource { + Width = image.Width(), + Height = image.Height(), + ColorSpace = colorSpace, + BitsPerComponent = 8, // TODO I would like this to be figured out from the pixel format in the image + ImageData = image, + Filters = filters, + }; + + if (filters is null) + { + result.Filters = new[] {PdfFilter.AsciiHex}; + } + + return result; + } +} + +public static class PdfImageResourceExtensions +{ + public static PdfPage UseImage(this PdfPage page, string resourceName, string filename, FileFormat format, PdfColorSpace colorSpace = PdfColorSpace.DeviceRGB, PdfFilter[] filters = null) + { + if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName)); + if (page.Resources.ContainsKey(resourceName)) throw new ArgumentException($"Resource '{resourceName}' already exists as a resource for this page :("); + page.Resources.Add(resourceName, PdfImageResource.FromFile(filename, format, colorSpace, filters)); + return page; + } + + public static PdfDocument UseImage(this PdfDocument doc, string resourceName, string filename, FileFormat format, PdfColorSpace colorSpace = PdfColorSpace.DeviceRGB, PdfFilter[] filters = null) + { + if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName)); + if (doc.Resources.ContainsKey(resourceName)) throw new ArgumentException($"Resource '{resourceName}' already exists as a resource for this document :("); + doc.Resources.Add(resourceName, PdfImageResource.FromFile(filename, format, colorSpace, filters)); + return doc; + } + + public static PdfPage UseImage(this PdfPage page, string resourceName, IImage image, PdfColorSpace colorSpace = PdfColorSpace.DeviceRGB, PdfFilter[] filters = null) + { + if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName)); + if (page.Resources.ContainsKey(resourceName)) throw new ArgumentException($"Resource '{resourceName}' already exists as a resource for this page :("); + page.Resources.Add(resourceName, PdfImageResource.FromImage(image, colorSpace, filters)); + return page; + } + + public static PdfDocument UseImage(this PdfDocument doc, string resourceName, IImage image, PdfColorSpace colorSpace = PdfColorSpace.DeviceRGB, PdfFilter[] filters = null) + { + if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName)); + if (doc.Resources.ContainsKey(resourceName)) throw new ArgumentException($"Resource '{resourceName}' already exists as a resource for this document :("); + doc.Resources.Add(resourceName, PdfImageResource.FromImage(image, colorSpace, filters)); + return doc; + } +} \ No newline at end of file diff --git a/BlastPDF/Builder/Resources/PdfResource.cs b/BlastPDF/Builder/Resources/PdfResource.cs new file mode 100644 index 0000000..3e6b685 --- /dev/null +++ b/BlastPDF/Builder/Resources/PdfResource.cs @@ -0,0 +1,22 @@ +using System; + +namespace BlastPDF.Builder.Resources; + +public static class PdfResource +{ + public static PdfPage UseObject(this PdfPage page, string resourceName, PdfObject resource) + { + if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName)); + if (page.Resources.ContainsKey(resourceName)) throw new ArgumentException($"Resource '{resourceName}' already exists as a resource for this page :("); + page.Resources.Add(resourceName, resource); + return page; + } + + public static PdfDocument UseObject(this PdfDocument doc, string resourceName, PdfObject resource) + { + if (string.IsNullOrEmpty(resourceName)) throw new ArgumentNullException(nameof(resourceName)); + if (doc.Resources.ContainsKey(resourceName)) throw new ArgumentException($"Resource '{resourceName}' already exists as a resource for this document :("); + doc.Resources.Add(resourceName, resource); + return doc; + } +} \ No newline at end of file diff --git a/BlastPDF/Exporter/Basic/BasicExporter.cs b/BlastPDF/Exporter/Basic/BasicExporter.cs index 0461872..f0839b3 100644 --- a/BlastPDF/Exporter/Basic/BasicExporter.cs +++ b/BlastPDF/Exporter/Basic/BasicExporter.cs @@ -7,6 +7,8 @@ using BlastPDF.Builder.Graphics; using BlastPDF.Builder.Graphics.Drawing; using BlastPDF.Builder.Resources; +using BlastPDF.Builder.Resources.Font; +using BlastPDF.Builder.Resources.Image; using BlastPDF.Filter; using BlastSharp.Lists; using SharperImage.Enumerators; @@ -110,9 +112,16 @@ private static PdfExporterResults Export(this PdfPage page, Stream stream, int o List<(string, int)> x_objects = new(); foreach (var res in page.Resources) { + var startOffset = stream.Position; + crossReferences.Add((nextStart, stream.Position)); x_objects.Add((res.Key, nextStart)); res.Value.Export(stream, nextStart); - nextStart += 1; + var endOffset = stream.Position; + crossReferences.Add((nextStart + 1, stream.Position)); + stream.Write($"{nextStart + 1} 0 obj\n".ToUTF8()); + stream.Write($"({endOffset - startOffset})\n".ToUTF8()); + stream.Write("endobj\n\n".ToUTF8()); + nextStart += 2; } // build page resource dictionary var pageResources = nextStart; @@ -190,6 +199,7 @@ private static PdfExporterResults Export(this PdfObject pdfObject, Stream stream return pdfObject switch { PdfGraphicsObject graphics => graphics.Export(stream, objectNumber), + PdfImageResource imageRes => imageRes.Export(stream, objectNumber), _ => throw new Exception("Unhandled subtype of PdfObject") }; } @@ -220,18 +230,50 @@ private static PdfExporterResults Export(this PdfFontType1 pdfFontType1, Stream return new PdfExporterResults(); } - /*private static PdfExporterResults Export(this PdfEmbeddedImage embed, Stream stream, int objectNumber) + private static PdfExporterResults Export(this PdfImageResource image, Stream stream, int objectNumber) { stream.Write($"{objectNumber} 0 obj\n".ToUTF8()); stream.Write("<<\n/Type /XObject\n/Subtype /Image\n".ToUTF8()); - stream.Write($"/Width {embed.Width}\n/Height {embed.Height}".ToUTF8()); - stream.Write($"/ColorSpace /{embed.ColorSpace}\n/BitsPerComponent {embed.BitsPerComponent}\n".ToUTF8()); - stream.Write($"/Length {embed.ImageData.LongLength}\n".ToUTF8()); + stream.Write($"/Width {image.Width}\n/Height {image.Height}\n".ToUTF8()); + stream.Write($"/ColorSpace /{image.ColorSpace}\n/BitsPerComponent {image.BitsPerComponent}\n".ToUTF8()); + var filterNames = image.Filters.Select(x => x switch + { + PdfFilter.Ascii85 => "/A85", + PdfFilter.AsciiHex => "/AHx", + PdfFilter.Lzw => "/LZW", + PdfFilter.RunLength => "/RL", + _ => throw new NotImplementedException() + }).Join(" "); + stream.Write($"/Length {objectNumber + 1} 0 R\n".ToUTF8()); + stream.Write($"\t/F [{filterNames}]\n".ToUTF8()); stream.Write(">>\nstream\n".ToUTF8()); - stream.Write(embed.ImageData); + var imageData = image.ImageData.ToRowRankPixelEnumerable().SelectMany(p => + { + switch (image.ColorSpace) + { + case PdfColorSpace.DeviceGray: + return new[] { (byte)(0.299 * p.Red + 0.587 * p.Green + 0.114 * p.Blue) }; + case PdfColorSpace.DeviceRGB: + return new[] { p.Red, p.Green, p.Blue }; + case PdfColorSpace.DeviceCMYK: + var rPrime = p.Red / 255.0; + var gPrime = p.Green / 255.0; + var bPrime = p.Blue / 255.0; + var k = 1 - Math.Max(rPrime, Math.Max(gPrime, bPrime)); + var c = (1 - rPrime - k) / (1 - k); + var m = (1 - gPrime - k) / (1 - k); + var y = (1 - bPrime - k) / (1 - k); + return new[] { (byte)(c * 255), (byte)(m * 255), (byte)(y * 255), (byte)(k * 255) }; + default: + throw new NotImplementedException(); + } + }).ToList(); + imageData = image.Filters.Reverse().Aggregate(imageData, (current, filter) => filter.Encode(current).ToList()); + + stream.Write(imageData.ToArray()); stream.Write("\nendstream\nendobj\n".ToUTF8()); return new PdfExporterResults(); - }*/ + } private static PdfExporterResults Export(this PdfGraphicsObject graphicsObject, Stream stream, int objectNumber) { // TODO I have a feeling this could still be better @@ -256,6 +298,7 @@ private static PdfExporterResults Export(this PdfGraphicsObject graphicsObject, case PdfXObject xobj: return xobj.Export(stream, objectNumber); case PdfInlineImage image: return image.Export(stream, objectNumber); case PdfTextObject text: return text.Export(stream, objectNumber); + } foreach (var obj in graphicsObject.SubObjects) { diff --git a/ShowCase/PdfBuilderExample.cs b/ShowCase/PdfBuilderExample.cs index c04719f..d3f88f2 100644 --- a/ShowCase/PdfBuilderExample.cs +++ b/ShowCase/PdfBuilderExample.cs @@ -5,6 +5,8 @@ using BlastPDF.Builder.Graphics; using BlastPDF.Builder.Graphics.Drawing; using BlastPDF.Builder.Resources; +using BlastPDF.Builder.Resources.Font; +using BlastPDF.Builder.Resources.Image; using BlastPDF.Exporter.Basic; using BlastPDF.Filter; using BlastSharp.Dates; @@ -42,10 +44,7 @@ public static void Run(string outputPdfName) .UseHelvetica() .UseCourierBold() .UseTimesNewRomanItalic() - //.AddGraphics(PdfGraphicsObject.Create() - // .SetCMYK(0.0M, 0.0M, 1.0M, 0.0M) - // .Rect(0, 0, 1000, 1000) - // .Paint(PaintModeEnum.CloseFillStroke)) + .UseImage("Cat", "../../../images/bmp/cat.bmp", FileFormat.BMP, PdfColorSpace.DeviceRGB, new []{PdfFilter.AsciiHex, PdfFilter.Lzw}) .AddGraphics(PdfTextObject.Create() .TextLeading(12) .SetFont("Helvetica", 24) @@ -87,27 +86,26 @@ public static void Run(string outputPdfName) .ShowText("too far") .NextLineOffset(0, -6) .ShowText("too far")) - /*.AddGraphics(PdfGraphicsObject.Create() - .Translate(250, 702) - .Scale(50.0M, 50.0M) - .InlineImage("../../../images/bmp/w3c_home.bmp", FileFormat.BMP, PdfColorSpace.DeviceRGB, new []{PdfFilter.ASCII85})) - .AddGraphics(PdfGraphicsObject.Create() - .Translate(300, 702) - .Scale(50.0M, 50.0M) - .InlineImage("../../../images/bmp/w3c_home.bmp", FileFormat.BMP)) // ASCIIHex - .AddGraphics(PdfGraphicsObject.Create() - .Translate(350, 702) - .Scale(50.0M, 50.0M) - .InlineImage("../../../images/bmp/w3c_home.bmp", FileFormat.BMP, PdfColorSpace.DeviceRGB, new []{PdfFilter.LZW})) - .AddGraphics(PdfGraphicsObject.Create() - .Translate(400, 702) - .Scale(50.0M, 50.0M) - .InlineImage("../../../images/bmp/w3c_home.bmp", FileFormat.BMP, PdfColorSpace.DeviceRGB, new []{PdfFilter.ASCII85, PdfFilter.LZW})) - */ .AddGraphics(PdfGraphicsObject.Create() .Translate(200, 200) .Scale(600.0M, 600.0M) - .InlineImage("../../../images/bmp/cat.bmp", FileFormat.BMP, PdfColorSpace.DeviceRGB, new []{PdfFilter.AsciiHex, PdfFilter.Lzw})) + .Image("Cat") + .ResetState() + .Translate(250, 250) + .Scale(500.0M, 500.0M) + .Image("Cat") + .ResetState() + .Translate(300, 300) + .Scale(400.0M, 400.0M) + .Image("Cat") + .ResetState() + .Translate(350, 350) + .Scale(300.0M, 300.0M) + .Image("Cat") + .ResetState() + .Translate(400, 400) + .Scale(200.0M, 200.0M) + .Image("Cat")) ).Save(fs); //if (File.Exists("template_test.pdf")) {