PDFPipe

Migrating from iText / An imperative drawing API

Migrating off iText, and the licensing question underneath it

A mature PDF library with a document model and a low-level API, where the technical migration is ordinary and the reason for it is frequently the licence rather than the code.

What ports and what does not

The data layer. iText's higher-level document model, with Paragraph, Table and Cell objects, is closer to a document than a canvas, so the structure reads across to markup reasonably well. The low-level content stream code does not. Two things are specific to this migration. iText is dual licensed, with an open source licence that has obligations many organisations cannot accept and a commercial licence that costs money, and that is often what prompts the move, which means the deadline may be commercial rather than technical. And iText codebases frequently also do PDF manipulation, form filling, merging and stamping, which is a different capability from rendering: merge exists here at /v1/pdf/merge, and the rest does not.

What the call becomes

Document, PdfWriter and the add calls become a POST to /v1/pdf with html. Page size and margins, set on the Document, become options.format and options.margin. Page event handlers, which iText used for headers, footers and watermarks, become options.header_html and options.footer_html or CSS. Any form filling, stamping or field manipulation has no counterpart and needs a separate tool in the pipeline, which is the part of the migration to scope before starting.

Side by side

The old shape and the new one, with the parts that have no counterpart called out in comments rather than quietly omitted.

java
// Before
Document document = new Document(PageSize.A4, 40, 40, 50, 50);
PdfWriter writer = PdfWriter.getInstance(document, out);
writer.setPageEvent(new HeaderFooter());   // draws furniture per page
document.open();
document.add(new Paragraph("Invoice", headingFont));
PdfPTable table = new PdfPTable(new float[] { 220f, 80f, 80f });
table.setHeaderRows(1);
document.add(table);
document.close();

// After
String body = mapper.writeValueAsString(Map.of(
    "html", html,
    "options", Map.of(
        "format", "A4",
        "margin", Map.of("top", "50pt", "bottom", "50pt",
                         "left", "40pt", "right", "40pt"),
        "header_html", "<div style=\"font:bold 12pt sans-serif\">Invoice</div>")
));

/* Scope this before starting, because it is not a rendering feature:
     merging documents        -> POST /v1/pdf/merge
     filling AcroForm fields  -> no counterpart, needs another tool
     stamping an existing PDF -> no counterpart
     digital signatures       -> no counterpart
   An iText codebase that only renders is a straightforward migration.
   One that also manipulates existing PDFs is two projects. */

The CSS delta, in both directions

What the old engine accepted that a current one does not, and what it refused that a current one wants. The second direction is the one people forget, and it is where the value of the migration is.

  • Nothing to migrate. The Document constructor's margins are in points and transcribe directly, and PdfPTable's float column widths become CSS widths.
  • setHeaderRows becomes a thead, which repeats on every page for the same reason.
  • Page event handlers become the header and footer options, which is a narrowing: an event handler could draw anywhere on the page and the header and footer are two regions.
  • Everything CSS offers is new, since the old document had no stylesheet at all.

What changes without anything erroring

The dangerous list. Each of these produces a different document and no diagnostic, so none of them are caught by a test that only checks the render succeeded.

  • Everything visual, because it is a rewrite.
  • Page count.
  • Anything a page event drew away from the top and bottom edges.
  • Any PDF manipulation the codebase did alongside rendering, which stops happening rather than failing.

What to diff before cutting over

Before diffing anything, inventory what the codebase does with PDFs besides creating them. Search for form field access, stamping and signing. That inventory decides whether this is one project or two, and it is much better known at the start.

The one thing that always breaks

The page event handler. It is registered on the writer, it runs on every page, and it is the mechanism iText codebases use for letterheads, footers, watermarks and page numbers all at once. Removing the writer removes all of it together, and because it is registered rather than called it does not appear anywhere in the document-building code somebody reads to plan the work.

Frequently asked

Do I have to rewrite my templates to leave iText?

Effectively yes, and it is better to plan for that than to discover it. The old tool did not have HTML templates to port, so the document has to be expressed as markup for the first time. The value in the old code is the data layer underneath the drawing, and that part survives untouched.

Can I run both for a while?

Yes, and it is the safest way to do it. Put both behind one internal function that takes your data and returns bytes, switch on an environment variable, and run the new path on real traffic while the old one still serves. You get a diff on real documents rather than on fixtures, and you keep a way back that does not involve a deploy.

What about the documents already generated?

Nothing here changes them. They are files that already exist. What is worth deciding before the cutover is whether a regenerated document has to match the original byte for byte or merely say the same thing, because for anything with a legal or audit character the answer is usually to keep the original file rather than to be able to reproduce it.

Other migrations

The nearest neighbours first, then others that started from the same kind of tool, because the model matters more than the language.

Render one of your existing documents through the playground before changing any code. That comparison is the whole of the risk assessment for this migration.