Migrating from html2canvas with jsPDF / Already HTML and CSS
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.
What ports and what does not
The markup ports and the CSS mostly ports, with an important caveat: html2canvas reimplemented rendering rather than using the browser's, so it supported a subset and your templates were written inside it. What does not port is the entire approach. The old output was a picture of a page. There was no selectable text, no searchable content, no real pagination, and file sizes were driven by image resolution rather than by content. Everything downstream that treats the file as a document rather than as an image is newly possible and newly your responsibility.
What the call becomes
The canvas capture, the image conversion and the addImage call all disappear. The document becomes a POST to /v1/pdf with html, which also means the render moves off the client and onto a server, and that is a larger architectural change than the call itself. Anything that depended on the document being produced in the user's browser, including access to logged-in state, local component state and anything not reachable from a URL, needs the data assembled server side instead.
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: a picture of the DOM, embedded in a PDF.
const canvas = await html2canvas(document.querySelector("#invoice"), { scale: 2 });
const img = canvas.toDataURL("image/png");
const doc = new jsPDF({ unit: "mm", format: "a4" });
doc.addImage(img, "PNG", 0, 0, 210, 297);
doc.save("invoice.pdf");
// After: the markup is rendered as a document, server side.
const res = await fetch("/api/invoice/1/pdf"); // your endpoint
// ...which calls:
const pdf = 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: renderInvoiceHtml(invoice), // the same markup, built on the server
filename: "invoice.pdf",
options: { format: "A4" },
}),
});
/* What changes beyond the call:
the text is text: selectable, searchable, extractable
the document paginates instead of being one tall image cut up
file size is driven by content rather than by capture resolution
the render needs the data, not the logged-in browser session */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.
- html2canvas supported a subset of CSS and approximated the rest, so a great deal starts rendering correctly for the first time. Shadows, filters, transforms and newer layout that were approximated or dropped now apply properly.
- Anything tuned to look right through html2canvas's approximation will look different, because it was compensating for a renderer that no longer sits in the path.
- Page breaks become real. The old approach either produced one enormous page or sliced a tall image at fixed intervals, so the templates contain no break rules at all and every one of them has to be added.
- Fonts become embedded rather than rasterised, so text is crisp at any zoom and the file no longer carries a high resolution bitmap of it.
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.
- The document becomes multiple pages where it was one image, or paginates at different points than the fixed slicing did.
- File size usually falls substantially, which is a change worth expecting rather than being surprised by.
- Anything that relied on the capture scale for legibility, since text is now vector and the scale option no longer exists.
- Content that was only present in the browser, which is the architectural half of this migration and the part that is not about CSS at all.
What to diff before cutting over
Do not diff visually first: the outputs are different kinds of object and a pixel comparison will tell you nothing useful. Extract the text from the new output and confirm it contains what the document is supposed to say, which is a check the old pipeline could never pass. Then look at pagination, because that is genuinely new behaviour rather than changed behaviour.
The one thing that always breaks
Pagination. The old pipeline had none: it captured an element and put the picture on a page, so the templates have no break rules, no repeating table headers and no notion of a page boundary at all. The first real render puts a page break through the middle of a table row, and every long document needs its break behaviour written from scratch.
Frequently asked
Do I have to rewrite my templates to leave html2canvas with jsPDF?
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 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.
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.
Migrating off self-hosted Puppeteer, an operational migration
The same browser engine on the other side, so nothing about the document changes and everything about running it does.
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.
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.