【Rails6.1】画像を保存する際のリサイズとActiveStorageのvariantについて

# はじめに
画像ってなんでもかんでも登録できたら、データベースを圧迫してしまうのでは…？という心配が浮かびました。
そこで、保存時に画像をMiniMagickで加工してリサイズしてから登録する設定にしてみました。


# 実装内容

## 準備

画像の扱いには、MiniMagickとActiveStorageを使用しています。
また、MiniMagickを使用するにはImageMagickが必要とのことだったのでインストールしておきます。

https://github.com/minimagick/minimagick

```
 $ brew install imagemagick
```
```ruby:Gemfile
# ActiveStorage
gem 'image_processing', '~> 1.2'
# MiniMagick
gem 'mini_magick'
```

## 画像保存時のコントローラの記述
商品を登録する際のコントローラの記述がこちら。
ちなみに長くなるので省きましたが、同じような記述をupdateアクションにも追加しています。

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

  def create
    @item = Item.new(item_params)
    @item.shop_id = current_shop.id
    
    # ここから画像を圧縮してjpegで保存するための内容
    if params[:item][:item_image].present?
      # 送られてきたitem_imageにresize_image_set_dpiを実行
      resized_images = resize_image_set_dpi(params[:item][:item_image])
      # オリジナルファイル名から拡張子を除去したベース名を取得
      original_filename_base = File.basename(params[:item][:item_image].original_filename, ".*")
      # 圧縮された画像をActiveStorageに添付
      @item.item_image.attach(
        io: resized_images,
        filename: "#{original_filename_base}.jpg",
        # ここでcontent_typeを指定しないと名前は.jpgの拡張子が付いているけどデータはpngみたいなことが起こる可能性がある
        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)
    # uploaded_file.tempfileから画像を読み込み、MiniMagickオブジェクトとして扱う
    image = MiniMagick::Image.read(uploaded_file.tempfile)
    # 画像の縦幅を1350ピクセルにリサイズ
    image.resize 'x1350'
    # 画像の解像度を96 DPIに設定
    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

```
:::note info
<b>Tempfile</b>
Rubyの標準ライブラリに含まれており、一時ファイルの生成と管理を簡単に行うためのクラスです。
一時ファイルは、プログラムの実行中に一時的にデータを保存するために使用され、プログラムが終了すると自動的に削除されるのが一般的です。
:::

https://docs.ruby-lang.org/ja/latest/class/Tempfile.html


## 保存した画像を表示する際のビューとモデルの記述

画像を表示する際のメソッドをモデルで作成しました。
width, heightでサイズを指定して、詳細ページや一覧ページなどのビューによって違う表示が出来るようにしています。

```ruby:models/item.rb
  def get_item_image(width, height)
    item_image.variant(resize_to_fill: [width, height]).processed
  end
```

```html:保存した画像を表示する
<%= image_tag @item.get_item_image(1080,1350) %>
```


### Active Storageのvariantについて

:::note warn
ここで注意の必要な点が、variantを使っているところです。
<b>variant</b>は、ファイルに対して変換（リサイズなど）を行うためのActive Storageで使用されるメソッド。
<b>processed</b>を付けることによって、variantで定義された指示を実際に実行し、画像を処理されます。

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

メンターの方に聞いたですが、ActiveStorageで保存された画像はstorageファイルに入り、このstorageファイルの中を見ると、細かく分けられたファイルが格納されています。
こんな感じです。

![2024-06-23画像のリサイズ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3762011/cccba09d-0cb3-5734-634d-dda4fd739048.png)

見ると中身が空のフォルダなども存在しており、どうやら元画像だけでなくvariantでリサイズされた画像もここに格納されるそうです。
ですが、実際は元画像さえあれば画像の表示は可能になるため、variantでリサイズされた画像は長期的に保存する必要のないファイルになります。
そのため、一定時間が経つとリサイズされた画像はなくなり、空のフォルダだけが残されるのだとか…。

聞いた話なので説明が合っているかは怪しいですが、画像表示のためにリサイズして保存するとは…？と思っていた際に説明していただき、すごく納得感のあるお話でした。

公式のドキュメントからはいまいちそういった内容を読み取れなかったのですが、

>This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController can then produce on-demand.
>
>Google翻訳
>これにより、その特定のバリアントを持つ特定の BLOB の URL が作成され、ActiveStorage::RepresentationsController がオンデマンドで生成できるようになります。

という記述があり、確かにどうやらvariantでデータのURLが作成されるらしい？ということが分かります。

https://edgeapi.rubyonrails.org/classes/ActiveStorage/Variant.html

## 画像登録フォームは特に変更なし

画像の登録フォームにはいつも通りfile_fieldを使っています。

```html:画像登録フォーム
<%= f.file_field :item_image, accept: "image/*" %>
```

# おわりに
実は、画像のリサイズについて考える際に、webpの導入も行おうとしていたため、だいぶ苦戦してしまいました。

苦戦したポイントは、「variantでも画像のリサイズ＆保存が行われている」ということを認識できていなかったことが大きな要因だったように思います。
この辺の試行錯誤についてはまた別の記事に記録しておこうと思います。