Google's Puppeteer is a Node API that allows you to control an instance of Chrome using a Node service. Leveraging this technology, we are easily able to generate PDFs using any Javascript framework we wish.

In this post we will go over how to use a React project alongside a Node service to generate styled PDFs.

This structure is the backbone of how PDFTron's web-to-pdf works. If you are looking for a quick PDF generator using web technology, we recommend checking it out! It comes with many features that we do not cover in this article, such as converting a remote PDF to a url, pagination, dynamic content, and much more.

React is not required!
Please note that React components are not required for web-to-pdf to work. It supports all frameworks, and even vanilla JS/HTML/CSS.

linkStep 1 - Prepare Project

We will be using create-react-app to quickly set up our front end dev environment. Navigate to an empty directory and install create-react-app by running

npm i create-react-app

Now lets generate our project using the following command

npx create-react-app react-to-pdf

Once the project is generated, lets start the local server by running

cd react-to-pdf
npm run start

Your default browser will open automatically and will display the default create-react-app screen!

linkStep 2 - Create the UI

Now we will clean up the project and get our UI set up. First, start by deleting all the code in src/App.js, except the outer div. Lets also remove the logo.svg import. App.js should now look like this:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">

      </div>
    );
  }
}

export default App;

Lets also delete all the unused files the come with the template. Delete src/App.test.js and src/logo.svg.

To make sure our UI will fit inside the bounds of a PDF, lets set up our UI to roughly match the default size of a PDF. Inside App.js, lets create a new div that will hold the contents of our PDF. App.js should look something like this:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className='pdf'>
        
        </div>
      </div>
    );
  }
}

export default App;

Now, we can set .pdf to match the size of a PDF. In src/App.css, delete all the existing CSS and replace it with:

html, body, #root, .App {
  width: 100%;
  height: 100%;
  background-color: #d1d1d1
}

/* 612px x 792px is our default size of a PDF */
.pdf {
  width: 612px;
  height: 792px;
  background-color: white;
  margin: 0 auto;
}

If you check out http://localhost:3000/, you should see a grey background with a white 'pdf' in the middle!

Now lets put some content inside our PDF. For this example, we are going to use some fake data, but you can do whatever you want here (fetch data from a remote server, for example).

In the render function of App.js, lets add some fake data like so:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {

    const data = [
      { firstName: "John", lastName: "Smath" },
      { firstName: "Paul", lastName: "Smeth" },
      { firstName: "Cody", lastName: "Smith" },
      { firstName: "Jordan", lastName: "Smoth" },
      { firstName: "Jim", lastName: "Smuth" },
    ]

    return (
      <div className="App">
        <div className='pdf'>
        
        </div>
      </div>
    );
  }
}

export default App;

Now, lets create a React component to render this data. Create the file src/components/User/User.js and set up a component to render a user. The component should accept a user object as a prop. Ths file could look something like this:

import React from 'react';

export default ({ user }) => {
  return (
    <div className='user' style={{ display: 'flex', justifyContent: 'center'}}>
      <p>{user.firstName} {user.lastName}</p>
    </div>
  )
}

Now, back in App.js, lets import this component and use it to render our list of users.

import React, { Component } from 'react';
import './App.css';
import User from './components/User/User';

class App extends Component {
  render() {

    const data = [
      { firstName: "John", lastName: "Smath" },
      { firstName: "Paul", lastName: "Smeth" },
      { firstName: "Cody", lastName: "Smith" },
      { firstName: "Jordan", lastName: "Smoth" },
      { firstName: "Jim", lastName: "Smuth" },
    ]

    return (
      <div className="App">
        <div className='pdf'>
          {
            data.map(user => <User user={user} key={user.lastName}/>)
          }
        </div>
      </div>
    );
  }
}

export default App;

If you check out http://localhost:3000/ now, you should see a simple list of users rendered in your "pdf".

Go crazy with your UI!
We are going to leave the UI as is, but you can do anything you want at this point to make the PDF look the way you want. Feel free to go crazy! No limitations here.

It is important to note that what you see is what you get. When we get to generating the PDF, the PDF will look exactly like the content displayed at http://localhost:3000/. You can use images, canvas, 3rd party libraries, whatever you want!

linkStep 3 - Create the Node Service

Our Node service will need a few extra dependencies (Puppeteer and Babel-node). Install these with the following command.

npm i puppeteer @babel/core @babel/node --save-dev

Now lets create our service. From the root of the project, make the file scripts/generate-pdf.js. This is the file that the service will live in.

The code for the service is actually pretty simple. Lets start creating a new instance of Puppeteer and navigating it to our local server.

Note
The local server we set up in parts 1 & 2 needs to be running! This means that http://localhost:3000/ needs to be serving your React project.

generate-pdf.js should look something like this:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/', {waitUntil: 'networkidle2'});
})()

This code basically creates an in-memory instance of Chrome, creates a new tab, and navigates the tab to http://localhost:3000/. If you are unfamiliar with async/await syntax, I recommend learning about it here.

Now, we are ready to generate the PDF! Luckily Puppeteer provides an amazing API for this.

We are going to add some code that tells Chrome to emulate a screen when its generating the PDF, and also a call to page.pdf() to generate the PDF. We'll pass in a couple options to make the PDF look the way we want.

Update generate-pdf.js to look like this:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://localhost:3000/');
  await page.emulateMedia('screen');
  await page.pdf({
    path: './react.pdf', // path (relative to CWD) to save the PDF to.
    printBackground: true,// print background colors
    width: '612px', // match the css width and height we set for our PDF
    height: '792px',
  });
  await browser.close();
})()

Additional options can be found here.

And thats it! Our service is done and we are ready to generate our PDF.

linkStep 4 - Execute the Service

The last step will be actually executing our script. In the root of the project there should be a package.json file. Lets write a script to execute our file here. Add the following line to the scripts section in package.json.

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
+   "generate": "npx babel-node scripts/generate-pdf.js"
  },

This script uses babel-node to transpile our JS, and then executes the transpiled source code using Node.

Now, in the root of the project, making sure http://localhost:3000 is still running, run the following command.

npm run generate

When the script is finished, you should find a react.pdf at the root of the project that matches your React UI.

linkConclusion

Google's Puppeteer project has created a ton of opportunity in the PDF space. We are now able to easily generate PDF's using HTML, Javascript and CSS, which are all geared towards UI in the first place!

If you would like to view and annotate your generated PDFs in your website, consider downloading a trial of Webviewer, PDFTron's web SDK that allows you to open, edit, and annotate PDFs right in the browser (and much more!).

If you have any questions regarding Webviewer, this article, or anything else, please feel free to contact us and we will be happy to help.