Kinda Like Gmail
Posted 2008-08.
Found out one day while browsing the mailing list. If you want to see for yourself:
- Login to Gmail
- Click on the Calendar link, Print link, then the Save As.. button in the popup window.
- Open in Acrobat Reader, right click, select Document Properties...
So I figured why not give it a shot. I had been putting off learning how to use the PdfPCellEvent Interface for way too long. Found some good examples to get started, including adding graphics, annotations, and forms to a PdfPCell.
using System;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;
public class calendar {
// ----------------------------------------
static void Main(string[] args) {
calendar c = new calendar(2008, 8);
c.create();
}
// ----------------------------------------
const int UPPER_RIGHT_X = 792;
const int UPPER_RIGHT_Y = 612;
const int DAY_HIGHLIGHT_WIDTH = 15;
const int DAY_HIGHLIGHT_CORNER = 4;
private DateTime _dt;
private int _rows, _first_offset_of_month, _days_in_month;
private static float _cell_leading;
private PdfPTable _ppt;
private static Font _font_day, _font_event_even, _font_event_odd;
// ----------------------------------------
public calendar(int year, int month) {
_init_fonts();
_cell_leading = _font_day.Size - 1;
_init_calendar_dates(year, month);
_init_table();
}
// ----------------------------------------
private void _init_fonts() {
_font_day = new Font(Font.FontFamily.HELVETICA, 8);
_font_event_even = new Font(_font_day);
_font_event_odd = new Font(_font_day);
_font_day.SetStyle(Font.BOLD);
_font_event_odd.SetColor(0, 0, 255);
}
// ----------------------------------------
// calculate where all the days go
private void _init_calendar_dates(int year, int month) {
_dt = new DateTime(year, month, 1);
_first_offset_of_month = (int) _dt.DayOfWeek;
_days_in_month = DateTime.DaysInMonth(_dt.Year, _dt.Month);
int row_days = _first_offset_of_month + _days_in_month;
_rows = row_days > 28 && row_days <= 35
? 5 : row_days > 35
? 6 : 4;
}
// ----------------------------------------
// initialize calendar headings
private void _init_table() {
_ppt = new PdfPTable(7);
PdfPCell table_header = new PdfPCell();
table_header.GrayFill = 0.8F;
table_header.HorizontalAlignment = Element.ALIGN_CENTER;
// row1 => month, year
table_header.Phrase = new Phrase(_dt.ToString("y"), _font_day);
table_header.Colspan = 7;
_ppt.AddCell(table_header);
// row2 => days of week
string[] days = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
table_header.Colspan = 1;
foreach (string day in days) {
table_header.Phrase = new Phrase(day, _font_day);
_ppt.AddCell(table_header);
}
_ppt.WidthPercentage = 100;
}
// ----------------------------------------
// write the table
public void create() {
Document document = new Document();
document.SetPageSize(new Rectangle(UPPER_RIGHT_X, UPPER_RIGHT_Y));
try {
PdfWriter writer = PdfWriter.GetInstance(
document,
new FileStream("calendar.pdf", FileMode.Create)
);
document.Open();
Document.Compress = false; // debugging
// calculate fixed, equal height for all cells (rows)
float height = (UPPER_RIGHT_Y
- document.TopMargin - document.BottomMargin - 25)
/ _rows
;
PdfPCell day = new PdfPCell();
day.PaddingTop = 0;
_add_event ae = new _add_event();
int count = 0;
int day_counter = 0;
int last_offset_of_month = _first_offset_of_month + _days_in_month;
for (int i = 0; i < _rows; ++i) {
// set fixed row height
day.FixedHeight = height;
for (int j = 0; j < 7; ++j) {
string daynum = count >= _first_offset_of_month
&& count < last_offset_of_month
? (++day_counter).ToString() : "";
// we're re-using the CellEvent object, so reset it when not needed!
day.CellEvent = daynum != "" && day_counter % 5 == 0
? ae : null;
day.Phrase = new Phrase(daynum, _font_day);
_ppt.AddCell(day);
++count;
}
}
document.Add(_ppt);
}
catch {
} finally {
if (document != null && document.IsOpen()) document.Close();
}
}
// ----------------------------------------
// custom functionality when writing each day's event to each cell
private class _add_event : IPdfPCellEvent {
public void CellLayout(
PdfPCell cell, Rectangle position, PdfContentByte[] canvases
)
{
// rounded rectangle, highlighted days with event(s)
PdfContentByte cbb = canvases[PdfPTable.BACKGROUNDCANVAS];
cbb.SetColorStroke(new GrayColor(0.4f));
cbb.SetColorFill(Color.YELLOW);
cbb.RoundRectangle(
position.Left, // lower-left x-coordinate
position.Top - // lower-left y-coordinate
_cell_leading - 3,
DAY_HIGHLIGHT_WIDTH, // highlight rectangle width
_cell_leading + 3, // highlight rectangle height
DAY_HIGHLIGHT_CORNER // corner "roundness"
);
cbb.FillStroke();
PdfContentByte cb = canvases[PdfPTable.TEXTCANVAS];
ColumnText ct = new ColumnText(cb);
// set exact coordinates for ColumnText
ct.SetSimpleColumn(
position.Left + 2, // lower-left x; add some padding
position.Bottom, // lower-left y
position.Right, // upper-right x
position.Top // upper-right x; adjust for existing content
- _cell_leading - 3
);
string[] lines = {
"event one", "event two",
"event three continuing over more than one line.", "event four"
};
// visually separate events by font color
for (int i = 0; i < lines.Length; ++i) {
ct.AddElement(new Phrase(
_cell_leading,
lines[i],
i % 2 == 0 ? _font_event_even: _font_event_odd
));
}
ct.Go();
}
}
}
Notes
Just like most of the example code on this site, you should get a lot from reading the embedded comments. And obviously you don't need iText to do the date calculations; all we're doing there is:
- Calculating the number of rows needed for the calendar month table, which in turn is used to maximize usable page space.
- Setting the variables required to put each day in the correct table cell.
In the create() method, you'll notice I commented out the call to Document.Compress. That helped me figure out a stupid error I made on my first try; since the CellEvent object is being re-used, it must be reset to null between iterations, otherwise it will execute unnecessarily. In this case the calendar event strings were being written to the cell multiple times, making it seem like the text was extremely bolded. Document.Compress lets you see the raw content in a text editor - it was easy to figure out after I saw the same text being written over and over in the same location. Note this is not recommended unless debugging, try it and see how big your PDF file gets.
The whole purpose of the PdfPCellEvent interface is to allow you to set custom properties for individual cells. It only requires that you implement a single method, CellLayout. This custom functionality is made available by objects iText categorizes as Direct Content. Simply put, you get direct access to the PDF file's text and graphics layers to draw lines and shapes, add graphical images, and to add text at exact locations.
In the example's CellLayout() method, creating the rounded, highlighted rectangle is something you would typically do. You typically would NOT create the ColumnText object and add text, since you could do the same thing when adding PdfPCells to the table. That was just my personal experiment with ColumnText.
PDF result, calendar.pdf