namespace UglyToad.PdfPig.Writer { using System; using System.Collections.Generic; using Content; using Core; using Geometry; 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; /// /// A builder used to add construct a page in a PDF document. /// public class PdfPageBuilder { private readonly PdfDocumentBuilder documentBuilder; private readonly List operations = new List(); internal IReadOnlyList Operations => operations; /// /// 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 AdvancedEditing Advanced { get; } internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder) { this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder)); PageNumber = number; Advanced = new AdvancedEditing(operations); } /// /// 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) { operations.Add(new SetLineWidth(lineWidth)); } operations.Add(new BeginNewSubpath(from.X, from.Y)); operations.Add(new AppendStraightLineSegment(to.X, to.Y)); operations.Add(StrokePath.Value); if (lineWidth != 1) { operations.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. public void DrawRectangle(PdfPoint position, decimal width, decimal height, decimal lineWidth = 1) { if (lineWidth != 1) { operations.Add(new SetLineWidth(lineWidth)); } operations.Add(new AppendRectangle(position.X, position.Y, width, height)); operations.Add(StrokePath.Value); if (lineWidth != 1) { operations.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) { operations.Add(Push.Value); operations.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) { operations.Add(Push.Value); operations.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) { operations.Add(Push.Value); operations.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b))); } /// /// Restores the stroke, text and fill color to default (black). /// public void ResetColor() { operations.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); operations.Add(BeginText.Value); operations.Add(new SetFontAndSize(font.Name, fontSize)); operations.Add(new MoveToNextLineWithOffset(position.X, position.Y)); operations.Add(new ShowText(text)); operations.Add(EndText.Value); return letters; } private static 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(fontSize * horizontalScaling, 0, 0, fontSize, 0, rise); var width = 0m; 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, width, fontSize, font.Name, fontSize); 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.Min(1, res); 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 AdvancedEditing { /// /// The operations making up the page content stream. /// public List Operations { get; } /// /// Create a new . /// internal AdvancedEditing(List operations) { Operations = operations; } } } }