PDFPipe

Migrating from pdfmake / A declared document structure

Migrating off pdfmake, from a document definition to markup

A library taking a JSON document definition, where the structure maps onto markup reasonably directly and the styling vocabulary does not map at all.

What ports and what does not

The structure ports as a translation. A pdfmake document is a content array of blocks, each with a type and a style, and that tree corresponds to markup closely enough that the conversion is mechanical rather than creative. The styles object does not port: pdfmake has its own property names and its own layout model, and it is a much smaller vocabulary than CSS. The code that builds the definition from your data survives entirely, because it is producing a data structure and it can produce a string of markup instead with the same inputs.

What the call becomes

createPdfKitDocument or the browser-side createPdf, plus whichever output method you used, becomes a POST to /v1/pdf with html. The pageSize property becomes options.format, pageOrientation becomes options.landscape, and pageMargins, which pdfmake took as an array, becomes options.margin as an object with named sides. The header and footer properties, which pdfmake called as functions receiving the page number, become options.header_html and options.footer_html with substituted class names, and anything the function computed beyond the page number has no counterpart.

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
const dd = {
  pageSize: "A4",
  pageMargins: [40, 60, 40, 60],
  header: (page, count) => ({ text: `Page ${page} of ${count}`, alignment: "right" }),
  content: [
    { text: "Invoice", style: "h1" },
    { table: { widths: ["*", 60, 80], body: rows }, layout: "lightHorizontalLines" },
    { text: total, style: "total" },
  ],
  styles: { h1: { fontSize: 18, bold: true }, total: { fontSize: 14, bold: true } },
};
pdfMake.createPdf(dd).getBuffer(cb);

// After: the same builder, producing markup instead of a definition.
const html = `
  <style>
    .h1 { font-size: 18pt; font-weight: 700; }
    .total { font-size: 14pt; font-weight: 700; }
    table { width: 100%; border-collapse: collapse; table-layout: fixed; }
    td, th { border-bottom: 0.5pt solid #ccc; padding: 4pt; }
  </style>
  <h1 class="h1">Invoice</h1>
  ${renderTable(rows)}
  <p class="total">${total}</p>`;

await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
             "Content-Type": "application/json" },
  body: JSON.stringify({
    html,
    options: {
      format: "A4",
      margin: { top: "60pt", bottom: "60pt", left: "40pt", right: "40pt" },
      header_html: '<div style="text-align:right;font-size:8pt">Page ' +
                   '<span class="pageNumber"></span> of ' +
                   '<span class="totalPages"></span></div>',
    },
  }),
});

// pageMargins was [left, top, right, bottom]. Getting that order wrong
// when converting it to named sides is the most common error here.

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.

  • pdfmake's style properties become CSS properties, and most have an obvious counterpart: fontSize becomes font-size in points, bold becomes font-weight, alignment becomes text-align. The mapping is tedious rather than difficult.
  • The table layout property, which selected one of pdfmake's named line-drawing schemes, has no counterpart. Table borders become CSS borders, which is more control rather than less, but every table needs its rules written.
  • Column widths expressed as star notation become percentages or fixed lengths with table-layout: fixed.
  • Everything CSS can do that pdfmake could not becomes available: real page-break control, repeating table headers, margin boxes and the whole of paged media.

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.

  • Page count, because pdfmake's layout engine and a browser's disagree about almost everything.
  • Table column widths, since star notation and CSS table layout distribute space differently.
  • Page margins, if the array order is transcribed wrongly, which produces a document that renders perfectly and is laid out incorrectly.
  • Header content beyond the page number, which reduces to what markup can express.

What to diff before cutting over

Convert one document first and compare it against the old output page by page, because the style mapping is the whole of the work and getting it right once gives you the vocabulary for everything else. Then check the margins against the original array, and diff page counts across the rest.

The one thing that always breaks

The page margins array. pdfmake took four numbers in an order that is easy to misremember, and translating them into named sides transposes two of them more often than not. Nothing errors, the document renders, and the text block sits in the wrong place on every page by an amount small enough to survive a glance.

Frequently asked

Do I have to rewrite my templates to leave pdfmake?

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.

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.