PDF Generation using QuestPDF in ASP.NET Core — Part 2

Olufemi Oyedepo
6 min readAug 24, 2024

--

From the previous article about this C#-first PDF generation library (QuestPDF), we generated a one-page report ideal for invoices, investment or premium purchase reports, etc, or any kind of report that fits into a single page.

This time around, we will be doing something more advanced. This tutorial covers concepts like:

  1. Conditional highlighting
  2. Page footer display
  3. Datatable rendering

By the end of this tutorial, you should be able to see something like this:

We will be working with the code repository from part 1, which can be found here

Let’s get started 😎

  • Add this endpoint to the existing QuestPdfController.cs in the Controllers folder.
[HttpGet]
[Route("generate-pharmacy-report")]
public ActionResult GeneratePharmacyReport()
{
var byteArray = QuestPdfService.GeneratePharmacyReportBytes();
return File(byteArray, "application/pdf", "PharmacyReport.pdf");
}
  • Create a model class inside the “Models” folder and name it PharmacyReportInfo like so:
public class PharmacyReportInfo
{
public int Id { get; set; }
public string CustomerName { get; set; }
public DateTime TransactonDate { get; set; }
public string CustomerId { get; set; }
public double AmountDue { get; set; }
public double AmountPaid{ get; set; }
public double OutstandingAmount { get; set; }
}
  • Check the “Assets” directory for two image files (medical-logo.png, medical-logo.png2) and add them to your “Assets” folder, your folder should look like this:
  • Reference the two images from the QuestPdfService.cs class like this:
public static Image _medicalLogo1 { get; } = Image.FromFile("Assets/medical-logo.PNG");
public static Image _medicalLogo2 { get; } = Image.FromFile("Assets/medical-logo2.PNG");
  • Add static a method to the QuestPdfService.cs class and name it GeneratePharmacyReportBytes. It returns a byte array representation of the final PDF document.
public static byte[] GeneratePharmacyReportBytes()
{
byte[] reportBytes;
Document document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4.Landscape());
page.Margin(0.8f, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Tahoma", "Arial", ""));
page.Content().Border(1);
page.Content().Element(ComposePharmacyReportContent);
page.Footer().Element(ComposeFooterContent);
});
});

reportBytes = document.GeneratePdf();
return reportBytes;
}
  • Add another static method to the QuestPdfService.cs class and name it GetPharmacyReport. It returns a statically typed mock list of records that will populate the report sheet.
