Migrating from FPDF and fpdf2 / An imperative drawing API
Migrating off FPDF, from Cell coordinates to markup
A minimal drawing library built around Cell and MultiCell calls, where the layout is arithmetic and the arithmetic is the only specification of the document that exists.
What ports and what does not
The data layer. Nothing else. FPDF documents are sequences of Cell calls with explicit widths, heights and line-break flags, and a subclassed Header and Footer that run per page. There is no styling, no template and no separation between layout and content: the code interleaves them, which is what makes these modules hard to read and easy to underestimate. The upside is that the arithmetic is unambiguous, so it makes a precise specification for the markup you write.
What the call becomes
AddPage, SetFont, Cell, MultiCell and Output collapse into a single POST to /v1/pdf with html. The constructor's orientation and format arguments become options.landscape and options.format. SetMargins becomes options.margin. SetAutoPageBreak becomes the bottom margin plus break-inside rules. AliasNbPages, which FPDF substituted at output time to give a total page count, becomes the totalPages class in the footer markup. The Header and Footer methods become options.header_html and options.footer_html.
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.
<?php
// Before
class Invoice extends FPDF {
function Header() {
$this->SetFont('Arial', 'B', 12);
$this->Cell(0, 10, 'Invoice', 0, 1, 'L');
}
function Footer() {
$this->SetY(-15);
$this->SetFont('Arial', '', 8);
$this->Cell(0, 10, 'Page ' . $this->PageNo() . '/{nb}', 0, 0, 'C');
}
}
$pdf = new Invoice('P', 'mm', 'A4');
$pdf->AliasNbPages();
$pdf->AddPage();
$pdf->SetFont('Arial', '', 10);
foreach ($rows as $r) {
$pdf->Cell(80, 6, $r['desc'], 'B', 0, 'L');
$pdf->Cell(30, 6, $r['qty'], 'B', 0, 'R');
$pdf->Cell(30, 6, $r['amt'], 'B', 1, 'R');
}
$out = $pdf->Output('S');
// After: the widths in the Cell calls are the column widths.
$html = $twig->render('invoice.html.twig', ['rows' => $rows]);
$response = $client->post('https://api.pdfpipe.xyz/v1/pdf', [
'headers' => ['Authorization' => 'Bearer ' . getenv('PDFPIPE_KEY')],
'json' => [
'html' => $html,
'options' => [
'format' => 'A4',
'header_html' => '<div style="font:bold 12pt Arial">Invoice</div>',
'footer_html' => '<div style="font-size:8pt;text-align:center">Page '
. '<span class="pageNumber"></span>/'
. '<span class="totalPages"></span></div>',
],
],
]);
/* The Cell widths 80, 30, 30 in millimetres are the table's column
widths. Read them out of the loop rather than guessing:
td.desc { width: 80mm } td.qty { width: 30mm } td.amt { width: 30mm }
The 'B' border argument is border-bottom. The 1 in the last call is
the line break, which is the end of the table row. */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.
- There is nothing to migrate and everything to write. The Cell arguments are the specification: the widths are column widths in millimetres, the border flags are borders, and the alignment characters are text-align.
- The line-break flag on each Cell marks where a row ends, which is how you recover the table structure from a flat sequence of calls.
- SetAutoPageBreak's trigger margin becomes the bottom page margin, and the manual page-break checks scattered through the loop become break-inside rules.
- Everything else in CSS is new capability that the old document could not express 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.
- Fonts, from FPDF's core font set and its own font files to @font-face.
- Any manual page-break arithmetic in the loop, which stops running and is replaced by pagination that works differently.
What to diff before cutting over
Diff the extracted text against the old output to prove the content came across. Then check the column widths against the Cell arguments, because those numbers are the only record of the intended layout and transcribing them is where an error hides.
The one thing that always breaks
The total page count. FPDF's AliasNbPages worked by writing a placeholder string into the document and replacing it at output, and templates use the raw brace token in their footer text. Copied into markup, that token is just characters, so the footer prints the literal placeholder next to the page number rather than a total.
Frequently asked
Do I have to rewrite my templates to leave FPDF and fpdf2?
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.
Migrating off ReportLab, where there are no templates to port
A Python library with a low-level canvas and a higher-level flowable system, where nothing in the layout is markup and the document has to be written as HTML for the first time.
Migrating off TCPDF, where half the document is drawn and half is HTML
A PHP library that is a drawing API with an HTML renderer bolted on, so most codebases using it are half template and half coordinates, and the two halves migrate differently.
Migrating off Prawn, from bounding boxes to normal flow
A Ruby drawing library organised around bounding boxes and a text cursor, with a real table extension, where the box structure is the closest thing to a layout you have.
Migrating off jsPDF, from text at coordinates to a document
A JavaScript drawing library whose documents are sequences of text and shape calls at coordinates, frequently run in the browser, so the migration moves the render to a server as well as changing its shape.
Migrating off PDFKit for Node, from a stream of calls to markup
A Node drawing library with a chainable API and a streaming output model, where the document is a sequence of calls and the stream is the part that changes shape.
Every migration, grouped by what you are coming from
The full list, sorted by the model the old tool used rather than by its name.
What this API actually does
One page per option and endpoint that exists, which is what the mappings above point at.
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.