Generating PDF file using C#

andruhovski

Andriy Andruhovski

Posted on April 2, 2018

Generating PDF file using C#

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.

The invoice layout

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }                
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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();

        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create an object of the FloatingBox class;
  2. Add text fragments to the left column;
  3. Add 1 text fragment to the to the right column with properties IsFirstParagraphInColumn=true and HorizontalAlignment = HorizontalAlignment.Right;
  4. 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
andruhovski
Andriy Andruhovski

Posted on April 2, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Generating PDF file using C#
csharp Generating PDF file using C#

April 2, 2018