DOMをそのまま画像化する　〜html2canvas と dom-to-image の比較と他の方法〜

HTMLのDOMをそのまま画像化したいと思ったことはありませんか？

ネット上で色々な情報がありますが、いわゆる「そのままの状態でスクショしたい」に答えるのは意外と難しいです。

# 主な選択肢と特徴
- html2canvas
    - ユーザーにブラウザ上でダウンロードしてもらいたい場合におすすめです。ios, safariでも使えます
- dom-to-image
    - ユーザーが特定のブラウザのみでダウンロードできればいい時におすすめです。safariでは正常に動かないことがあります
- スクレイピングによる方法
    - ユーザーがダウンロードしなくても良く自分で特定のサイトからダウンロードする場合におすすめです
- ブラウザの開発者ツール
    - 要素単位でのダウンロードが実装なしでできます
- ブラウザの拡張機能
    - 拡張機能によりますが、ページ全体を綺麗にダウンロードするのには向いています


# 何にどの選択肢を使ったか
https://highsto.net/original-card/

オリジナルのカードを作れるシステムで、ユーザーが入力したテキストや画像を元にカード画像を作る時に利用しました。

ios, safari では html2canvas, それ以外では dom-to-image を採用しました。

以下が主な理由です。
- dom-to-image では ios, safari で正常に動かない
- html2canvas では 縦書き（`writing-mode: vertical-rl`）が使えない

# それぞれの詳細