private static List<PharmacyReportInfo> GetPharmacyReport()
{
var pharmacyReport = new List<PharmacyReportInfo>
{
new PharmacyReportInfo() { AmountDue = 65900.00, AmountPaid = 45350.00, CustomerId = "CUST/90/2891", CustomerName = "Ibrahim Hassan", Id = 1, TransactonDate = new DateTime(2024, 3, 19) },
new PharmacyReportInfo() { AmountDue=4500, AmountPaid = 4500, CustomerId="CUST/23/8811", CustomerName = "Ayodeji Peter", Id=2, TransactonDate=new DateTime(2024, 4, 22) },
new PharmacyReportInfo() { AmountDue=8900.34, AmountPaid = 8000, CustomerId="CUST/11/7399", CustomerName = "James Okuzor", Id=3, TransactonDate=new DateTime(2024, 6, 5) },
new PharmacyReportInfo() { AmountDue=67500, AmountPaid = 67500, CustomerId="CUST/77/2833", CustomerName = "Olufemi Oyedepo", Id=4, TransactonDate=new DateTime(2024, 1, 5) },
new PharmacyReportInfo() { AmountDue=20000, AmountPaid = 19800, CustomerId="CUST/14/8900", CustomerName = "Tessy Okotie", Id=5, TransactonDate=new DateTime(2024, 2, 10) },
new PharmacyReportInfo() { AmountDue=1250, AmountPaid = 1250, CustomerId="CUST/92/2011", CustomerName = "Adewale Suleiman", Id=6, TransactonDate=new DateTime(2024, 2, 3) },
new PharmacyReportInfo() { AmountDue=81320, AmountPaid = 81320, CustomerId="CUST/34/6500", CustomerName = "John Barnes", Id=7, TransactonDate=new DateTime(2023, 7, 7) },
new PharmacyReportInfo() { AmountDue=48750, AmountPaid = 48750, CustomerId="CUST/11/4390", CustomerName = "Jabilla Muhammed", Id=8, TransactonDate=new DateTime(2024, 4, 2) },
new PharmacyReportInfo() { AmountDue=32900, AmountPaid = 30000, CustomerId="CUST/34/1872", CustomerName = "Kolawole Emmanuel", Id=9, TransactonDate=new DateTime(2024, 2, 18) },
new PharmacyReportInfo() { AmountDue=43500, AmountPaid = 23850, CustomerId="CUST/14/3390", CustomerName = "James Toney", Id=10, TransactonDate=new DateTime(2024, 3, 3) },
new PharmacyReportInfo() { AmountDue=77000, AmountPaid = 77000, CustomerId="CUST/10/1019", CustomerName = "Gordon Davis", Id=11, TransactonDate=new DateTime(2024, 5, 5) },
new PharmacyReportInfo() { AmountDue=2300, AmountPaid = 2300, CustomerId="CUST/28/2011", CustomerName = "Esteban Juan Rodriguez", Id=12, TransactonDate=new DateTime(2024, 8, 18) },
new PharmacyReportInfo() { AmountDue=6750, AmountPaid = 4400, CustomerId="CUST/27/2899", CustomerName = "Jacques Du Plessis", Id=13, TransactonDate=new DateTime(2024, 1, 19) },
new PharmacyReportInfo() { AmountDue=3200, AmountPaid = 950, CustomerId="CUST/29/9902", CustomerName = "Donald Mafa", Id=14, TransactonDate=new DateTime(2024, 8, 11) },
new PharmacyReportInfo() { AmountDue=5000, AmountPaid = 5000, CustomerId="CUST/11/3381", CustomerName = "Angelo Gabriel", Id=15, TransactonDate=new DateTime(2024, 7, 23 )},
new PharmacyReportInfo() { AmountDue=7800, AmountPaid = 7800, CustomerId="CUST/15/2401", CustomerName = "Naseem Muhammad", Id=16, TransactonDate=new DateTime(2024, 9, 30) },
new PharmacyReportInfo() { AmountDue=65900, AmountPaid = 65700, CustomerId="CUST/15/6654", CustomerName = "Yu Xiao Ping", Id=17, TransactonDate=new DateTime(2024, 4, 4) },
new PharmacyReportInfo() { AmountDue=25600, AmountPaid = 24000, CustomerId="CUST/13/8899", CustomerName = "Jane Ashcroft-Peters", Id=18, TransactonDate=new DateTime(2024, 8, 9) },
new PharmacyReportInfo() { AmountDue=4000, AmountPaid = 4000, CustomerId="CUST/55/9109", CustomerName = "Ashley Nelly", Id=19, TransactonDate=new DateTime(2024, 7, 21) },
new PharmacyReportInfo() { AmountDue=21000, AmountPaid = 5000, CustomerId="CUST/21/4300", CustomerName = "Simone Clemente", Id=20, TransactonDate=new DateTime(2024, 3, 28) },
};

var summary = new PharmacyReportInfo()
{
AmountDue = pharmacyReport.Sum(s => s.AmountDue),
AmountPaid = pharmacyReport.Sum(s => s.AmountPaid),
OutstandingAmount = pharmacyReport.Sum(s => s.AmountDue) - pharmacyReport.Sum(s => s.AmountPaid),
};

pharmacyReport.Add(summary);

return pharmacyReport;
}
  • Add a static void method to the QuestPdfService.cs class and name it ComposeFooterContent with the following content:
static void ComposeFooterContent(IContainer container)
{
container.Column(footer =>
{
footer.Item().Row(row =>
{
row.AutoItem().Text($"{DateTime.Today.ToString("ddd MMM dd, yyyy")}").FontSize(8);
row.RelativeItem().AlignCenter().Text($"ANY ALTERATION ON THIS INCOME REPORT SHEET RENDERS IT INVALID").FontSize(8);
row.AutoItem().AlignRight().Text(text =>
{
text.Span("Page ").FontSize(8);
text.CurrentPageNumber().FontSize(8);
text.Span(" of ").FontSize(8);
text.TotalPages().FontSize(8);
});
});
});
}
  • Add two more static methods to the QuestPdfService.cs class and name them DefaultCellStyle & EvaluateOutstandingBalanceBackgroundColor. They are responsible for the look and feel of the normal table cells and the Outstanding Amount cells.
static IContainer DefaultCellStyle(IContainer container, string backgroundColor = "")
{
return container
.Border(1)
.BorderColor(Colors.Grey.Lighten1)
.Background(!string.IsNullOrEmpty(backgroundColor) ? backgroundColor : Colors.White)
.PaddingVertical(7)
.PaddingHorizontal(3);
}

static IContainer EvaluateOutstandingBalanceBackgroundColor(IContainer container, double amountDue, double amountPaid)
{
if (amountDue - amountPaid > 0)
{
// this applies a grey background to the outstanding balance cell
return container
.Border(1)
.BorderColor(Colors.Grey.Lighten1)
.Background(!string.IsNullOrEmpty(backgroundColor) ? backgroundColor : Colors.White)
.PaddingVertical(7)
.PaddingHorizontal(3);
.PaddingVertical(7)
.PaddingHorizontal(3)
.Background(Colors.Grey.Lighten2);
}
}
  • Add one last static method and name it ComposePharmacyReportContent and add the following code:
