My wife and I have been going to the filmfestival in Ghent for quite some years now. And in the first years, we always used lists similar to the ones described on the previous page. These lists were always very useful to select the movies we wanted to watch, but now and then, we experienced a problem with these lists. Sometimes two consecutive movies on our personal list overlapped: the second movie started before the first movie ended. We could avoid this by calculating the time each movie ends, and add this time to our list, but there’s even a more elegant way to do it: why not make a calendar.pdf? Here’s how it’s done: MakeCalender.
The constructor and the main method of class MakeCalendar is the spitting image of the main method of MakeList:
public MakeList(FilmfestivalQuery q) { this.query = q; } public static void main(String[] args) { FilmfestivalDatabase database = new FilmfestivalDatabase(); try { database.open(); FilmfestivalQuery q = new FilmfestivalQuery( FilmfestivalDatabase.getDays(), false, // only press false, // no press false, // comp false, // eXploreZone false // alternative screenings ); FileOutputStream out = new FileOutputStream(PATH + "calendar.pdf"); MakePdf pdf = new MakeCalendar(q); pdf.writeTo(out); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (DocumentException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } database.close(); }
But we keep track of some extra float values that will correspond with widths, heights, and coordinates on our page:
private float column_width; private float row_height; private float left; private float top; private float minute;
These values will be calculated for every page, and we’ll use them to draw lines and add content at absolute positions. Let’s have a look at the way the writeTo method is implemented.
The only difference with the writeTo method in the list example, is that we aren’t using any basic building blocks in step 4 (yet). Instead we calculate some values. The query instance knows for each day on how many locations a visitor can go to a screening. This value is used to calculate the height of each row on our calendar sheet.
Note that we now create a document in landscape (PageSize.A4.rotate()). We also define an ArtBox for PdfWriter; that’s a specific page boundary. It is smaller than the MediaBox (defining the actual page size in this example). Other possible boundaries are the CropBox, BleedBox, and TrimBox. We are going to use the ArtBox as the Rectangle in which the actual data is rendered.
public void writeTo(OutputStream out) throws DocumentException, SQLException { // step 1 Rectangle rect = PageSize.A4.rotate(); Document document = new Document(rect); // step 2 PdfWriter writer = PdfWriter.getInstance(document, out); Rectangle art = new Rectangle(rect.getLeft(36), rect.getBottom(36), rect.getRight(36), rect.getTop(36)); writer.setBoxSize("art", art); // step 3 document.open(); // step 4 for (int i = 0; i < query.getNumberOfDays(); i++) { // on how many places are there screenings on a given day? List places = query.getPlaces(i); // initialize the dimensions for this day top = art.getTop(); row_height = art.getHeight() / places.size(); if (row_height > 70) { row_height = 70; } column_width = art.width() / COLUMNS; left = art.getLeft() + column_width; minute = (2f * column_width) / 60f; // content drawGrid(writer, places, query.getDay(i)); List movies = query.getMovies(i); for (Iterator it = movies.iterator(); it.hasNext(); ) { drawMovie(writer, (FilmfestivalMovie)it.next(), places); } document.newPage(); } // step 5 document.close(); }
Again the 5 steps in the creation process of a PDF document with iText are present, but we don’t see anything added to the document in step 4. That’s done in the helper methods.
With the drawGrid() method, we draw all the lines of our calendar grid, and add the names of the locations and the text indicating the time. The way we are adding this content is completely different from what we did before. We are writing to the direct content of a page in the PDF document.
The PdfContentByte object that is returned by the method PdfWriter.getDirectContent() is like a canvas. You can draw lines on it with:
PdfContentByte.moveTo(),PdfContentByte.lineTo(), andPdfContentByte.stroke().This code will write operators and operands directly into the content stream of a PDF file similar to this snippet of PDF syntax:
806 559 m 806 489 l S
You can draw text with:
PdfContentByte.beginText(),PdfContentByte.setFontAndSize(),PdfContentByte.showTextAligned(), andPdfContentByte.endText().This sequence could correspond with PDF syntax similar to this snippet:
BT /F1 12 Tf 0 1 -1 0 31 497.63 Tm (2006-10-06)Tj 1 0 0 1 0 0 Tm ET
In other words: the many methods of class PdfContentByte give you a means to write PDF syntax directly to the content stream of a page. That’s exactly what we’re doing in the method drawGrid():
private void drawGrid(PdfWriter writer, List list, String day) throws SQLException { // CANVAS PdfContentByte directcontent = writer.getDirectContent(); Rectangle art = writer.getBoxSize("art"); // LINES directcontent.setLineWidth(1); float bottom = art.getTop(); // rows int rows = list.size(); for (int i = 0; i <= rows; i++) { directcontent.moveTo(art.getLeft(), art.getTop() - (i * row_height)); directcontent.lineTo(art.getRight(), art.getTop() - (i * row_height)); bottom = art.getTop() - (i * row_height); } // Rectangle directcontent.moveTo(art.getLeft(), art.getTop()); directcontent.lineTo(art.getLeft(), bottom); directcontent.moveTo(left, art.getTop()); directcontent.lineTo(left, bottom); directcontent.moveTo(art.getLeft() + (COLUMNS * column_width), art.getTop()); directcontent.lineTo(art.getLeft() + (COLUMNS * column_width), bottom); directcontent.stroke(); // columns directcontent.saveState(); directcontent.setLineWidth(0.3f); directcontent.setColorStroke(SILVER); directcontent.setLineDash(3, 1); for (int i = 2; i < COLUMNS; i++) { directcontent.moveTo(art.getLeft() + (i * column_width), art.getTop()); directcontent.lineTo(art.getLeft() + (i * column_width), bottom); } directcontent.stroke(); directcontent.restoreState(); // TEXT // date directcontent.beginText(); directcontent.setFontAndSize(FONT, 12); directcontent.showTextAligned(Element.ALIGN_RIGHT, day, art.getLeft() - 5, art.getTop(), 90); directcontent.endText(); // time for (int i = 1; i < COLUMNS; i++) { directcontent.beginText(); directcontent.setFontAndSize(FONT, 8); directcontent.showTextAligned(Element.ALIGN_LEFT, TIME[i - 1], art.getLeft() + (i * column_width) + 5, top + 5, 90); directcontent.endText(); } // places for (int i = 0; i < rows; i++) { directcontent.beginText(); directcontent.setFontAndSize(FONT, 12); directcontent.showTextAligned(Element.ALIGN_CENTER, (String)list.get(i), art.getLeft() + 16, art.getTop() - ((i + 0.5f) * row_height), 90); directcontent.endText(); } }
Chapters 10 and 11 of the book iText in Action explain in great detail all the methods that were used in the previous method. But we have to move on, we have one more method to discuss:
We have added content not knowing anything about PDF when we created our list using basic building blocks; we have added (complex?) PDF syntax to the content of a page using PdfContentByte. Now we are going to use a very powerful building block that more or less combines both approaches: ColumnText.
We’ll make limited use of this object to draw a Phrase at an absolute position (inside a well defined rectangle). ColumnText is the object you’ll use to draw almost any of the basic building blocks at an absolute position, see chapter 7 of iText in Action to learn more about it.
The drawMovie() method also shows the difference between PdfWriter.getDirectContent() and PdfWriter.getDirectContentUnder(). First we define a rectangle that will be put somewhere on the grid:
These coordinates are calculated based on the location (corresponding with a row in our grid), and the duration (corresponding with the with of the rectangle) of the screening. We draw a black rectangle (a border with a thickness of 1 pt) on the foreground (PdfWriter.getDirectContent()) and write the movie title as a Phrase. (If the title doesn’t fit the rectangle, it will be truncated.) These lines and glyphs are written after the grid was painted, so it will cover the gridlines.
Now we want to color the rectangle, but we don’t want that paint to cover our gridlines. That’s why we are going to use PdfWriter.getDirectContentUnder(). It will give us a layer that goes underneath the content we have already added.
private void drawMovie(PdfWriter writer, FilmfestivalMovie movie, List places) throws DocumentException { // defining the rectangle FilmfestivalScreening screening = movie.getScreening(); int place = places.indexOf(screening.getPlace()); float llx, urx, lly, ury; llx = left + minute * screening.getMinutesAfter930(); urx = llx + minute * (movie.getExtra() + movie.getDuration()); ury = top - place * row_height; lly = top - ((place + 1) * row_height); // FOREGROUND PdfContentByte foreground = writer.getDirectContent(); // rectangle foreground.setColorStroke(BLACK); foreground.setLineWidth(1); foreground.moveTo(urx, lly); foreground.lineTo(llx, lly); foreground.lineTo(llx, ury); foreground.lineTo(urx, ury); foreground.closePathStroke(); // title ColumnText ct = new ColumnText(foreground); ct.setSimpleColumn(new Phrase(movie.getTitle(), SMALLEST), llx, lly, urx, ury, 14, Element.ALIGN_CENTER); ct.go(); // BACKGROUND PdfContentByte background = writer.getDirectContentUnder(); // draw background background.setColorFill(COLOR[movie.getCategory_id()]); background.moveTo(urx, lly); background.lineTo(llx, lly); background.lineTo(llx, ury); background.lineTo(urx, ury); background.fill(); // draw a white P for press screenings if (screening.isPress()) { background.saveState(); background.beginText(); background.setFontAndSize(FONT, 24); background.setColorFill(WHITE); background.showTextAligned(Element.ALIGN_CENTER, "P", (llx + urx) / 2f, (lly + ury) / 2f - 12, 0); background.endText(); background.restoreState(); } // draw shortfilm background.setColorStroke(WHITE); if (movie.getExtra() > 0) { background.moveTo(llx + (minute * movie.getExtra()), lly); background.lineTo(llx + (minute * movie.getExtra()), ury); background.stroke(); } }
If you know something about PDF syntax, you’ll see that a rectangle is painted in a color corresponding with on of the filmfestival categories (’official selection’, ‘history of film’, ‘world cinema’,...). If it’s a press screening, a white letter P is written inside this rectangle. If there’s a shortfilm, a white line indicates when the shortfilm ends, and when the full feature starts.
Although the resulting PDF also has a more or less tabular form, you used a completely different approach to draw the content on the page. For more info: see part 3 of the book iText in Action. Note that this example was integrated into a Servlet deployed on itext.ugent.be.
In the next section, we’ll discuss yet another approach to add content to a PDF document, more specifically: to an existing PDF document that has an AcroForm.