はじめに
タイトル通りです。
- 描画したいhtml要素をCSSごとSVGにする
- SVGをbase64かBlobにしてcanvasに描画する
といった手順になります。
canvasがSVGの描画に対応しているブラウザで動作します。
SVGImageElement
の描画はSafariが対応していないっぽいのでHTMLImageElement
として描画します。
CanvasRenderingContext2D API: drawImage: SVGImageElement as source image | Can I use…
canvasに描画できるとなにが嬉しいの?
クライアントサイドだけでhtml要素をラスター画像として出力できます。
プレビューはhtml要素で表示して、保存するときだけcanvasに描画して画像として出力するなどが可能になります。
canvasはブラウザごとの差異が大きく(テキストの描画など)、コードが煩雑になることが多いです。
なのでここの部分をすべてhtml要素で完結出来るのはかなり嬉しいと思います。
動作サンプル
実際の動作を確認したいと思うので先にサンプルを置いておきます。
ソースコードと解説
canvasにSVGを描画した場合、SVGに含まれる外部リソースはロードされません。
なので画像やフォントなどはbase64にして直接埋め込む必要があります。
SVGにする部分
- 対象となるHTMLElementを
cloneNode
でコピーして画像を全てbase64に変換します。 XMLSerializer.serializeToString
を使ってシリアライズします。- SVG内に
<foreignObject>
を使ってシリアライズしたHTML(XHTML)を埋め込みます。
画像を埋め込む場合は同じホストかaccess-control-allow-origin: *
である必要があります。
access-control-allow-origin: *
の画像を埋め込む際は<img>
のcrossorigin
属性を"anonymous"
にする必要があります。
(jsではImage.crossOrigin
)
ソースコードでは画像はロード済みと仮定しています。
そうでない場合は間にImage.onload
などを挟んでください。
// HTMLElementとCSSをSVGに埋め込んで返す
function elementToSvgText($target, cssText) {
const $clone = $target.cloneNode(true);
const $targetImages = $target.querySelectorAll("img");
const $cloneImages = $clone.querySelectorAll("img");
for (let i=0; i < $targetImages.length; i++) {
$cloneImages[i].src = imageToBase64($targetImages[i]);
}
const clientRect = $target.getBoundingClientRect();
const width = clientRect.width;
const height = clientRect.height;
const xmlText = new XMLSerializer().serializeToString($clone);
return `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<foreignObject width="100%" height="100%" requiredExtensions="http://www.w3.org/1999/xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
<style>
${cssText}
</style>
${xmlText}
</div>
</foreignObject>
</svg>
`;
}
// 画像をbase64にして返す
function imageToBase64($img, mimeType="image/png") {
const $canvas = document.createElement("canvas");
$canvas.width = $img.naturalWidth;
$canvas.height = $img.naturalHeight;
const ctx = $canvas.getContext("2d");
ctx.drawImage($img, 0, 0);
return $canvas.toDataURL(mimeType);
}
CSSに外部リソースが含まれている場合
@font-face
やbackground-image
のurl(...)
をbase64にして埋め込みます。
// CSS内のurlをbase64にして埋め込む
async function replaceUrlToBase64(cssText) {
const regex = /url\s*\(["']*(.+?)["']*\)/g;
const promises = [];
cssText.replace(regex, async (match, url) => {
const promise = fetchFileBase64(url)
.then(base64 => `url(${base64})`);
promises.push(promise);
});
const data = await Promise.all(promises);
return cssText.replace(regex, () => data.shift());
}
// ファイルを取得してbase64で返す
async function fetchFileBase64(fileUrl) {
const req = new Request(fileUrl);
const res = await fetch(req);
if (!res.ok) {
throw new Error(`fetch error ${res.status}`);
return;
}
const mimeType = res.headers.get("content-type").split(";")[0];
const aBuff = await res.arrayBuffer();
let binary = "";
const bytes = new Uint8Array(aBuff);
const length = bytes.byteLength;
for (let i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return `data:${mimeType};base64,${window.btoa(binary)}`;
}
参考
- XMLSerializer - Web API | MDN
- <foreignObject> - SVG: Scalable Vector Graphics | MDN
- CanvasRenderingContext2D.drawImage() - Web APIs | MDN
- Allowing cross-origin use of images and canvas - HTML: HyperText Markup Language | MDN
おわりに
iOS 15.1のSafariでも動作することを確認。
ただ数回描画しなおさないと画像やフォントが読み込まれない場合があり要検証。
実際に導入する場合は動作確認をしっかりしないとダメ。
OSS化検討中・・・