namespace UglyToad.PdfPig.Writer { using Content; using Core; using Fonts; using Graphics.Colors; using Graphics.Operations; using Graphics.Operations.General; using Graphics.Operations.PathConstruction; using Graphics.Operations.SpecialGraphicsState; using Graphics.Operations.TextObjects; using Graphics.Operations.TextPositioning; using Graphics.Operations.TextShowing; using Graphics.Operations.TextState; using Images; using System; using System.Collections.Generic; using System.IO; using PdfFonts; using Tokens; using Graphics.Operations.PathPainting; using Images.Png; /// /// A builder used to add construct a page in a PDF document. /// public class PdfPageBuilder { private readonly PdfDocumentBuilder documentBuilder; private readonly List contentStreams; private readonly Dictionary resourcesDictionary = new Dictionary(); //a sequence number of ShowText operation to determine whether letters belong to same operation or not (letters that belong to different operations have less changes to belong to same word) private int textSequence; private int imageKey = 1; internal IReadOnlyDictionary Resources => resourcesDictionary; /// /// The number of this page, 1-indexed. /// public int PageNumber { get; } /// /// The current size of the page. /// public PdfRectangle PageSize { get; set; } /// /// Access to the underlying data structures for advanced use cases. /// public ContentStream CurrentStream { get; private set; } /// /// Access to /// public IReadOnlyList ContentStreams { get; } internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder) { this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder)); PageNumber = number; CurrentStream = new ContentStream(); ContentStreams = contentStreams = new List() { CurrentStream }; } /// /// Allow to append a new content stream before the current one and select it /// public void NewContentStreamBefore() { var index = Math.Max(contentStreams.IndexOf(CurrentStream) - 1, 0); CurrentStream = new ContentStream(); contentStreams.Insert(index, CurrentStream); } /// /// Allow to append a new content stream after the current one and select it /// public void NewContentStreamAfter() { var index = Math.Min(contentStreams.IndexOf(CurrentStream) + 1, contentStreams.Count); CurrentStream = new ContentStream(); contentStreams.Insert(index, CurrentStream); } /// /// Select a content stream from the list, by his index /// /// index of the content stream to be selected public void SelectContentStream(int index) { if (index < 0 || index >= ContentStreams.Count) { throw new IndexOutOfRangeException(nameof(index)); } CurrentStream = ContentStreams[index]; } /// /// Draws a line on the current page between two points with the specified line width. /// /// The first point on the line. /// The last point on the line. /// The width of the line in user space units. public void DrawLine(PdfPoint from, PdfPoint to, decimal lineWidth = 1) { if (lineWidth != 1) { CurrentStream.Add(new SetLineWidth(lineWidth)); } CurrentStream.Add(new BeginNewSubpath((decimal)from.X, (decimal)from.Y)); CurrentStream.Add(new AppendStraightLineSegment((decimal)to.X, (decimal)to.Y)); CurrentStream.Add(StrokePath.Value); if (lineWidth != 1) { CurrentStream.Add(new SetLineWidth(1)); } } /// /// Draws a rectangle on the current page starting at the specified point with the given width, height and line width. /// /// The position of the rectangle, for positive width and height this is the bottom-left corner. /// The width of the rectangle. /// The height of the rectangle. /// The width of the line border of the rectangle. /// Whether to fill with the color set by . public void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1, bool fill = false) { if (lineWidth != 1) { CurrentStream.Add(new SetLineWidth(lineWidth)); } CurrentStream.Add(new AppendRectangle((decimal)position.X, (decimal)position.Y, width, height)); if (fill) { CurrentStream.Add(FillPathEvenOddRuleAndStroke.Value); } else { CurrentStream.Add(StrokePath.Value); } if (lineWidth != 1) { CurrentStream.Add(new SetLineWidth(lineWidth)); } } /// /// Sets the stroke color for any following operations to the RGB value. Use to reset. /// /// Red - 0 to 255 /// Green - 0 to 255 /// Blue - 0 to 255 public void SetStrokeColor(byte r, byte g, byte b) { CurrentStream.Add(Push.Value); CurrentStream.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); } /// /// Sets the stroke color with the exact decimal value between 0 and 1 for any following operations to the RGB value. Use to reset. /// /// Red - 0 to 1 /// Green - 0 to 1 /// Blue - 0 to 1 internal void SetStrokeColorExact(decimal r, decimal g, decimal b) { CurrentStream.Add(Push.Value); CurrentStream.Add(new SetStrokeColorDeviceRgb(CheckRgbDecimal(r, nameof(r)), CheckRgbDecimal(g, nameof(g)), CheckRgbDecimal(b, nameof(b)))); } /// /// Sets the fill and text color for any following operations to the RGB value. Use to reset. /// /// Red - 0 to 255 /// Green - 0 to 255 /// Blue - 0 to 255 public void SetTextAndFillColor(byte r, byte g, byte b) { CurrentStream.Add(Push.Value); CurrentStream.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); } /// /// Restores the stroke, text and fill color to default (black). /// public void ResetColor() { CurrentStream.Add(Pop.Value); } /// /// Calculates the size and position of each letter in a given string in the provided font without changing the state of the page. /// /// The text to measure each letter of. /// The size of the font in user space units. /// The position of the baseline (lower-left corner) to start drawing the text from. /// /// A font added to the document using /// or methods. /// /// The letters from the input text with their corresponding size and position. public IReadOnlyList MeasureText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font) { if (font == null) { throw new ArgumentNullException(nameof(font)); } if (text == null) { throw new ArgumentNullException(nameof(text)); } if (!documentBuilder.Fonts.TryGetValue(font.Id, out var fontProgram)) { throw new ArgumentException($"No font has been added to the PdfDocumentBuilder with Id: {font.Id}. " + $"Use {nameof(documentBuilder.AddTrueTypeFont)} to register a font.", nameof(font)); } if (fontSize <= 0) { throw new ArgumentOutOfRangeException(nameof(fontSize), "Font size must be greater than 0"); } var fm = fontProgram.GetFontMatrix(); var textMatrix = TransformationMatrix.FromValues(1, 0, 0, 1, position.X, position.Y); var letters = DrawLetters(text, fontProgram, fm, fontSize, textMatrix); return letters; } /// /// Draws the text in the provided font at the specified position and returns the letters which will be drawn. /// /// The text to draw to the page. /// The size of the font in user space units. /// The position of the baseline (lower-left corner) to start drawing the text from. /// /// A font added to the document using /// or methods. /// /// The letters from the input text with their corresponding size and position. public IReadOnlyList AddText(string text, decimal fontSize, PdfPoint position, PdfDocumentBuilder.AddedFont font) { if (font == null) { throw new ArgumentNullException(nameof(font)); } if (text == null) { throw new ArgumentNullException(nameof(text)); } if (!documentBuilder.Fonts.TryGetValue(font.Id, out var fontProgram)) { throw new ArgumentException($"No font has been added to the PdfDocumentBuilder with Id: {font.Id}. " + $"Use {nameof(documentBuilder.AddTrueTypeFont)} to register a font.", nameof(font)); } if (fontSize <= 0) { throw new ArgumentOutOfRangeException(nameof(fontSize), "Font size must be greater than 0"); } var fm = fontProgram.GetFontMatrix(); var textMatrix = TransformationMatrix.FromValues(1, 0, 0, 1, position.X, position.Y); var letters = DrawLetters(text, fontProgram, fm, fontSize, textMatrix); CurrentStream.Add(BeginText.Value); CurrentStream.Add(new SetFontAndSize(font.Name, fontSize)); CurrentStream.Add(new MoveToNextLineWithOffset((decimal)position.X, (decimal)position.Y)); var bytesPerShow = new List(); foreach (var letter in text) { if (char.IsWhiteSpace(letter)) { CurrentStream.Add(new ShowText(bytesPerShow.ToArray())); bytesPerShow.Clear(); } var b = fontProgram.GetValueForCharacter(letter); bytesPerShow.Add(b); } if (bytesPerShow.Count > 0) { CurrentStream.Add(new ShowText(bytesPerShow.ToArray())); } CurrentStream.Add(EndText.Value); return letters; } /// /// Adds the JPEG image represented by the input bytes at the specified location. /// public AddedImage AddJpeg(byte[] fileBytes, PdfRectangle placementRectangle) { using (var stream = new MemoryStream(fileBytes)) { return AddJpeg(stream, placementRectangle); } } /// /// Adds the JPEG image represented by the input stream at the specified location. /// public AddedImage AddJpeg(Stream fileStream, PdfRectangle placementRectangle) { var startFrom = fileStream.Position; var info = JpegHandler.GetInformation(fileStream); byte[] data; using (var memory = new MemoryStream()) { fileStream.Seek(startFrom, SeekOrigin.Begin); fileStream.CopyTo(memory); data = memory.ToArray(); } var imgDictionary = new Dictionary { {NameToken.Type, NameToken.Xobject }, {NameToken.Subtype, NameToken.Image }, {NameToken.Width, new NumericToken(info.Width) }, {NameToken.Height, new NumericToken(info.Height) }, {NameToken.BitsPerComponent, new NumericToken(info.BitsPerComponent)}, {NameToken.ColorSpace, NameToken.Devicergb}, {NameToken.Filter, NameToken.DctDecode}, {NameToken.Length, new NumericToken(data.Length)} }; var reference = documentBuilder.AddImage(new DictionaryToken(imgDictionary), data); if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict) || !(xobjectsDict is DictionaryToken xobjects)) { xobjects = new DictionaryToken(new Dictionary()); resourcesDictionary[NameToken.Xobject] = xobjects; } var key = NameToken.Create($"I{imageKey++}"); resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference)); CurrentStream.Add(Push.Value); // This needs to be the placement rectangle. CurrentStream.Add(new ModifyCurrentTransformationMatrix(new [] { (decimal)placementRectangle.Width, 0, 0, (decimal)placementRectangle.Height, (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y })); CurrentStream.Add(new InvokeNamedXObject(key)); CurrentStream.Add(Pop.Value); return new AddedImage(reference, info.Width, info.Height); } /// /// Adds the JPEG image previously added using , /// this will share the same image data to prevent duplication. /// /// An image previously added to this page or another page. /// The size and location to draw the image on this page. public void AddJpeg(AddedImage image, PdfRectangle placementRectangle) => AddImage(image, placementRectangle); /// /// Adds the image previously added using /// or sharing the same image to prevent duplication. /// public void AddImage(AddedImage image, PdfRectangle placementRectangle) { if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict) || !(xobjectsDict is DictionaryToken xobjects)) { xobjects = new DictionaryToken(new Dictionary()); resourcesDictionary[NameToken.Xobject] = xobjects; } var key = NameToken.Create($"I{imageKey++}"); resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(image.Reference)); CurrentStream.Add(Push.Value); // This needs to be the placement rectangle. CurrentStream.Add(new ModifyCurrentTransformationMatrix(new[] { (decimal)placementRectangle.Width, 0, 0, (decimal)placementRectangle.Height, (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y })); CurrentStream.Add(new InvokeNamedXObject(key)); CurrentStream.Add(Pop.Value); } /// /// Adds the PNG image represented by the input bytes at the specified location. /// public AddedImage AddPng(byte[] pngBytes, PdfRectangle placementRectangle) { using (var memoryStream = new MemoryStream(pngBytes)) { return AddPng(memoryStream, placementRectangle); } } /// /// Adds the PNG image represented by the input stream at the specified location. /// public AddedImage AddPng(Stream pngStream, PdfRectangle placementRectangle) { var png = Png.Open(pngStream); byte[] data; var pixelBuffer = new byte[3]; using (var memoryStream = new MemoryStream()) { for (var rowIndex = 0; rowIndex < png.Height; rowIndex++) { for (var colIndex = 0; colIndex < png.Width; colIndex++) { var pixel = png.GetPixel(colIndex, rowIndex); pixelBuffer[0] = pixel.R; pixelBuffer[1] = pixel.G; pixelBuffer[2] = pixel.B; memoryStream.Write(pixelBuffer, 0, pixelBuffer.Length); } } data = memoryStream.ToArray(); } var compressed = DataCompresser.CompressBytes(data); var imgDictionary = new Dictionary { {NameToken.Type, NameToken.Xobject }, {NameToken.Subtype, NameToken.Image }, {NameToken.Width, new NumericToken(png.Width) }, {NameToken.Height, new NumericToken(png.Height) }, {NameToken.BitsPerComponent, new NumericToken(png.Header.BitDepth)}, {NameToken.ColorSpace, NameToken.Devicergb}, {NameToken.Filter, NameToken.FlateDecode}, {NameToken.Length, new NumericToken(compressed.Length)} }; var reference = documentBuilder.AddImage(new DictionaryToken(imgDictionary), compressed); if (!resourcesDictionary.TryGetValue(NameToken.Xobject, out var xobjectsDict) || !(xobjectsDict is DictionaryToken xobjects)) { xobjects = new DictionaryToken(new Dictionary()); resourcesDictionary[NameToken.Xobject] = xobjects; } var key = NameToken.Create($"I{imageKey++}"); resourcesDictionary[NameToken.Xobject] = xobjects.With(key, new IndirectReferenceToken(reference)); CurrentStream.Add(Push.Value); // This needs to be the placement rectangle. CurrentStream.Add(new ModifyCurrentTransformationMatrix(new[] { (decimal)placementRectangle.Width, 0, 0, (decimal)placementRectangle.Height, (decimal)placementRectangle.BottomLeft.X, (decimal)placementRectangle.BottomLeft.Y })); CurrentStream.Add(new InvokeNamedXObject(key)); CurrentStream.Add(Pop.Value); return new AddedImage(reference, png.Width, png.Height); } private List DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix) { var horizontalScaling = 1; var rise = 0; var letters = new List(); var renderingMatrix = TransformationMatrix.FromValues((double)fontSize * horizontalScaling, 0, 0, (double)fontSize, 0, rise); var width = 0.0; textSequence++; for (var i = 0; i < text.Length; i++) { var c = text[i]; if (!font.TryGetBoundingBox(c, out var rect)) { throw new InvalidOperationException($"The font does not contain a character: {c}."); } if (!font.TryGetAdvanceWidth(c, out var charWidth)) { throw new InvalidOperationException($"The font does not contain a character: {c}."); } var advanceRect = new PdfRectangle(0, 0, charWidth, 0); advanceRect = textMatrix.Transform(renderingMatrix.Transform(fontMatrix.Transform(advanceRect))); var documentSpace = textMatrix.Transform(renderingMatrix.Transform(fontMatrix.Transform(rect))); var letter = new Letter(c.ToString(), documentSpace, advanceRect.BottomLeft, advanceRect.BottomRight, width, (double)fontSize, FontDetails.GetDefault(font.Name), GrayColor.Black, (double)fontSize, textSequence); letters.Add(letter); var tx = advanceRect.Width * horizontalScaling; var ty = 0; var translate = TransformationMatrix.GetTranslationMatrix(tx, ty); width += tx; textMatrix = translate.Multiply(textMatrix); } return letters; } private static decimal RgbToDecimal(byte value) { var res = Math.Max(0, value / (decimal)byte.MaxValue); res = Math.Round(Math.Min(1, res), 4); return res; } private static decimal CheckRgbDecimal(decimal value, string argument) { if (value < 0) { throw new ArgumentOutOfRangeException(argument, $"Provided decimal for RGB color was less than zero: {value}."); } if (value > 1) { throw new ArgumentOutOfRangeException(argument, $"Provided decimal for RGB color was greater than one: {value}."); } return value; } /// /// Provides access to the raw page data structures for advanced editing use cases. /// public class ContentStream { /// /// The operations making up the page content stream. /// public List Operations { get; } /// /// Create a new . /// internal ContentStream() { Operations = new List(); } internal void Add(IGraphicsStateOperation newOperation) { Operations.Add(newOperation); } } /// /// A key representing an image available to use for the current document builder. /// Create it by adding an image to a page using . /// public class AddedImage { /// /// The Id uniquely identifying this image on the builder. /// internal Guid Id { get; } /// /// The reference to the stored image XObject. /// internal IndirectReference Reference { get; } /// /// The width of the raw image in pixels. /// public int Width { get; } /// /// The height of the raw image in pixels. /// public int Height { get; } /// /// Create a new . /// internal AddedImage(IndirectReference reference, int width, int height) { Id = Guid.NewGuid(); Reference = reference; Width = width; Height = height; } } } }