Taking Full Page Screenshots with Headless Chrome

A returning subject on this blog, how to automate device screenshots with Node.js and Chrome. This post will cover installation and running the script on either Mac OS or Linux. If you’re brave, you can use Windows too ๐Ÿ˜‰

Update: A Chrome update actually broke the code for full page screenshots using forceViewport, the code samples have been updated to support the change.

Full error message:

(node:30456) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Emulation.forceViewport is not a function
(node:30456) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Automating Screenshots of your Website

There’s a ton of services for this out there, but if you have some kind of edge case or simply just need a better way of taking screenshots, please read on.

The example code makes use of async and await, so please be on Node.js 7.8+.

As always I recommend NVM for installing different versions of Node.

Getting / Starting Chrome Headless

Assuming you want to run this on a linux server, after installing Google Chrome via apt/pacman/yum, run: google-chrome --version

Expected output: Google Chrome 59.0.3071.115 (or higher)

Install Guides for:

To start the headless chrome browser, simply run:

google-chrome --headless --hide-scrollbars --remote-debugging-port=9222 --disable-gpu

Note: Your terminal will now be occupied by running that chrome browser in headless mode, if you want to detach it and keep it in the background, append a & to the command like so:

google-chrome --headless --hide-scrollbars --remote-debugging-port=9222 --disable-gpu &

Another tip, if you want to know the process ID of your chrome headless browser, append &; echo $!, which will output the process ID of what you just started:

google-chrome --headless --hide-scrollbars --remote-debugging-port=9222 --disable-gpu &; echo $!

so you can use that to kill the process, if you want to restart it or just be rid of it.

Taking Screenshots with Headless Chrome

Let’s get to the code! If you copy the below and save it as screenshot.js and in the same directory run

npm install minimist chrome-remote-interface
node screenshot.js

Copy & paste ready code ๐Ÿ˜‰


const CDP = require('chrome-remote-interface');
const argv = require('minimist')(process.argv.slice(2));
const fs = require('fs');

const targetURL = argv.url || 'https://jonathanmh.com';
const viewport = [1440,900];
const screenshotDelay = 2000; // ms
const fullPage = argv.fullPage || false;

if(fullPage){
  console.log("will capture full page")
}

CDP(async function(client){
  const {DOM, Emulation, Network, Page, Runtime} = client;

  // Enable events on domains we are interested in.
  await Page.enable();
  await DOM.enable();
  await Network.enable();

  // change these for your tests or make them configurable via argv
  var device = {
    width: viewport[0],
    height: viewport[1],
    deviceScaleFactor: 0,
    mobile: false,
    fitWindow: false
  };

  // set viewport and visible size
  await Emulation.setDeviceMetricsOverride(device);
  await Emulation.setVisibleSize({width: viewport[0], height: viewport[1]});

  await Page.navigate({url: targetURL});

  Page.loadEventFired(async() => {
    if (fullPage) {
      const {root: {nodeId: documentNodeId}} = await DOM.getDocument();
      const {nodeId: bodyNodeId} = await DOM.querySelector({
        selector: 'body',
        nodeId: documentNodeId,
      });

      const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId});
      await Emulation.setVisibleSize({width: device.width, height: height});
      await Emulation.setDeviceMetricsOverride({width: device.width, height:height, screenWidth: device.width, screenHeight: height, deviceScaleFactor: 1, fitWindow: false, mobile: false});
      await Emulation.setPageScaleFactor({pageScaleFactor:1});
    }
  });

  setTimeout(async function() {
    const screenshot = await Page.captureScreenshot({format: "png", fromSurface: true});
    const buffer = new Buffer(screenshot.data, 'base64');
    fs.writeFile('desktop.png', buffer, 'base64', function(err) {
      if (err) {
        console.error(err);
      } else {
        console.log('Screenshot saved');
      }
    });
      client.close();
  }, screenshotDelay);

}).on('error', err => {
  console.error('Cannot connect to browser:', err);
});

You should be good to go.

You can specify the URL by running:

node screenshot.js --url https://gegenwind.dk

To make a full page screenshot (careful, this behaviour is still buggy), simply include --fullPage true in your command.

Credits for code to:

  • this post on medium which didn’t work for me on Mac OS X and also I needed the delay much higher than none
  • some other snippets I can’t seem to dig up any more

Automated Mobile Testing

In order to test which devices have which real and which CSS resolution you can have a look at mydevice.io which has a reasonable collection of smartphones and their resolutions and their device pixel ratios.

If you want to check out your own screen/phone you can use the device pixel ratio test.

To use headless chrome and node to test how your page looks on mobile simply change the following defaults (or change them via command line arguments):

const viewport = [375,667]; // iPhone 7

and

  var device = {
    width: viewport[0],
    height: viewport[1],
    deviceScaleFactor: 3,
    mobile: true,
    fitWindow: false
  };

Voilรก!

Summary

Thank you very much for reading, actually the post I once upon a time about phantom.js was pretty successful, so I felt a bit nostalgic writing this post.

Why do you want to take screenshots? What’s your use case? I’m super curious!

Further Reading

Check out the Google Blog Post

3 thoughts on “Taking Full Page Screenshots with Headless Chrome”

  1. Hey Jonathan, thanks for that great post! But, the full page screenshots are not correctly taken if the site uses “vh” and “vw” viewport sizes on CSS. Viewport size should be fixed and the capture size needs to be changed with body’s height but I couldn’t figure this out even though I work on it for hours. :/ Would you recommend something to solve that?
    Sample site: https://www.twelve12.com

    1. It appears you are correct! The behavior occurs both with my desktop screenshot script as also on desktop chrome (using the inspector and the UI element for fullpage screenshot).

      What one could do is to not resize the viewport, but to take a screenshot, use the scroll, capture the next screenshot and stitch them all together.

      Let me know if you write that code, would love to see it! ๐Ÿ™‚

Leave a Reply

Your email address will not be published. Required fields are marked *