[Rails] OGP画像の作成をrmagickからimage_processing内のlibvipsに乗り換える

2023/04/19

Rails
libvips
image_processing

こんにちは。

みなさんは画像処理のgemはどれを使っていますか?

昔だとrmagick一強な印象がありましたが、最近だとimage_processingをRailsガイドも推奨するようになりました。

image_processingは、libvipsやimage magickという画像処理ライブラリをRubyで使えるようにしたものです。

また、image_processing内で使用するライブラリは選択可能で、libvipsかimage magickを選べます。

今回は、RailsでOGP画像を作成する方法として、rmagickからimage_processing内のlibvipsに乗り換える方法を紹介します。

単純にするために、Postモデルにtitleというカラムがあり、それを使用したOGP画像を作成するメソッドを#create_imageとします。

1. 既存のOGP画像作成

Dockerfile
# Dockerfile
FROM ruby:3.2.1-slim
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev vim git imagemagick
# 以下省略
Gemfile
# Gemfile
gem 'mini_magick'
app/models/post.rb
# app/models/post.rb
class Post < ApplicationRecord
  require 'mini_magick'

  OGP_BASE_IMAGE_PATH = './public/images/posts/ogp_base.png'.freeze
  OGP_GRAVITY = 'center'.freeze
  OGP_TEXT_POSITION = '0,0'.freeze
  OGP_FONT_PATH = './public/fonts/NotoSansJP-Bold.otf'.freeze
  OGP_FONT_SIZE = 70
  OGP_INDENTION_COUNT = 15
  OGP_ROW_LIMIT = 8
  OGP_TEXT_COLOR = 'white'.freeze

  def set_ogp_image
    image = create_image
    ogp_image.attach(io: StringIO.open(image.to_blob), filename: 'ogp.png')
  end

  private

  def create_image
    text = prepare_text(title.truncate(OGP_TRUNCATE_LENGTH))
    image = MiniMagick::Image.open(OGP_BASE_IMAGE_PATH)
    image.combine_options do |config|
      config.font OGP_FONT_PATH
      config.fill OGP_TEXT_COLOR
      config.gravity OGP_GRAVITY
      config.pointsize OGP_FONT_SIZE
      config.draw "text #{OGP_TEXT_POSITION} '#{text}'"
    end
    image
  end
end

これで、#set_ogp_imageを呼び出すと、OGP画像が作成され、ActiveStorageで管理されるようになります。

2. image_processingを使ったOGP画像作成

それでは、ここからはimage_processingを使ったOGP画像作成に乗り換えていきます。

Dockerfile
# Dockerfile
FROM ruby:3.2.1-slim
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev vim git imagemagick libvips # libvipsを追加
# 以下省略
Gemfile
# Gemfile

# mini_magickを削除し、image_processingを追加
gem 'image_processing'
app/models/post.rb
# app/models/post.rb
class Post < ApplicationRecord
  require 'image_processing/vips'

  OGP_BASE_IMAGE_PATH = './public/images/posts/ogp_base.png'.freeze
  OGP_FONT = 'Noto Sans JP 32'.freeze
  OGP_INDENTION_COUNT = 16
  OGP_TEXT_COLOR = [255, 255, 255].freeze
  OGP_TRUNCATE_LENGTH = 60
  OGP_ROW_LIMIT = 8
  OGP_DPI = 140

  def set_ogp_image
    image = create_image
    ogp_image.attach(io: StringIO.open(image.write_to_buffer('.png')), filename: 'ogp.png')
  end

  private

  def create_image
    base_image = Vips::Image.new_from_file(OGP_BASE_IMAGE_PATH)

    text_mask = Vips::Image.text(text, fontfile: OGP_FONT_PATH, font: OGP_FONT, dpi: OGP_DPI,
                                       width: base_image.width.to_i - 100, height: base_image.height.to_i - 100, align: 'centre')
    rgb = text_mask.new_from_image(OGP_TEXT_COLOR).copy(interpretation: 'srgb')
    text_image = rgb.bandjoin(text_mask)

    x = (base_image.width.to_i - text_image.width.to_i) / 2
    y = (base_image.height.to_i - text_image.height.to_i) / 2

    base_image.composite(text_image, 'over', x:, y:)
  end
end

これで、先ほどと同様#set_ogp_imageを呼び出すと、OGP画像が作成され、ActiveStorageで管理されるようになります。

3. 変更点の解説

まず触ってみた感想として、rmagickかなり親切だな…という感じでした。

そもそも、rmagickでは#drawで文字列を描画することができるのですが、libvipsでは.textで文字列から画像を作成し、それを重ね合わせるという方法で文字列を描画する必要があります。

この辺りのドキュメントも少なく、drawに変わるメソッドがないか探していましたが、見つからず、この方法で実装しました。

https://www.rubydoc.info/gems/ruby-vips/Vips/Image#text-class_method

そのほかにも、rmagickではたとえばOGP_GRAVITYで指定しているように、中央揃えの場合はcenterなどのように文字列で指定できるのでわかりやすいですが、libvipsではx, yの座標で指定する必要があります。

そのため、#create_image内で、text_imageのx, yの座標を計算しています。

なんとなくrmagickの方が全体的なドキュメントは豊富な印象ですが、libvipsの方が高速で、メモリを圧迫しないというメリットがある(らしい)ので、今後も使っていきたいと思います。
(ちゃんと計測しろよという話でもあるので、また別の機会に試してみます)

今回はこのあたりで。