JavaScript
の標準機能としてスクリーンショットを撮る機能はないため、ライブラリを使う必要があり、選択肢としてサーバーサイドに頼る方法とクライアントサイドで解決する方法があります。
PuppeteerやPlaywrightを使う#
Puppeteer
や Playwright
は、サーバーサイドでブラウザを操作するためのライブラリです。
これらにはスクリーンショットを撮る機能が備わってるそれを利用します。
このブログのOGPの生成にも Playwright
のスクリーンショット機能を使ってます。
サーバーサイドにAPIを作って、React
からリクエストを送ることでスクリーンショットを撮ることができます。
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}
しかし、サーバー内でブラウザーを立ち上げるのでサーバー側の負荷が高くなるデメリットがあります。
また、Docker
での設定が面倒だったりイメージの容量が重かったりします。
html2canvasを使う#
html2canvas
は Canvas API
を使ってスクリーンショットを取ってくれるライブラリーです。
サーバー側を介さなくいいので便利です。
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}>スクリーンショットを撮る</button>16 {image && <img src={image} />}17 </div>18 )19}
CORS画像#
自分でhostingしてる画像なら useCORS
オプションなどを使って対応できますが、外部の画像を使う場合はCORSのエラーが出てしまいます。
サーバーに頼ることになりますが、proxy
オプションを使うことで回避できます。
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のレイアウトが崩れる#
html2canvas
はFlexbox要素の文字がズレてしまうことが多かったので、解決のためにレポジトリをクローンして自分で修正して使いました。
トリッキーなので参考程度に。
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); // <-- ここを修正6 } 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}
画像をダウンロードする#
<a>
タグの download
属性を使うことで、画像をダウンロードすることができます。
iOSでは画像のダウンロードがUX的に良くないと思ったので、新しいページにレンダリングして長押しで保存できるようにします。
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};