こんにちは,ぽち@pchwです.
昨今Twitterに画像を付けて投稿をしてバズを生み出す的なサービスが増えたように思います.
自分も似たようなコンセプトで何個かサービスを出していて,
ta1usho.com
no1tweet.com
animiteru.com
作品タイトルで一番面白いこと言った奴、優勝などは,AWS Lambda上で用意した画像テンプレートに対して指定された文字を合成をする程度です.
実装的には,gmを使って合成し,Twitterに投稿しています.
しかし,Animiteruでは画像とタイトルをいい感じで並べて合成する必要があり,gmの合成で全てやるには少し荷が重い感じでした.
(とはいえ,画像間の隙間を計算したり,文字のサイズを適切に指定して帯の矩形を合成した上に配置して・・・ということをやれば可能です)
そこで,色々と模索したのを残しておきます.
まず,考えついたのがHTMLでなら簡単にレイアウト出来るということです.それを画像化出来れば,簡単にデザインの変更も出来ますし筋が良いように思えました.
しかし,それをサーバサイドで行うとなると,手法が限られました.
jsdom
などでHTML文字列からDOMを構築して,canvasに描画し画像化とする
- ヘッドレスブラウザ的なものでHTML文字列を解釈してキャプチャを撮って画像化する
といったところでした.
(1) に関しては,「canvasに描画し」というところでnode-canvas
がネイティブのモジュールcairo
を要求したりで,AWS Lambda上に構築するには容量とかその辺りでかなり厳しいものがありました.
(2) に関しても,AWS Lambda上でヘッドレスブラウザ的なものを使うようにするためには容量とかその辺りが厳しい.
つまり両方の手法ともに,AWS Lambdaにバンドルされているものでは実現出来ないため,パッケージを上げるモジュールの中に含める必要があり,そこには容量の制限があるため難しいという形になりました.
やはりgmで地道にレイアウトしかないのか・・・と思ったのですが,Animiteruは少し裏技的な方法で実現しています.
それは,画像の生成をサーバサイドではなくクライアントサイドで行うことです.
クライアントサイドにはdomを理解するものも,canvasも備えています.(当たり前ですが)
- クライアントサイドでレイアウトしHTMLを描画
- そのHTMLをcanvasに描画して画像を得る
- 投稿
というフローを取っています.
本当は,投稿のところでサーバを介せずクライアントサイドだけで出来れば良かったのですが,TwitterのAPIを叩く必要があり,ConsumerKey/Secretをクライアントサイドに埋め込むわけにはいかなかったので,投稿の部分はサーバサイドを介しています.
「クライアントでレイアウトしてHTMLを描画」の部分は特に言うことはありません.普通にHTMLを降らすかjsonで描画に必要なデータを降らせてクライアント側でHTMLを構築します.
Animiteruではjsonでデータを降らせて,HTMLを構築しています.見た目の部分はFoundationのグリッドレイアウトがベースです.
「HTMLをcanvasに描画して画像を得る」の部分はhtml2canvas
というモジュールを使っています.
import html2canvas from 'html2canvas'
html2canvas(captureTarget, {
logging: true,
useCORS: true
}).then( canvas => {
const image = canvas.toDataURL('image/jpeg', {quality: 0.5});
});
という感じです.
ここで障害になったのは,HTMLをCanvas化するときにimgタグは同じドメインでないとダメです.
Animiteru では,その制限を回避するためにAWS Lambdaを使って画像をプロキシするURLを用意しています.
severless.yml
で言うと,このあたり.
functions:
thumbnail:
handler: handler.thumbnail
timeout: 15
events:
- http:
path: /thumbnail/{text}
method: get
cors: true
「投稿」の部分は普通にTwitterに得られた画像を投稿しています.
上記に書いたように本当ならクライアントサイドで投稿が完結すればよかったのですが,文字投稿はポップアップウィンドウで投稿するみたいなことが出来ますが,画像はTwitter APIを使う必要があります.
media/upload
のAPIで画像を投稿し,得られたmedia_id_string
を伴ってstatuses/update
のAPIを叩きます.
// Twitterへ投稿
const Twitter = require('twitter');
const client = new Twitter({
consumer_key: conf.consumerKey,
consumer_secret: conf.consumerSecret,
access_token_key: user.accessToken,
access_token_secret: user.accessTokenSecret
});
const image = body.image.replace('data:image/jpeg;base64,', '');
const media = yield client.post('media/upload', {
media: new Buffer(image, 'base64')
});
const result = yield client.post('statuses/update', {
status: statusText,
media_ids: media.media_id_string
});
端折りましたが,こんな感じです.
AnimiteruではこのようにクライアントサイドでDOMを画像化するという手法を選びましたが,色々デメリットがあるのであまりオススメしません.
まず,クライアントサイドで行うために描画結果の見た目が安定しません.フォントが違っていたり,ブラウザによって余白などに差異が出てしまうこともあります.
canvasに画像を描画させるには同じドメインから画像を配信する必要があり,直接APIなどで取れる画像のパスを使えないケースがあります.
他にも,imgタグが正しく描画される前にcanvasに描画させるとimgタグの画像が描画されずに画像化されてしまったりします.
簡単なレイアウトであれば,gmで計算して画像合成していくほうが良かったですね.
なんか他にいい方法があれば教えてください! → @pchw