💭

n番煎じなRMagickを使ったOGP生成|Offers Tech Blog

2022/10/06に公開

こんにちは、Offers を運営している株式会社 overflow のバックエンドエンジニアの shun です。今回は n 番煎じにはなりますが、OGP 画像の生成方法について書きたいと思います。

OGPとは

OGP とは「Open Graph Protcol」の略で、Facebook や Twitter などの SNS でシェアした際に、設定した WEB ページのタイトルやイメージ画像、詳細などを正しく伝えるための html 要素です。

OGPとは?設定するべき理由と設定方法について の記事参照

RMagickとは

README にもあるように、Ruby と画像処理ライブラリ ImageMagick の間のインターフェースです。Ruby を使って画像生成・リサイズ・合成などができるものです。

なぜRMagickを使うのか

ユーザやその他の情報によって変化しない画像生成であれば 1 枚いい感じの OGP 画像があれば問題ないです。しかし「ユーザ名やユーザアイコンを表示したい」などの要件の場合は動的にユーザ名やユーザアイコンファイルを読み込み、それを表示する必要があります。なのでデータに紐づいてるところで画像生成したいというわけでございます。

RMagick のメモリ圧迫問題を解消すべく MiniMagick という gem もあれば、最近だと cairo という gem も出てきています。ただし、今回は RMagick を用いて説明します。

今回作るもの

成果物としては以下になります。

今回作るOGP画像のサンプル

要素としては、以下が動的な箇所になります。

  • 質問タイトル
  • 質問本文
  • 質問投稿者のユーザアイコン画像
  • 質問投稿者の職種

OGP画像をRMagickで作ってみる

静的なベース画像を用意

ベースとなる、静的な背景画像を用意。ここはデザイナーの方から用意してもらうことが多いです。デザイナーに依頼する際は以下のことに気をつけるべきです。

  • 以下の画像を例にすると、以下が対象です。
    • 右下の「Offers」部分
    • 右上の「Q」部分
    • 青い枠部分
    • 枠内の背景色部分

▼ ベース背景画像
OGP画像のベース画像

OGP画像に入れたい要素の情報を整理

今回は以下の項目を入れる必要があります。それぞれ仮で詳細を決定します。これもデザイナーと打ち合わせることで認識齟齬なく進められます。

  • 質問タイトル
    • フォントサイズ:41px
    • 1 行に表示する上限:20 文字
    • 上限文字数:50 文字
      • 投稿時にタイトル 50 文字制限しているので上限超えは考慮しない
  • 質問本文
    • フォントサイズ:20px
    • 1 行に表示する上限:41 文字
    • 上限文字数:130 文字
    • 上限を超えている場合は末尾を「...」にする
  • 質問投稿者のユーザアイコン画像
    • 円形にする
    • サイズ:100×100
  • 質問投稿者の職種
    • フォントサイズ:18px

質問タイトルをベース画像に埋め込む