static void ComposePharmacyReportContent(IContainer container)
{

var transactions = GetPharmacyReport();
int serialNumber = 0;


container.Column(mainContentColumn =>
{
mainContentColumn.Item().Row(row =>
{
row.AutoItem().Column(column =>
{
column.Item().Width(1, Unit.Inch).Image(_medicalLogo1);
});

row.RelativeItem().AlignCenter().Column(column =>
{
column
.Item().Text("MEDIPLUS DIAGNOSTIC CENTRE")
.FontSize(20).SemiBold();

column
.Item().AlignCenter().PaddingBottom(0.5f, Unit.Centimetre).Text("Lagos, Nigeria.")
.FontSize(13).SemiBold();

column
.Item().AlignCenter().Text("PHARMACY INCOME REPORT").Underline()
.FontSize(16);
});

row.AutoItem().AlignRight().Column(column =>
{
column.Item().Width(1, Unit.Inch).Image(_medicalLogo2);
});
});


mainContentColumn.Item().PaddingTop(0.8f, Unit.Centimetre).Row(row =>
{
row.RelativeItem().Shrink().Border(1).Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(40);
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.ConstantColumn(90);
columns.ConstantColumn(90);
columns.ConstantColumn(100);
columns.ConstantColumn(200);
});

// please be sure to call the 'header' handler!
table.Header(header =>
{
header.Cell().Element(CellStyle).AlignCenter().Text("S/N").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).Text("Customer ID").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).Text("Customer Name").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).Text("Transaction Date").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).AlignRight().Text("Amount Due (₦)").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).AlignRight().Text("Amount Paid (₦)").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).AlignRight().Text("Outstanding Bal.(₦)").FontSize(9).SemiBold();
header.Cell().Element(CellStyle).Text("Remarks").FontSize(9).SemiBold();

// you can extend existing styles by creating additional methods
IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.Grey.Lighten3);
});

var summaryEntry = transactions.LastOrDefault();
foreach (var transaction in transactions)
{
serialNumber += 1;

// conditionally renders the row (table content summary)
if (transaction.Equals(summaryEntry))
{
table.Cell().Text("");
table.Cell().Text("");
table.Cell().Text("");
table.Cell().Text("");

table.Cell().Element(CellStyle).AlignRight().Text($"{transaction.AmountDue:N2}").FontSize(10).SemiBold();
table.Cell().Element(CellStyle).AlignRight().Text($"{transaction.AmountPaid:N2}").FontSize(10).SemiBold();
table.Cell().Element(CellStyle).AlignRight()
.Text($"{summaryEntry.OutstandingAmount:N2}").FontSize(10).SemiBold();

continue;
}

table.Cell().Element(CellStyle).AlignCenter().Text($"{serialNumber}").FontSize(9);

table.Cell().Element(CellStyle).Text($"{transaction.CustomerId}").FontSize(9);
table.Cell().Element(CellStyle).Text($"{transaction.CustomerName}").FontSize(9);
table.Cell().Element(CellStyle).Text($"{transaction.TransactonDate.ToString("yyyy-MMM-dd")}").FontSize(9);
table.Cell().Element(CellStyle).AlignRight().Text($"{transaction.AmountDue:N2}").FontSize(9);
table.Cell().Element(CellStyle).AlignRight().Text($"{transaction.AmountPaid:N2}").FontSize(9);
table.Cell().Element(OustandingBalanceCellStyle).AlignRight().Text($"{transaction.AmountDue - transaction.AmountPaid:N2}").FontSize(9);
table.Cell().Element(CellStyle).Text($"").FontSize(9);

IContainer CellStyle(IContainer container) => DefaultCellStyle(container).ShowOnce();
IContainer OustandingBalanceCellStyle(IContainer container) => EvaluateOutstandingBalanceBackgroundColor(container, transaction.AmountDue, transaction.AmountPaid).ShowOnce();
}

});
});

mainContentColumn.Item().PaddingTop((float)4.0, Unit.Centimetre).Row(row => {
row.RelativeItem().Column(column =>
{
column.Item().Text("_______________________________");
column.Item().PaddingLeft(1.2f, Unit.Centimetre).PaddingTop(0.4f, Unit.Centimetre).Text("Doctor signature & date");
});

row.RelativeItem().AlignRight().Column(column =>
{
column.Item().Text("_______________________________");
column.Item().PaddingLeft(1.2f, Unit.Centimetre).PaddingTop(0.4f, Unit.Centimetre).Text("Accountant signature & date");
});
});
});
}

And that should be it 😁, call the generate-pharmacy-report endpoint, and you should see something like this.

The source code can be found here

…and that brings us to the end of the second part of this QuestPDF series, drop a comment if you would like to see more tutorials or if you have a use case you would like me to write on.

Cheers!!! ✌️✌️✌️

--

--