OGP画像やバナー画像を自動生成するときの改行問題をなんとかする
タイトルや作者の名前が出るだけの OGP 画像?SVG で作って PNG に変換するだけじゃん。自動化してやんよ
カチャカチャカチャ…ッターン!
………。^^;
OGP 画像を自動生成したい
Slack や SNS に URL を貼り付けて投稿したとき、OGP 画像がないとちょっと寂しいですよね。しかしブログのようなページをたくさん作るサイトだと、いちいち画像編集ソフトを立ち上げて作成するのは面倒です。面倒なのでスクリプトを書きます。
おおまかな方針としては以下の流れです
スクリプトはこんな感じ
const { createSVGWindow } = require("svgdom");
const { SVG, registerWindow } = require("@svgdotjs/svg.js");
const sharp = require("sharp");
const fs = require("fs");
// パラメータたち
const templateFileName = "template.svg";
const outputFileName = "output.png";
const title = "OGP画像やバナー画像を自動生成するときの改行問題をなんとかする";
const size = 80;
const lineHeight = size * 1.3
const main = async () => {
// svg.js を使うための準備
const window = createSVGWindow();
const document = window.document;
registerWindow(window, document);
const draw = SVG(document.documentElement);
// template.svg を読み込んで id="title" の要素に title を流し込む
const svgContent = fs.readFileSync(templateFileName, "utf-8");
draw.svg(svgContent);
const text = SVG("#title")
.text(title)
.font("size", size)
.font("anchor", "middle")
.font("family", "'Helvetica, メイリオ'")
.attr({ dy: "0.3em" })
// PNG 形式に変換する
const svgText = draw.svg();
const svgBuffer = Buffer.from(svgText);
await sharp(svgBuffer)
.resize(1280, 720)
.png()
.toFile(outputFileName);
};
main();
SVG のテンプレートはシンプルにグラデーションのついた外枠とタイトルのみのものを用意しました
<?xml version="1.0"?>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1280 720"
>
<defs>
<linearGradient id="Gradient1" gradientTransform="rotate(90)">
<stop offset="0%" stop-color="#0C1DB8" />
<stop offset="90%" stop-color="#C8699E" />
<stop offset="100%" stop-color="#FCC5E4" />
</linearGradient>
</defs>
<rect x="30" y="30" width="1220" height="660" fill="#FFF" stroke="url(#Gradient1)" stroke-width="60" />
<text id="title" x="640" y="360" dominant-baseline="central">ここにタイトル</text>
</svg>
テキストが改行されない
そして生み出されたのが冒頭の画像です。
どうやら SVG の Text ノードには折り返しがないようです。
幸い、SVG.js では改行文字を入れると tspan と dy 属性を駆使して SVG での改行を実現してくれるようです。なのでいい感じのところに改行文字を入れます。
すぐに思いつくアプローチとして、一行あたりの文字数を決めてその文字数ごとに改行文字を挿入するとう方法があります。
この記事のタイトルを15文字ごとに改行してみましょう
OGP画像やバナー画像を自動生
成するときの改行問題をなんとか
する
ちょっとかっこ悪いですね。日本語は英語と違って単語間に空白がないのでいい感じの位置で改行するのが難しいです。
救世主 BudouX
自力では難しそうだったので何かないものかと探していると、素晴らしいものを見つけました
BudouX は Google が開発するオープンソースの日本語・中国語の改行位置を推定してくれるライブラリで、Python, JAVA, JavaScript に対応しています。
早速試してみます
const { loadDefaultJapaneseParser} = require("budoux");
const title = "OGP画像やバナー画像を自動生成するときの改行問題をなんとかする";
const parser = loadDefaultJapaneseParser();
const parsed = parser.parse(title);
console.log(parsed); // [ 'OGP画像や', 'バナー画像を', '自動生成する', 'ときの', '改行問題を', 'なんとか', 'する' ]
文節で区切れていますね。あとはこれを3行以下にするだけです。
3行にする
3行にすると言っても適切な方法が思いつかなかったので、素朴にやっていきます。
アプローチとしては以下の流れです
- 総文字数の1/3を目標文字数とする
- 改行候補位置を手前から見ていく
- 次の改行位置の方が目標から遠いなら改行する
- 改行文字が2個挿入されたら終了
function balanceStrings(inputArray, maxLines) {
// 最大行数以下ならそのまま返す
if (inputArray.length <= maxLines) return inputArray;
// 1行あたりの文字数の平均を計算
const totalLength = inputArray.reduce((acc, str) => acc + Array.from(str).length, 0);
const averageLength = totalLength / maxLines;
const result = [];
let currentPart = '';
let splitCount = 0;
inputArray.forEach((str) => {
const currentPartLength = Array.from(currentPart).length;
if (splitCount >= maxLines - 1 || Math.abs(averageLength - currentPartLength) > Math.abs(averageLength - (currentPartLength + Array.from(str).length))) {
// 次の改行位置の方が平均文字数に近づく
currentPart += str;
} else {
// 次の改行位置だと平均文字数から遠ざかる
result.push(currentPart);
currentPart = str;
splitCount++;
}
});
if (currentPart !== '') {
result.push(currentPart);
}
return result;
}
この処理を入れて生成した画像がこちらです。
おわりに
SVG なら楽勝じゃんと思って舐めてました。ブラウザって優秀なんだなと改めて思いました。今度やるならヘッドレスブラウザからスクリーンショットを撮るというアプローチを試してみたいです。
Discussion