How to Take Screenshots in React + Express App
Aug 10, 2023
- React
- TypeScript
- Express
- Playwright
- html2canvas
Since there is no built-in feature in JavaScript to take screenshots, you need to use libraries. There are two approaches: relying on the server-side and solving it on the client-side.
Using Puppeteer or Playwright#
Puppeteer and Playwright are libraries for controlling browsers on the server-side. These libraries come with screenshot-taking capabilities that we can leverage. The screenshot functionality of playwright is used for generating OGP images in this blog. You can create an API on the server-side and send requests from React to take screenshots.
1$ npm install playwright
1// server.ts23import express from "express";4import cors from "cors";5import playwright from "playwright";67const app = express();8app.use(express.json());9app.post('/screenshot', async(res, req, next) => {10 try {11 const url = req.query.url;12 const width = req.query.width;13 const height = req.query.height;1415 if (!url || !width || !height) {16 throw new Error();17 }1819 // puppeteerの起動20 const browser = await puppeteer.launch();21 const page = await browser.newPage();22 await page.setViewportSize({23 width: parseInt(width, 10),24 height: parseInt(height, 10),25 });26 await page.goto(url);2728 const base64 = (29 await page.screenshot({30 fullPage: true,31 })32 ).toString("base64");33 res.send(base64);34 } catch(e) {35 next(e);36 } finally {37 await browser.close();38 }39});4041app.listen(8080, () => {42 console.log("Server is running on port 8080");43});
1// useScreenshot.ts23import axios from "axios";4import { useMutation } from "react-query";56export const useScreenshot = () => {7 return useMutation("getScreenshot", () =>8 axios.post(9 `${process.env.API_ENDPOINT}/screenshot?url=${10 url11 }&width=${12 document.body.clientWidth13 }&height=${14 document.body.clientHeight15 }`));16}
However, there's a drawback with this method, as it can put a heavy load on the server since it launches a browser on the server-side. Additionally, setting up in Docker might be cumbersome, and the image size can be substantial.
Using html2canvas#
html2canvas is a library that captures screenshots using the Canvas API. This makes it quite convenient since we don't need to rely on the server-side.
1$ npm install html2canvas
1// useScreenshot.ts23import html2canvas from "html2canvas";45export const useScreenshot = () => {6 const capture = (element: HTMLElement) => {7 try {8 if (!element) return;9 const canvas = await html2canvas(element);10 return canvas.toDataURL("image/png");11 } catch (e) {12 console.error(e);13 }14 };15};
1import { useScreenshot } from "./useScreenshot";23export const App = () => {4 const { capture } = useScreenshot();5 const ref = useRef<HTMLDivElement>(null);6 const [image, setImage] = useState<string | null>(null);7 const handleClick = () => {8 const image = capture(ref.current);9 setImage(image);10 };1112 return (13 <div ref={ref}>14 {/* content */}15 <button onClick={handleClick}>take a screenshot</button>16 {image && <img src={image} />}17 </div>18 )19}
CORS Images#
For images hosted on your own server, you can handle CORS
using the useCORS
option, but when using external images, you might encounter CORS errors.
In this case, you'll need to rely on the server and use the proxy
option to bypass CORS errors.
1// server.ts23...45app.get('/proxy', (res, req, next)=> {6 try {7 const url = req.query.url;8 if (!url) {9 throw new Error();10 }11 const response = await fetch(url);12 response.arrayBuffer().then((buffer) => {13 res.send(Buffer.from(buffer));14 });15 } catch (e) {16 next(e);17 }18})19...
1// useScreenshot.ts2import html2canvas from "html2canvas";3import { useState } from "react";45export const useScreenshot = () => {6 const [isLoading, setIsLoading] = useState(false);7 const capture = async (element: HTMLElement) => {8 try {9 if (!element || !name) return;10 setIsLoading(true);11 const canvas = await html2canvas(element, {12 proxy: `${process.env.API_ENDPOINT}/proxy`,13 });14 return canvas.toDataURL("image/png");15 } catch (e) {16 console.error(e);17 } finally {18 setIsLoading(false);19 }20 };2122 return { capture, isLoading };23};
Flexbox Layout Issues#
html2canvas
often has problems with text alignment in Flexbox elements.
To resolve this, I cloned the repository and made modifications by myself.
Keep in mind that this is a workaround.
1// src/render/canvas/canvas-renderer.ts23renderTextWithLetterSpacing(text: TextBounds, letterSpacing: number, baseline: number): void {4 if (letterSpacing === 0) {5 this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline / 2 + 2); // <-- I fixed this line6 } else {7 const letters = segmentGraphemes(text.text);8 letters.reduce((left, letter) => {9 this.ctx.fillText(letter, left, text.bounds.top + baseline);1011 return left + this.ctx.measureText(letter).width;12 }, text.bounds.left);13 }14}
Downloading Images#
You can use the <a>
tag's download attribute to download images.
For a better user experience on iOS,
I chose to render the image on a new page and allow users to long-press to save.
1import html2canvas from "html2canvas";2import { useState } from "react";3import { isIos } from "../utils/isIos";45export const useScreenshot = () => {6 const [isLoading, setIsLoading] = useState(false);7 const capture = async (element: HTMLElement) => {8 try {9 if (!element) return;10 setIsLoading(true);11 const canvas = await html2canvas(element, {12 proxy: `${process.env.API_ENDPOINT}/proxy`,13 });14 const data = canvas.toDataURL("image/png");1516 if (isIos()) {17 const image = new Image();18 image.src = data;19 const w = window.open("about:blank");20 w?.document.write(image.outerHTML);21 w?.document.close();22 } else {23 const link = document.createElement("a");24 link.download = "download.png"25 link.href = data;26 link.click();27 }28 } catch (e) {29 console.error(e);30 } finally {31 setIsLoading(false);32 }33 };3435 return { capture, isLoading };36};