Migrating from mPDF / Already HTML and CSS
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.
What ports and what does not
Standard markup ports. The dialect does not. mPDF extended HTML with its own elements for the things it could not express in CSS, so templates carry tags for page breaks, for page headers and footers, for barcodes and for watermarks, and none of them mean anything to a browser engine. They do not error either: an unknown element renders as an inline box with no styling, so a page break tag becomes nothing and a header tag becomes a stray line of text in the flow. That translation is the migration.
What the call becomes
The Mpdf constructor, WriteHTML and Output collapse into one POST to /v1/pdf. The constructor's format and orientation become options.format and options.landscape, and its margin arguments become options.margin. The setter methods have no counterpart because their jobs move into CSS: page headers and footers become options.header_html and options.footer_html or margin boxes in your stylesheet, and the watermark methods become an element in the markup with fixed positioning.
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
$mpdf = new \Mpdf\Mpdf([
'format' => 'A4',
'margin_top' => 20,
'margin_bottom' => 25,
]);
$mpdf->SetHTMLHeader('<div>Invoice</div>');
$mpdf->SetHTMLFooter('<div>{PAGENO} of {nbpg}</div>');
$mpdf->SetWatermarkText('DRAFT');
$mpdf->showWatermarkText = true;
$mpdf->WriteHTML($html);
$pdf = $mpdf->Output('', 'S');
// After: the setters become options and CSS.
$response = $client->post('https://api.pdfpipe.xyz/v1/pdf', [
'headers' => ['Authorization' => 'Bearer ' . getenv('PDFPIPE_KEY')],
'json' => [
'html' => $html, // with the mPDF-only tags translated out
'options' => [
'format' => 'A4',
'margin' => ['top' => '20mm', 'bottom' => '25mm',
'left' => '15mm', 'right' => '15mm'],
'header_html' => '<div style="font-size:8pt">Invoice</div>',
'footer_html' => '<div style="font-size:8pt">'
. '<span class="pageNumber"></span> of '
. '<span class="totalPages"></span></div>',
],
],
]);
/* Translations to make in the markup:
<pagebreak /> -> <div style="break-before: page"></div>
<htmlpageheader ...> -> header_html, or an @page margin box
<htmlpagefooter ...> -> footer_html, or an @page margin box
<watermarktext content=..> -> a fixed-position element, see
/pdf/sections/watermark
The page number tokens {PAGENO} and {nbpg} are mPDF's own and do not
exist here: the header and footer substitute .pageNumber and
.totalPages instead. */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.
- mPDF's own tags have no CSS equivalent because they were never CSS. Each one becomes either a render option or a piece of styled markup, and the mapping is one you make deliberately rather than one a tool can do for you.
- Flexbox and grid become available, and as with dompdf your existing table layouts keep working, so the benefit is deferred until somebody takes it.
- mPDF's page numbering tokens are string substitutions performed by mPDF on its own header and footer strings. The header and footer here substitute a small set of class names instead, so the token becomes an empty span with a class on it.
- Column support differs. mPDF had its own multi-column implementation driven by method calls; CSS columns are the replacement and they behave differently at page boundaries.
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.
- Every mPDF-only tag disappears from the output rather than erroring, so page breaks stop happening and the document reflows into fewer, longer pages.
- Page count, from the reflow above and from ordinary metric differences.
- The watermark, which stops being drawn at all once the method call is gone and before the CSS replacement is written.
- Fonts, for the same reason as dompdf: mPDF carried its own font configuration and templates name families that a browser engine has never heard of.
What to diff before cutting over
Grep the templates for mPDF's tag vocabulary before rendering anything, because that list is your work plan and it is much cheaper to read than to discover. Then diff page counts, which will be dramatically different until the page break tags are translated, and use that difference as the check that you found all of them.
The one thing that always breaks
Page breaks. The pagebreak element is the most used tag in the mPDF dialect, it appears throughout long templates, and it renders as nothing. The document comes back complete, correct and paginated entirely differently, and unlike a missing font it does not look wrong on any single page.
Frequently asked
Do I have to rewrite my templates to leave mPDF?
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 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 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.
Migrating off WeasyPrint, and what happens to your JavaScript
A Python renderer with genuinely good paged CSS support and no JavaScript engine, so the migration adds a capability you never had and changes very little else.
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.