画像周りの扱い方

# これは何？
Railsアプリケーションを扱うときに、画像周りを扱うのは必須。
必須事項の現時点でのベストプラクティスを記事に残す。
下記の環境で開発を行ってます。

# 環境
- Mac OS High Sierra バージョン 10.13.6
- Ruby 2.5.1
- Rails 5.2.1
- MySQL 5.7.23
- RSpec
- CarrierWave
- MiniMagick
- Fog
- S3

# [CarrierWave](https://github.com/carrierwaveuploader/carrierwave)

`gem 'carrierwave'`
=> まずはGemを追加

`bundle exec rails g uploader Picture`
=> uploaderの設定ファイルを生成
=> この設定ファイルについては後述

### ▼ 単一画像
`bundle exec rails g migration add_picture_to_users picture:string`
=> 画像をDBで扱うためにカラム追加

```ruby:models/user.rb
class User < ApplicationRecord
 mount_uploader :picture, PictureUploader
end
```
=> ActiveRecordで扱うための処理を記述

```erb:views/users/new.html.erb
<%= form_with(model: User.new, multipart: true, local: true) do |form| %>
 <%= form.file_field :picture %>
<% end %>
```

```ruby:controllers/users_controller.rb
def show 
 @user = User.find(params[:id])
end

def create
 user = User.new(user_params)
end

private
def user_params
 params.require(:user).permit(..., picture)
end
```

```erb:views/users/show.html.erb
<%= image_tag(@user.picture.url, size: "30x30") unless @user.picture.file.nil? %>
```

### ▼ 複数画像
`bundle exec rails g migration add_pictures_to_users pictures:json`
=> 画像をDBで扱うためにカラム追加

```ruby:models/user.rb
class User < ApplicationRecord
 mount_uploaders :pictures, PictureUploader
end
```
=> `mount_uploader`ではなく、`mount_uploaders`なので注意

```erb:views/users/new.html.erb
<%= form_with(model: User.new, multipart: true, local: true) do |form| %>
 <%= form.file_field :pictures, multiple: true %>
<% end %>
```

```ruby:controllers/users_controller.rb
def show
 @user = User.find(params[:id])
end

def create
 user = User.new(user_params)
end

private
def user_params
 params.require(:user).permit(..., {pictures: []})
end
```

```erb:views/users/show.html.erb
<% @user.pictures.each do |picture| %>
 <%= image_tag(picture.url, size: "30x30") unless picture.file.nil? %>
<% end %>
```

### ▼ Uploaderの設定
```ruby:picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
 # 環境によって保存される場所を変更する
 if Rails.env.production?
  storage :fog # Fogについては後述
 else
  storage :file
 end

 # storage :file の時の画像の保存場所を指定
 def store_dir
  if Rails.env.test? #テスト画像は一括削除できるようにフォルダを別にする
   "uploads_#{Rails.env}/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  else
   "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end
 end

 # セキュリティのためにファイル形式を制限
 def extension_whitelist
  %w(jpg jpeg gif png)
 end
end
```

### ▼ URLからCarrierWaveに保存
Twitterログインした後に、Twitterのアイコン画像をCarrierWave経由で保存するケースです。

`tw_image_url` にアイコン画像がURLで格納されてるとします。

Userモデルには既に
`mount_uploader :picture, PictureUploader`
が設定されているため、
`remote_picture_url`という仮想属性が定義されます。

```ruby:models/user.rb
mount_uploader :picture, PictureUploader
user = User.new
user.remote_picture_url = tw_image_url
user.save
```
=> これでTwitterから取得した画像をCarrierWave経由で保存可能です。

# [MiniMagick](https://github.com/carrierwaveuploader/carrierwave#adding-versions)
画像のリサイズや、別のバージョンのファイル（サムネイルなど）を用意するためのGemとしてCarrierWaveと相性が良いです。
RMagickもありますが、MiniMagickが推奨されてます。
また、MiniMagickはImagemagick上で機能するRuby Gemのため、Imagemagickがインストールされてる必要があります。

Macに直接インストールするなら、
`brew install imagemagick`
ですし、
Dockerなどにインストールするなら、
それぞれインストールしてから使うようにしてください。

`gem 'mini_magick'`
=> gemをインストールします。

あとは、CarrierWaveで生成したUploaderを変えていきます。

```ruby:picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base
 # MiniMagickを有効にします
 include CarrierWave::MiniMagick

 # このアップローダーを利用した画像の最大数を指定します。
 process resize_to_fit: [800, 800]
 
 # 上記とは別にサムネイルを別サイズで用意します。
 version :thumb do
  process resize_to_fill: [200, 200]
 end
end
```
=> `resize_to_fit:` アスペクト比そのまま
=> `resize_to_fill:` アスペクト比無視
=> `resize_to_limit:` 最大を決定

バージョン違いで保存したファイルは下記のように参照可能。

`image_tag(user.picture.thumb.url)`

# [Fog](https://github.com/carrierwaveuploader/carrierwave#fog)
`gem 'fog'`
=> Fogを使えるように追加。Productionにだけ追加するで問題なし。

```ruby:initializers/carrier_wave.rb
if Rails.env.production?
 CarrierWave.configure do |config|
  config.fog_credentials = {
   :provider              => 'AWS',
   :region                => Credentials.env.dig(:S3_REGION),
   :aws_access_key_id     => Credentials.env.dig(:S3_ACCESS_KEY),
   :aws_secret_access_key => Credentials.env.dig(:S3_SECRET_KEY)
  }
  config.fog_directory    =  Credentials.env.dig(:S3_BUCKET)
 end
end
```

S3側の設定などは検索をしてください。

# RSpec
テストでは下記の2つを確認しようかと思います。

1. 適切なファイルフォーマットなら保存可能か、不適切なら保存不可か
2. リサイズが適切にされているか。サムネイルも指定サイズで作成されているか

### ▼ 下準備
ファイルの大きさなどを検証するためのmatchersを追加するために下準備します。

```ruby:spec/rails_helper.rb
require 'carrierwave/test/matchers'
```

```ruby:spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
 include CarrierWave::Test::Matchers
end
```

```file:spec/fixtures
fixtures配下にいろんなファイル形式、サイズのものを保存しておいてください。
- sample.jpg <= かなり大きいサイズの画像にしておく（リサイズ検証のため）
- sample.jpeg
- sample.gif
- sample.png
- sample.rb
```

### ▼ ファイルフォーマット
```ruby:spec/models/user_spec.rb
it "is valid with jpg, jpeg, gif, png" do
 formats = %w(jpg jpeg gif png)
 formats.each do |format|
  image_path = File.join(Rails.root, "spec/fixtures/sample.#{format}")
  user = FactoryBot.build(:user, picture: File.open(image_path))
  expect(user).to be_valid
 end
end

it "is invalid with rb" do
 image_path = File.join(Rails.root, "spec/fixtures/sample.rb")
 user = FactoryBot.build(:user, picture: File.open(image_path))
 expect(user).not_to be_valid
end
```

### ▼ リサイズ
```ruby:spec/models/user_spec.rb
it "resize to small size" do
 image_path = File.join(Rails.root, "spec/fixtures/sample.jpg")
 user = FactoryBot.create(:user, picture: File.open(image_path))
 expect(user.picture).to be_no_larger_than(800, 800)
 expect(user.picture.thumb).to have_dimensions(30, 30)
end
```