Filling out a Learning Agreement Form

iText’s was used for the first time in a production environment at Ghent University, more specifically in the applications for the Student Administration. One of these applications involves the implementation of ECTS: the European Credit Transfer System. In the article ICT for ECTS, I explain how iText is used in this context. One of the forms that is used for ECTS, is the ECTS Learning Agreement.

The Learning Agreement Form

The learning agreement contains the list of courses to be taken with the ECTS credits which will be awarded for each course. In chapters 17 and 18 of iText in Action (and in the article), I created a form that can be used to retrieve data from the user. It has all the necessary fields and a submit button.
In this example, we are going to use a blank form, without a submit button. Using iText (LearningAgreementForm.java), I have made another PDF form that resembles the official ECTS form: learning_agreement.pdf. As you can see, there’s no submit button. You will use the form as a template, and fill it with data stored in a database.
Note that we have added some structure in the fieldnames for the courses. We have a series of fields without any hierarchy:

  • academic_year
  • field_of_study
  • student_name
  • sending_institution
  • sending_country
  • receiving_institution
  • receiving_country

But we also have fields named course_0, course_1, ..., course_15. Each of these course_X fields has three kids:

  • code
  • name
  • credits

The names code, name, and credits are partial names. The fully qualified names are for instance:

  • course_0.code
  • course_0.name
  • course_0.credits

Those fully qualified names are the ones you’re going to use in your setField method.

The Learning Agreement Database

As with the filmfestival example, all the database queries and objects are kept in a separate package com.lowagie.ects.database:

The fields in the LearningAgreementData object correspond with the fields in the form. As a matter of fact, I made an abstract object that knows all the names of the fields: LearningAgreementFields.java. The class we used to create the form (LearningArgeementForm) extends this class, as well as a helper class that knows how to fill the form with LearningAgreementData: LearningAgreementFiller.
Let’s have a look at this LearningAgreementFiller.

Filling the form

Our helper class has one important static method:

public static void fill(AcroFields form, LearningAgreementData data)
  throws IOException, DocumentException {
  form.setField(ACADEMIC_YEAR, String.valueOf(data.getAcademic_year()));
  form.setField(FIELD_OF_STUDY, data.getField_of_study());
  form.setField(STUDENT_NAME, data.getStudent_name());
  form.setField(SENDING_INSTITUTION, data.getSending_institution());
  form.setField(SENDING_COUNTRY, data.getSending_country());
  form.setField(RECEIVING_INSTITUTION, data.getReceiving_institution());
  form.setField(RECEIVING_COUNTRY, data.getReceiving_country());
  int i = 0;
  List courses = data.getCourses();
    if (courses == null) return;
    LearningAgreementCourse course;
    for (Iterator it = courses.iterator(); it.hasNext(); i++) {
      course = (LearningAgreementCourse)it.next();
      form.setField(getCourseCodeFieldname(i), course.getCode());
      form.setField(getCourseNameFieldname(i), course.getName());
      form.setField(getCourseCreditsFieldname(i), String.valueOf(course.getCredits()));
    }
}

I also added a main method to the LearningAgreementFiller object for testing purposes:

public static void main(String[] args) {
  LearningAgreementData data = new LearningAgreementData();
  data.setAcademic_year(2007);
  data.setField_of_study("ICT");
  data.setStudent_name("Bruno Lowagie");
  data.setSending_institution("Ghent University");
  data.setSending_country("Belgium");
  data.setReceiving_institution("Technological University of Foobar");
  data.setReceiving_country("Foobar");
 
  ArrayList courses = new ArrayList();
  LearningAgreementCourse course0 = new LearningAgreementCourse();
  course0.setCode("8001");
  course0.setName("POJO: Plain Old Java Objects");
  course0.setCredits(6);
  courses.add(course0);
  LearningAgreementCourse course1 = new LearningAgreementCourse();
  course1.setCode("8010");
  course1.setName("Eclipse");
  course1.setCredits(3);
  courses.add(course1);
  LearningAgreementCourse course2 = new LearningAgreementCourse();
  course2.setCode("SCRP");
  course2.setName("iText in Action");
  course2.setCredits(15);
  courses.add(course2);
 
  data.setCourses(courses);
 
  try {
    PdfReader reader = new PdfReader(LearningAgreementForm.FILENAME);
    PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(FILENAME));
    fill(stamper.getAcroFields(), data);
    stamper.setFormFlattening(true);
    stamper.close();
  } catch (IOException e) {
    e.printStackTrace();
  } catch (DocumentException e) {
    e.printStackTrace();
  }
}

In this code snippet only the last part is really important (the first part is just filling the LearningAgreementData object). As you can see, our fill is very convenient. The resulting PDF file looks like this: learning_agreement_test.pdf

Batch process

Of course, you are not going to fill the data object using hard coded data, instead you are going to fill the form with data coming from a database. This is typically done in a batch process. First let’s see how to create a separate PDF for each agreement in our database:

Filling forms in batch: separate PDFs

Basically class BatchFillSeparateFiles brings nothing new. We just add some code to query the database. We’ve seen similar code before:

