PDFPipe

Migrating from PhantomJS / Already HTML and CSS

Migrating off PhantomJS, an engine that stopped a decade ago

A scriptable headless browser whose development ended years ago, so the CSS delta is the largest in the cluster and almost all of it is in your favour.

What ports and what does not

The markup ports. The CSS ports in the sense that it still parses, but the templates were written against a browser engine from the early part of the last decade, so they contain vendor prefixes for things that no longer need them, workarounds for bugs that no longer exist, and no modern layout at all. The driving script does not port: PhantomJS documents were produced by a JavaScript program that created a page, set a paper size, opened content and called render, and that program has no counterpart.

What the call becomes

The script collapses to a POST to /v1/pdf. page.content set directly becomes html; page.open with a URL becomes url. page.paperSize, which carried format, orientation, margin and its own header and footer callbacks, splits into options.format, options.landscape, options.margin, and options.header_html and options.footer_html. The header and footer callbacks are the part with no equivalent: PhantomJS called a JavaScript function per page and let it return markup, and header_html here is one piece of markup with substituted class names, so anything the callback computed has to be expressible without running code.

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.

js
// Before
var page = require('webpage').create();
page.paperSize = {
  format: 'A4',
  orientation: 'portrait',
  margin: '1cm',
  header: {
    height: '1cm',
    contents: phantom.callback(function (pageNum, numPages) {
      return '<div>Page ' + pageNum + ' of ' + numPages + '</div>';
    })
  }
};
page.content = html;
page.render('invoice.pdf');
phantom.exit();

// After
curl -X POST https://api.pdfpipe.xyz/v1/pdf \
  -H "Authorization: Bearer $PDFPIPE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!doctype html>...",
    "options": {
      "format": "A4",
      "margin": "1cm",
      "header_html": "<div style=\"font-size:8pt\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
    }
  }'

// The callback becomes markup with substituted class names. Anything the
// callback computed per page beyond the page number has no equivalent,
// because there is no per-page function to run.

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.

  • Everything modern becomes available at once: flexbox, grid, custom properties, current selectors and the paged media properties. The templates use none of it.
  • Vendor prefixes throughout the templates are now unnecessary. They are harmless, and cleaning them up is a separate task from the migration.
  • Workarounds for engine bugs are working around nothing, and some of them will now be actively wrong, because they were compensating for behaviour that no longer happens.
  • Web font support is materially better, so text that was rendered as an image to get a typeface can become text.

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.

  • Layout, in more places than any other migration here, because the engine gap is the widest. Expect real visual differences rather than drift.
  • Page count, substantially.
  • Per-page header content that the callback computed, which reduces to whatever can be expressed as markup.
  • Fonts, which PhantomJS resolved from the host system.

What to diff before cutting over

Treat this one as a rebuild rather than a diff. The engine gap is wide enough that a page-count comparison mostly tells you the two are different, which you already know. Render a representative set through the new engine and review them as though the templates were new, because in the ways that matter they are.

The one thing that always breaks

The per-page header and footer callbacks. They were JavaScript functions receiving the page number and the total, and they frequently did real work: conditional content on the first page, a different footer on the last, a running section title. None of that survives, because there is no function to call, and header_html is one static piece of markup for every page.

Frequently asked

Do I have to rewrite my templates to leave PhantomJS?

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.