SVGでOGP画像を作りたいなと思い、いろいろ調べたときの備忘録。
SVGはまるのも多いけど、いろいろできるので、
SVGからOGP画像をつくるのいいかもしれない。
ちょろっとなるなら、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つを試した。
いろんな画像フォーマットに対応しているsharpがよさそう。
・sharp - High performance Node.js image processing
.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();
ユーザに情報を入力してもらった内容をSVGで表示して、
OGP画像にするという感じが多いので、こっちがメイン。
保存先のFirebase StorageがData URL形式に対応しているので、
PNGのData URLが取得できればOK。
大まかな流れは、こんな感じ。
<image>
を用意する<svg>
を文字列に変換<image>
にDataURL形式でSVGをセットして、読み込み開始// 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画像で保存できる(´ω`)
画像化する方法でhtml2canvasもあるけど、うまくいかなかった。。
スクロール位置や画面サイズによって、
うまく撮れるときと撮れない時があって、この方法に。。
この後出てくる小ネタ集でスタイルで装飾する方法を使ったところ、
うまくいかない感じに。。
svg内にsytleをもたせたらうまくいった(´ω`)
ブラウザ上 = 全体のCSSが適用
保存画像 = SVG配下のCSSのみ適用
なので、SVGだけで完結しないといけない...
フォントとかも指定しないと、見た目が変わってしまう。。
Canvasの仕様っぽく、外部リンクでCORSで引っかかると画像が表示されないよう...
SVGで完結するように、読み込んだ画像をDataURL形式で指定するようにしたらうまくいった(´ω`)
「SVGで完結」が大事らしい。。
<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>
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を設定しないといけない。
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;
});
}
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);
// ... 略
});
}
以上!!
積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!
積読ハウマッチは、Nuxt.js+Firebaseで開発してます!
もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ
要望・感想・アドバイスなどあれば、
公式アカウント(@MemoryLoverz)や開発者(@kira_puka)まで♪
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント