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;
}
}
}
}