【Rails6.1】初心者なりに試行錯誤してwebpを使おうとした話

# はじめに
前職のWebショップで、ページの表示スピードが遅いからコンサルタントの方にアドバイスいただいて、webpを導入してもらったという経験がありました。
その記憶から、webpを使えばページが軽くなるのでは？と思い、ポートフォリオの画像にwebpを使うことにしました。

# 実装内容
結論から言うと、もともと用意してあったvariantを使った画像表示のためのメソッドに「, format: :webp」を追加するだけでwebpの導入はできました。

```diff_ruby:models/item.rb
  has_one_attached :item_image
  
  def get_item_image(width, height)
+    item_image.variant(resize_to_fill: [width, height], format: :webp).processed
-    item_image.variant(resize_to_fill: [width, height]).processed
  end
```


## モデル
モデルでvariantを使って、ビューに合わせた画像のリサイズとwebpへの変換を行っています。
```ruby:models/item.rb
  has_one_attached :item_image
  
  def get_item_image_webp(width, height)
    # variant:ファイルに対して変換（リサイズなど）を行うために使用
    # processed:variantで定義された指示を実際に実行
    item_image.variant(resize_to_fill: [width, height], format: :webp).processed
  end
```

## ビュー
後はモデルで作成したメソッドを使ってビューで表示するだけです。
```html:画像を表示する
<%= image_tag @item.get_item_image_webp(1080,1350) %>
```

## 確認
確認のため画像の表示を、

```html:画像を表示する
<%= image_tag @item.item_image %>
<%= image_tag @item.get_item_image_webp(1080,1350) %>
```
として、元画像とwebpに変換したバージョンで比べてみました。
表示された画像にたいして違いは感じられなかったので、画像をダウンロードして情報を見てみます。

これがjpg
![2024-06-23webp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/be1467cd-f18e-4863-344c-2521449554a7.png)

これがwebp
![2024-06-23webp2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/dc413458-422e-210e-527e-c49c0209acea.jpeg)

きちんとファイルの種類がwebpになっていて、サイズも128KBから50.9KBになっていました。
画像サイズが小さくなっています！

# 試行錯誤の過程
webpの実装自体はこれで完了です。
すごく単純でしたが、私はこれにだいぶ苦戦しました。

ここからは私が誤を悩ませた試行錯誤の過程について記録していきます。

## 画像を保存する際のコントローラの記述
webpの導入とは直接関係ないのですが、今回私が悩まされた要因として、画像保存時にリサイズを行う設定を追加したという点があるので、コントローラの内容も一緒にのせておきます。

詳しい解説は他の記事で行っているので省きます。
記事はこちら↓

https://qiita.com/plant0322/items/5b4845132a6c67818711

（コントローラの中の記述は上記の記事の内容と同じです）

```ruby:models/item.rb
  def new
    @item = Item.new
  end

  def create
    @item = Item.new(item_params)
    @item.shop_id = current_shop.id
    
    if params[:item][:item_image].present?
      resized_images = resize_image_set_dpi(params[:item][:item_image])
      original_filename_base = File.basename(params[:item][:item_image].original_filename, ".*")
      @item.item_image.attach(
        io: resized_images,
        filename: "#{original_filename_base}.jpg",
        content_type: 'image/jpg'
        )

    if @item.save
      flash[:notice] = "商品を登録しました"
      redirect_to item_path(@item)
    else
      flash.now[:alert] = "入力内容に誤りがあります"
      render :new
    end
  end

  private

  def resize_image_set_dpi(uploaded_file)
    image = MiniMagick::Image.read(uploaded_file.tempfile)
    image.resize 'x1350'
    image.density '96'

    tempfile_jpg = Tempfile.new('resized')
    image.write (tempfile_jpg.path)
    tempfile_jpg.rewind
    tempfile_jpg
  end

  def item_params
    params.require(:item).permit(:item_image, :name, :introduction, :size, :price, :stock, :deadline, :is_active)
  end
```

## 実装に躓いた要因
1. 保存時のリサイズとwebpの使用を混同してしまった
1. モデルでvariantを使って画像サイズを変えた際に、リサイズして保存されているという認識がなかった
1. 画像の扱いや確認方法が分かっていなかった