public static void main(String[] args) {
  LearningAgreementDatabase database = new LearningAgreementDatabase();
  try {
    database.open();
    LearningAgreementQuery query = new LearningAgreementQuery(database);
    List agreements = query.getAgreements();
    LearningAgreementData data;
    int i = 1;
    for (Iterator it = agreements.iterator(); it.hasNext(); i++) {
      data = (LearningAgreementData)it.next();
      fillAgreementForm(new FileOutputStream(PATH + "agreement_" + i + ".pdf"), data);
    }
  } catch (SQLException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  } catch (DocumentException e) {
    e.printStackTrace();
  }
  database.close();
}

The iText business logic can be found in method fillAgreementForm():

public static void fillAgreementForm(OutputStream stream, LearningAgreementData data)
  throws IOException, DocumentException {
  PdfReader reader = new PdfReader(LearningAgreementForm.FILENAME);
  PdfStamper stamper = new PdfStamper(reader, stream);
  LearningAgreementFiller.fill(stamper.getAcroFields(), data);
  stamper.setFormFlattening(true);
  stamper.close();
}

There are 10 agreement records in the database, so this code sample produces 10 PDF files: agreement_1.pdf, agreement_2.pdf, ..., agreement_10.pdf.
At Ghent University, we use this approach if we want to generate PDF documents that have to be sent to students by e-mail. But what if we want a document we can send to a printing office that sends every page to a student by snail mail? In that case, it’s not a good idea to have all these separate pages: we want all the pages in one PDF document.

Filling forms in batch: all in one PDF

There are different ways to achieve this. Soon I’ll add an example to this Wiki describing how to create a portable collection; this is one PDF file containing a collection of smaller PDF documents. In chapter 16 (16.1.5) of the book http://itext.ugent.be/itext-in-action/, I mention two other possibilities:

  • create the separate PDFs in memory; concatenate them using PdfCopy
  • retrieve the field positions, flatten the form, add the field data at absolute positions, add the flattened form in a page event

You can compare both approaches by trying the example I wrote for an article on the Belgian eID. The first solution has the disadvantage that the content stream of the template is duplicated for every page: you get very large PDF files (in filesize). I advise using the second possibility, but I am aware that it’s not the most straight forward solution.
Fortunately iText is a library that is very much alive, and since iText 2.0, there’s a third possibility that brings you the best of both worlds: using PdfSmartCopy. PdfSmartCopy extends PdfCopy, so it’s very similar, but it demands more CPU and more memory because PdfSmartCopy examines the streams that were already added to the document. Let’s do the test. BatchFillOneLargeFile extends BatchFillSeparateFiles (because we want to reuse the fillAgreementForm() method), but its main method is overriden:

public static void main(String[] args) {
  LearningAgreementDatabase database = new LearningAgreementDatabase();
  Document document = new Document();
  try {
    database.open();
    LearningAgreementQuery query = new LearningAgreementQuery(database);
    List agreements = query.getAgreements();
    LearningAgreementData data;
    ByteArrayOutputStream baos;
    PdfReader reader;
    PdfSmartCopy copy = new PdfSmartCopy(document,
      new FileOutputStream(PATH + "learningagreement_all_in_one.pdf"));
    document.open();
    for (Iterator it = agreements.iterator(); it.hasNext(); ) {
      data = (LearningAgreementData)it.next();
      baos = new ByteArrayOutputStream();
      fillAgreementForm(baos, data);
      reader = new PdfReader(baos.toByteArray());
      copy.addPage(copy.getImportedPage(reader, 1));
    }
  } catch (SQLException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  } catch (DocumentException e) {
    e.printStackTrace();
  }
  database.close();
  document.close();
}

Note that it is very important that the forms are flattened before you concatenate them. Otherwise you’d have numerous AcroFields with the same name on different pages in the same document. While this is not illegal, it’s probably not what you want, because fields with the same name need to have the same value.
The main method in class BatchFillOneLargerFile is almost identical, except that PdfSmartCopy (the new class) is replaced by PdfCopy (the class discussed in the book). When printed, the documents generated by both classes look identical, but if you look at the file sizes, there’s a huge difference: learningagreement_all_in_one.pdf (35 K) versus learningagreement_duplicate_xobjects.pdf (176K). Imagine what the difference would be if we didn’t have 10 pages, but 100, or even 1000!
Pdf(Smart)Copy is another one of iText’s PDF manipulation classes. Basically you’ll use PdfStamper if you want to manipulate one and only existing PDF document, filling fields, stamping data, changing metadata,... You’re going to use PdfCopy to concatenate different PDF documents on a page per page basis without changing anything of their content.

Conclusion

In the filmfestival example, you found out how to fill out an AcroForm programatically. In this example, you have used this knowledge to fill out forms in Batch. You filled the form multiple times using PdfStamper, and subsequently concatenated all the results using PdfSmartCopy. The result was one large file with all the flattened forms.
There are plenty of other examples and other PDF manipulation classes in chapter 2 of the book iText in Action. You’ll also need chapter 16 to learn how to make the data fit into the field rectangle. In other words, this is just an example to get you started. You may have to adapt it (or even redesign it) to meet your own specific project requirements.

 
Back to top
forms/learningagreement.txt · Last modified: 2007/03/19 04:51 by segraves