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
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/[email protected]/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
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/[email protected]/install.ts" to download a supported browser binary.
at ChromeLauncher.launch (https://deno.land/x/[email protected]/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/[email protected]/install.ts" to download a supported browser binary.
if (missingText) throw new Error(missingText);
^
at ChromeLauncher.launch (https://deno.land/x/[email protected]/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
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/[email protected]/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.
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.