## 躓きポイント
### ①画像の保存時にwebpに変換しようとする

保存時の画像リサイズのためのコントローラの記述が完成した後、投稿された画像をwebpで保存する設定を追加しました。
その際に、<b>webpが対応していない場合もあるからjpgでも保存した方が良いのでは？</b>と考えて両方で保存される設定にしました。

そもそも、variantでwebpに変換できるという発想がなかったので、最初この形になったのは仕方ないことかなとも思いますが、これが一番の混乱の原因でした。

ちなみに最終的にwebpに対応していない場合の設定を追加するのを忘れていたことと、軽く調べたら、webpに対応していないような古いブラウザ向けの開発はあまりないという意見も見つけたので、対応してない場合とか考えるのはやめました。

ちなみにその時の記述はこちらです。
```ruby:コントローラ
  def create
    @item = Item.new(item_params)
    @item.shop_id = current_shop.id
    if params[:item][:item_image].present?
      # 圧縮されたjpgとwebpをActiveStorageに添付
      resized_image = resize_image_set_dpi(params[:item][:item_image])
      @item.item_image.attach(io: resized_image[0], filename: params[:item][:item_image].original_filename, content_type: params[:item][:item_image].content_type)
      @item.item_image_webp.attach(io: resized_image[1], filename: "webp_#{params[:item][:item_image].original_filename}", content_type: 'image/webp')
    end
    if @item.save
      flash[:notice] = "商品を登録しました"
      redirect_to item_path(@item)
    else
      flash.now[:alert] = "入力内容に誤りがあります"
      render :new
    end
  end

  def resize_image_set_dpi(uploaded_file)
    image = MiniMagick::Image.read(uploaded_file.tempfile)
    # ここで幅を指定しているから横長の画像だと小さくなっちゃう。後にx1350にする
    image.resize '1080x1350'
    image.density '96'
    
    tempfile_jpg = Tempfile.new(['resized','.jpg'])
    image.write (tempfile_jpg.path)
    tempfile_jpg.rewind

    tempfile_webp = Tempfile.new(['resized', '.webp'])
    image.format 'webp'
    image.write(tempfile_webp.path)
    tempfile_webp.rewind
    
    # jpgとwebpで保存したものを呼び出す
    [tempfile_jpg, tempfile_webp]
  end
```


### ②横長の画像を登録したら画質が落ちた

ここで、画像の表示をjpgとwebpを並べて確認してみようと思い、詳細ページに以下のように記述。
```html:
<%= image_tag @item.get_item_image(1080,1350) %>
<%= image_tag @item.get_item_image_webp(1080,1350) %>
```
すると、横長の画像を登録した際になんだかぼやける…。
ここでだいぶ時間を溶かしたのですが、原因は、コントローラ内のresize_image_set_dpiの中で<b>「image.resize '1080x1350'」</b>で幅を指定してリサイズしていたからでした。

保存時に幅を1080の解像度96にして、さらに詳細ページで表示する時にもリサイズされたことにより画質が落ちてしまっていたようです。

これを修正して再度確認するときれいに表示されました。

```diff_ruby:
+ image.resize 'x1350'
- image.resize '1080x1350'
```

### ③webpで保存したはずの画像がpngになっている
これで完成！と思い、画像をデベロッパーツールで確認してみたら、おかしなことに画像の<b>拡張子がpngになっていました。</b>

とりあえず、variantを掛ける前の保存した元画像を確認しなければと思い、strageフォルダを見たり、「rails c」を使って画像をダウンロードしてみたり、どうやったら画像は確認できるんだ…と頭を抱えながらまた時間を溶かします。

結局、item_imageをそのまま記述するだけでビューで確認することが出来ました。
簡単なことでした…。

```html:
<%= image_tag @item.item_image %>
<%= image_tag @item.item_image_webp %>
```
※ちなみに、モデルはこんな感じでitem_image_webpも別に保存していたので、webpの元画像を表示することもできます。
```ruby:
  has_one_attached :item_image
  has_one_attached :item_image_webp
```
これで保存された状態の画像を確認すると…拡張子はwebpでした。
どうやら保存した時点ではwebpにはなっているようです。

ということは、保存してから表示されるまでの間に何かが起こってpngになってしまっているようです。

