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 ;
2020-03-10 06:01:37 +08:00
using System.IO ;
2018-12-09 02:04:02 +08:00
using Content ;
2018-11-28 04:00:38 +08:00
using Core ;
2019-12-29 01:47:50 +08:00
using Fonts ;
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 ;
2020-03-10 06:01:37 +08:00
using Images ;
using Tokens ;
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 > ( ) ;
2020-03-10 06:01:37 +08:00
private readonly Dictionary < NameToken , IToken > resourcesDictionary = new Dictionary < NameToken , IToken > ( ) ;
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)
2019-12-22 02:09:49 +08:00
private int textSequence ;
2019-08-12 02:55:59 +08:00
2020-03-10 06:01:37 +08:00
private int imageKey = 1 ;
2018-12-25 18:37:00 +08:00
internal IReadOnlyList < IGraphicsStateOperation > Operations = > operations ;
2018-11-25 03:38:16 +08:00
2020-03-10 06:01:37 +08:00
internal IReadOnlyDictionary < NameToken , IToken > Resources = > resourcesDictionary ;
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 ) ) ;
}
2019-12-22 02:09:49 +08:00
operations . Add ( new BeginNewSubpath ( ( decimal ) from . X , ( decimal ) from . Y ) ) ;
operations . Add ( new AppendStraightLineSegment ( ( decimal ) to . X , ( decimal ) to . Y ) ) ;
2018-12-12 08:09:15 +08:00
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 ) ) ;
}
2019-12-22 02:09:49 +08:00
operations . Add ( new AppendRectangle ( ( decimal ) position . X , ( decimal ) position . Y , width , height ) ) ;
2018-12-12 08:09:15 +08:00
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 ) ) ;
2019-12-22 02:09:49 +08:00
operations . Add ( new MoveToNextLineWithOffset ( ( decimal ) position . X , ( decimal ) position . Y ) ) ;
2020-01-04 18:04:02 +08:00
var bytesPerShow = new List < byte > ( ) ;
2019-12-28 22:42:27 +08:00
foreach ( var letter in text )
{
2020-01-04 18:04:02 +08:00
if ( char . IsWhiteSpace ( letter ) )
{
operations . Add ( new ShowText ( bytesPerShow . ToArray ( ) ) ) ;
bytesPerShow . Clear ( ) ;
}
2019-12-28 22:42:27 +08:00
var b = fontProgram . GetValueForCharacter ( letter ) ;
2020-01-04 18:04:02 +08:00
bytesPerShow . Add ( b ) ;
}
if ( bytesPerShow . Count > 0 )
{
operations . Add ( new ShowText ( bytesPerShow . ToArray ( ) ) ) ;
2019-12-28 22:42:27 +08:00
}
2018-12-25 18:37:00 +08:00
operations . Add ( EndText . Value ) ;
2018-12-09 02:04:02 +08:00
return letters ;
2018-11-25 21:56:27 +08:00
}
2020-03-10 06:01:37 +08:00
/// <summary>
/// Adds the JPEG image represented by the input bytes at the specified location.
/// </summary>
public void AddJpeg ( byte [ ] fileBytes , PdfRectangle placementRectangle )
{
using ( var stream = new MemoryStream ( fileBytes ) )
{
AddJpeg ( stream , placementRectangle ) ;
}
}
/// <summary>
/// Adds the JPEG image represented by the input stream at the specified location.
/// </summary>
public void 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 , IToken >
{
{ 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 < NameToken , IToken > ( ) ) ;
resourcesDictionary [ NameToken . Xobject ] = xobjects ;
}
var key = NameToken . Create ( $"I{imageKey++}" ) ;
resourcesDictionary [ NameToken . Xobject ] = xobjects . With ( key , new IndirectReferenceToken ( reference ) ) ;
operations . Add ( Push . Value ) ;
// This needs to be the placement rectangle.
operations . Add ( new ModifyCurrentTransformationMatrix ( new [ ]
{
( decimal ) placementRectangle . Width , 0 ,
0 , ( decimal ) placementRectangle . Height ,
( decimal ) placementRectangle . BottomLeft . X , ( decimal ) placementRectangle . BottomLeft . Y
} ) ) ;
operations . Add ( new InvokeNamedXObject ( key ) ) ;
operations . Add ( Pop . Value ) ;
}
2019-12-22 02:09:49 +08:00
private 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 =
2019-12-22 02:09:49 +08:00
TransformationMatrix . FromValues ( ( double ) fontSize * horizontalScaling , 0 , 0 , ( double ) fontSize , 0 , rise ) ;
2018-12-09 02:04:02 +08:00
2019-12-22 02:09:49 +08:00
var width = 0.0 ;
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-12-22 02:09:49 +08:00
var letter = new Letter ( c . ToString ( ) , documentSpace , advanceRect . BottomLeft , advanceRect . BottomRight , width , ( double ) fontSize , font . Name ,
GrayColor . Black ,
( double ) fontSize ,
2019-08-17 06:34:57 +08:00
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 ) ;
2020-01-04 18:04:02 +08:00
res = Math . Round ( Math . Min ( 1 , res ) , 4 ) ;
2018-12-30 22:12:04 +08:00
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
}
}