Search code examples
javascriptpdfprintingmedia-queries

How do I save a webpage styled with CSS media print rules as a PDF using JavaScript?


Certain pages of my app are used to generate invoices, agreements, waivers, etc. Those pages are already set up to use media queries to achieve certain layout and style for printing.

Now, I want to be able to download a PDF, made exactly like the print media. Nothing more, nothing less. A perfect PDF in this sense would be if user clicked print, then selected "Print to PDF" as a printer and saved that PDF. I would like to achieve this, but on the click of a button, and without using the print dialog.

I have tried libraries like jsPDF, html2canvas and such, but they don't have anything to do with the print media. They are for building PDFs manually. This is not what I want. I believe there is a much simpler solution. I only need a way to skip opening the print dialog, and selecting the print to PDF option.


Solution

  • Problem

    There's no JavaScript API to manipulate the active media type (screen/print) so libraries like jsPDF can't tell the browser to style the elements using the CSS rules defined for print media.

    Solution

    You could work around it by using a class with your CSS rules for printed media. For example:

    #page {
      color: black;
    }
    
    #page.print {
      color: red;
    }
    

    Then, when the Download PDF button is pressed, add the class to the element's class list. To avoid the print styles being displayed on screen you could add the class to a clone of the element. This element can be passed to the library, like jsPDF, for converting to a PDF. For example:

    function saveAsPDF() {
      const page = document.getElementById("page").cloneNode(true);
      const pdf = new jsPDF();
      
      page.classList.add("print");
      
      pdf.html(page, {
        callback(pdf) {
          pdf.save("file.pdf");
        },
      });
    }
    
    const button = document.getElementById("button");
    
    button.addEventListener("click", saveAsPDF);
    

    Demo in this JSFiddle.

    Unfortunately, I couldn't get the demo to work in a Stack Snippet because of browser security around CORS. However, the Stack Snippet is below in case the JSFiddle suffers from link rot or vandalism.

    const { jsPDF } = window.jspdf;
    
    function saveAsPDF() {
      const page = document.getElementById("page").cloneNode(true);
      const pdf = new jsPDF();
      
      page.classList.add("print");
      
      pdf.html(page, {
        callback(pdf) {
          pdf.save("file.pdf");
        },
      });
    }
    
    const button = document.getElementById("button");
    
    button.addEventListener("click", saveAsPDF);
    body {
      font-family: sans-serif;
    }
    
    #page {
      color: black;
    }
    
    #page.print {
      color: red;
    }
    <div id="page">
      <h1>Page title</h1>
      <p>Page text</p>
    </div>
    
    <button id="button">Download PDF</button>
    
    <script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <script crossorigin src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.js"></script>

    Additional note

    If you decide to use jsPDF, it has a dependency on html2canvas. html2canvas under the hood clones the element for you and has an onclone option for manipulating the cloned element. In theory, this means you don't need to clone the element yourself and can add the class via the option, which is slightly cleaner code. Unfortunately it doesn't work because of how jsPDF and html2canvas are integrated. jsPDF's maintainers are tracking this issue here: #2987: html2canvas Option onclone Not Working as Expected with jsPDF . Once that is fixed, you should be able to do this:

    function saveAsPDF() {
      const page = document.getElementById("page");
      const pdf = new jsPDF();
      
      pdf.html(page, {
        html2canvas: {
          onclone(clonedDocument) {
            clonedDocument.getElementById("page").classList.add("print");
          },
        },
        callback(pdf) {
          pdf.save("file.pdf");
        },
      });
    }