Taking Browser Screenshots with Deno

Today, I needed to turn SVGs into PNGs. I decided to use Deno to do it. Some cursory searching showed Puppeteer should be up to the task. I also found deno-puppeteer which seemed like it would provide a reasonable way to make this work.

To start, let’s set up a deno project

Terminal window
deno init deno-browser-screenshots
deno-browser-screenshots

Using puppeteer

Now, add some code to render an SVG with Chrome via puppeteer.

import puppeteer from 'https://deno.land/x/puppeteer@16.2.0/mod.ts';
const svgString = `
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#87CEEB"/>
<circle cx="256" cy="256" r="100" fill="#FFD700"/>
<path d="M 100 400 Q 256 300 412 400" stroke="#1E90FF" stroke-width="20" fill="none"/>
</svg>`;
if (import.meta.main) {
try {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox'],
});
const page = await browser.newPage();
await page.setViewport({ width: 512, height: 512 });
await page.setContent(svgString);
await page.screenshot({
path: 'output.png',
clip: {
x: 0,
y: 0,
width: 512,
height: 512,
},
});
await browser.close();
} catch (error) {
console.error('Error occurred:', error);
console.error('Make sure Chrome is installed and the path is correct');
throw error;
}
}

When we run this code, we get the following error

Terminal window
deno run -A main.ts
Error occurred: Error: Could not find browser revision 1022525. Run "PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts" to download a supported browser binary.
at ChromeLauncher.launch (https://deno.land/x/puppeteer@16.2.0/src/deno/Launcher.ts:99:30)
at eventLoopTick (ext:core/01_core.js:175:7)
at async file:///Users/danielcorin/dev/lab/deno-puppeteer/main.ts:12:21
Make sure Chrome is installed and the path is correct
error: Uncaught (in promise) Error: Could not find browser revision 1022525. Run "PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts" to download a supported browser binary.
if (missingText) throw new Error(missingText);
^
at ChromeLauncher.launch (https://deno.land/x/puppeteer@16.2.0/src/deno/Launcher.ts:99:30)
at eventLoopTick (ext:core/01_core.js:175:7)
at async file:///Users/danielcorin/dev/lab/deno-puppeteer/main.ts:12:21

Using npm’s puppeteer, we can install Chrome via npx

Terminal window
npx puppeteer browsers install chrome

However, this still did not resolve the error for me. It turns out the npx command install the browser to ~/.cache/puppeteer.

To use it, we need the following modifications to our code to provide an executablePath.

import puppeteer from 'https://deno.land/x/puppeteer@16.2.0/mod.ts';
const svgString = `
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#87CEEB"/>
<circle cx="256" cy="256" r="100" fill="#FFD700"/>
<path d="M 100 400 Q 256 300 412 400" stroke="#1E90FF" stroke-width="20" fill="none"/>
</svg>`;
if (import.meta.main) {
try {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox'],
executablePath:
Deno.env.get('HOME') +
'/.cache/puppeteer/chrome/mac_arm-131.0.6778.108/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
});
const page = await browser.newPage();
await page.setViewport({ width: 512, height: 512 });
await page.setContent(`
<style>
body { margin: 0; background: transparent; }
svg { display: block; }
</style>
${svgString}
`);
await page.screenshot({
path: 'output.png',
clip: {
x: 0,
y: 0,
width: 512,
height: 512,
},
});
await browser.close();
} catch (error) {
console.error('Error occurred:', error);
console.error('Make sure Chrome is installed and the path is correct');
throw error;
}
}

With that, the code runs successfully and we get output.png in our working directory.

Using astral

There is another, Deno-native library called astral (no relation to the Python tooling company). I was curious to see how the DX compared for this simple use case.

The following code worked for me without issue

import { launch } from 'jsr:@astral/astral';
const svgString = `
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#87CEEB"/>
<circle cx="256" cy="256" r="100" fill="#FFD700"/>
<path d="M 100 400 Q 256 300 412 400" stroke="#1E90FF" stroke-width="20" fill="none"/>
</svg>`;
if (import.meta.main) {
try {
const browser = await launch();
const page = await browser.newPage();
await page.setViewportSize({ width: 512, height: 512 });
await page.setContent(`
<style>
body { margin: 0; background: transparent; }
svg { display: block; }
</style>
${svgString}
`);
const screenshot = await page.screenshot();
Deno.writeFileSync('output_astral.png', screenshot);
await browser.close();
} catch (error) {
console.error('Error occurred:', error);
throw error;
}
}

On first run, astral manages the downloading of Chrome for you.

Terminal window
deno run -A main_astral.ts
Downloading chrome 125.0.6400.0 100.00%
Download complete (chrome version 125.0.6400.0)
Inflating /Users/danielcorin/Library/Caches/astral/125.0.6400.0 100.00%
Browser saved to /Users/danielcorin/Library/Caches/astral/125.0.6400.0

This code works but unexpectedly outputs a PNG with size 1024 x 1024 and I’m not entirely sure why.

Adding the following not-very-nice-looking code seemed to fix this issue - maybe there is a better way.

await page.unsafelyGetCelestialBindings().Emulation.setDeviceMetricsOverride({
width: 512,
height: 512,
deviceScaleFactor: 1,
mobile: false,
});

With each approach, there were different rough edges. Both were able to fit my use case with a few tweaks.