loading...

Take a better picture with puppeteer.

tetra2000 profile image Takahiko Inayama ・2 min read

This article is also available on medium.

Take a better picture with puppeteer.

Puppeteer is awesome. It allows me to capture screenshots of web a lot easier.

But sometimes I can't get intended result because of animations within pages.
For example, I'll write these kind of code when I'm trying to capture SFMOMA.

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://www.sfmoma.org/", {
  waitUntil: "networkidle0",
});
await page.screenshot({ path: "example.png" });

This will results in this image.

Screenshot before animation

This capture is taken before animation have finished.

Expected result is this.

How should I avoid this?

Plan 1: Sleep fixed time.

Most easiest way is just sleep specific time. This is useful when I know duration of animations.

function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

await page.goto("https://www.sfmoma.org/", {
  waitUntil: "networkidle0",
});
sleep(1000);
await page.screenshot({ path: "example.png" });

But usually I have to specify longer time than actual animation for safe. This will problematic when I'm using puppeteer on CI. I don't want to waste CI's build time.

Plan 2: Speed up!!

Another way is to speed up CSS animations. I can fast forward them by using DevTools Protocol.

await page._client.send('Animation.setPlaybackRate', { playbackRate: 2 });

Virtually fast forward to take a screenshot after animations #453

Animation.setPlaybackRate

Plan 3: Find the page movement

Finally I'll find page movement by capturing multiple screenshots.
I use blink-diff to find differences between images.

Then I wrote this small function.

import * as BlinkDiff from "blink-diff";
import { Page } from "puppeteer";

export async function waitTillPageStoped(
  page: Page,
  interval: number = 200,
  timeout: number = 3000,
  fullPage: boolean = true,
): Promise<boolean> {
  const t0 = new Date().getTime();
  let previousBuffer: Buffer;
  while (new Date().getTime() - t0 < timeout) {
    await sleep(interval);

    const currentBuffer: Buffer = Buffer.from(await page.screenshot({
      encoding: "base64",
      fullPage,
    }), "base64");
    if (previousBuffer == null) {
      previousBuffer = currentBuffer;
      continue;
    }

    const diff = new BlinkDiff({ imageA: previousBuffer, imageB: currentBuffer });
    const result = await diff.runWithPromise();
    if (result.differences === 0) {
      return true;
    }

    previousBuffer = currentBuffer;
  }

  throw new Error("Timeouted!!");
}

This captures a screenshot in each interval specified till timed out. If no pixel differences found, simply stop waiting.

I can use this function like this.

await page.goto("https://www.sfmoma.org/", {
  waitUntil: "networkidle0",
});
await waitTillPageStoped(page);
await page.screenshot({ path: "example.png" });

And I've got an expected result!!

Alt Text

Discussion

markdown guide