この記事はDenoのカレンダーの2日目の記事です
Deno+CanvasでPowerline風のOGP画像を動的に設定できるアプリケーションを作ったのでその紹介です
Powerline風のOGP生成ツール
イメージ
主にOGP用途で、こんな画像を返すサーバを実装した
画像Generator
terminal-image.deno.dev/generator
https://terminal-image.deno.dev/generatorterminal-image.deno.dev
GitHub
背景
本ブログ以外にも自前でブログを持っていてそちらのOGP画像が欲しいと思っていた
ブログのOGPなので文字列だけ動的に入れ込めるようにしたい
どうせならタグとかも載せたい
Powerlineが好きなのでPowerline風にしたい
動的な処理が必要なのでサーバが必要だがDenoであればDeno Deployでサクッとデプロイできる
これは簡単に作れそうでは?
という感じで作ってみた
使い方
GeneratorでURLを生成できるのでそれをmetaタグのOGP設定に指定するだけ
タイトルやTagなど動的に変更したい箇所は適宜パラメータを調整する
カスタマイズ
書き始めたときはpng画像を返すサーバの実装のみだったがコード書いていくうちにあれこれパラメータとして渡したい欲求にかられ結局ある程度自由にパラメータを渡せるようにした
そこまで作り込んだら次はGeneratorを作ってカスタマイズできるようにしようということでこちらも作った
独自の色味を作れる
いくつか馴染みのあるテーマ風のテンプレートを用意したので以下に例を載せる
実装
ここからは実装内容からいくつか絞って取り上げる
大したことはしていない…
Canvas
DenoでCanvasを扱うためのモジュールがあるのでそれを使う
https://deno.land/x/canvas@v1.4.1deno.land
- Deno.serveでCanvasから画像レスポンスを返すサンプルコード
import { createCanvas } from "https://deno.land/x/canvas@v1.4.1/mod.ts"; const port = 8080; const handler = async (request: Request): Promise<Response> => { const [width, height] = [1200, 630]; const canvas = createCanvas(width, height); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#0000FF"; ctx.fillRect(0, 30, 1200, 80); const headers = new Headers(); headers.set("content-type", "image/png"); const response = new Response(canvas.toBuffer(), { headers: headers, status: 200, }); return response; }; Deno.serve({ port }, handler);
createCanvas
を使用してCanvasのコンテキストを生成
ctx.fillStyle
やctx.fillRect
など普通にCanvasをいじるときと同様の操作をする
サンプルでは適当な線を引かせてみた
canvas.toBuffer()
の結果をResponseに含めて画像のレスポンスを返す
deno run
でローカルサーバを立ててアクセスしてみる
こんな感じ
日本語対応フォント
Canvas上で日本語を表示させるための処理が必要
特にフォント指定しない場合
このように日本語は表示されない
NotoSansCJKのフォントを取得してcanvas.loadFont
でフォントを読ませるようにした
このloadFont
はDenoのモジュールが提供しているもののよう
family
で指定した文字列をctx.font
で指定する
- NotoSansCJKフォントを読み込んでCanvasで表示させるためのサンプルコード
import { createCanvas } from "https://deno.land/x/canvas@v1.4.1/mod.ts"; const port = 8080; const handler = async (request: Request): Promise<Response> => { const [width, height] = [1200, 630]; const canvas = createCanvas(width, height); const ctx = canvas.getContext("2d"); const font = await Deno.readFile("./NotoSansCJK-Regular.ttc"); canvas.loadFont(font, { family: "notosans", }); ctx.font = "50pt notosans"; ctx.fillStyle = "#00DDDD"; ctx.fillText("日本語のタイトル", 50, 300) const headers = new Headers(); headers.set("content-type", "image/png"); const response = new Response(canvas.toBuffer(), { headers: headers, status: 200, }); return response; }; Deno.serve({ port }, handler);
いい感じ
分かち書き
長いタイトルの場合、変な箇所で区切られてしまうのは困る
単語区切りのちょうどよいところで改行させたい
分かち書きといえばMeCabなどが有名だが、実装時はtiny-segmenterというライブラリを使った
- 辞書なし
- わずか25kb
- 文字種情報と組み合わせを特徴量として学習、分類
と言った特徴がある
作ったときは知らなかったが、今実装するならbudouxにすると思う
budoux/javascript at main · google/budoux
確か同じようなコンセプトだったはず、どこかで差し替えたい
tiny-segmenterはdenoには公開されていないがnpmとしては公開されているのでesm.sh
で読み込める
- tiny_segmenter_sample.ts
import TinySegmenter from "https://esm.sh/tiny-segmenter@0.2.0"; const title = "Slack APIで特定チャンネルの会話履歴を取得する"; const segmenter = new TinySegmenter(); const segments = segmenter.segment(title); console.log(title);
- 実行結果
$ deno run tiny_segmenter_sample.ts [ "Slack", " ", "API", "で", "特定", "チャンネル", "の", "会話", "履歴", "を", "取得", "する" ]
うまく分割できた
マルチバイトの文字列幅
改行位置を特定するということはタイトルの文字列の長さを把握して描画領域の幅と比較し、タイトルが上回る場合は改行させるという処理が必要
テキストの長さの情報が必要となる
Canvasで文字列幅を取得するにはctx.measureText("文字列")
で可能だが、モジュールのREADMEに「asciiのみ対応」と書いてあり、実際に試すとa
でもあ
でも同じ文字列幅が返ってきた…
なのでワークアラウンド感はあるが、ブラウザ側でCanvasを用意し実際にいくつか測ってみて、ブラウザ側(Generator)ではそのままmeasureText
の値を使用、サーバ側では特定の比率分だけ調整するというような処理をいれた
- measureTextが機能するかチェックする関数
const measuredCorrectly = (ctx: CanvasRenderingContext2D) => { return ctx.measureText("あ").width !== ctx.measureText("a").width; }
これで判断することにした
Generator
この画面の話
パラメータを弄ってサーバにアクセスして確認して…というフローが面倒だなと感じ始め、プレビューできる画面も欲しくなったので作った
簡単なhtmlを用意してserveしているだけ
- index.htmlファイルをserveするサンプル(一部抜粋)
import { serveFile } from "https://deno.land/std@0.208.0/http/file_server.ts"; const url = new URL(request.url); if (url.pathname.startsWith("/generator")) { return await serveFile(request, `${Deno.cwd()}/static/index.html`); }
パラメータを弄ったらリアルタイムにプレビューが変わるが、サーバ側のロジックをフロントでも流用している
deno_emit
のbundle
で複数ファイルをトランスパイル、一部加工して一つのapp.js
にまとめる処理を書いて、htmlからはapp.js
を読む
デプロイ時にはapp.js
などの成果物も含めてデプロイすることで静的に配信している
denoland/deno_emit: Transpile and bundle JavaScript and TypeScript under Deno and Deno Deploy
ワークアラウンドもりもりなので一応リンクだけ…
deno-terminal-image/bundle.ts at main · swfz/deno-terminal-image
まとめ
Deno+CanvasでPowerline風OGP生成ツールを作った
- Canvasで画像レスポンスを生成
- 日本語対応フォントを読ませ日本語に対応
- 分かち書きを用いて適切な改行位置を特定
感想としては
- Deno deployが手軽で良い
- サクッとサーバが必要なものを試せるのがすごく楽
- npmの資産も活かせる
- Canvasの操作は結局たいへん
何かサクッと試したいなと思ったときに選択肢の1つとして良いなと感じた
この記事書いてたら、ここまでやるならFreshで書き直したほうがよい?プレビューはサーバ側に直接リクエストしてもよい?と思えてきたのでどこかでやりたい
最後に、もう一度Generatorのリンクを置いておくので良ければ使ってみてください
terminal-image.deno.dev/generator
https://terminal-image.deno.dev/generatorterminal-image.deno.dev