mirror of
https://github.com/UglyToad/PdfPig.git
synced 2025-07-19 06:29:58 +08:00

- In the Letter class: - Renaming 'Location' to 'StartBaseLine' and adding 'EndBaseLine' for better localisation of the letter ('Location' is also kept). - Adding TextDirection.
319 lines
13 KiB
C#
319 lines
13 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// A builder used to add construct a page in a PDF document.
|
|
/// </summary>
|
|
public class PdfPageBuilder
|
|
{
|
|
private readonly PdfDocumentBuilder documentBuilder;
|
|
private readonly List<IGraphicsStateOperation> operations = new List<IGraphicsStateOperation>();
|
|
internal IReadOnlyList<IGraphicsStateOperation> Operations => operations;
|
|
|
|
/// <summary>
|
|
/// The number of this page, 1-indexed.
|
|
/// </summary>
|
|
public int PageNumber { get; }
|
|
|
|
/// <summary>
|
|
/// The current size of the page.
|
|
/// </summary>
|
|
public PdfRectangle PageSize { get; set; }
|
|
|
|
/// <summary>
|
|
/// Access to the underlying data structures for advanced use cases.
|
|
/// </summary>
|
|
public AdvancedEditing Advanced { get; }
|
|
|
|
internal PdfPageBuilder(int number, PdfDocumentBuilder documentBuilder)
|
|
{
|
|
this.documentBuilder = documentBuilder ?? throw new ArgumentNullException(nameof(documentBuilder));
|
|
PageNumber = number;
|
|
Advanced = new AdvancedEditing(operations);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws a line on the current page between two points with the specified line width.
|
|
/// </summary>
|
|
/// <param name="from">The first point on the line.</param>
|
|
/// <param name="to">The last point on the line.</param>
|
|
/// <param name="lineWidth">The width of the line in user space units.</param>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws a rectangle on the current page starting at the specified point with the given width, height and line width.
|
|
/// </summary>
|
|
/// <param name="position">The position of the rectangle, for positive width and height this is the bottom-left corner.</param>
|
|
/// <param name="width">The width of the rectangle.</param>
|
|
/// <param name="height">The height of the rectangle.</param>
|
|
/// <param name="lineWidth">The width of the line border of the rectangle.</param>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the stroke color for any following operations to the RGB value. Use <see cref="ResetColor"/> to reset.
|
|
/// </summary>
|
|
/// <param name="r">Red - 0 to 255</param>
|
|
/// <param name="g">Green - 0 to 255</param>
|
|
/// <param name="b">Blue - 0 to 255</param>
|
|
public void SetStrokeColor(byte r, byte g, byte b)
|
|
{
|
|
operations.Add(Push.Value);
|
|
operations.Add(new SetStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the stroke color with the exact decimal value between 0 and 1 for any following operations to the RGB value. Use <see cref="ResetColor"/> to reset.
|
|
/// </summary>
|
|
/// <param name="r">Red - 0 to 1</param>
|
|
/// <param name="g">Green - 0 to 1</param>
|
|
/// <param name="b">Blue - 0 to 1</param>
|
|
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))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the fill and text color for any following operations to the RGB value. Use <see cref="ResetColor"/> to reset.
|
|
/// </summary>
|
|
/// <param name="r">Red - 0 to 255</param>
|
|
/// <param name="g">Green - 0 to 255</param>
|
|
/// <param name="b">Blue - 0 to 255</param>
|
|
public void SetTextAndFillColor(byte r, byte g, byte b)
|
|
{
|
|
operations.Add(Push.Value);
|
|
operations.Add(new SetNonStrokeColorDeviceRgb(RgbToDecimal(r), RgbToDecimal(g), RgbToDecimal(b)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores the stroke, text and fill color to default (black).
|
|
/// </summary>
|
|
public void ResetColor()
|
|
{
|
|
operations.Add(Pop.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the size and position of each letter in a given string in the provided font without changing the state of the page.
|
|
/// </summary>
|
|
/// <param name="text">The text to measure each letter of.</param>
|
|
/// <param name="fontSize">The size of the font in user space units.</param>
|
|
/// <param name="position">The position of the baseline (lower-left corner) to start drawing the text from.</param>
|
|
/// <param name="font">
|
|
/// A font added to the document using <see cref="PdfDocumentBuilder.AddTrueTypeFont"/>
|
|
/// or <see cref="PdfDocumentBuilder.AddStandard14Font"/> methods.
|
|
/// </param>
|
|
/// <returns>The letters from the input text with their corresponding size and position.</returns>
|
|
public IReadOnlyList<Letter> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws the text in the provided font at the specified position and returns the letters which will be drawn.
|
|
/// </summary>
|
|
/// <param name="text">The text to draw to the page.</param>
|
|
/// <param name="fontSize">The size of the font in user space units.</param>
|
|
/// <param name="position">The position of the baseline (lower-left corner) to start drawing the text from.</param>
|
|
/// <param name="font">
|
|
/// A font added to the document using <see cref="PdfDocumentBuilder.AddTrueTypeFont"/>
|
|
/// or <see cref="PdfDocumentBuilder.AddStandard14Font"/> methods.
|
|
/// </param>
|
|
/// <returns>The letters from the input text with their corresponding size and position.</returns>
|
|
public IReadOnlyList<Letter> 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<Letter> DrawLetters(string text, IWritingFont font, TransformationMatrix fontMatrix, decimal fontSize, TransformationMatrix textMatrix)
|
|
{
|
|
var horizontalScaling = 1;
|
|
var rise = 0;
|
|
var letters = new List<Letter>();
|
|
|
|
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, advanceRect.BottomRight, 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides access to the raw page data structures for advanced editing use cases.
|
|
/// </summary>
|
|
public class AdvancedEditing
|
|
{
|
|
/// <summary>
|
|
/// The operations making up the page content stream.
|
|
/// </summary>
|
|
public List<IGraphicsStateOperation> Operations { get; }
|
|
|
|
/// <summary>
|
|
/// Create a new <see cref="AdvancedEditing"/>.
|
|
/// </summary>
|
|
internal AdvancedEditing(List<IGraphicsStateOperation> operations)
|
|
{
|
|
Operations = operations;
|
|
}
|
|
}
|
|
}
|
|
} |