Migrating from jsPDF / An imperative drawing API
Migrating off jsPDF, from text at coordinates to a document
A JavaScript drawing library whose documents are sequences of text and shape calls at coordinates, frequently run in the browser, so the migration moves the render to a server as well as changing its shape.
What ports and what does not
The data. The drawing does not: doc.text at an x and a y is not a template, and the layout exists only as the arithmetic between the calls. Two things make this migration bigger than it looks. The document was usually produced in the browser, so it had access to logged-in state and component state that a server render does not, and the data has to be assembled somewhere else. And many jsPDF codebases also use the html plugin or html2canvas for part of the page, which is a separate migration in the same file.
What the call becomes
new jsPDF, the sequence of text and shape calls, and save or output collapse into a POST to /v1/pdf with html, made from a server endpoint rather than from the page. The constructor's format and orientation become options.format and options.landscape. The unit argument matters: whatever unit the coordinates were in is a valid CSS length, so the arithmetic transcribes directly. addPage becomes break-before: page. setFontSize becomes font-size in the same units.
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.
// Before, in the browser
const doc = new jsPDF({ unit: "mm", format: "a4" });
doc.setFontSize(18);
doc.text("Invoice", 15, 25);
doc.setFontSize(10);
let y = 40;
rows.forEach((r) => {
doc.text(r.desc, 15, y);
doc.text(String(r.amt), 180, y, { align: "right" });
y += 6;
if (y > 270) { doc.addPage(); y = 25; }
});
doc.save("invoice.pdf");
// After: a server endpoint, because the render moved off the client.
app.get("/invoice/:id/pdf", async (req, res) => {
const invoice = await loadInvoice(req.params.id); // was in the browser
const html = renderInvoice(invoice);
const r = 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,
filename: "invoice.pdf",
options: { format: "A4" },
}),
});
res.type("application/pdf").send(Buffer.from(await r.arrayBuffer()));
});
/* The coordinates are the specification. 15mm from the left is the left
margin. 180mm right-aligned is the right edge of the text block. The
6mm step is the line height. The 270 check is the bottom margin, and
it disappears entirely: pagination is not your arithmetic any more. */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.
- Nothing to migrate; everything to write. The coordinates give you the margins, the column positions and the line height, all in a unit that is valid CSS.
- The manual y tracking and the page-height check become ordinary flow plus break-inside rules, which is the single biggest simplification in this migration.
- Text wrapping becomes automatic. jsPDF required splitTextToSize to wrap a string, and every call site that used it is a place where the markup just works.
- Fonts change from jsPDF's registered fonts to @font-face, and web fonts become straightforward where they were awkward.
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 visual, because it is a rewrite.
- Where the render happens, which is the architectural half and affects authentication, data access and latency.
- Any part of the document produced by html2canvas or the html plugin, which is a different migration with different consequences.
- Page count, since pagination moves from your arithmetic to the layout engine.
What to diff before cutting over
Confirm first that the server can see everything the browser could, because that is the failure that stops the migration rather than degrading it. Then diff extracted text against a sample of old documents.
The one thing that always breaks
Access to browser-only data. The old code ran where the user was logged in and where the component state lived, so it could put things on the page that the server has no route to: a filtered view, an unsaved edit, a computed total held in memory. The server render produces a document that is correct and missing something, and which thing is missing differs per document.
Frequently asked
Do I have to rewrite my templates to leave jsPDF?
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.
Migrating off html2canvas and jsPDF, and getting real text back
A pair of libraries that screenshot the DOM into a canvas and embed the image in a PDF, so the output was never text and the migration is the first time these documents become documents.
Migrating off PDFKit for Node, from a stream of calls to markup
A Node drawing library with a chainable API and a streaming output model, where the document is a sequence of calls and the stream is the part that changes shape.
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.
Migrating off ReportLab, where there are no templates to port
A Python library with a low-level canvas and a higher-level flowable system, where nothing in the layout is markup and the document has to be written as HTML for the first time.
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.
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.