generate_ogp_image.rb
class GenerateOgpImage
  def self.generate_ogp_image(title:)
    base_image = Magick::ImageList.new('.images/base.jpeg') # ベースイメージの読み込み

    cap_title = title.gsub(/[\r\n]/,"").truncate(50).scan(/.{1,#{20}}/).join("\n")

    # ベース画像にタイトルを埋め込む
    draw = Magick::Draw.new do
      self.gravity = Magick::NorthWestGravity
    end

    # 画像、幅、高さ、X座標、Y座標, 文字列
    draw.annotate(base_image, 0, 0, 75, 75, cap_title) do
      self.font = './fonts/NotoSansJP-Bold.otf'
      self.pointsize = 41
    end

    base_image.write('.images/output.png') # 成果物を見ながらポジションなど調整する流れです
  end
end

以下を実行

> title = "フリーランスになって良かったこと/不安なことは?"
> GenerateOgpImage.generate_ogp_image(title: title)

この時点で出力される画像は以下になります。

質問タイトルをベース画像に埋め込んだOGP画像

質問本文をベース画像に埋め込む

generate_ogp_image.rb
class GenerateOgpImage
  def self.generate_ogp_image(title:, text: )
    base_image = Magick::ImageList.new('.images/base.jpeg') # ベースイメージの読み込み

    cap_title = title.gsub(/[\r\n]/,"").truncate(50).scan(/.{1,#{20}}/).join("\n")
    cap_text = text.gsub(/[\r\n]/,"").truncate(130).scan(/.{1,#{41}}/).join("\n")

    # ベース画像にタイトルを埋め込む
    draw = Magick::Draw.new do
      self.gravity = Magick::NorthWestGravity
    end

    # 画像、幅、高さ、X座標、Y座標, 文字列
    draw.annotate(base_image, 0, 0, 75, 75, cap_title) do
      self.font = './fonts/NotoSansJP-Bold.otf' # 文字フォント
      self.pointsize = 41 # 文字サイズ
    end

    #### !!以下新規追加!! ####
    # ベース画像に本文を埋め込む
    draw.annotate(base_image, 0, 0, 75, 280, cap_text) do
      self.font = self.font = './fonts/NotoSansJP-Medium.otf' # 文字フォント
      self.pointsize = 20 # 文字サイズ
    end

    base_image.write('.images/output.png')
  end
end

以下を実行

> title = "フリーランスになって良かったこと/不安なことは?"
> text = <<~TEXT
直近、フリーランスに転向しようか迷っております。
しかし、今まで正社員としてやってきた自分からするとなかなか一歩が踏み出せません。

フリーランスの方の生の声を伺いたく質問させていただきます。
ご回答の程よろしくお願いします。
TEXT
> GenerateOgpImage.generate_ogp_image(title: title, text: text)

この時点で出力される画像は以下になります。

質問本文をベース画像に埋め込んだOGP画像

質問投稿者の職種ベース画像に埋め込む

generate_ogp_image.rb
class GenerateOgpImage
  def self.generate_ogp_image(title:, text:, occupation_name:)
    base_image = Magick::ImageList.new('.images/base.jpeg') # ベースイメージの読み込み

    cap_title = title.gsub(/[\r\n]/,"").truncate(50).scan(/.{1,#{20}}/).join("\n")
    cap_text = text.gsub(/[\r\n]/,"").truncate(130).scan(/.{1,#{41}}/).join("\n")

    # ベース画像にタイトルを埋め込む
    draw = Magick::Draw.new do
      self.gravity = Magick::NorthWestGravity
    end

    # 画像、幅、高さ、X座標、Y座標, 文字列
    draw.annotate(base_image, 0, 0, 75, 75, cap_title) do
      self.font = ('./fonts/NotoSansJP-Bold.otf')
      self.pointsize = 41
    end

    # ベース画像に本文を埋め込む
    draw.annotate(base_image, 0, 0, 75, 280, cap_text) do
      self.font = self.font = ('./fonts/NotoSansJP-Medium.otf')
      self.pointsize = 20
    end

    #### !!以下新規追加!! ####
    # 職業表示用の長方形を作成
    occupation_image = Magick::Image.new(120, 40) # 120×40の長方形を画像として作成

    draw = Magick::Draw.new do
      self.font = FONT
      self.gravity = Magick::CenterGravity
    end

    draw.fill = "#345462" # 職種用のダミー画像背景色を設定して、
    draw.roundrectangle(0, 0, 120, 40, 8, 8) # x:0, y:0, radius:8 で丸みをつけて、
    draw.draw(occupation_image) # 描画する

    # 職種画像と職種テキストを重ねる
    draw.annotate(occupation_image, 0, 0, 0, 0, occupation_name) do
      self.pointsize = 18
      self.fill = "white"
      self.stroke = 'transparent'
    end

    # 職種画像をベース画像に重ねる
    base_image.composite!(occupation_image, 140, 425, Magick::OverCompositeOp)
    base_image.write('.images/output.png')
  end
end

以下を実行

> title = "フリーランスになって良かったこと/不安なことは?"
> text = <<~TEXT
直近、フリーランスに転向しようか迷っております。
しかし、今まで正社員としてやってきた自分からするとなかなか一歩が踏み出せません。

フリーランスの方の生の声を伺いたく質問させていただきます。
ご回答の程よろしくお願いします。
TEXT
> occupation_name = "エンジニア"
> icon_img_url = ""
> GenerateOgpImage.generate_ogp_image(title: title, text: text, occupation_name: occupation_name)

この時点で出力される画像は以下になります。

職種をベース画像に埋め込んだOGP画像

質問投稿者のユーザアイコン画像をベース画像に埋め込む

generate_ogp_image.rb
class GenerateOgpImage
  def self.generate_ogp_image(title:, text:, occupation_name:, icon_img_url:)
    base_image = Magick::ImageList.new('.images/base.jpeg') # ベースイメージの読み込み

    cap_title = title.gsub(/[\r\n]/,"").truncate(50).scan(/.{1,#{20}}/).join("\n")
    cap_text = text.gsub(/[\r\n]/,"").truncate(130).scan(/.{1,#{41}}/).join("\n")

    # ベース画像にタイトルを埋め込む
    draw = Magick::Draw.new do
      self.gravity = Magick::NorthWestGravity
    end

    # 画像、幅、高さ、X座標、Y座標, 文字列
    draw.annotate(base_image, 0, 0, 75, 75, cap_title) do
      self.font = ('./fonts/NotoSansJP-Bold.otf')
      self.pointsize = 41
    end

    # ベース画像に本文を埋め込む
    draw.annotate(base_image, 0, 0, 75, 280, cap_text) do
      self.font = self.font = ('./fonts/NotoSansJP-Medium.otf')
      self.pointsize = 20
    end

    # 職業表示用の長方形を作成
    occupation_image = Magick::Image.new(120, 40) # 120×40の長方形を画像として作成

    draw = Magick::Draw.new do
      self.font = FONT
      self.gravity = Magick::CenterGravity
    end

    draw.fill = "#345462" # 職種用のダミー画像背景色を設定して、
    draw.roundrectangle(0, 0, 120, 40, 8, 8) # x:0, y:0, radius:8 で丸みをつけて、
    draw.draw(occupation_image) # 描画する

    # 職種画像と職種テキストを重ねる
    draw.annotate(occupation_image, 0, 0, 0, 0, occupation_name) do
      self.pointsize = 18
      self.fill = "white"
      self.stroke = 'transparent'
    end

    # 職種画像をベース画像に重ねる
    base_image.composite!(occupation_image, 140, 425, Magick::OverCompositeOp)

    #### !!以下新規追加!! ####
    # ユーザのアイコンを読み取り
    user_icon_image = Magick::Image.read(icon_img_url).first.resize(50, 50)

    # 丸の画像を作成し、アイコン画像をfill inさせる
    user_icon_image = Magick::Image.new(50, 50) do
      self.background_color = 'transparent'
    end

    draw = Magick::Draw.new
    draw.stroke('none')
    draw.stroke_width(0)
    draw.fill('white')
    draw.roundrectangle(0, 0, 50, 50, 50, 50)
    draw.draw(user_icon_image)

    worker_icon_image.composite!(user_icon_image, 0, 0, Magick::CopyAlphaCompositeOp)

    # アイコン画像をベース画像に重ねる
    completed_image = base_image.composite!(worker_icon_image, 75, 420, Magick::OverCompositeOp)

    base_image.write('.images/output.png')
  end
end

以下を実行

> title = "フリーランスになって良かったこと/不安なことは?"
> text = <<~TEXT
直近、フリーランスに転向しようか迷っております。
しかし、今まで正社員としてやってきた自分からするとなかなか一歩が踏み出せません。

フリーランスの方の生の声を伺いたく質問させていただきます。
ご回答の程よろしくお願いします。
TEXT
> occupation_name = "エンジニア"
> icon_img_url = "https://2.bp.blogspot.com/-gLpYfUZDgII/VyNc9aY2xFI/AAAAAAAA6Jo/GzEcU25yfS8Ru06FwaYeM3VxBDqnwrImwCLcB/s800/allergy_dog.png"
> GenerateOgpImage.generate_ogp_image(title: title, text: text, occupation_name: occupation_name)

この時点で出力される画像は以下になります。

ユーザアイコンをベース画像に埋め込んだOGP画像

完成!

まとめ

今回は RMagick を使った OGP 画像の生成方法について記述しました。気になる記事などのリンクをシェアした時に OGP 画像がないと違和感があるため、パブリックなページにおいて OGP は欠かせないものになっていると思います。

また、OGP 画像の生成方法についても様々で、大量の画像データを必要とするクックパッドでは puppeteer を使い対象ページのスクショをとり、OGP 画像の生成しているようです。(参考記事

最後にはなりますが、本記事を最後まで読んで頂き、ありがとうございました。「こんな記事を書いてほしい!」などありましたらコメントいただけると幸いです。

Offers Tech Blog

Discussion