Migrating from TCPDF / Already HTML and CSS
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.
What ports and what does not
The writeHTML portions port, within the limits of what TCPDF accepted, which is a narrow subset of HTML with its own attribute extensions. The drawing portions do not port at all, because they are not markup: they are Cell, MultiCell, Line, Rect and SetXY calls with the layout expressed as arithmetic. Almost every real TCPDF codebase mixes both, typically drawing the header and the fixed furniture and using writeHTML for the body, and that mixture is why these migrations take longer than they look.
What the call becomes
There is no single mapping, because there was no single call. The writeHTML string becomes part of the html body of a POST to /v1/pdf. AddPage, SetMargins, SetAutoPageBreak and the page setup calls become options. The drawing calls become markup that has to be written: a Cell at a coordinate becomes a positioned element, a Line becomes a border, and a MultiCell becomes a block with a width. The header and footer methods, which TCPDF invoked by subclassing, become options.header_html and options.footer_html or CSS margin boxes.
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: a subclass with drawn furniture and an HTML body.
class InvoicePdf extends TCPDF {
public function Header() {
$this->SetFont('helvetica', 'B', 12);
$this->Cell(0, 10, 'Invoice', 0, 1, 'L');
$this->Line(15, 25, 195, 25);
}
public function Footer() {
$this->SetY(-15);
$this->Cell(0, 10, 'Page ' . $this->getAliasNumPage(), 0, 0, 'C');
}
}
$pdf = new InvoicePdf('P', 'mm', 'A4');
$pdf->AddPage();
$pdf->writeHTML($bodyHtml);
$out = $pdf->Output('', 'S');
// After: the drawn furniture becomes markup, the body stays a string.
$response = $client->post('https://api.pdfpipe.xyz/v1/pdf', [
'headers' => ['Authorization' => 'Bearer ' . getenv('PDFPIPE_KEY')],
'json' => [
'html' => $bodyHtml,
'options' => [
'format' => 'A4',
'margin' => ['top' => '25mm', 'bottom' => '20mm',
'left' => '15mm', 'right' => '15mm'],
'header_html' =>
'<div style="font:bold 12pt Helvetica; '
. 'border-bottom:0.5pt solid #000; width:100%">Invoice</div>',
'footer_html' =>
'<div style="font-size:8pt; text-align:center">Page '
. '<span class="pageNumber"></span></div>',
],
],
]);
/* SetAutoPageBreak becomes the bottom margin plus break-inside rules.
getAliasNumPage becomes the .pageNumber class in the footer.
Cell and Line become elements and borders: there is no mechanical
translation, and the arithmetic in the old code is the specification. */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.
- TCPDF accepted a narrow HTML subset with its own attributes, and templates written for it are much simpler than templates written for a browser. Almost everything gets easier and nothing in the old markup stops working.
- TCPDF's units were configured at construction, usually millimetres, and its drawing coordinates are in those units. Millimetres are a valid CSS length, so the arithmetic in the old code translates directly into CSS values, which makes the drawn portions less painful to port than they first appear.
- The auto page break, which TCPDF managed by tracking a Y position against a bottom margin, becomes the page margin plus break-inside rules on the blocks that must not split.
- Page numbering aliases, which TCPDF replaced during output, become the substituted class names in the header and footer markup.
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 drawn disappears until it is rewritten, and because it was usually the header, the footer and the rules, the body still renders and looks nearly right.
- Page count, because auto page break tracked a running Y position and CSS pagination does not work that way.
- Text positioning inside anything that used MultiCell with a fixed height, since that call clipped or overflowed by its own rules.
- Fonts, which TCPDF carried as its own converted font files referenced by name.
What to diff before cutting over
Inventory the drawing calls before starting: count the Cell, MultiCell, Line, Rect, Image and SetXY calls in the codebase. That count, not the size of the HTML, is the size of the migration, and it is worth knowing before committing to a date. Then diff extracted text to confirm the HTML half came across intact.
The one thing that always breaks
The subclassed Header and Footer methods. They are invoked by TCPDF on every page and they are code, not markup, so they simply stop existing. The body renders correctly and the document loses its letterhead, its rules and its page numbers all at once, which is obvious on inspection and invisible to any automated check that only looks at the body text.
Frequently asked
Do I have to rewrite my templates to leave TCPDF?
No. The templates are HTML and CSS and they carry across. What needs attention is the delta above: the places the two engines disagree, and the configuration that used to live outside the document and now lives inside it, or the other way round.
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 dompdf, and getting flexbox back
A pure PHP renderer supporting a subset of CSS 2.1, where the migration's real content is everything your templates could not do and had to work around.
Migrating off mPDF, and replacing its custom tags
A PHP renderer with its own HTML dialect, where the templates contain tags and attributes that exist only in mPDF and have to be translated rather than carried across.
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.
Migrating off wkhtmltopdf without changing what the pages look like
A command line tool built on a fork of an old browser engine, driven entirely by flags, where almost every flag you were passing has a different home on the other side.
Migrating off wicked_pdf or Snappy, the wkhtmltopdf wrappers
Language wrappers that shell out to the wkhtmltopdf binary, where the framework integration is the thing you are actually replacing and the engine change comes along with it.
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.