2018-11-25 03:38:16 +08:00
namespace UglyToad.PdfPig.Writer
{
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 ;
2020-04-02 23:33:03 +08:00
using System ;
using System.Collections.Generic ;
2020-12-21 03:13:19 +08:00
using System.Diagnostics ;
2020-04-02 23:33:03 +08:00
using System.IO ;
2020-12-21 03:13:19 +08:00
using System.Linq ;
2020-04-26 00:15:26 +08:00
using PdfFonts ;
2020-03-10 06:01:37 +08:00
using Tokens ;
2020-04-26 00:15:26 +08:00
using Graphics.Operations.PathPainting ;
2020-08-21 17:50:17 +08:00
using Images.Png ;
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
2021-02-11 02:27:12 +08:00
{
2021-02-09 02:37:09 +08:00
// parent
2021-02-11 02:27:12 +08:00
private readonly PdfDocumentBuilder documentBuilder ;
2021-02-09 02:37:09 +08:00
// all page data other than content streams
internal readonly Dictionary < NameToken , IToken > pageDictionary = new Dictionary < NameToken , IToken > ( ) ;
2021-02-11 02:27:12 +08:00
2021-02-09 02:37:09 +08:00
// streams
2021-02-07 02:24:53 +08:00
internal readonly List < IPageContentStream > contentStreams ;
2021-02-11 02:27:12 +08:00
private IPageContentStream currentStream ;
2021-02-09 02:37:09 +08:00
// maps fonts added using PdfDocumentBuilder to page font names
2021-02-08 00:37:31 +08:00
private readonly Dictionary < Guid , NameToken > documentFonts = new Dictionary < Guid , NameToken > ( ) ;
2021-02-11 02:27:12 +08:00
internal int nextFontId = 1 ;
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 ;
2021-02-09 02:37:09 +08:00
internal IReadOnlyDictionary < string , IToken > Resources = > pageDictionary . GetOrCreateDict ( NameToken . Resources ) ;
2020-03-10 06:01:37 +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>
2021-02-07 02:24:53 +08:00
public IContentStream CurrentStream = > currentStream ;
2020-12-19 18:41:36 +08:00
/// <summary>
/// Access to
/// </summary>
2021-02-07 02:24:53 +08:00
public IReadOnlyList < IContentStream > ContentStreams = > contentStreams ;
2019-01-04 06:25:26 +08:00
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 ;
2020-12-19 18:41:36 +08:00
2021-02-07 02:24:53 +08:00
currentStream = new DefaultContentStream ( ) ;
contentStreams = new List < IPageContentStream > ( ) { currentStream } ;
2020-12-19 18:41:36 +08:00
}
2021-02-07 02:24:53 +08:00
internal PdfPageBuilder ( int number , PdfDocumentBuilder documentBuilder , IEnumerable < CopiedContentStream > copied ,
2021-02-09 02:37:09 +08:00
Dictionary < NameToken , IToken > pageDict )
2021-02-07 02:24:53 +08:00
{
this . documentBuilder = documentBuilder ? ? throw new ArgumentNullException ( nameof ( documentBuilder ) ) ;
PageNumber = number ;
2021-02-09 02:37:09 +08:00
pageDictionary = pageDict ;
2021-02-07 02:24:53 +08:00
contentStreams = new List < IPageContentStream > ( ) ;
contentStreams . AddRange ( copied ) ;
currentStream = new DefaultContentStream ( ) ;
contentStreams . Add ( currentStream ) ;
}
2020-12-19 18:41:36 +08:00
/// <summary>
/// Allow to append a new content stream before the current one and select it
/// </summary>
public void NewContentStreamBefore ( )
{
2021-02-07 02:24:53 +08:00
var index = Math . Max ( contentStreams . IndexOf ( currentStream ) - 1 , 0 ) ;
2020-12-19 18:41:36 +08:00
2021-02-07 02:24:53 +08:00
currentStream = new DefaultContentStream ( ) ;
contentStreams . Insert ( index , currentStream ) ;
2020-12-19 18:41:36 +08:00
}
/// <summary>
/// Allow to append a new content stream after the current one and select it
/// </summary>
public void NewContentStreamAfter ( )
{
2021-02-07 02:24:53 +08:00
var index = Math . Min ( contentStreams . IndexOf ( currentStream ) + 1 , contentStreams . Count ) ;
2020-12-19 18:41:36 +08:00
2021-02-07 02:24:53 +08:00
currentStream = new DefaultContentStream ( ) ;
contentStreams . Insert ( index , currentStream ) ;
2020-12-19 18:41:36 +08:00
}
/// <summary>
/// Select a content stream from the list, by his index
/// </summary>
/// <param name="index">index of the content stream to be selected</param>
public void SelectContentStream ( int index )
{
2021-02-07 02:24:53 +08:00
if ( index < 0 | | index > = contentStreams . Count )
2020-12-19 18:41:36 +08:00
{
throw new IndexOutOfRangeException ( nameof ( index ) ) ;
}
2021-02-07 02:24:53 +08:00
currentStream = contentStreams [ index ] ;
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 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( new SetLineWidth ( lineWidth ) ) ;
2018-12-12 08:09:15 +08:00
}
2021-02-07 02:24:53 +08:00
currentStream . Add ( new BeginNewSubpath ( ( decimal ) from . X , ( decimal ) from . Y ) ) ;
currentStream . Add ( new AppendStraightLineSegment ( ( decimal ) to . X , ( decimal ) to . Y ) ) ;
currentStream . Add ( StrokePath . Value ) ;
2018-12-12 08:09:15 +08:00
if ( lineWidth ! = 1 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( new SetLineWidth ( 1 ) ) ;
2018-12-12 08:09:15 +08:00
}
}
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>
2020-11-18 05:00:13 +08:00
/// <param name="fill">Whether to fill with the color set by <see cref="SetTextAndFillColor"/>.</param>
public void DrawRectangle ( PdfPoint position , decimal width , decimal height , decimal lineWidth = 1 , bool fill = false )
2018-12-12 08:09:15 +08:00
{
if ( lineWidth ! = 1 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( new SetLineWidth ( lineWidth ) ) ;
2018-12-12 08:09:15 +08:00
}
2021-02-07 02:24:53 +08:00
currentStream . Add ( new AppendRectangle ( ( decimal ) position . X , ( decimal ) position . Y , width , height ) ) ;
2020-11-18 05:00:13 +08:00
if ( fill )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( FillPathEvenOddRuleAndStroke . Value ) ;
2020-11-18 05:00:13 +08:00
}
else
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( StrokePath . Value ) ;
2020-11-18 05:00:13 +08:00
}
2018-12-12 08:09:15 +08:00
if ( lineWidth ! = 1 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( new SetLineWidth ( lineWidth ) ) ;
2018-12-12 08:09:15 +08:00
}
}
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 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( Push . Value ) ;
currentStream . Add ( new SetStrokeColorDeviceRgb ( RgbToDecimal ( r ) , RgbToDecimal ( g ) , RgbToDecimal ( b ) ) ) ;
2018-12-30 22:12:04 +08:00
}
/// <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 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( Push . Value ) ;
currentStream . 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 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( Push . Value ) ;
currentStream . Add ( new SetNonStrokeColorDeviceRgb ( RgbToDecimal ( r ) , RgbToDecimal ( g ) , RgbToDecimal ( b ) ) ) ;
2018-12-30 22:12:04 +08:00
}
/// <summary>
/// Restores the stroke, text and fill color to default (black).
/// </summary>
public void ResetColor ( )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( Pop . Value ) ;
2018-12-30 22:12:04 +08:00
}
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 ) ) ;
}
2020-12-21 03:13:19 +08:00
if ( ! documentBuilder . Fonts . TryGetValue ( font . Id , out var fontStore ) )
2018-11-25 21:56:27 +08:00
{
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" ) ;
}
2020-12-21 03:13:19 +08:00
var fontProgram = fontStore . FontProgram ;
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
2021-02-07 05:13:22 +08:00
var letters = DrawLetters ( null , 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
2020-12-21 03:13:19 +08:00
if ( ! documentBuilder . Fonts . TryGetValue ( font . Id , out var fontStore ) )
2018-12-25 18:37:00 +08:00
{
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
2021-02-07 05:13:22 +08:00
var fontName = GetAddedFont ( font ) ;
2020-12-21 03:13:19 +08:00
var fontProgram = fontStore . FontProgram ;
2018-12-25 18:37:00 +08:00
var fm = fontProgram . GetFontMatrix ( ) ;
var textMatrix = TransformationMatrix . FromValues ( 1 , 0 , 0 , 1 , position . X , position . Y ) ;
2021-02-07 05:13:22 +08:00
var letters = DrawLetters ( fontName , text , fontProgram , fm , fontSize , textMatrix ) ;
2018-12-25 18:37:00 +08:00
2021-02-07 02:24:53 +08:00
currentStream . Add ( BeginText . Value ) ;
2021-02-07 05:13:22 +08:00
currentStream . Add ( new SetFontAndSize ( fontName , fontSize ) ) ;
2021-02-07 02:24:53 +08:00
currentStream . 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 ) )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( new ShowText ( bytesPerShow . ToArray ( ) ) ) ;
2020-01-04 18:04:02 +08:00
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 )
{
2021-02-07 02:24:53 +08:00
currentStream . Add ( new ShowText ( bytesPerShow . ToArray ( ) ) ) ;
2019-12-28 22:42:27 +08:00
}
2021-02-07 02:24:53 +08:00
currentStream . Add ( EndText . Value ) ;
2018-12-25 18:37:00 +08:00
2018-12-09 02:04:02 +08:00
return letters ;
2021-02-08 00:37:31 +08:00
}
private NameToken GetAddedFont ( PdfDocumentBuilder . AddedFont font )
2021-02-07 05:13:22 +08:00
{
if ( ! documentFonts . TryGetValue ( font . Id , out NameToken value ) )
{
2021-02-08 00:37:31 +08:00
value = NameToken . Create ( $"F{nextFontId++}" ) ;
2021-02-09 02:37:09 +08:00
var resources = pageDictionary . GetOrCreateDict ( NameToken . Resources ) ;
var fonts = resources . GetOrCreateDict ( NameToken . Font ) ;
while ( fonts . ContainsKey ( value ) )
2021-02-07 05:13:22 +08:00
{
value = NameToken . Create ( $"F{nextFontId++}" ) ;
}
documentFonts [ font . Id ] = value ;
2021-02-09 02:37:09 +08:00
fonts [ value ] = font . Reference ;
2021-02-07 05:13:22 +08:00
}
return value ;
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>
2020-03-16 01:30:30 +08:00
public AddedImage AddJpeg ( byte [ ] fileBytes , PdfRectangle placementRectangle )
2020-03-10 06:01:37 +08:00
{
using ( var stream = new MemoryStream ( fileBytes ) )
{
2020-03-16 01:30:30 +08:00
return AddJpeg ( stream , placementRectangle ) ;
2020-03-10 06:01:37 +08:00
}
}
/// <summary>
/// Adds the JPEG image represented by the input stream at the specified location.
/// </summary>
2020-03-16 01:30:30 +08:00
public AddedImage AddJpeg ( Stream fileStream , PdfRectangle placementRectangle )
2020-03-10 06:01:37 +08:00
{
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 ) ;
2021-02-11 02:27:12 +08:00
var resources = pageDictionary . GetOrCreateDict ( NameToken . Resources ) ;
2021-02-09 02:37:09 +08:00
var xObjects = resources . GetOrCreateDict ( NameToken . Xobject ) ;
2020-03-10 06:01:37 +08:00
var key = NameToken . Create ( $"I{imageKey++}" ) ;
2021-02-09 02:37:09 +08:00
xObjects [ key ] = reference ;
2020-03-10 06:01:37 +08:00
2021-02-07 02:24:53 +08:00
currentStream . Add ( Push . Value ) ;
2020-03-10 06:01:37 +08:00
// This needs to be the placement rectangle.
2021-02-07 02:24:53 +08:00
currentStream . Add ( new ModifyCurrentTransformationMatrix ( new [ ]
2020-03-10 06:01:37 +08:00
{
( decimal ) placementRectangle . Width , 0 ,
0 , ( decimal ) placementRectangle . Height ,
( decimal ) placementRectangle . BottomLeft . X , ( decimal ) placementRectangle . BottomLeft . Y
} ) ) ;
2021-02-07 02:24:53 +08:00
currentStream . Add ( new InvokeNamedXObject ( key ) ) ;
currentStream . Add ( Pop . Value ) ;
2020-03-16 01:30:30 +08:00
2021-02-07 02:24:53 +08:00
return new AddedImage ( reference . Data , info . Width , info . Height ) ;
2020-03-16 01:30:30 +08:00
}
/// <summary>
/// Adds the JPEG image previously added using <see cref="AddJpeg(byte[],PdfRectangle)"/>,
/// this will share the same image data to prevent duplication.
/// </summary>
/// <param name="image">An image previously added to this page or another page.</param>
/// <param name="placementRectangle">The size and location to draw the image on this page.</param>
2020-08-22 22:08:59 +08:00
public void AddJpeg ( AddedImage image , PdfRectangle placementRectangle ) = > AddImage ( image , placementRectangle ) ;
/// <summary>
/// Adds the image previously added using <see cref="AddJpeg(byte[], PdfRectangle)"/>
/// or <see cref="AddPng(byte[], PdfRectangle)"/> sharing the same image to prevent duplication.
/// </summary>
public void AddImage ( AddedImage image , PdfRectangle placementRectangle )
2020-03-16 01:30:30 +08:00
{
2021-02-09 02:37:09 +08:00
var resources = pageDictionary . GetOrCreateDict ( NameToken . Resources ) ;
var xObjects = resources . GetOrCreateDict ( NameToken . Xobject ) ;
2020-03-16 01:30:30 +08:00
var key = NameToken . Create ( $"I{imageKey++}" ) ;
2021-02-09 02:37:09 +08:00
xObjects [ key ] = new IndirectReferenceToken ( image . Reference ) ;
2020-03-16 01:30:30 +08:00
2021-02-07 02:24:53 +08:00
currentStream . Add ( Push . Value ) ;
2020-03-16 01:30:30 +08:00
// This needs to be the placement rectangle.
2021-02-07 02:24:53 +08:00
currentStream . Add ( new ModifyCurrentTransformationMatrix ( new [ ]
2020-03-16 01:30:30 +08:00
{
( decimal ) placementRectangle . Width , 0 ,
0 , ( decimal ) placementRectangle . Height ,
( decimal ) placementRectangle . BottomLeft . X , ( decimal ) placementRectangle . BottomLeft . Y
} ) ) ;
2021-02-07 02:24:53 +08:00
currentStream . Add ( new InvokeNamedXObject ( key ) ) ;
currentStream . Add ( Pop . Value ) ;
2020-03-10 06:01:37 +08:00
}
2020-08-21 17:50:17 +08:00
/// <summary>
/// Adds the PNG image represented by the input bytes at the specified location.
/// </summary>
public AddedImage AddPng ( byte [ ] pngBytes , PdfRectangle placementRectangle )
{
using ( var memoryStream = new MemoryStream ( pngBytes ) )
{
return AddPng ( memoryStream , placementRectangle ) ;
}
}
/// <summary>
/// Adds the PNG image represented by the input stream at the specified location.
/// </summary>
public AddedImage AddPng ( Stream pngStream , PdfRectangle placementRectangle )
{
var png = Png . Open ( pngStream ) ;
byte [ ] data ;
var pixelBuffer = new byte [ 3 ] ;
using ( var memoryStream = new MemoryStream ( ) )
{
for ( var rowIndex = 0 ; rowIndex < png . Height ; rowIndex + + )
{
for ( var colIndex = 0 ; colIndex < png . Width ; colIndex + + )
{
var pixel = png . GetPixel ( colIndex , rowIndex ) ;
pixelBuffer [ 0 ] = pixel . R ;
pixelBuffer [ 1 ] = pixel . G ;
pixelBuffer [ 2 ] = pixel . B ;
memoryStream . Write ( pixelBuffer , 0 , pixelBuffer . Length ) ;
}
}
data = memoryStream . ToArray ( ) ;
}
var compressed = DataCompresser . CompressBytes ( data ) ;
var imgDictionary = new Dictionary < NameToken , IToken >
{
{ NameToken . Type , NameToken . Xobject } ,
{ NameToken . Subtype , NameToken . Image } ,
{ NameToken . Width , new NumericToken ( png . Width ) } ,
{ NameToken . Height , new NumericToken ( png . Height ) } ,
{ NameToken . BitsPerComponent , new NumericToken ( png . Header . BitDepth ) } ,
{ NameToken . ColorSpace , NameToken . Devicergb } ,
{ NameToken . Filter , NameToken . FlateDecode } ,
{ NameToken . Length , new NumericToken ( compressed . Length ) }
} ;
var reference = documentBuilder . AddImage ( new DictionaryToken ( imgDictionary ) , compressed ) ;
2021-02-09 02:37:09 +08:00
var resources = pageDictionary . GetOrCreateDict ( NameToken . Resources ) ;
var xObjects = resources . GetOrCreateDict ( NameToken . Xobject ) ;
2020-08-21 17:50:17 +08:00
var key = NameToken . Create ( $"I{imageKey++}" ) ;
2021-02-09 02:37:09 +08:00
xObjects [ key ] = reference ;
2020-08-21 17:50:17 +08:00
2021-02-07 02:24:53 +08:00
currentStream . Add ( Push . Value ) ;
2020-08-21 17:50:17 +08:00
// This needs to be the placement rectangle.
2021-02-07 02:24:53 +08:00
currentStream . Add ( new ModifyCurrentTransformationMatrix ( new [ ]
2020-08-21 17:50:17 +08:00
{
( decimal ) placementRectangle . Width , 0 ,
0 , ( decimal ) placementRectangle . Height ,
( decimal ) placementRectangle . BottomLeft . X , ( decimal ) placementRectangle . BottomLeft . Y
} ) ) ;
2021-02-07 02:24:53 +08:00
currentStream . Add ( new InvokeNamedXObject ( key ) ) ;
currentStream . Add ( Pop . Value ) ;
2020-08-21 17:50:17 +08:00
2021-02-07 02:24:53 +08:00
return new AddedImage ( reference . Data , png . Width , png . Height ) ;
2020-08-21 17:50:17 +08:00
}
2020-12-21 03:13:19 +08:00
/// <summary>
/// Copy a page from unknown source to this page
/// </summary>
/// <param name="srcPage">Page to be copied</param>
public void CopyFrom ( Page srcPage )
{
2021-02-07 02:24:53 +08:00
if ( currentStream . Operations . Count > 0 )
2020-12-21 03:13:19 +08:00
{
NewContentStreamAfter ( ) ;
}
2021-02-07 02:24:53 +08:00
var destinationStream = currentStream ;
2020-12-21 03:13:19 +08:00
if ( ! srcPage . Dictionary . TryGet ( NameToken . Resources , srcPage . pdfScanner , out DictionaryToken srcResourceDictionary ) )
{
// If the page doesn't have resources, then we copy the entire content stream, since not operation would collide
// with the ones already written
destinationStream . Operations . AddRange ( srcPage . Operations ) ;
return ;
}
// TODO: How should we handle any other token in the page dictionary (Eg. LastModified, MediaBox, CropBox, BleedBox, TrimBox, ArtBox,
// BoxColorInfo, Rotate, Group, Thumb, B, Dur, Trans, Annots, AA, Metadata, PieceInfo, StructParents, ID, PZ, SeparationInfo, Tabs,
// TemplateInstantiated, PresSteps, UserUnit, VP)
var operations = new List < IGraphicsStateOperation > ( srcPage . Operations ) ;
// We need to relocate the resources, and we have to make sure that none of the resources collide with
// the already written operation's resources
2021-02-09 02:37:09 +08:00
var resources = pageDictionary . GetOrCreateDict ( NameToken . Resources ) ;
2020-12-21 03:13:19 +08:00
foreach ( var set in srcResourceDictionary . Data )
{
var nameToken = NameToken . Create ( set . Key ) ;
if ( nameToken = = NameToken . Font | | nameToken = = NameToken . Xobject )
{
// We have to skip this two because we have a separate dictionary for them
continue ;
}
2021-02-09 02:37:09 +08:00
if ( ! resources . ContainsKey ( nameToken ) )
2020-12-21 03:13:19 +08:00
{
// It means that this type of resources doesn't currently exist in the page, so we can copy it
// with no problem
2021-02-09 02:37:09 +08:00
resources [ nameToken ] = documentBuilder . CopyToken ( srcPage . pdfScanner , set . Value ) ;
2020-12-21 03:13:19 +08:00
continue ;
}
// TODO: I need to find a test case
// It would have ExtendedGraphics or colorspaces, etc...
}
// Special cases
// Since we don't directly add font's to the pages resources, we have to go look at the document's font
if ( srcResourceDictionary . TryGet ( NameToken . Font , srcPage . pdfScanner , out DictionaryToken fontsDictionary ) )
2021-02-11 02:27:12 +08:00
{
2021-02-09 02:37:09 +08:00
var pageFontsDictionary = resources . GetOrCreateDict ( NameToken . Font ) ;
2020-12-21 03:13:19 +08:00
foreach ( var fontSet in fontsDictionary . Data )
{
2021-02-07 05:13:22 +08:00
var fontName = NameToken . Create ( fontSet . Key ) ;
2021-02-09 02:37:09 +08:00
if ( pageFontsDictionary . ContainsKey ( fontName ) )
2020-12-21 03:13:19 +08:00
{
// This would mean that the imported font collide with one of the added font. so we have to rename it
2021-02-07 05:13:22 +08:00
var newName = NameToken . Create ( $"F{nextFontId++}" ) ;
2021-02-09 02:37:09 +08:00
while ( pageFontsDictionary . ContainsKey ( newName ) )
2021-02-07 05:13:22 +08:00
{
newName = NameToken . Create ( $"F{nextFontId++}" ) ;
}
2020-12-21 03:13:19 +08:00
// Set all the pertinent SetFontAndSize operations with the new name
operations = operations . Select ( op = >
{
if ( ! ( op is SetFontAndSize fontAndSizeOperation ) )
{
return op ;
}
if ( fontAndSizeOperation . Font . Data = = fontName )
{
2021-02-07 05:13:22 +08:00
return new SetFontAndSize ( newName , fontAndSizeOperation . Size ) ;
2020-12-21 03:13:19 +08:00
}
return op ;
} ) . ToList ( ) ;
fontName = newName ;
}
if ( ! ( fontSet . Value is IndirectReferenceToken fontReferenceToken ) )
{
throw new PdfDocumentFormatException ( $"Expected a IndirectReferenceToken for the font, got a {fontSet.Value.GetType().Name}" ) ;
}
2021-02-07 05:13:22 +08:00
pageFontsDictionary . Add ( fontName , documentBuilder . CopyToken ( srcPage . pdfScanner , fontReferenceToken ) ) ;
2020-12-21 03:13:19 +08:00
}
}
// Since we don't directly add xobjects's to the pages resources, we have to go look at the document's xobjects
if ( srcResourceDictionary . TryGet ( NameToken . Xobject , srcPage . pdfScanner , out DictionaryToken xobjectsDictionary ) )
{
2021-02-09 02:37:09 +08:00
var pageXobjectsDictionary = resources . GetOrCreateDict ( NameToken . Xobject ) ;
2020-12-21 03:13:19 +08:00
var xobjectNamesUsed = Enumerable . Range ( 0 , imageKey ) . Select ( i = > $"I{i}" ) ;
foreach ( var xobjectSet in xobjectsDictionary . Data )
{
var xobjectName = xobjectSet . Key ;
if ( xobjectName [ 0 ] = = 'I' & & xobjectNamesUsed . Any ( s = > s = = xobjectName ) )
{
// This would mean that the imported xobject collide with one of the added image. so we have to rename it
var newName = $"I{imageKey++}" ;
// Set all the pertinent SetFontAndSize operations with the new name
operations = operations . Select ( op = >
{
if ( ! ( op is InvokeNamedXObject invokeNamedOperation ) )
{
return op ;
}
if ( invokeNamedOperation . Name . Data = = xobjectName )
{
return new InvokeNamedXObject ( NameToken . Create ( newName ) ) ;
}
return op ;
} ) . ToList ( ) ;
xobjectName = newName ;
}
if ( ! ( xobjectSet . Value is IndirectReferenceToken fontReferenceToken ) )
{
throw new PdfDocumentFormatException ( $"Expected a IndirectReferenceToken for the XObject, got a {xobjectSet.Value.GetType().Name}" ) ;
}
2021-02-09 02:37:09 +08:00
pageXobjectsDictionary [ xobjectName ] = documentBuilder . CopyToken ( srcPage . pdfScanner , fontReferenceToken ) ;
2020-12-21 03:13:19 +08:00
}
}
destinationStream . Operations . AddRange ( operations ) ;
}
2021-02-07 05:13:22 +08:00
private List < Letter > DrawLetters ( NameToken name , 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 ) ) ) ;
2021-02-07 05:13:22 +08:00
var letter = new Letter ( c . ToString ( ) , documentSpace , advanceRect . BottomLeft , advanceRect . BottomRight , width , ( double ) fontSize , FontDetails . GetDefault ( name ) ,
2019-12-22 02:09:49 +08:00
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
2021-02-08 00:37:31 +08:00
/// <summary>
/// Provides access to the raw page data structures for advanced editing use cases.
2021-02-07 02:24:53 +08:00
/// </summary>
public interface IContentStream
2021-02-08 00:37:31 +08:00
{
/// <summary>
/// The operations making up the page content stream.
2019-01-04 06:25:26 +08:00
/// </summary>
2021-02-07 02:24:53 +08:00
List < IGraphicsStateOperation > Operations { get ; }
}
2021-02-11 10:18:03 +08:00
internal interface IPageContentStream : IContentStream
{
bool ReadOnly { get ; }
bool HasContent { get ; }
void Add ( IGraphicsStateOperation operation ) ;
IndirectReferenceToken Write ( IPdfStreamWriter writer ) ;
}
2021-02-07 02:24:53 +08:00
internal class DefaultContentStream : IPageContentStream
{
private readonly List < IGraphicsStateOperation > operations ;
public DefaultContentStream ( ) : this ( new List < IGraphicsStateOperation > ( ) )
{
}
public DefaultContentStream ( List < IGraphicsStateOperation > operations )
{
this . operations = operations ;
}
public bool ReadOnly = > false ;
2021-02-08 00:37:31 +08:00
public bool HasContent = > operations . Any ( ) ;
2021-02-07 02:24:53 +08:00
public void Add ( IGraphicsStateOperation operation )
{
operations . Add ( operation ) ;
}
public List < IGraphicsStateOperation > Operations = > operations ;
public IndirectReferenceToken Write ( IPdfStreamWriter writer )
2020-12-19 18:41:36 +08:00
{
2021-02-07 02:24:53 +08:00
using ( var memoryStream = new MemoryStream ( ) )
{
foreach ( var operation in operations )
{
operation . Write ( memoryStream ) ;
}
var bytes = memoryStream . ToArray ( ) ;
var stream = DataCompresser . CompressToStream ( bytes ) ;
return writer . WriteToken ( stream ) ;
}
2020-12-19 18:41:36 +08:00
}
2021-02-07 02:24:53 +08:00
}
internal class CopiedContentStream : IPageContentStream
{
private readonly IndirectReferenceToken token ;
2021-02-08 00:37:31 +08:00
public bool ReadOnly = > true ;
2021-02-09 02:37:09 +08:00
public bool HasContent = > true ;
2021-02-07 02:24:53 +08:00
public CopiedContentStream ( IndirectReferenceToken indirectReferenceToken )
{
token = indirectReferenceToken ;
}
public IndirectReferenceToken Write ( IPdfStreamWriter writer )
{
return token ;
}
2021-02-08 00:37:31 +08:00
2021-02-07 02:24:53 +08:00
public void Add ( IGraphicsStateOperation operation )
2019-01-04 06:25:26 +08:00
{
2021-02-07 02:24:53 +08:00
throw new NotSupportedException ( "Writing to a copied content stream is not supported." ) ;
2019-01-04 06:25:26 +08:00
}
2021-02-07 02:24:53 +08:00
2021-02-08 00:37:31 +08:00
public List < IGraphicsStateOperation > Operations = >
2021-02-07 02:24:53 +08:00
throw new NotSupportedException ( "Reading raw operations is not supported from a copied content stream." ) ;
2019-01-04 06:25:26 +08:00
}
2020-03-16 01:30:30 +08:00
2021-02-07 02:24:53 +08:00
2020-03-16 01:30:30 +08:00
/// <summary>
/// A key representing an image available to use for the current document builder.
/// Create it by adding an image to a page using <see cref="AddJpeg(byte[],PdfRectangle)"/>.
/// </summary>
public class AddedImage
{
/// <summary>
/// The Id uniquely identifying this image on the builder.
/// </summary>
internal Guid Id { get ; }
/// <summary>
/// The reference to the stored image XObject.
/// </summary>
internal IndirectReference Reference { get ; }
/// <summary>
/// The width of the raw image in pixels.
/// </summary>
public int Width { get ; }
/// <summary>
/// The height of the raw image in pixels.
/// </summary>
public int Height { get ; }
/// <summary>
/// Create a new <see cref="AddedImage"/>.
/// </summary>
internal AddedImage ( IndirectReference reference , int width , int height )
{
Id = Guid . NewGuid ( ) ;
Reference = reference ;
Width = width ;
Height = height ;
}
}
2021-02-09 02:37:09 +08:00
2018-11-25 03:38:16 +08:00
}
}