RMagickを使って生成した画像をHerokuで表示する2つの方法

Herokuは、単体ではファイルをサーバー上に永続的に保存することはできない1ので、RMagickを使って画像を生成してもそれを保存することはできない。ただし、生成した画像をその場で単に表示するだけの目的であれば、永続的に保存する必要はないので、次の2つの方法でRMagickで生成した画像を利用できる。

  • 生成した画像を保存せずにバイナリデータを直接表示する方法
  • 生成した画像を一時保存フォルダに保存して表示する方法

生成した画像を保存せずにバイナリデータを直接表示する方法

生成したRMagickのImageオブジェクトを、to_blobメソッドを使って、直接メモリー上にバイナリデータとして展開し、それを出力する方法。これはメモリーの消費量が多そう。毎回1度だけ表示するようなジェネレータ系には適しているかも。この方法のサンプルコードが以下。

require 'rubygems'
require 'sinatra'
require 'RMagick'
text = 'Hello, World'
size = 30
get '/pic1' do
  blob = create_pic
  content_type "image/png"
  blob  #=> バイナリデータを直接表示
end
def create_pic
  img = Magick::Image.new(400, 300) { self.background_color = '#336699' }
  img.format = 'png'
  draw = Magick::Draw.new
  draw.annotate(img, 0, 0, 50, 100 + size, text) do
    self.font = 'Verdana-Bold'
    self.fill = '#FFFFFF'
    self.align = Magick::LeftAlign
    self.stroke = 'transparent'
    self.pointsize = 30
    self.text_antialias = true
    self.kerning = 1
  end
  img.to_blob  #=> バイナリデータ化
end

生成した画像を一時保存フォルダに保存して表示する方法

もうひとつの方法は、こちらの記事で知ったのだけど、Herokuでは/tmpフォルダのみ一時保存フォルダとして利用することが可能で、そこにはファイルを書き出せるとのこと。実際にやってみたら出来た。一時保存なので時間が経過すると消去される。消去された画像にアクセスしようとすると、Internal Server Errorが起きた覚え。

/tmpフォルダのファイルが消去される条件は、自分で試してみた2限りでは、アプリがスピンダウン状態になると消去されるっぽくて、そのスピンダウン状態には、最後にサイトにアクセスがあってから一定時間が経過するとなる。その一定時間は正確にはわからないけど、感覚的には10~30分くらいかな?ここらへん、詳しい人がいたら教えてほしい。というわけで、生成した画像を/tmpフォルダに一旦書き出し、それを再度読み込んで表示するというサンプルコードが以下。

require 'rubygems'
require 'sinatra'
require 'RMagick'
text = 'Hello, World'
size = 30
get '/pic1' do
  path = create_pic
  file = File.open(path, "rb") {|f| f.read }  #=> 一時保存したパスから画像を読み込み
  content_type "image/png"
  file  #=> 読み込んだ画像を表示
end

def create_pic
  img = Magick::Image.new(400, 300) { self.background_color = '#336699' }
  img.format = 'png'
  draw = Magick::Draw.new
  draw.annotate(img, 0, 0, 50, 100 + size, text) do
    self.font = 'Verdana-Bold'
    self.fill = '#FFFFFF'
    self.align = Magick::LeftAlign
    self.stroke = 'transparent'
    self.pointsize = 30
    self.text_antialias = true
    self.kerning = 1
  end
  path = './tmp/temp.png'  #=> /tmpフォルダ内(配下)じゃないと書き込めない
  img.write(path)  #=> 一時保存フォルダに一旦書き出し
  path  #=> 一時保存したパスをリターン
end

どっちの方法がいいかは利用する状況によると思うけど、生成した画像に複数回アクセスするのであれば、後者のほうが/tmpフォルダに保存された画像が存在する限りはそれを使いまわせるので、リソースの消費は抑えられそう。ただし、ファイルの書き込みと読み込みのオーバーヘッドが発生するので、1回目の処理(画像生成部分)は遅くなりそう。全部憶測なのは、こういう場合のベンチマークの取り方を知らないから…。もし、上記以外にも良い方法があれば教えてください。


Rubyでtempfileのエンコーディングを指定する。 - このブログは証明できない。

  1. Heroku単体ではなく、Amazon S3とか別のサービスを併せて利用すれば可能みたいだけど、使ったことないので詳しくはわからない

  2. 正確ではない可能性が大