Andriy Andruhovski
Posted on April 2, 2018
In this post, I want to share some of my experience in generating PDF documents using Aspose.PDF for .NET and introduce basic render techniques.
As an example, I chose a frequently used document - an invoice. Our document will consist of 5 sections:
- header section - contains the corporate logo and general info;
- address section - contains the info about the seller and the customer;
- grid section - contains the table of product items and the totals subsection;
- terms section - contains the list of strings for additional;
- footer - optional section with one string placed at the bottom of the page.
Disclaimer: please note, that Apose.PDF is a paid product, but a trial version is fully appropriate to make some experiments like creating a document up to 4 pages.
Step 0. Creating a model of the document. For convenient work, we will create several classes for describing invoice details.
Class LogoImage
will be used for a render company logo in the upper left corner.
namespace Aspose.PDF.Invoicer
{
public class LogoImage
{
public string FileName;
public int Width;
public int Height;
public LogoImage(string filename, int width, int height)
{
FileName = filename;
Width = width;
Height = height;
}
}
}
Class TotalRow
will be used for render row in the totals subsection.
namespace Aspose.PDF.Invoicer
{
public class TotalRow
{
public string Text;
public decimal Value;
public TotalRow(string text, decimal value)
{
Text = text;
Value = value;
}
}
}
Class ProductItem
represents one product item in the grid section.
namespace Aspose.PDF.Invoicer
{
public class ProductItem
{
public string Id;
public string Name;
public decimal Price;
public int Quantity;
public decimal Total => Price * Quantity;
public ProductItem(string id, string name, decimal price, int quantity)
{
Id = id;
Name = name;
Price = price;
Quantity = quantity;
}
}
}
And the last class will be used to represent a PDF document.
using System;
using System.Collections.Generic;
using System.IO;
using Aspose.Pdf;
using Aspose.Pdf.Text;
namespace Aspose.PDF.Invoicer
{
public class Invoice: IDisposable
{
#region Private memebers
private static readonly License Licence = new License();
private Color _textColor, _backColor;
private readonly Font _timeNewRomanFont;
private readonly TextBuilder _builder;
private readonly Page _pdfPage;
private readonly Document _pdfDocument;
private readonly Rectangle _logoPlaceHolder;
#endregion
public string ForegroundColor
{
get { return _textColor.ToString(); }
set { _textColor = Color.Parse(value); }
}
public string BackgroundColor
{
get { return _backColor.ToString(); }
set { _backColor = Color.Parse(value); }
}
//Invoice details
public string Number;
public uint PaymentPeriod = 14;
public LogoImage Logo;
public List<string> BillFrom;
public List<string> BillTo;
public List<ProductItem> Items;
public List<TotalRow> Totals;
public List<string> Details;
public string Footer;
public Invoice()
{
_pdfDocument = new Document();
_pdfDocument.PageInfo.Margin.Left = 36;
_pdfDocument.PageInfo.Margin.Right = 36;
_pdfPage = _pdfDocument.Pages.Add();
_textColor = Color.Black;
_backColor = Color.Transparent;
_logoPlaceHolder = new Rectangle(20, 700, 120, 800);
_timeNewRomanFont = FontRepository.FindFont("Times New Roman");
_builder = new TextBuilder(_pdfPage);
}
public void Save(Stream stream)
{
HeaderSection();
AddressSection();
GridSection();
TermsSection();
FooterSection();
_pdfDocument.Save(stream);
}
private void HeaderSection()
{
// TODO: Generate header section
}
private void AddressSection()
{
// TODO: Generate Address section
}
private void GridSection()
{
// TODO: Generate Grid section
}
private void TermsSection()
{
// TODO: Generate Terms section
}
private void FooterSection()
{
// TODO: Generate Footer section
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
_pdfPage.Dispose();
_pdfDocument.Dispose();
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}
}
Step 1. Writing a simple application for testing. In this console application, we create the invoice with C:\aspose\company-logo-design.png
as a logo image, 3 product items, some comments, and a demo footer.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Aspose.PDF.Invoicer;
namespace ConsoleDemo
{
class Program
{
static void Main()
{
var productItems = new List<ProductItem> {
new ProductItem ("1", "Chocolate with Milk", 166.66M, 20),
new ProductItem ("2", "Chocolate with Nuts", 166.66M, 20),
new ProductItem ("3", "Chocolate with Pepper", 166.66M, 20)
};
var subTotal = productItems.Sum(i => i.Total);
var invoice = new Invoice
{
ForegroundColor = "#0000CC",
BackgroundColor = "#FFFFFF",
Number = "ABC-123",
Logo = new LogoImage(@"C:\aspose\company-logo-design.png", 160, 120),
BillFrom = new List<string> { "Eastern Chocolate Factory", "Eastern Chocolate House", "44 Shirley Ave.", "West Chicago", "IL 60185" },
BillTo = new List<string> { "Eastern Chocolate Cafe", "Eastern Chocolate House", "44 Shirley Ave.", "West Chicago", "IL 60185" },
Items = productItems,
Totals = new List<TotalRow>
{
new TotalRow("Sub Total", subTotal),
new TotalRow("VAT @ 20%", subTotal*0.2M),
new TotalRow("Total", subTotal*1.2M)
},
Details = new List<string> {
"Terms & Conditions",
"Payment is due within 15 days",
string.Empty,
"If you have any questions concerning this invoice, contact our sales department at sales@east-chocolate.factory",
"", "Thank you for your business."
},
Footer = "https://www.facebook.com/AsposePDF/"
};
var fileStream = new FileStream(@"C:\\aspose\\invoice.pdf", FileMode.OpenOrCreate);
invoice.Save(fileStream);
fileStream.Close();
}
}
}
Step 2. Rendering the header section. The Aspose.PDF library provides several ways to place elements in the document. The simplest way to place a sequence of elements with minimal settings is by using the Document Object Model. According to this conception, the Document object contains a collection of Page objects. This collection, in turn, contains a collection of BaseParagraph objects. Descendants of BaseParagraph is TextFragment, FloatingBox, Table etc.
We will use the TextFragment object to place lines "Invoice", "DATE:" and "DUE DATE". The first line will be centered, and the rest is right-aligned. Logo image breaks the hierarchy of DOM elements and must be placed in the upper left corner, so to place the logo we will apply another technique - adding a resource to the page. A detailed explanation of using resources is not the aim of this post. We will return to this technique in future posts. In short, we complete the following steps:
- Add image to Images collection of Page Resources
- Save current graphics state
- Define a Matrix object for placing the image in the appropriate location
- Apply matrix to the image
- Restore graphics state
The following snippet contains the full implementation of the HeaderSection()
method.
private void HeaderSection()
{
var lines = new TextFragment[3];
// Create text fragment
lines[0] = new TextFragment($"INVOICE #{Number}");
lines[0].TextState.FontSize = 20;
lines[0].TextState.ForegroundColor = _textColor;
lines[0].HorizontalAlignment = HorizontalAlignment.Center;
_pdfPage.Paragraphs.Add(lines[0]);
lines[1] = new TextFragment($"DATE: {DateTime.Today:MM/dd/yyyy}");
lines[2] = new TextFragment($"DUE DATE: {DateTime.Today.AddDays(PaymentPeriod):MM/dd/yyyy}");
for (var i = 1; i < lines.Length; i++)
{
// Set text properties
lines[i].TextState.Font = _timeNewRomanFont;
lines[i].TextState.FontSize = 12;
lines[i].HorizontalAlignment = HorizontalAlignment.Right;
// Add fragment to paragraph
_pdfPage.Paragraphs.Add(lines[i]);
}
// Logo
// Set coordinates
_logoPlaceHolder.URX = _logoPlaceHolder.LLX + Logo.Width;
_logoPlaceHolder.URY = _logoPlaceHolder.LLY + Logo.Height;
// Load image into stream
var imageStream = new FileStream(Logo.FileName, FileMode.Open);
// Add image to Images collection of Page Resources
_pdfPage.Resources.Images.Add(imageStream);
// Using GSave operator: this operator saves current graphics state
_pdfPage.Contents.Add(new Operator.GSave());
// Create Rectangle and Matrix objects
var matrix = new Matrix(new[] { _logoPlaceHolder.URX - _logoPlaceHolder.LLX, 0, 0,
_logoPlaceHolder.URY - _logoPlaceHolder.LLY,
_logoPlaceHolder.LLX, _logoPlaceHolder.LLY });
// Using ConcatenateMatrix (concatenate matrix) operator: defines how image must be placed
_pdfPage.Contents.Add(new Operator.ConcatenateMatrix(matrix));
var ximage = _pdfPage.Resources.Images[_pdfPage.Resources.Images.Count];
// Using Do operator: this operator draws image
_pdfPage.Contents.Add(new Operator.Do(ximage.Name));
// Using GRestore operator: this operator restores graphics state
_pdfPage.Contents.Add(new Operator.GRestore());
}
Step 3. Rendering the address section. We apply the two-column layout with mirror alignment: the left column is left-aligned and the right column is right-aligned. Such fragments can be easily rendered with FloatingBox class. To perform this layout we need to complete 4 steps:
- Create an object of the FloatingBox class;
- Add text fragments to the left column;
- Add 1 text fragment to the to the right column with properties
IsFirstParagraphInColumn=true
andHorizontalAlignment = HorizontalAlignment.Right
; - Add the rest fragments to the right column with the right alignment.
The following snippet shows these steps:
private void AddressSection()
{
var box = new FloatingBox(524, 120)
{
ColumnInfo =
{
ColumnCount = 2,
ColumnWidths = "252 252"
},
Padding =
{
Top = 20
}
};
TextFragment fragment;
BillFrom.Insert(0, "FROM:");
foreach (var str in BillFrom)
{
fragment = new TextFragment(str);
fragment.TextState.Font = _timeNewRomanFont;
fragment.TextState.FontSize = 12;
// Add fragment to paragraph
box.Paragraphs.Add(fragment);
}
fragment = new TextFragment("BILL TO:") { IsFirstParagraphInColumn = true };
fragment.TextState.Font = _timeNewRomanFont;
fragment.TextState.FontSize = 12;
fragment.TextState.HorizontalAlignment = HorizontalAlignment.Right;
box.Paragraphs.Add(fragment);
foreach (var str in BillTo)
{
fragment = new TextFragment(str);
fragment.TextState.Font = _timeNewRomanFont;
fragment.TextState.FontSize = 12;
fragment.TextState.HorizontalAlignment = HorizontalAlignment.Right;
// Add fragment to paragraph
box.Paragraphs.Add(fragment);
}
_pdfPage.Paragraphs.Add(box);
}
Step 4. Rendering the grid section. We will use the Table object to represent data in tabular format. First, we need to define table columns and the default decoration of cells. Please, note that the ColumnWidths
property implicitly defines a number of columns by setting their widths. In our grid, we will use an inverse color scheme for header rows. The following algorithm is used to create table rows:
- Call
table.Rows.Add()
to create the row and get the reference to the new row; - Call
row.Cells.Add()
to create each cell and get the reference to the new cell; - Set cell decoration if needed;
The totals subsection is created similarly, but for a better view, we join up the 4 first cells by using the ColSpan
property.
private void GridSection()
{
// Initializes a new instance of the Table
var table = new Table
{
ColumnWidths = "26 257 78 78 78",
Border = new BorderInfo(BorderSide.Box, 1f, _textColor),
DefaultCellBorder = new BorderInfo(BorderSide.Box, 0.5f, _textColor),
DefaultCellPadding = new MarginInfo(4.5, 4.5, 4.5, 4.5),
Margin =
{
Bottom = 10
},
DefaultCellTextState =
{
Font = _timeNewRomanFont
}
};
var headerRow = table.Rows.Add();
var cell = headerRow.Cells.Add("#");
cell.Alignment = HorizontalAlignment.Center;
headerRow.Cells.Add("Item");
headerRow.Cells.Add("Price");
headerRow.Cells.Add("Quantity");
headerRow.Cells.Add("Sum");
foreach (Cell headerRowCell in headerRow.Cells)
{
headerRowCell.BackgroundColor = _textColor;
headerRowCell.DefaultCellTextState.ForegroundColor = _backColor;
}
foreach (var productItem in Items)
{
var row = table.Rows.Add();
cell = row.Cells.Add(productItem.Id);
cell.Alignment = HorizontalAlignment.Center;
row.Cells.Add(productItem.Name);
cell = row.Cells.Add(productItem.Price.ToString("C2"));
cell.Alignment = HorizontalAlignment.Right;
cell = row.Cells.Add(productItem.Quantity.ToString());
cell.Alignment = HorizontalAlignment.Right;
cell = row.Cells.Add(productItem.Total.ToString("C2"));
cell.Alignment = HorizontalAlignment.Right;
}
foreach (var totalRow in Totals)
{
var row = table.Rows.Add();
var nameCell = row.Cells.Add(totalRow.Text);
nameCell.ColSpan = 4;
var textCell = row.Cells.Add(totalRow.Value.ToString("C2"));
textCell.Alignment = HorizontalAlignment.Right;
}
_pdfPage.Paragraphs.Add(table);
}
Step 5. Rendering theTerms and Conditions
section.
This section contains only text fragments, and their rendering is the same as described above.
private void TermsSection()
{
foreach (var detail in Details)
{
var fragment = new TextFragment(detail);
fragment.TextState.Font = _timeNewRomanFont;
fragment.TextState.FontSize = 12;
_pdfPage.Paragraphs.Add(fragment);
}
}
Step 6. Rendering Footer section
According to the previously declared layout, the last section is optional and must be placed at the bottom of the page. That is means we can't use adding the text fragment to the paragraph collection because this adding placed our fragment already after the 'Terms and Conditions' section.
In this case, we need to use a TextBuilder class. This class helps us to place text in any location. Additionally, the following snippet shows how we can add a hyperlink to the text fragment.
private void FooterSection()
{
var fragment = new TextFragment(Footer);
var len = fragment.TextState.MeasureString(fragment.Text);
fragment.Position = new Position(_pdfPage.PageInfo.Width / 2 - len / 2, 20);
fragment.Hyperlink = new WebHyperlink(Footer);
var builder = new TextBuilder(_pdfPage);
builder.AppendText(fragment);
}
Conclusion
In this post, we considered how to create a simple PDF document from scratch. Most of the parameters such as font size, the width of the cell's border, column's width in the table are adjusted manually based on A4 page size.
It's obvious that we are able to tune these parameters in proportion to the size of the page, but this will slightly complicate the example, but will not change the technique of creating the document as a whole.
A fundamentally different approach is working with some templates and replacing part of the content.
Posted on April 2, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.