SVGでOGP画像を作りたいなと思い、いろいろ調べたときの備忘録。
SVGはまるのも多いけど、いろいろできるので、
SVGからOGP画像をつくるのいいかもしれない。
とりあえず、SVGを書いてみる
ちょろっとなるなら、HTMLでベタ書きしてみるのもいい。
動作確認とか楽ちん。
<html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </body> </html>
こんな感じ。
<rect>
で四角を書いて、<circle>
で丸を書いてる。
上から順に描画されるので、下にある方が前面にくる感じ。
なので、四角の上に丸が描画されてる。
<rect>
の座標(x,y)は左上の場所なので、
横幅と縦幅分の半分だけ、全体の中心からずらしてる。
画像化する
最終的にOGPで使うPNG画像にしたい。
画像化する方法について、ローカルとブラウザ上の2つを試した。
1. ローカルで画像化する
いろんな画像フォーマットに対応しているsharpがよさそう。
・sharp - High performance Node.js image processing
sharpでsvgをpngに変換してみる
まずは、.svgとして扱えるように、
xmlnsとかをちゃんとつけたsample.svgを用意する。
<!-- sample.svg --> <?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 1200 630" width="1200" height="630" fill="lightgray"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg>
パッケージをインストール
$ npm i sharp
変換するコードはこんな感じ。
// generate.js const sharp = require("sharp"); async function main() { await sharp("sample.svg") .png() .toFile("output.png"); } main().then();
そして、実行。
$ node generate.js
すると、output.pngというPNGファイルを作成してくれる。
ただ、svg
のstyleは反映してくれないので注意。
// generate.js const sharp = require("sharp"); async function main() { await sharp("sample.svg") .png() .toFile("output.png"); } main().then();
背景色をつける場合は、sharp側で設定すればOK
// generate.js const sharp = require("sharp"); async function main() { await sharp("sample.svg") .flatten({ background: "lightgray" }) // 背景色 .png() .toFile("output.png"); } main().then();
2. ブラウザ上のSVGを画像化する
ユーザに情報を入力してもらった内容をSVGで表示して、
OGP画像にするという感じが多いので、こっちがメイン。
保存先のFirebase StorageがData URL形式に対応しているので、
PNGのData URLが取得できればOK。
大まかな流れは、こんな感じ。
- Canvasを用意する
- SVGを読み込む
<image>
を用意する <svg>
を文字列に変換- 作成した
<image>
にDataURL形式でSVGをセットして、読み込み開始 - 読み込みが完了したら、Canvasに書き出して
- CanvasからDataURLを取得する
// svg2DataURL.ts /** * svgをpngに変換 * @param svgElement <svg>のHTML要素 */ export default function svg2DataURL( svgElement: HTMLElement ): Promise<HTMLCanvasElement> { return new Promise((resolve, reject) => { // 1. Canvasを用意する const canvas = document.createElement("canvas"); canvas.width = 1200; canvas.height = 630; const ctx = canvas.getContext("2d"); if (!ctx) { reject(Error("Create Canvas Error...")); return; } // 2. SVGを読み込む<image>を用意する const image = new Image(); image.decoding = "async"; image.onload = () => { // 5. 読み込みが完了したら、Canvasに書き出して、 ctx.drawImage(image, 0, 0, 1200, 630); // 6. CanvasからDataURLを取得する resolve(canvas.toDataURL()); }; image.onerror = e => reject(e); // 3. <svg>を文字列に変換 const svgXml = new XMLSerializer().serializeToString(svgElement); const svgData = btoa(unescape(encodeURIComponent(svgXml))); // 4. 作成した<image>にDataURL形式でセットして、読み込み開始 image.src = `data:image/svg+xml;charset=utf-8;base64,${svgData}`; }); }
あとは、好きなタイミングで呼び出せばOK。
Nuxt.jsでの例はこんな感じ。
<template> <div> <!-- document.getElementByIdできるように、idをつけておく --> <svg id="svg" viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </div> <div> <a class="button" @click="saveSVG">画像を保存</a> </div> </template> <script lang="ts"> import { Component, Vue } from "nuxt-property-decorator"; // 初期化済みのfirebaseインスタンス。詳細は略 import firebase from "~/plugins/firebase"; import svg2DataURL from "./svg2DataURL"; @Component() export default class SaveSvgPage extends Vue { // 画像を保存する処理 async saveSVG() { // svgのHTML要素を取得 const elm = document.getElementById("svg"); if (!elm) return; // さっきの処理: HTML要素からDataURLを取得 const dataURL = await svg2DataURL(elm); // Cloud Storage for Firebaseへ保存 const filePath = "...保存する先のパス..." const storage = firebase.storage(); const fileRef = storage.ref().child(filePath); await fileRef.putString(dataURL, "data_url"); } </script>
これでSVGがCloud Storage for FirebaseにPNG画像で保存できる(´ω`)
ハマったポイント
1. 画像化するときにhtml2canvasを使うといいかんじにならない
画像化する方法でhtml2canvasもあるけど、うまくいかなかった。。
スクロール位置や画面サイズによって、
うまく撮れるときと撮れない時があって、この方法に。。
2. ブラウザ上と保存した画像が違う
この後出てくる小ネタ集でスタイルで装飾する方法を使ったところ、
うまくいかない感じに。。
svg内にsytleをもたせたらうまくいった(´ω`)
ブラウザ上 = 全体のCSSが適用 保存画像 = SVG配下のCSSのみ適用
なので、SVGだけで完結しないといけない...
フォントとかも指定しないと、見た目が変わってしまう。。
3. 背景画像など外部リンクがあるとうまく保存できない...
Canvasの仕様っぽく、外部リンクでCORSで引っかかると画像が表示されないよう...
SVGで完結するように、読み込んだ画像をDataURL形式で指定するようにしたらうまくいった(´ω`)
「SVGで完結」が大事らしい。。
4. Nuxt.jsでbuildすると<style>
がきえる...
minifyするときになぜか<style>
が消えてしまう現象が...
<style>
だけのコンポーネントを作って、いれると大丈夫な感じ(´ω`)
小ネタ集
svg内でstyleが使える
<style>
タグがあるらしい。
classを設定して、いろいろできる。便利。
<html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <style> .rect { fill: green; } </style> <rect class="rect" x="600" y="315" width="100" height="100"/> </svg> </body> </html>
折り返し文字を表示してみる(foreignObject)
SVGにも<text>
があるけど、自動折り返しに対応していない。
文字数を計算して自分で分割すればできるけど、めんどくさい。。
文字の折り返ししたい場合、<foreignObject>
という
HTMLを追加できるのがあるので、それを使うといいらしい。
styleでfont-sizeとかも設定できるのでいろいろできそう(´ω`)
<html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <style> @import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&display=swap&subset=japanese"); .item { font-family: "Noto Sans JP", sans-serif; font-size: 60px; border: 2px solid green; } </style> <foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" width="200" height="200" x="500" y="215" > <div class="item"> こんにちは </div> </foreignObject> </svg> </body> </html>
こんな感じ
ただ、foreignObjectはサイズを自動計算してくれるわけではないので、
widthとheightを設定しないといけない。
画像を表示する(image)
SVGで画像を使うときは、<image>
を使う。
<img>
とは違う。
<html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <image xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" width="200" height="200" x="500" y="215" /> </svg> </body> </html>
こんな感じ。
ハマったポイントにも書いたとおり、このままCanvasに書き出すと、
保存時に画像が表示されないので、動的にDataURLを取得してセットするといい感じ。
<html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <image xlink:href="data:image/svg+xml;charset=utf-8;base64,...略" width="200" height="200" x="500" y="215" /> </svg> </body> </html>
URLからDataURLに変換するのは、こんな感じ。
// svg2DataURL.ts /** * URLをDataURLに変換 * @param 変換したい画像のURL */ export default function url2DataURL( url: string ): Promise<HTMLCanvasElement> { return new Promise((resolve, reject) => { // 1. Canvasを用意する const canvas = document.createElement("canvas"); canvas.width = 1200; canvas.height = 630; const ctx = canvas.getContext("2d"); if (!ctx) { reject(Error("Create Canvas Error...")); return; } // 2. SVGを読み込む<image>を用意する const image = new Image(); image.decoding = "async"; image.onload = () => { // 4. 読み込みが完了したら、Canvasに書き出して、 ctx.drawImage(image, 0, 0, 1200, 630); // 5. CanvasからDataURLを取得する resolve(canvas.toDataURL()); }; image.onerror = e => reject(e); // 3. 作成した<image>にURLでセットして、読み込み開始 image.src = url; }); }
画面サイズに合うように表示する(resize対応)
OGP画像としていい感じのサイズで保存したいけど、
表示するときは画面サイズにあった感じにしたい。。
svgにstyleが使えるので、リサイズ時にスケールを計算して、
transform: scale();
で縮小する感じにしてみた。
<template> <div id="svg-wrapper" class="svg-wrapper"> <svg class="svg-content" :viewbox="`0 0 ${svgWidth} ${svgHeight}`" :width="svgWidth" :height="svgHeight" :style="style"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </div> </template> <script lang="ts"> import { Component, Vue } from "nuxt-property-decorator"; @Component() export default class SvgPage extends Vue { private svgWidth: number = 1200; private svgHeight: number = 630; private scale: number = 1; mounted() { // マウント時にリサイズする this.$nextTick(() => this.handleResize()); // windowのresizeイベントのリスナーに登録して、 // 画面サイズが変わったら、スケールを再計算するようにする window.addEventListener("resize", this.handleResize); } beforeDestroy() { // 破棄されるときに、リスナーの登録を解除する window.removeEventListener("resize", this.handleResize); } // リサイズ用のスケールを計算する処理 private handleResize() { const elm = document.getElementById("svg-wrapper"); if (!elm) return; this.rect = elm.getBoundingClientRect(); this.scale = this.rect.width / this.svgWidth; } // **************************************************** // * computed // **************************************************** private get style() { // 計算したスケールで縮小するようにtransformを設定する return { transform: `scale(${this.scale})` }; } } </script> <style> svg { transform-origin: 0 0; } .svg-wrapper { position: relative; width: 100%; height: auto; } .svg-wrapper:before { content: ""; display: block; padding-top: 52.5%; /* 630 / 1200 x 100 */ } .svg-content { position: absolute; top: 0; left: 0; } </style>
ただ、このまま保存すると縮小されたままになるので、 deepコピーでクローンして、transformをクリアしてから書き出すようにする。
// svg2DataURL.ts /** * svgをpngに変換 * @param svgElement <svg>のHTML要素 */ export default function svg2DataURL( svgElement: HTMLElement ): Promise<HTMLCanvasElement> { return new Promise((resolve, reject) => { // deepコピーでクローン const elm = svgElement.cloneNode(true) as HTMLElement; // transformをクリア elm.style.transform = ""; // ... 略 // 3. <svg>を文字列に変換 // ※ transformを削除したelmでsvgの文字列を取得 const svgXml = new XMLSerializer().serializeToString(elm); // ... 略 }); }
以上!!
参考にしたサイト様
- SVG: Scalable Vector Graphics | MDN - 画像とキャンバスをオリジン間で利用できるようにする - HTML: HyperText Markup Language | MDN
- SVG: Scalable Vector Graphics | MDN - Node.js向け画像編集ライブラリSharp - kamoqq.info
- sharp - High performance Node.js image processing
- Does vue 2.0 break SVG foreignObject? - Get Help - Vue Forum
- Vueでsvgファイルをいい感じに扱う - じまろぐ
- svgにhtmlを組み込んで、テキストを折り返したりcanvasを使ったり - cocuh's note
- 文字レイヤーを支える技術 - pixiv inside
- VueコンポーネントでWindowサイズ変更検知&値取得 - Qiita
- 一発芸!SVGでHTMLを画像化する - Qiita
- SVG Fonts - SVG: Scalable Vector Graphics | MDN
- CSSの@font-faceでGoogle Fontsのwebフォントを利用する方法 | Free Style
- window.onresize - Web API | MDN
- CSSだけでアスペクト比を固定するテク - Qiita
- Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう - Qiita
- 画像生成してOGPに設定する - Qiita
- 【CSS】 テキストを折り返す方法!自動で改行・レスポンシブにも対応 | creive【クリーブ】