## html2canvas
[html2canvas](https://github.com/niklasvh/html2canvas) は、その名の通り、HTMLの要素をHTML canvas要素に変換するライブラリです。

### Usage（GitHubより）
```javascript
html2canvas(document.body).then(function(canvas) {
    document.body.appendChild(canvas);
});
```

append せずに [HTMLCanvasElement.toDataURL](https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/toDataURL)を使って
`HTMLCanvasElement.toDataURL("image/png") ` のようにすればpngなどの画像でダウンロードできます。


### 特徴
- HTMLやCSSでの描画を `CanvasRenderingContext2D` で自力で1から再現したライブラリです
    - GitHubのコードを見ると、domをコピーして子要素をスタックして、下の方に描画される要素から1つ1つcanvasに描画しているのがわかります
- 環境の影響をあまり受けず、safariやios端末でもほぼ同じ動作をします
- `writing-mode` など、再現されていないプロパティがあります（今後の拡大に期待）
    - サポートされているプロパティは[docs/features.md](https://github.com/niklasvh/html2canvas/blob/master/docs/features.md)を参照
- 同一ドメインの要素しか描画できません（外部の画像などは描画できないので注意）


## dom-to-image
[dom-to-image](https://github.com/tsayen/dom-to-image) は、domを指定した拡張子の画像に変換するライブラリです。

### Usage（GitHubより）
```javascript
var node = document.getElementById('my-node');

domtoimage.toPng(node)
    .then(function (dataUrl) {
        var img = new Image();
        img.src = dataUrl;
        document.body.appendChild(img);
    })
    .catch(function (error) {
        console.error('oops, something went wrong!', error);
    });
```

### 特徴
- DOMを `XMLSerializer().serializeToString()` （[MDNでの説明](https://developer.mozilla.org/ja/docs/Web/API/XMLSerializer/serializeToString)）でSVGにして、そこから`Canvas`などを経て様々な画像に変換するライブラリです
    - 以下の「実装の主要部分」を参照
- `XMLSerializer` に依存し、ios, safariでは `text-shadow` がうまく再現できなかったり、画像がうまく描画されない時があったりと不安定でした
- 同一ドメインの要素しか描画できません（外部の画像などは描画できないので注意）


[実装の主要部分](https://github.com/tsayen/dom-to-image/blob/master/src/dom-to-image.js#L329)
```javascript:src/dom-to-image.js
function makeSvgDataUri(node, width, height) {
        return Promise.resolve(node)
            .then(function (node) {
                node.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
                return new XMLSerializer().serializeToString(node);
            })
            .then(util.escapeXhtml)
            .then(function (xhtml) {
                return '<foreignObject x="0" y="0" width="100%" height="100%">' + xhtml + '</foreignObject>';
            })
            .then(function (foreignObject) {
                return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '">' +
                    foreignObject + '</svg>';
            })
            .then(function (svg) {
                return 'data:image/svg+xml;charset=utf-8,' + svg;
            });
    }
```


## スクレイピングによる方法
[Puppeter](https://pptr.dev/) や [Selenium](https://www.selenium.dev/ja/documentation/) などのスクレイピングツールでは、スクリーンショットを撮ることができます。

例えば Puppeter では以下のメソッドが使えるので、

https://pptr.dev/api/puppeteer.page.screenshot

このように実装できます。
```javascript
const puppeteer = require('puppeteer');

const main = async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  await page.goto('https://qiita.com/zuisho-1848');
  await page.screenshot({ path: 'example.png' });
  
  await browser.close();
}

main();
```

あまり使っていないので解説は他の記事に任せます。
例えば、以下の記事など参考になるかもしれません。

https://qiita.com/karusai/items/c60d23f4702d494f56e1

定期的にサイトを自動で訪れてスクショを撮りたいなど、人間が見ずにスクショを撮るのに便利だと思います。

（もしかしたら、Google の PageSpeed Insights で表示されるスクショはこれで撮ってるのかもしれないと思っています）




## ブラウザの開発者ツール
開発者ツールの `Element` から画像化したいノードをサブクリックし、 `Caputure node screenshot` を押すとpngで保存できます。

![スクリーンショット 2024-09-25 22.32.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601515/2f28dcf4-9d28-a1f2-d8c3-e149239e2bc0.png)

safariなどでも撮影できるようです。
以下の記事を引用しておきます。

https://qiita.com/xrxoxcxox/items/8f7d6772021c1f5429a1

レスポンシブの画面にして、右上の下矢印マークからも撮れます。（`Caputure full size screenshot` だとページ全体を撮れます）
![スクリーンショット 2024-09-25 22.36.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2601515/9c7b609f-b77b-3586-209f-2407a6b2c88b.png)


### 特徴
- 外部のライブラリに頼らなくて良いです
- 非エンジニアでも実装せずにパッと使えます
- 再現度はあまり試せていないのでなんとも言えません
- スマホからはできないので、一般のユーザーにやってもらうのは難しいです


## ブラウザの拡張機能
ページ全体の画像を綺麗に撮りたい時に役立つ拡張機能があります。

使ったことがあるのは、以下のChromeの拡張機能です。
スクショというよりも、ページ全体を印刷しなければいけない時に、cssなどを綺麗に反映したい時に使いました（通常のページの印刷だとレスポンシブなどがひどいことになるので。。）

https://chromewebstore.google.com/detail/%E7%B4%A0%E6%99%B4%E3%82%89%E3%81%97%E3%81%84%E7%94%BB%E9%9D%A2%E3%81%AE%E4%B8%A6%E3%81%B9%E6%9B%BF%E3%81%88%E3%81%A8%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88/nlipoenfbbikpbjkfpfillcgkoblgpmj

他にもあると思うので、探してみてください。

# まとめ
HTML DOM の画像化は意外とシンプルにはいかないようで、ライブラリも結構強引にやっているものばかりです。
いつか、ブラウザのAPIなどで対応するといいなと思っています。
（ `HTMLElement.toImage()` みたいなメソッドがあればいいのに。）

ブラウザ開発者からしたら難しいよって話なんですかね？
「レンダリングする代わりにその結果を画像化する」みたいな。
専門外なのでこの程度にしておきます。

# ちょっと宣伝
Hi!story【ハイスト】チーム（株式会社Highsto）ではメンバーを募集しています。

https://highsto.net/join-us/

もし興味があったら覗いてみてください！


Xのフォローもよろしくです！

https://x.com/zuisho_tech