2018-11-25 03:38:16 +08:00
namespace UglyToad.PdfPig.Writer
{
using System ;
2018-11-28 04:00:38 +08:00
using System.Collections.Generic ;
2018-12-09 02:04:02 +08:00
using Content ;
2018-11-28 04:00:38 +08:00
using Core ;
2018-11-25 03:38:16 +08:00
using Geometry ;
2019-08-06 02:26:10 +08:00
using Graphics.Colors ;
2018-11-28 04:00:38 +08:00
using Graphics.Operations ;
2018-12-12 08:09:15 +08:00
using Graphics.Operations.General ;
using Graphics.Operations.PathConstruction ;
2018-12-30 22:12:04 +08:00
using Graphics.Operations.SpecialGraphicsState ;
2018-11-28 04:00:38 +08:00
using Graphics.Operations.TextObjects ;
2018-12-03 00:14:55 +08:00
using Graphics.Operations.TextPositioning ;
2018-11-28 04:00:38 +08:00
using Graphics.Operations.TextShowing ;
using Graphics.Operations.TextState ;
2018-11-25 03:38:16 +08:00
2018-12-25 18:37:00 +08:00
/// <summary>
/// A builder used to add construct a page in a PDF document.
/// </summary>
2018-12-29 00:55:46 +08:00
public class PdfPageBuilder
2018-11-25 03:38:16 +08:00
{
private readonly PdfDocumentBuilder documentBuilder ;
2018-11-28 04:00:38 +08:00
private readonly List < IGraphicsStateOperation > operations = new List < IGraphicsStateOperation > ( ) ;
2019-08-12 02:55:59 +08:00
//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 static int textSequence = 0 ;
2018-12-25 18:37:00 +08:00
internal IReadOnlyList < IGraphicsStateOperation > Operations = > operations ;
2018-11-25 03:38:16 +08:00
2018-12-25 18:37:00 +08:00
/// <summary>
/// The number of this page, 1-indexed.
/// </summary>
2018-11-25 03:38:16 +08:00
public int PageNumber { get ; }
2019-04-20 04:33:31 +08:00
2018-12-25 18:37:00 +08:00
/// <summary>
/// The current size of the page.
/// </summary>
2018-11-25 03:38:16 +08:00
public PdfRectangle PageSize { get ; set ; }
2019-01-04 06:25:26 +08:00
/// <summary>
/// Access to the underlying data structures for advanced use cases.
/// </summary>
public AdvancedEditing Advanced { get ; }
2018-12-25 18:37:00 +08:00
internal PdfPageBuilder ( int number , PdfDocumentBuilder documentBuilder )
2018-11-25 03:38:16 +08:00
{
this . documentBuilder = documentBuilder ? ? throw new ArgumentNullException ( nameof ( documentBuilder ) ) ;
PageNumber = number ;
2019-01-04 06:25:26 +08:00
Advanced = new AdvancedEditing ( operations ) ;
2018-11-25 03:38:16 +08:00
}
2018-11-25 21:56:27 +08:00
2018-12-25 18:37:00 +08:00
/// <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>
2018-12-12 08:09:15 +08:00
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 ) ) ;
}
}
2018-12-25 18:37:00 +08:00
/// <summary>
/// Draws a rectangle on the current page starting at the specified point with the given width, height and line width.
/// </summary>
2018-12-30 22:39:49 +08:00
/// <param name="position">The position of the rectangle, for positive width and height this is the bottom-left corner.</param>
2018-12-25 18:37:00 +08:00
/// <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>
2018-12-12 08:09:15 +08:00
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 ) ) ;
}
}
2018-12-30 22:12:04 +08:00
/// <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 ) ;
2019-04-20 04:33:31 +08:00
operations . Add ( new SetStrokeColorDeviceRgb ( CheckRgbDecimal ( r , nameof ( r ) ) ,
2018-12-30 22:12:04 +08:00
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 ) ;
}
2018-12-25 18:37:00 +08:00
/// <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 )
2018-11-25 21:56:27 +08:00
{
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" ) ;
}
2018-12-12 05:29:39 +08:00
var fm = fontProgram . GetFontMatrix ( ) ;
2018-12-09 02:04:02 +08:00
var textMatrix = TransformationMatrix . FromValues ( 1 , 0 , 0 , 1 , position . X , position . Y ) ;
2018-11-28 04:00:38 +08:00
2018-12-09 02:04:02 +08:00
var letters = DrawLetters ( text , fontProgram , fm , fontSize , textMatrix ) ;
2018-12-25 18:37:00 +08:00
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 )
2018-11-28 04:00:38 +08:00
{
2018-12-25 18:37:00 +08:00
throw new ArgumentNullException ( nameof ( font ) ) ;
}
2018-11-28 04:00:38 +08:00
2018-12-25 18:37:00 +08:00
if ( text = = null )
{
throw new ArgumentNullException ( nameof ( text ) ) ;
}
2018-11-28 04:00:38 +08:00
2018-12-25 18:37:00 +08:00
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 ) ) ;
2018-11-28 04:00:38 +08:00
}
2018-12-25 18:37:00 +08:00
if ( fontSize < = 0 )
2018-11-28 04:00:38 +08:00
{
2018-12-25 18:37:00 +08:00
throw new ArgumentOutOfRangeException ( nameof ( fontSize ) , "Font size must be greater than 0" ) ;
2018-11-28 04:00:38 +08:00
}
2018-11-25 21:56:27 +08:00
2018-12-25 18:37:00 +08:00
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 ) ;
2018-12-09 02:04:02 +08:00
return letters ;
2018-11-25 21:56:27 +08:00
}
2018-12-09 02:04:02 +08:00
private static List < Letter > DrawLetters ( string text , IWritingFont font , TransformationMatrix fontMatrix , decimal fontSize , TransformationMatrix textMatrix )
2018-11-25 21:56:27 +08:00
{
2018-12-09 02:04:02 +08:00
var horizontalScaling = 1 ;
var rise = 0 ;
var letters = new List < Letter > ( ) ;
var renderingMatrix =
TransformationMatrix . FromValues ( fontSize * horizontalScaling , 0 , 0 , fontSize , 0 , rise ) ;
2018-11-25 21:56:27 +08:00
var width = 0 m ;
2018-12-09 02:04:02 +08:00
2019-08-12 02:55:59 +08:00
textSequence + + ;
2018-11-25 21:56:27 +08:00
for ( var i = 0 ; i < text . Length ; i + + )
{
var c = text [ i ] ;
2018-12-25 18:37:00 +08:00
if ( ! font . TryGetBoundingBox ( c , out var rect ) )
2018-11-25 21:56:27 +08:00
{
throw new InvalidOperationException ( $"The font does not contain a character: {c}." ) ;
}
2018-12-09 02:04:02 +08:00
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 ) ) ) ;
2019-08-06 02:26:10 +08:00
var letter = new Letter ( c . ToString ( ) , documentSpace , advanceRect . BottomLeft , advanceRect . BottomRight , width , fontSize , font . Name ,
GrayColor . Black ,
2019-08-17 06:34:57 +08:00
fontSize ,
textSequence ) ;
2018-12-09 02:04:02 +08:00
letters . Add ( letter ) ;
2018-12-25 18:37:00 +08:00
2018-12-09 02:04:02 +08:00
var tx = advanceRect . Width * horizontalScaling ;
var ty = 0 ;
var translate = TransformationMatrix . GetTranslationMatrix ( tx , ty ) ;
width + = tx ;
textMatrix = translate . Multiply ( textMatrix ) ;
2018-11-25 21:56:27 +08:00
}
2018-12-09 02:04:02 +08:00
return letters ;
2018-11-25 21:56:27 +08:00
}
2018-12-30 22:12:04 +08:00
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 ;
}
2019-01-04 06:25:26 +08:00
/// <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 ;
}
}
2018-11-25 03:38:16 +08:00
}
}