{
    "componentChunkName": "component---src-templates-blog-post-jsx",
    "path": "/post/convert-any-html-element-to-image/",
    "result": {"data":{"site":{"siteMetadata":{"title":"WEB EGG","author":"Leko - CTO at Yuimedi"}},"markdownRemark":{"id":"84c18e00-9062-5876-b160-15acdabeda21","excerpt":"ソースコードを画像に変換できるCarbonという web アプリをご存知でしょうか。 ↑ のような画像が生成できるサービスです。フォントやテーマなどがとても素敵な画像が生成されるため、TL…","html":"<p>ソースコードを画像に変換できる<a href=\"https://carbon.now.sh\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Carbon</a>という web アプリをご存知でしょうか。<br>\n↑ のような画像が生成できるサービスです。フォントやテーマなどがとても素敵な画像が生成されるため、TL でよく見るようになってきました。</p>\n<p>画像の生成をサーバサイドでやっているのかと持ったのですが、開発者ツールでデバッグしてみても、画像 export 時に通信は発生していません。フォントを fetch してくるだけで、それ以外はブラウザ内部で生成されているようです。</p>\n<p>このような画像を生成できる仕組みを調べて実証してみました。</p>\n<!--more-->\n<h2 id=\"作ったもの\" style=\"position:relative;\"><a href=\"#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE\" aria-label=\"作ったもの permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>作ったもの</h2>\n<p>こちらになります。<br>\n※OffscreenCanvas という API を使用しているため、おそらく Chrome でしか動作しません (<a href=\"https://caniuse.com/#feat=offscreencanvas\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">ブラウザ互換</a>)</p>\n<blockquote>\n<p>— <a href=\"https://vm2lz0ppql.codesandbox.io/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Example of rendering HTML element into canvas</a></p>\n</blockquote>\n<p>動作しない環境のためにスクショも載せておきます。<br>\ntextarea と最終成果物の PNG 画像、中間データである SVG をデモ用に表示、highlight.js でシンタックスハイライト済みの HTML を最下部に表示しています。</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 668px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 155.688622754491%; position: relative; bottom: 0; left: 0; background-image: url('data:image/svg+xml,%3csvg%20xmlns=\\'http://www.w3.org/2000/svg\\'%20width=\\'400\\'%20height=\\'622\\'%20viewBox=\\'0%200%20400%20622\\'%20preserveAspectRatio=\\'none\\'%3e%3cpath%20d=\\'M191%2024c0%209%200%209%202%209s2%200%202-5c0-4%200-5%202-5l1%205c0%205%200%205%202%205s3%200%202-6c0-5-1-5-4-5s-3-1-3-4c0-2%200-3-2-3s-2%200-2%209m34%200c0%209%200%209%202%209s2%200%202-5l1-5c2%200%202%201%202%205%200%205%200%205%202%205s2%200%202-6c0-5-1-5-4-5s-3-1-3-4c0-2%200-3-2-3s-2%200-2%209m42%200c0%209%200%209%202%209s2%200%202-5c0-4%200-5%202-5l1%205c0%205%200%205%202%205s3%200%202-4c0-8-1-8-4-8-3%201-3%200-3-3%200-2%200-3-2-3s-2%200-2%209m41-6c0%203%200%203-2%203-4%200-7%204-5%208%201%203%201%204%206%204h6l-1-9c0-9%200-9-2-9s-3%200-2%203M8%2018c-4%203-4%2010-1%2013%204%203%2013%203%2014-1l1-4c1-1%200-1-3-1l-4%201%201%201%201%203-2%202c-6%200-8-10-3-14%202-1%207%200%207%202l1%201c2%200%201-4-1-4-3-2-9-1-11%201m211%203c-7%201-7%201-7%207v7c-2%201%201%204%204%204%208%200%2010-7%203-8-4%200-5-2-2-2l4-1%201-8-3%201m40%200c-5%201-7%203-5%206v3l-1%205c0%203%203%205%208%203s5-7-1-7l-3-1c-1-1%200-1%202-1l4-1v-8l-4%201m109%200c-4%201-6%203-5%205v9c-2%201%201%204%204%204%208%200%2010-7%203-8-4%200-5-2-2-2l4-1%201-8-5%201m-232%206c0%206%200%206%202%206s2%200%202-5c0-4%200-5%202-5l1%205c0%205%200%205%202%205s3%200%202-6v-5l-6-1h-5v6m23-5c0%202%200%202%202%201h3c1%202%201%203-3%203-3%201-3%201-3%204%200%202%201%203%208%203l8-1v-2c2-1%204%200%203%202-1%201%200%201%203%201s3%200%202-2c-4-3-4-5-2-8l2-2h-2l-2%201-1%202-1-1c0-2-1-2-3-2h-3l2%203c2%203%203%204%200%206-3%203-4%202-4-3v-5l-5-1-4%201m170%205c0%206%200%206%202%206s2%200%202-5c0-4%200-5%202-5l1%205c0%205%200%205%202%205s2%200%202-5c0-4%200-5%202-5l1%205c0%205%200%205%202%205s3%200%202-6v-5l-9-1h-9v6M3%20311v40h367v-79H262l-183-1H3v40m0%20155v40h367v-80H3v40m2%2071v6h2c3%200%203%200%202-1-2-2-1-5%202-5%202%200%202%200%202%203-1%203%200%203%202%203s3%200%202-1v-6c0-4%200-5%201-4h2l2-1%201%206-1%206h3c3%200%203%200%202-1-2-2-1-11%201-11l2%201h1c0-2-1-2-9-2H12l1%203c0%202%200%202-2%202-3%200-4-3-2-4%201-1%200-1-1-1H5v7m28%200l1%206c2%200%202%200%201-1l-1-5%201-3%202%204c2%205%202%205%205%200l2-4v4c-1%205-1%205%201%205%203%200%203%200%202-6l1-6c1-1%201-1-2-1-2%200-3%200-4%204l-2%203-2-3-3-4c-2%200-2%200-2%207M3%20564v56h397v-56H3\\'%20fill=\\'%23d3d3d3\\'%20fill-rule=\\'evenodd\\'/%3e%3c/svg%3e'); background-size: cover; display: block;\"\n  ></span>\n  <picture>\n          <source\n              srcset=\"/static/faac9ad9f6c1c00e1f73891a544ea6c0/5251b/screenshot.webp 167w,\n/static/faac9ad9f6c1c00e1f73891a544ea6c0/7390e/screenshot.webp 334w,\n/static/faac9ad9f6c1c00e1f73891a544ea6c0/7c056/screenshot.webp 668w,\n/static/faac9ad9f6c1c00e1f73891a544ea6c0/dcb2d/screenshot.webp 895w\"\n              sizes=\"(max-width: 668px) 100vw, 668px\"\n              type=\"image/webp\"\n            />\n          <source\n            srcset=\"/static/faac9ad9f6c1c00e1f73891a544ea6c0/21521/screenshot.png 167w,\n/static/faac9ad9f6c1c00e1f73891a544ea6c0/86d36/screenshot.png 334w,\n/static/faac9ad9f6c1c00e1f73891a544ea6c0/74866/screenshot.png 668w,\n/static/faac9ad9f6c1c00e1f73891a544ea6c0/fcbaf/screenshot.png 895w\"\n            sizes=\"(max-width: 668px) 100vw, 668px\"\n            type=\"image/png\"\n          />\n          <img\n            class=\"gatsby-resp-image-image\"\n            src=\"/static/faac9ad9f6c1c00e1f73891a544ea6c0/74866/screenshot.png\"\n            alt=\"screenshot\"\n            title=\"screenshot\"\n            loading=\"lazy\"\n            decoding=\"async\"\n            style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n          />\n        </picture>\n    </span></p>\n<p>PNG の画質が荒かったりスタイルが洗練されていなかったり、生成される画像のクオリティでは Carbon に到底及びませんが、HTML の要素を画像に変換する PoC としては十分伝わると思います。</p>\n<p>ソースコードはこちらです。</p>\n<blockquote>\n<p>— <a href=\"https://codesandbox.io/s/objective-keller-vm2lz0ppql\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Generate image of syntax highlighted code from highlight.js - CodeSandbox</a></p>\n</blockquote>\n<h2 id=\"仕組み\" style=\"position:relative;\"><a href=\"#%E4%BB%95%E7%B5%84%E3%81%BF\" aria-label=\"仕組み permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>仕組み</h2>\n<blockquote class=\"twitter-tweet\" data-lang=\"en\"><p lang=\"ja\" dir=\"ltr\">シンタックスハイライトを画像化してみた。SVGのforeignObjectにstyleとハイライト後のHTMLを突っ込んでBlobに変換、blob://のままだとtaintedになりCanvasがPNG出力できないのでFileReaderでdata://に変換した&lt;img /&gt;をdrawImageしてconvertToBlobすれば、ブラウザだけでHTMLをPNGまで変換できる。 <a href=\"https://t.co/HAgziJhZmN\">pic.twitter.com/HAgziJhZmN</a></p>&mdash; れこ | 6/18 TS meetup #1 (@L_e_k_o) <a href=\"https://twitter.com/L_e_k_o/status/1128364583071014912?ref_src=twsrc%5Etfw\">May 14, 2019</a></blockquote>\n<script async src=\"https://platform.twitter.com/widgets.js\" charset=\"utf-8\"></script>\n<p>一言に詰め込むとこんな作りになっています。<br>\n１つ１つの要素について説明していきます。</p>\n<h3 id=\"シンタックスハイライトして-html-を作る\" style=\"position:relative;\"><a href=\"#%E3%82%B7%E3%83%B3%E3%82%BF%E3%83%83%E3%82%AF%E3%82%B9%E3%83%8F%E3%82%A4%E3%83%A9%E3%82%A4%E3%83%88%E3%81%97%E3%81%A6-html-%E3%82%92%E4%BD%9C%E3%82%8B\" aria-label=\"シンタックスハイライトして html を作る permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>シンタックスハイライトして HTML を作る</h3>\n<p>これはただの HTML+JS の操作です。<br>\n<a href=\"https://github.com/highlightjs/highlight.js/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">highlight.js</a>のドキュメントの通りにシンタックスハイライトを実行します。</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">import</span> hljs <span class=\"token keyword\">from</span> <span class=\"token string\">'highlight.js/lib/highlight'</span>\n<span class=\"token keyword\">import</span> typescript <span class=\"token keyword\">from</span> <span class=\"token string\">'highlight.js/lib/languages/typescript'</span>\n\n<span class=\"token comment\">// ハイライトしたい言語を登録しておく</span>\nhljs<span class=\"token punctuation\">.</span><span class=\"token function\">registerLanguage</span><span class=\"token punctuation\">(</span><span class=\"token string\">'typescript'</span><span class=\"token punctuation\">,</span> typescript<span class=\"token punctuation\">)</span>\n\n<span class=\"token keyword\">const</span> el <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">querySelector</span><span class=\"token punctuation\">(</span><span class=\"token string\">'#code code'</span><span class=\"token punctuation\">)</span>\nel<span class=\"token punctuation\">.</span>textContent <span class=\"token operator\">=</span> <span class=\"token string\">'コード'</span>\nhljs<span class=\"token punctuation\">.</span><span class=\"token function\">highlightBlock</span><span class=\"token punctuation\">(</span>el<span class=\"token punctuation\">)</span></code></pre></div>\n<p>単にハイライト済みの HTML 文字列を得たい場合は<code>highlightAuto</code>というメソッドが利用できますが、その場合得られた要素のサイズ（幅、高さ）を自前で計算する必要があります。</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">const</span> code<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token operator\">=</span> <span class=\"token string\">'コード'</span>\n<span class=\"token keyword\">const</span> html<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span> <span class=\"token operator\">=</span> hljs<span class=\"token punctuation\">.</span><span class=\"token function\">highlightAuto</span><span class=\"token punctuation\">(</span>code<span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span>value</code></pre></div>\n<p>抽象的に捉えるなら、要は HTML 形式の文字列と画像のサイズが手に入れば OK という話です。</p>\n<h3 id=\"html-を-svg-に変換する\" style=\"position:relative;\"><a href=\"#html-%E3%82%92-svg-%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B\" aria-label=\"html を svg に変換する permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>HTML を SVG に変換する</h3>\n<blockquote>\n<p>— <a href=\"https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><code>&#x3C;foreignObject></code> - SVG: Scalable Vector Graphics | MDN</a></p>\n</blockquote>\n<p>SVG の中で使える foreignObject という要素があります。この中には (X)HTML が書けます。<br>\nミニマムだとこのような SVG を文字列で生成することになります</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">const</span> parentEl <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">querySelector</span><span class=\"token punctuation\">(</span><span class=\"token string\">'#code'</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> codeEl <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">querySelector</span><span class=\"token punctuation\">(</span><span class=\"token string\">'code'</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> w <span class=\"token operator\">=</span> codeEl<span class=\"token punctuation\">.</span>clientWidth\n<span class=\"token keyword\">const</span> h <span class=\"token operator\">=</span> codeEl<span class=\"token punctuation\">.</span>clientHeight\n<span class=\"token keyword\">const</span> svg <span class=\"token operator\">=</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n&lt;svg\n  width=\"</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>w<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">px\"\n  height=\"</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>h<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">px\"\n  viewBox=\"0 0 </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>w<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\"> </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>h<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">\"\n  xmlns=\"http://www.w3.org/2000/svg\"\n>\n  &lt;foreignObject width=\"100\" height=\"50\">\n    </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>parentEl<span class=\"token punctuation\">.</span>outerHTML<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">\n  &lt;/foreignObject>\n&lt;/svg>\n</span><span class=\"token template-punctuation string\">`</span></span></code></pre></div>\n<p>ただし、この SVG を画像として表示させようとしても表示されないと思います。<br>\nforeignObject の中には<strong>X</strong>HTML を記述しないといけないため<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/XMLSerializer\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">XMLSerializer</a>を使用して XML のシリアライズします。<br>\nXMLSerializer には string ではなく Node(HTMLElement)を渡す必要があります。</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">const</span> serializer <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">XMLSerializer</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> svg <span class=\"token operator\">=</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n&lt;svg ...>\n  &lt;foreignObject ...>\n    </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>serializer<span class=\"token punctuation\">.</span><span class=\"token function\">serializeToString</span><span class=\"token punctuation\">(</span>parentEl<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">\n  &lt;/foreignObject>\n&lt;/svg>\n</span><span class=\"token template-punctuation string\">`</span></span></code></pre></div>\n<p>これで SVG 画像は描画できるようになったと思います。</p>\n<h3 id=\"svg-に-style-をつける\" style=\"position:relative;\"><a href=\"#svg-%E3%81%AB-style-%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B\" aria-label=\"svg に style をつける permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>SVG に style をつける</h3>\n<p>このままだとスタイルの当たってないただの HTML なので、スタイルを当てます。<br>\nforeignObject にはインライン CSS が書けます。</p>\n<p>例えば highlight.js のテーマ（CSS）を適用するならこんな感じにインライン化できます。\nXMLSerializer には string ではなく Node(HTMLElement)を渡す必要があるため、style タグを生成してシリアライズしています</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token keyword\">const</span> <span class=\"token function-variable function\">loadTheme</span> <span class=\"token operator\">=</span> name <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token function\">fetch</span><span class=\"token punctuation\">(</span><span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">https://unpkg.com/highlight.js@9.15.6/styles/</span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>name<span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">.css</span><span class=\"token template-punctuation string\">`</span></span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">then</span><span class=\"token punctuation\">(</span>\n    res <span class=\"token operator\">=></span> res<span class=\"token punctuation\">.</span><span class=\"token function\">text</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">const</span> css <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">loadTheme</span><span class=\"token punctuation\">(</span><span class=\"token string\">'dracula'</span><span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> style <span class=\"token operator\">=</span> document<span class=\"token punctuation\">.</span><span class=\"token function\">createElement</span><span class=\"token punctuation\">(</span><span class=\"token string\">'style'</span><span class=\"token punctuation\">)</span>\nstyle<span class=\"token punctuation\">.</span>textContent <span class=\"token operator\">=</span> css\n\n<span class=\"token keyword\">const</span> svg <span class=\"token operator\">=</span> <span class=\"token template-string\"><span class=\"token template-punctuation string\">`</span><span class=\"token string\">\n&lt;svg ...>\n  &lt;foreignObject ...>\n    &lt;div xmlns=\"http://www.w3.org/1999/xhtml\">\n      &lt;style>\n        // 独自スタイルもインラインで書ける\n      &lt;/style>\n      &lt;style></span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>serializer<span class=\"token punctuation\">.</span><span class=\"token function\">serializeToString</span><span class=\"token punctuation\">(</span>css<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">&lt;/style>\n      </span><span class=\"token interpolation\"><span class=\"token interpolation-punctuation punctuation\">${</span>serializer<span class=\"token punctuation\">.</span><span class=\"token function\">serializeToString</span><span class=\"token punctuation\">(</span>parentEl<span class=\"token punctuation\">)</span><span class=\"token interpolation-punctuation punctuation\">}</span></span><span class=\"token string\">\n    &lt;/div>\n  &lt;/foreignObject>\n&lt;/svg>\n</span><span class=\"token template-punctuation string\">`</span></span></code></pre></div>\n<p>SVG でもよければ、ここまでで終わります。<br>\nベクター画像なのでとても綺麗な反面、SVG は OGP の画像として使えなかったりと汎用性が高くないので、PNG 画像への変換に続きます。</p>\n<h3 id=\"canvas-に-svg-を描画する\" style=\"position:relative;\"><a href=\"#canvas-%E3%81%AB-svg-%E3%82%92%E6%8F%8F%E7%94%BB%E3%81%99%E3%82%8B\" aria-label=\"canvas に svg を描画する permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Canvas に SVG を描画する</h3>\n<p>SVG から PNG を得るために Canvas を使用します。</p>\n<p>Canvas に画像を render するには<a href=\"https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">drawImage</a>を使用します。ただし SVG の文字列をそのままは render できないので、作った SVG 文字列を<code>data:</code>の URL に変換し、 img タグに読み込ませ drawImage に与えます。</p>\n<p>DOM 依存の Canvas を使う必要はなく、オンメモリ上で計算ができる OffscreenCanvas を利用しています。</p>\n<div class=\"gatsby-highlight\" data-language=\"ts\"><pre class=\"language-ts\"><code class=\"language-ts\"><span class=\"token comment\">// `blob:`のままだとCanvasのCORS制限にひっかかってPNG出力できないので、`data:`に変換する</span>\n<span class=\"token keyword\">const</span> blobToDataUri <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>blobUri<span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span><span class=\"token builtin\">string</span><span class=\"token operator\">></span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">return</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\"><span class=\"token builtin\">Promise</span></span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">(</span>resolve<span class=\"token punctuation\">,</span> reject<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n    <span class=\"token keyword\">var</span> reader <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">FileReader</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n    reader<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'error'</span><span class=\"token punctuation\">,</span> reject<span class=\"token punctuation\">)</span>\n    reader<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'load'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n      <span class=\"token function\">resolve</span><span class=\"token punctuation\">(</span>reader<span class=\"token punctuation\">.</span>result<span class=\"token punctuation\">)</span>\n    <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n    reader<span class=\"token punctuation\">.</span><span class=\"token function\">readAsDataURL</span><span class=\"token punctuation\">(</span>blobUri<span class=\"token punctuation\">)</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">const</span> svgToDataUri <span class=\"token operator\">=</span> <span class=\"token punctuation\">(</span>svgStr<span class=\"token operator\">:</span> <span class=\"token builtin\">string</span><span class=\"token punctuation\">)</span><span class=\"token operator\">:</span> <span class=\"token builtin\">Promise</span><span class=\"token operator\">&lt;</span><span class=\"token builtin\">string</span><span class=\"token operator\">></span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> blob <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Blob</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">[</span>svgStr<span class=\"token punctuation\">]</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> type<span class=\"token operator\">:</span> <span class=\"token string\">'image/svg+xml;charset=utf-8'</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">return</span> <span class=\"token function\">blobToDataUri</span><span class=\"token punctuation\">(</span>blob<span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">const</span> dataUri <span class=\"token operator\">=</span> <span class=\"token keyword\">await</span> <span class=\"token function\">svgToDataUri</span><span class=\"token punctuation\">(</span>svg<span class=\"token punctuation\">)</span>\n<span class=\"token keyword\">const</span> img <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">Image</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\nimg<span class=\"token punctuation\">.</span>src <span class=\"token operator\">=</span> dataUri\nimg<span class=\"token punctuation\">.</span><span class=\"token function\">addEventListener</span><span class=\"token punctuation\">(</span><span class=\"token string\">'load'</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">const</span> canvas <span class=\"token operator\">=</span> <span class=\"token keyword\">new</span> <span class=\"token class-name\">OffscreenCanvas</span><span class=\"token punctuation\">(</span>w<span class=\"token punctuation\">,</span> h<span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">const</span> ctx <span class=\"token operator\">=</span> canvas<span class=\"token punctuation\">.</span><span class=\"token function\">getContext</span><span class=\"token punctuation\">(</span><span class=\"token string\">'2d'</span><span class=\"token punctuation\">)</span>\n  ctx<span class=\"token punctuation\">.</span><span class=\"token function\">drawImage</span><span class=\"token punctuation\">(</span>img<span class=\"token punctuation\">,</span> <span class=\"token number\">0</span><span class=\"token punctuation\">,</span> <span class=\"token number\">0</span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">const</span> pngBlob <span class=\"token operator\">=</span> canvas<span class=\"token punctuation\">.</span><span class=\"token function\">convertToBlob</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span>\n  <span class=\"token keyword\">const</span> pngObjectUri <span class=\"token operator\">=</span> <span class=\"token constant\">URL</span><span class=\"token punctuation\">.</span><span class=\"token function\">createObjectURL</span><span class=\"token punctuation\">(</span>blob<span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span></code></pre></div>\n<p><code>pngObjectUri</code>を img タグで表示すると、PNG 画像が表示されます。<br>\n画面に表示するのではなくファイルとして手に入れたい場合は、a タグをメモリに生成すればファイルダウンロードも可能です。</p>\n<blockquote>\n<p>— <a href=\"https://blog.leko.jp/post/how-to-download-with-a-tag-without-file-server/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">a タグの download 属性でサーバを介さずにファイルダウンロードする | WEB EGG</a></p>\n</blockquote>\n<p>これらすべてを含んだ最終的なソースコードはこちら（再掲）です。</p>\n<blockquote>\n<p>— <a href=\"https://codesandbox.io/s/objective-keller-vm2lz0ppql\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Generate image of syntax highlighted code from highlight.js - CodeSandbox</a></p>\n</blockquote>\n<p>以上の手順で HTML 要素を PNG まで変換できます。</p>\n<h2 id=\"webworker-でも動くのか\" style=\"position:relative;\"><a href=\"#webworker-%E3%81%A7%E3%82%82%E5%8B%95%E3%81%8F%E3%81%AE%E3%81%8B\" aria-label=\"webworker でも動くのか permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>WebWorker でも動くのか？</h2>\n<p>おそらく無理だと思います。少なくともこの記事を書いた時点では実現できませんでした。<br>\n処理の中で<code>new Image()</code>して SVG 画像をレンダリングした結果を canvas に渡しているのですが、<code>Image</code>は WebWorker には存在しない API なので、SVG を簡単にレンダリングする手段がありません。</p>\n<h2 id=\"応用範囲は広い\" style=\"position:relative;\"><a href=\"#%E5%BF%9C%E7%94%A8%E7%AF%84%E5%9B%B2%E3%81%AF%E5%BA%83%E3%81%84\" aria-label=\"応用範囲は広い permalink\" class=\"autolink-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>応用範囲は広い</h2>\n<p>本記事では仕組みの説明のためにシンタックスハイライトを題材に書きましたが、もっと抽象的な用途に応用できそうです。<br>\n要は HTML+インライン CSS を画像化できるので、例えば画面全体のスクリーンショットを撮れるはずです。</p>\n<p>画面内の<code>link[rel=\"stylesheet\"]</code>をかき集めてインライン化し、<code>&#x3C;style></code>タグをあわせて foreignObject の中に入れて、<code>document.body</code>を XMLSerializer に入れたらどうなるか試した結果、ある程度うまく行きました。</p>\n<p>動作デモはこちらです。<br>\nある程度複雑な UI を実現するために<a href=\"https://www.muicss.com/docs/v1/example-layouts/responsive-side-menu\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">MUI という CSS フレームワークのチュートリアル</a>を撮影対象にしました（数年ぶりに jQuery 書いた）</p>\n<blockquote>\n<p>— <a href=\"https://1uk01.codesandbox.io/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Capture screen &#x26; Download as PNG</a></p>\n</blockquote>\n<p>実際に生成される画像はこちらです。</p>\n<p><span\n      class=\"gatsby-resp-image-wrapper\"\n      style=\"position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 504px; \"\n    >\n      <span\n    class=\"gatsby-resp-image-background-image\"\n    style=\"padding-bottom: 122.1556886227545%; position: relative; bottom: 0; left: 0; background-image: url('data:image/svg+xml,%3csvg%20xmlns=\\'http://www.w3.org/2000/svg\\'%20width=\\'400\\'%20height=\\'490\\'%20viewBox=\\'0%200%20400%20490\\'%20preserveAspectRatio=\\'none\\'%3e%3cpath%20d=\\'M0%2026v25h401V0H0v26m207%2050v7H13v-5l-1-5v11h196V68l-1%208M80%20109c0%203%200%203-3%202-6-1-9%208-5%2013%202%202%202%202%205%201h3l2%201c2-2%201-20%200-20s-2%201-2%203m-66%208v9h4c9%200%2013-4%2010-8-2-1-2-2-1-4%202-4-1-7-8-7h-5v10m28-4c-2%202%200%203%202%201%203-1%205-1%205%202l-3%201c-4%200-6%202-6%205s3%205%206%203h5c2%202%204%200%202-1l-1-6-2-5c-2-2-6-2-8%200m59%200c-3%203-3%209%200%2011%205%204%2012%200%2011-7-1-6-8-8-11-4\\'%20fill=\\'%23d3d3d3\\'%20fill-rule=\\'evenodd\\'/%3e%3c/svg%3e'); background-size: cover; display: block;\"\n  ></span>\n  <picture>\n          <source\n              srcset=\"/static/d28cdf72a18d5678e869d9c4aedb7cbc/5251b/appendix.webp 167w,\n/static/d28cdf72a18d5678e869d9c4aedb7cbc/7390e/appendix.webp 334w,\n/static/d28cdf72a18d5678e869d9c4aedb7cbc/062aa/appendix.webp 504w\"\n              sizes=\"(max-width: 504px) 100vw, 504px\"\n              type=\"image/webp\"\n            />\n          <source\n            srcset=\"/static/d28cdf72a18d5678e869d9c4aedb7cbc/21521/appendix.png 167w,\n/static/d28cdf72a18d5678e869d9c4aedb7cbc/86d36/appendix.png 334w,\n/static/d28cdf72a18d5678e869d9c4aedb7cbc/08115/appendix.png 504w\"\n            sizes=\"(max-width: 504px) 100vw, 504px\"\n            type=\"image/png\"\n          />\n          <img\n            class=\"gatsby-resp-image-image\"\n            src=\"/static/d28cdf72a18d5678e869d9c4aedb7cbc/08115/appendix.png\"\n            alt=\"appendix\"\n            title=\"appendix\"\n            loading=\"lazy\"\n            decoding=\"async\"\n            style=\"width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;\"\n          />\n        </picture>\n    </span></p>\n<p>惜しい感じになりました。なぜかサイドバーが消えています…</p>\n<p>（WebWorker で動かないのでやや厳しいですが）ドラレコの要領で裏側で画面を撮影をしておきサーバに送れれば、ユーザの操作がリアルに見えて、ユーザテスト、アクセス解析、エラートラッキング etc に役立たちそうと思いました。マウスカーソルの座標を保持しておき、カーソルっぽい画像を Canvas に書き加えてから画像化すればマウスカーソル（を模したもの）を画像に写すこともできますし。</p>\n<p>やり方によっては悪用できそうなので、悪用はしないようお願いいたします。</p>","timeToRead":9,"frontmatter":{"title":"シンタックスハイライト済みのソースコードの画像をブラウザだけで作成したい","tags":["JavaScript","Canvas"],"date":"May 28, 2019","featuredImage":{"childImageSharp":{"fluid":{"tracedSVG":"data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='400'%20height='180'%20viewBox='0%200%20400%20180'%20preserveAspectRatio='none'%3e%3cpath%20d='M29%2029l-1%2062%201%2060h343V91l-1-62c-2-2-340-2-342%200'%20fill='%23d3d3d3'%20fill-rule='evenodd'/%3e%3c/svg%3e","aspectRatio":2.2266666666666666,"src":"/static/7ef5022542047c39e79f4e940ff571eb/8eab8/featured-image.png","srcSet":"/static/7ef5022542047c39e79f4e940ff571eb/1ec58/featured-image.png 334w,\n/static/7ef5022542047c39e79f4e940ff571eb/ccb4a/featured-image.png 668w,\n/static/7ef5022542047c39e79f4e940ff571eb/8eab8/featured-image.png 1336w,\n/static/7ef5022542047c39e79f4e940ff571eb/85e22/featured-image.png 2004w,\n/static/7ef5022542047c39e79f4e940ff571eb/a9ec1/featured-image.png 2672w,\n/static/7ef5022542047c39e79f4e940ff571eb/d1d3a/featured-image.png 3168w","srcWebp":"/static/7ef5022542047c39e79f4e940ff571eb/f7e47/featured-image.webp","srcSetWebp":"/static/7ef5022542047c39e79f4e940ff571eb/cd98f/featured-image.webp 334w,\n/static/7ef5022542047c39e79f4e940ff571eb/7535d/featured-image.webp 668w,\n/static/7ef5022542047c39e79f4e940ff571eb/f7e47/featured-image.webp 1336w,\n/static/7ef5022542047c39e79f4e940ff571eb/f6b67/featured-image.webp 2004w,\n/static/7ef5022542047c39e79f4e940ff571eb/f71bf/featured-image.webp 2672w,\n/static/7ef5022542047c39e79f4e940ff571eb/c44a1/featured-image.webp 3168w","sizes":"(max-width: 1336px) 100vw, 1336px"}}}}}},"pageContext":{"slug":"/convert-any-html-element-to-image/","previous":{"fields":{"slug":"/dynamic-import-tips-for-gatsby/"},"frontmatter":{"title":"GatsbyでReactコンポーネントをDynamic importしてCode Splitするwork around","tags":["JavaScript","Gatsby","React"]}},"next":{"fields":{"slug":"/jsconfeu2019/"},"frontmatter":{"title":"JSConf EU 2019に行ってきました","tags":["JavaScript","JSConf","Community"]}}}},
    "staticQueryHashes": ["2585454260","2954598359"]}