もうお手上げだと思い、ここでメンターの方に相談しました。


### ④variantで保存した際にpngに書き換わっていたのでvariantでwebpを指定
ここで、variantは画像を表示する際にリサイズして保存しているということを教えていただきました。

:::note warn
<b>variant</b>：ファイルに対して変換（リサイズなど）を行うためのメソッド。
<b>processed</b>：variantで定義された指示を実際に実行し、画像を処理。

つまり、createアクションで商品の保存を行った際に画像のリサイズ＆保存を行いましたが、<b>showアクションで詳細ページを表示する際にも画像のリサイズ＆保存が行われている</b>ということになります。
:::

Railsガイドを見ると、

https://railsguides.jp/v7.1/active_storage_overview.html#%E7%94%BB%E5%83%8F%E3%82%92%E5%A4%89%E5%BD%A2%E3%81%99%E3%82%8B

>バリアントがリクエストされると、Active Storageは画像フォーマットに応じて自動的に変形処理を適用します。
>
>Content-Typeが可変（config.active_storage.variable_content_typesの設定に基づく）で、Web画像を考慮しない場合（config.active_storage.web_image_content_types)の設定に基づく）は、PNGに変換される。

という記述があり、これは最新版のv-7.1に合った記述なので私の使っているv-6.1に関係しているのか分からないと思い、ドキュメントを探してみると、v-6.1の方にも以下のような記述がありました。

>config.active_storage.variable_content_types: Active StorageがImageMagickに変換可能なcontent typeを示す文字列の配列を受け取ります。デフォルトは%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop)です。

恐らくActiveStorageかvariantのデフォルトの設定によってpngに変換されていたんだと思われます。
ここで、画像形式のデフォルト値が存在しているということは、variantで画像の形式を指定して加工することもできるはずというご指摘をいただきました。

なので、モデルに記述したvariantで画像の形式をwebpに指定。

```ruby:models/item.rb
def get_item_image_webp(width, height)
    item_image.variant(resize_to_fill: [width, height], format: :webp).processed
end
```
これで無事、webpが使えるようになりました。
助けてくれたメンターの方に感謝です。

### ⑤iPhoneで投稿すると画像の向きが横になる
解決かと思いきや、また問題が発生しました。
PCだと普通に登録できたのに、iPhoneで登録した時だけ画像が横向きになってしまいます。

![2024-06-23webp3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/88c7d9f3-b905-7ca2-2540-a9dc58cd1183.png)

横向きに登録されている画像の元画像を確認すると、登録された時点で横向きになっていました。

そこで考えられる要因として、
* 登録時の画像の拡張子（HEIC）の問題？
* それとも撮影した際の画像の向き（EXIFデータ）の問題？

が浮かびましたが、どちらもややこしそうだしそろそろ疲れてきたし逃げたい…という気持ちが生まれ、いったん画像問題から距離を置くことにしました。
ちょっと離れてから向き合った方が解決策も見つかるかと思いまして…。

### ⑥無駄な記述があることに気づいたので修正する
画像問題に戻ってきたところで解決策は見つからなかったのですが、そもそも、画像の保存や表示の機能を修正した過程で、無駄な処理があることに気づきました。

現在、webpへの変換はモデル内に記載したvariantで行っています。
そのため、保存する際にjpgとwebpの両方で保存する必要はありません。
そこで、とりあえず保存時はjpgで保存し、webpで保存するための記述は削除することにしました。

こうして、やっと最初にのせた完成形のコントローラの記述が完成しました。

また、保存時の記述を書き換えたら、画像が横向きになってしまう問題も一緒に解決してしまいました。


# おわりに
最終的に、画像が横向きに登録される問題に関しては、明確な原因は分からないままなのですが、調べてみると画像処理の方法によってはEXIF情報が反映されない場合もあるとかないとか…。

メンターの方に確認した際にも、画像の処理を正しく把握することは難しいとおっしゃっていて、私の最初の記述では、加工した画像をさらに加工して表示するという形になっていたことから問題点も掴みにくくなっていました。

何よりも、余計な記述を残さずに、コードをスッキリさせることと、問題点を細分化させることの重要性を感じた課題でした。

画像についてもより理解が深まり、そのうち、伏線回収のように今回の原因が明確になることを祈っています。