Resource Center

Export HTML to PDF in Pure JavaScript: The Request That Made Me Want to Scream

Product requests are always wilder than you expect

Quick vent first.

Requirement came in this week. In the review meeting, product says it as casually as ordering coffee: "Let's add an export-to-PDF button on this page. Tables, the SVG charts, all the styling — it has to look exactly like what the user sees."

My face at that moment: the one where your eye wants to twitch but you're still being paid to smile.

What they actually wanted was a screenshot — snap the whole page, drop it into a PDF untouched — except the user clicks a button in the app and the download just happens. Nobody is pressing Ctrl+P.

Sure. Why not also make it fly.

And then the client added the kicker: "Just don't give me one of those files that opens as garbage." Great. Difficulty: maximum.

1. Walking every dead end first (so you don't have to)

First instinct on any requirement like this: go read what other people already suffered through. Don't build the wheel.

Option 1: Native browser print — dead on arrival

Obviously it works. Right-click, Print, save as PDF in Chrome, and the styling survives. But the user has to do it by hand, and every browser renders it differently — old IE and legacy Edge are a disaster zone. The client specifically said "triggered by a button in the system," so this one was out immediately.

Option 2: wkhtmltopdf

Big name. Install a binary on the server, hand it a URL or an HTML string, get a PDF back. I tried it in a local demo: plain text and tables were fine, the moment there was an SVG it came out blank or clipped in half. Dug through the issue tracker — SVG support has been its known weak spot for years, and the project isn't exactly moving fast anymore. Plus now I'm babysitting an external process in the deployment. Pass.

Option 3: iText on the backend

The Java world loves it for reports, and yes, the control is real. But it lays things out from scratch — you draw every element yourself. Ours is a dynamic page. The look comes from a pile of CSS, flexbox, and responsive rules. You want me to reimplement all of that in iText? Shipping HTML to the server loses the styling anyway. Pointless. Pass.

Three roads, all blocked.

I'll be honest, that part was demoralizing. You feel like you're circling a dead end. So I backed up a step: they want it to look like a screenshot. Fine — then I'll literally take a screenshot. HTML → image → image into a PDF. All of it in the browser with plain JS. Styling survives, one click downloads the file. The logic actually holds up.

2. The stack: three libraries, one job each

Once the approach was set, the tooling picked itself:

StageLibraryWhat it does
HTML → Canvashtml2canvasDraws the DOM onto a canvas as it currently looks
SVG → CanvascanvgCovers the SVG that html2canvas can't handle
Canvas / image → PDFjsPDFDrops the image in and triggers the download

There are a hundred html2canvas tutorials out there, so I'll skip the theory. The pain is in the other two — SVG and icon fonts are where the hours go.

Heads up: html2canvas is blind to SVG

If your markup contains a literal <svg>, html2canvas will run right past it and leave that area blank. It never parses the paths inside. So before calling html2canvas, you swap every SVG for a canvas — that's canvg's whole job.

And then the nastier one: glyph-style icon fonts. Those aren't images; they're a character code rendered out of a font file. Depending on browser and font-load timing you get wildly different results, and html2canvas will happily draw an empty box. My fix was ugly but effective: grab the icon's char code and redraw it myself with canvas fillText.

3. The code: three steps, in order, don't skip ahead

Step 1: turn every SVG into a canvas

svg2canvas: function(targetElem) {
  var svgElem = targetElem.find('svg');
  svgElem.each(function(index, node) {
    var parentNode = node.parentNode;
    // IE won't let you read the SVG node's content directly, so wrap it
    // in a throwaway div and pull the SVG string out via innerHTML
    var tempNode = document.createElement('div');
    tempNode.appendChild(node);
    var svg = tempNode.innerHTML;
    var canvas = document.createElement('canvas');
    // canvg takes over: paint the SVG string onto the canvas
    canvg(canvas, svg);
    parentNode.appendChild(canvas);
  });
}

Small detail that cost me the better part of an hour: in IE, reading svg.outerHTML directly doesn't give you the complete markup. Wrapping it in a temp div and reading innerHTML off that does. Writing compatibility shims will make you question your career choices.

Step 2: redraw the glyph icons yourself

If your page doesn't use icon fonts, skip this. Don't create work for yourself.

glyphicons2canvas: function(targetElem, fontClassName, fontFamilyName) {
  var iconElems = targetElem.find('.' + fontClassName);
  iconElems.each(function(index, inconNode) {   // yes, "inconNode" was a typo. It ships.
    var fontSize = $(inconNode).css("font-size");
    var iconColor = $(inconNode).css("color");
    var styleContent = $(inconNode).attr('style');
    fontSize = fontSize.replace("px", "");       // strip the unit

    var charCode = getCharCodeByGlyphiconsName(iconName);
    var myCanvas = document.createElement('canvas');
    // +2px on each axis, otherwise the icon edges get shaved off
    myCanvas.width = parseInt(fontSize) + 2;
    myCanvas.height = parseInt(fontSize) + 2;
    myCanvas.style = styleContent;
    var ctx = myCanvas.getContext('2d');
    ctx.fillStyle = iconColor;
    ctx.font = fontSize + 'px ' + fontFamilyName;
    ctx.fillText(String.fromCharCode(charCode), 1, parseInt(fontSize) + 1);
    $(inconNode).replaceWith(myCanvas);  // icon becomes canvas, nobody notices
  });
},

// icon class name -> char code. Fill this in against your own font file.
getCharCodeByGlyphiconsName: function(iconName) {
  switch (iconName) {
    case "glyphicons-resize-full":
      return "0xE216";
    case "glyphicons-chevron-left":
      return "0xE225";
    default:
      return "";
  }
}

It's a lookup table plus hand-drawing, nothing clever. You have to fill in the char codes against whatever icon font your project uses — I never found a clean automatic way. If someone has one, please tell me.

Step 3: HTML → canvas → image → PDF, end to end

html2canvas($("#myExportArea"), {
  onrendered: function(canvas) {
    var imgData = canvas.toDataURL('image/jpeg');
    var img = new Image();
    img.src = imgData;
    // Page size can only be set after the image loads — no width/height before that
    // The *0.225 is a px-to-mm fudge factor. Don't ask. It was tuned by hand.
    img.onload = function() {
      // Pick orientation from the ratio, or the PDF shows only part of the page
      if (this.width > this.height) {
        var doc = new jsPDF('l', 'mm', [this.width * 0.225, this.height * 0.225]);
      } else {
        var doc = new jsPDF('p', 'mm', [this.width * 0.225, this.height * 0.225]);
      }
      doc.addImage(imgData, 'jpeg', 0, 0,
        this.width * 0.225, this.height * 0.225);
      doc.save('report_pdf_' + new Date().getTime() + '.pdf');
    };
  },
  background: "#fff",
  // Without this, a root node with no background renders solid black. Spooky.
  allowTaint: true
  // Defaults to false and gives up entirely on cross-origin or unrecognized images.
  // true at least finishes the job, at the cost of minor artifacts.
}).catch(function(e){
  console.log("we're done for:", e);
});

Things that bit me, so they don't bite you:

  • That 0.225 magic number is px-to-mm conversion plus my page's own scale. Copying it will be wrong for you. Mine came out microscopic, then cropped, and took seven or eight passes to land
  • Don't skip the orientation check. Force a wide page into a portrait PDF and the right side just disappears. The client will find it before you do
  • allowTaint: true cuts both ways. It gets you through the render, but a cross-origin image taints the canvas and toDataURL throws. Real fix is CORS headers on the server, or inline the image as base64 first

4. Did it work? Honestly, a passing grade

Most of a day later, the flow ran: click → canvas → image → PDF. Everything the client asked for — tables present, SVG present, styling present, self-serve download — was delivered.

But be clear about one thing: sharpness is the weak spot.

This is a screenshot. Canvas resolution depends on the device and page zoom, so text edges go soft when you zoom in. It's not in the same league as the vector text you get from real browser printing. Printing a PDF gives you text that scales forever; embedding a bitmap is one step down by construction.

So if you need archive-grade or print-grade output, don't take this road — render server-side, or talk product down a notch. If, like me, the ask is "an electronic version of the report that looks right," this pure front-end setup is enough, and the deployment cost is almost nothing: drop a few JS files on the page and you're done.

One more thing: newer html2canvas has experimental SVG support, and canvg has moved on too. This combo is what shipped in that project, aged and all. On a new project, try doing it with fewer steps first.

And if you don't need a pixel-perfect replica — just a clean file — FreeConvertBox's Images to PDF builds one straight in the browser, no uploads, no retention, your privacy is fully protected. Already got a heavy PDF? The PDF Compressor will take it down a few sizes.

That's it. I'm going to go fix that inconNode typo now.

Note: some materials were produced with AI assistance.


Free Converter Toolbox   PDF Compressor, Image Format Converter