PDFPipe

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
<?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.

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.