ほぼ日刊サービス開発日誌

React, firebase, 機械学習など

sponsored

サーバーサイドjavascript・canvasでpng画像をリアルタイム描画する仕組み

これは以下の記事のつづきです。

www.daily-dev.net

これは https://text2ogp.com から、 もう一つ別に立てた画像生成サーバーにリクエストを送って、返却されたpng画像をプレビュー画像として表示するという仕組みで作っています。

将来的に自分が色々なサービスを作ることになったときに、中心的な画像処理サーバーにしたいなという気分でこういう構成にしました。

画像生成サーバー

サーバーサイドでcanvas画像を作成するところが今回のポイントです。 node-canvasというライブラリの2系を利用させてもらいました。

https://github.com/Automattic/node-canvas

※2系の利用は npm install canvas@next で可能です。

node-canvasはcairoという描画ライブラリをバックエンドとしています。

こいつがやや曲者で、cairoをスムーズに入れるため、自由度の低いAWS Lambda や herokuなどを使わず、EC2のubuntuの無料インスタンスを選びました。

デプロイ自動化は pm2のecosystemを使うと便利でした。

詳しくはこちらにて👇

Node.jsのWebアプリケーションの自動デプロイ【pm2のecosystem】

描画

まず描画領域を決めてコンテクストを定義します。

const canvas = createCanvas(1200, 630)
const ctx = canvas.getContext('2d');

フォントの読み込み

ubuntuに欲しいフォントは無い気がしたのでttfファイルをgoogle fontから落として読み込んでいます。

const { createCanvas, loadImage, registerFont } = require('canvas')

function fontFile(name) {
    return path.join(__dirname, '/fonts/', name)
}

registerFont(fontFile('NotoSerifJP-Bold.otf'), { family: 'noto' })

ctx.font = '50px noto'; // これでnotoが使えます!実際にはここはパラメータになっています。

画像の読み込み

const { createCanvas, loadImage, registerFont } = require('canvas')

function bgFile(name) {
    return path.join(__dirname, '/bg/', name)
}

loadImage(bgFile('fabian-grohs-597395-unsplash.jpg')).then(img => {
            ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
});

グラデーション

my_gradient = ctx.createLinearGradient(0, 0, 800, 0);
my_gradient.addColorStop(0, "#8E54E9");
my_gradient.addColorStop(1, "#4776E6");
ctx.fillStyle = my_gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);

文章を改行しつつ、真ん中に表示する

改行とセンタリングがやっかいです。普通のhtmlみたいに、折り返して次の行に行くということを自動でやってくれませんから、座標(x,y)どこから次の行を描画するかを指示しなくてはいけません.......。真ん中の位置も計算しないといけない。

// lines = ['やってみた', '改行を。']
ctx.textAlign = "center";
fixStartHeight = (canvas.height - fontSize * lineHeight * lines.length) / 2
    for (lines, i = 0, l = lines.length; l > i; i++) {
        var line = lines[i];
        var addY = fontSize;
        // 2行目以降の水平位置は行数とlineHeightを考慮する
        if (i) addY += fontSize * lineHeight * i;
        // fillText('text', x:左から, y:上から)
        ctx.fillText(line, canvas.width / 2, fixStartHeight + addY);
        console.log('fill text')
    }

これでいけました。これでいけたように見えたんだけどこれでいいのかな?lineHeight, fontSizeは文字数により変化させるので変数にしてます。

生成したcanvasオブジェクトをpngとして返す

res.setHeader('content-type', 'image/png');
res.writeHead(200, { 'Content-Type': 'image/png' })
canvas.toBuffer(function (err, buffer) {
        res.write(buffer)
        res.end()
})

CORS

text2ogp.comからプレビュー画像の表示が出来るようにAccess-Control-Allow-Originヘッダをセットします。

res.header("Access-Control-Allow-Origin","https://text2ogp.com");

SSL化

さて、こちらにリクエストを送ってtext2ogp.comで画像をダウンロードするためには、この画像生成サーバも独自ドメインでSSL化しなくてはなりません(ブロックされてしまう)。

なるべくお金を使わないため、letsencrypt を使おうかと思ったけど、更新トラブルがあったら面倒なので、サーバー証明書は自動更新してくれるAWS Certificate Managerで発行しました。こちらも無料です。

text2ogp.com 本体

ogp_of_text2ogp.png

概要

ユーザーにフォントやテキスト編集、背景選択をおこなうインターフェースを提供し、 画像生成サーバーにURLでパラメータを渡す役割を持っています。

具体的には ?string=あああ&font=a&color=3 のような形でパラメータを渡しています。

テキストは改行文字や記号などを含むので、パラメータはencodeURI()してから渡すのを忘れないようにします。

クリックしてリモートの画像生成サーバーの画像をダウンロードする

ダウンロードボタンを押したときに、発火し、文章内容、fontやcolorのパラメータ付きでリモートにURLを渡してあげます。

function forceDownload(url, fileName) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.responseType = "blob";
    xhr.onload = function () {
        var urlCreator = window.URL || window.webkitURL;
        var imageUrl = urlCreator.createObjectURL(this.response);
        var tag = document.createElement('a');
        tag.href = imageUrl;
        tag.download = fileName;
        document.body.appendChild(tag);
        tag.click();
        document.body.removeChild(tag);
    }
    xhr.send();
}

デプロイ

手っ取り早く公開したかった&ソースコードが公開されても問題がなかったので、nowというサービスの無料のOSSプランを使ってデプロイしました! $now コマンドでカレントディレクトリのnodeプロジェクトをデプロイできるというものなのですが、毎回ユニークなURLを生成してデプロイしてくれるので、そのURLでデバッグして、正しく動くようなら $now alias {自動生成URL} text2ogp.comのような形で独自ドメインに紐付けられて便利です。HTTPS化もデフォルトでなされています。

詳しくはこちらにて👇 【3分デプロイ】nodeプロジェクトをデプロイする最速の手段・now【登録から独自ドメインまで】

以上、テキストを画像化する方法、それをちょっとしたAPIサーバーにする方法についての雑なメモでした。

そのうち、この実装を使ったサービスを公開する予定なのでよろしくおねがいします。