たろすの技術メモ

Jot Down the Tech

ソフトウェアエンジニアのメモ書き

画像アップロード時にアスペクト比とサイズをチェックする方法

active storageをインストール

$ rails active_storage:install
$ rails db:migrate

model, migrationを作成

$ rails g model Hoge
$ rails db:migrate

Modelを作成

# app/models/hoge.rb
class Hoge < ApplicationRecord
  MAX_IMAGE_SIZE = 2 # 2MBまで

  has_one_attached :image
end

Controllerを作成

$ rails g controller Hoges new create
# app/controllers/hoges_controller.rb
class HogesController < ApplicationController
  def new
    @hoge = Hoge.new
  end

  def create
    @hoge = Hoge.new(hoge_params)
    if @hoge.save
      redirect_to '/'
    else
      render :new
    end
  end

  private

  def hoge_params
    params.require(:hoge).permit(:image)
  end
end

Routesを定義

Rails.application.routes.draw do
  resources :hoges, only: [:new, :create]
end

Viewを作成

# app/views/hoges/new.html.erb
<%= form_with model: @hoge, url: hoges_path, data: { turbo: false } do |f| %>
  <% aspect_width = 1 %>
  <% aspect_height = 1 %>
  <div data-controller='image-preview' data-image-preview-max-image-size-value="<%= Hoge:MAX_IMAGE_SIZE %>" data-image-preview-aspect-width-value="<%= aspect_width %>" data-image-preview-aspect-height-value="<%= aspect_height %>">
    <%= f.file_field :image, accept: 'image/jpeg,image/png', data: { action: 'change->image-preview#preview', target: 'image-preview.image' } %>
  </div>
  <%= f.submit '保存' %>
<% end %>

image_preview_controller.jsを使用するため、<div data-controller='image-preview'>で囲みます。また、アスペクト比(data-image-preview-aspect-width-value, data-image-preview-aspect-height-value)や上限サイズ(data-image-preview-max-image-size-value)の値を渡しています。

JSを作成

// app/javascript/controllers/image_preview_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = { maxImageSize: Number, aspectWidth: Number, aspectHeight: Number }
  static targets = ["image"]

  preview(event) {
    // this.imageTarget.files[0].sizeはバイト単位だから、÷1024でキロバイト、さらに÷1024でメガバイトに変換
    var size = this.imageTarget.files[0].size / 1024 / 1024;
    if (size > this.maxImageSizeValue) {
      alert(`ファイルサイズは${this.maxImageSizeValue}MB以下です。`);

      // 選択されたファイルを空にする
      this.imageTarget.value = '';
      return
    }
    const input = event.target
    if (input.files && input.files[0]) {
      const reader = new FileReader()

      // image.onload内のthisは画像自体のことなので使い分けるため_this変数にする
      const _this = this;
      const aspectWidth = this.aspectWidthValue;
      const aspectHeight = this.aspectHeightValue;
      reader.onload = (e) => {
        const image = new Image();
        image.onload = function () {
          // view側からdata-image-preview-aspect-width-valueやdata-image-preview-aspect-height-valueが渡されなかった場合、this.aspectWidthValue・this.aspectHeightValueは0が入る
          if (aspectWidth != 0 && aspectHeight != 0 && this.width / this.height !== aspectWidth / aspectHeight) {
            alert(`アスペクト比は${aspectWidth}:${aspectHeight}である必要があります。`);
            _this.imageTarget.value = '';
            return;
          }
        }
        image.src = e.target.result
      }
      reader.readAsDataURL(input.files[0])
    }
  }
}

これで上限以上のサイズや指定以外のアスペクト比の画像をアップロードしようとした場合にアラートを表示して阻止できます。今回は割愛しましたがgemなどを使ってファイルサイズやアスペクト比をモデル単位でバリデーションすると更に安全になります。