ブラウザで画像をリサイズしアップロード Rails canvas

やりたいこと

コンビニ食品の組み合わせ作成し成分表示するポートフォリオを作成しています。

携帯で撮った画像を添付しアップロードしたいのですが、携帯で撮った画像は容量がとても多くサーバーにかなり負担をかけてしまいます。

そこでブラウザで画像表示時に、リサイズしてからサーバーに送信することでかなり負担を減らせると思ったのでこの実装をしたい。

Image from Gyazo



テーブル

テーブル設計は以下を確認。

  • メイン料理:mainsテーブル

  • プラス一品料理:subsテーブル

  • ActiveStorage使って画像をアップロード

  • mainsテーブルとsubsテーブルの内容を同時にdbに送りたいのでform_objectを使用

Image from Gyazo



調査

javascript canvasを使った画像リサイズで検索。

www.mahirokazuko.com

tech-it.r-net.info



実装

mainsとsubsの両方に画像を添付したいので、classを用いて作成しました。

view

# mainのfile_field

  <label class="d-block">
    <span class="btn p-2 w-100 food_create_img_btn">
      <i class="fas fa-camera-retro"></i> 画像を選択する
      <%= main.file_field :image, class: "form-control", style: "display:none" %>
    </span>
  </label>
 <canvas id="main_canvas" width="0" height="0"></canvas>

# subのfile_field

  <label class="d-block">
    <span class="btn p-2 w-100 food_create_img_btn">
      <i class="fas fa-camera-retro"></i> 画像を選択する
      <%= main.file_field :image, class: "form-control", style: "display:none" %>
    </span>
  </label>
 <canvas id="sub_canvas" width="0" height="0"></canvas>

jsファイル

class Preview {
  constructor(obj) {
    this.MAX_WIDTH = 400; 
    this.MAX_HEIGHT = 400;
    this.$input = document.getElementById(obj.inputId);
    this.$canvas = document.getElementById(obj.canvasId);
   
    this.$input.addEventListener('change', () => {
      this.previewImg()
    })
  }
  previewImg() {
    const $file = this.$input.files[0];

    // 画像をリサイズする
    const image = document.createElement('img')
    const reader = new FileReader();

    reader.onload = (e) => {
      image.onload = () => {
        if(image.width > image.height){
          // 横長の画像は横のサイズを指定値にあわせる
          const ratio = image.height/image.width;
          width = this.MAX_WIDTH;
          height = this.MAX_WIDTH * ratio;
        } else {
          // 縦長の画像は縦のサイズを指定値にあわせる
          const ratio = image.width/image.height;
          width = this.MAX_HEIGHT * ratio;
          height = this.MAX_HEIGHT;
        }
        // canvasにwidthとheightの値を代入
        this.$canvas;
        this.$canvas.setAttribute('width', width);
        this.$canvas.setAttribute('height', height);

        const ctx = this.$canvas.getContext('2d');
        ctx.clearRect(0,0,width,height);
        // canvasにサムネイルを描画
        ctx.drawImage(image,0,0,image.width,image.height,0,0,width,height);
      }
      image.src = e.target.result;
    }
    reader.readAsDataURL($file);
  };
}

  // メインのプレビュー
  const MainPreview = new Preview({
    inputId: 'main_sub_form_image',
    canvasId: 'main_canvas'
  })

  // サブのプレビュー
  const SubPreview = new Preview({
    inputId: 'main_sub_form_subs_attributes_image',
    canvasId: 'sub_canvas'
  })



処理の流れ

    reader.onload = (e) => {
      image.onload = () => {

      }
   }

ファイルのロードが完了した時と、画像のロードが完了した時の、それぞれのコールバック関数


const ctx = this.$canvas.getContext('2d');

canvas 要素に描画するための 2D レンダリングコンテキストを提供します。図形、文字、画像、その他のオブジェクトを描画するのに使用します。 canvas 要素には、自分で描画する能力を持っていないので指定したwidthとheightを元にgetContext('2d')リファレンスを用いて画像を表示していきます。

CanvasRenderingContext2D - Web API | MDN

getContext('2d')のリファレンス一覧があります。

HTML5 canvas.getContext("2d") reference

git 基本的なコマンド3 fetch, pull

git fetch git mergeリモートから取得

git fetch <リモート名>と入力すると、リモートリポジトリからローカルリポジトリに情報を落とすことができます。ここで注意したいのが、自分のワークツリー(手元の作業場)には情報は反映されません。

じゃあ自分の手元の作業場に持ってくるには?

git mergeをしてあげます。


git hub上でhome.htmlという新しいファイルを作成し、文字を入力します。その後mainブランチにmerge。

Image from Gyazo

ターミナル上でgit fetch

% git fetch origin
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 678 bytes | 169.00 KiB/s, done.
From https://github.com/takuya178/git_tutorial
   69fcc61..299fed5  main       -> origin/main

では、git fetchで取得した情報が保存されているか確認します。

% git branch -a
* main
  remotes/origin/main

現在いるmainブランチに米印がついていて、remotes/origin/mainに先程の変更内容が保存されています。


% git checkout remotes/origin/main
% ls
home.html   index.html

home.htmlがあることが確認できました。

メインブランチに切り替えてから、home.htmlをmergeします。

% git checkout main
% git merge origin/main
% ls
home.html   index.html



git pull

git pull origin main

fetchとmergeを一度にしてくれる。

git 基本的なコマンド3 fetch, pull

git fetch git mergeリモートから取得

git fetch <リモート名>と入力すると、リモートリポジトリからローカルリポジトリに情報を落とすことができます。ここで注意したいのが、自分のワークツリー(手元の作業場)には情報は反映されません。

じゃあ自分の手元の作業場に持ってくるには?

git mergeをしてあげます。


git hub上でhome.htmlという新しいファイルを作成し、文字を入力します。その後mainブランチにmerge。

Image from Gyazo

ターミナル上でgit fetch

% git fetch origin
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 678 bytes | 169.00 KiB/s, done.
From https://github.com/takuya178/git_tutorial
   69fcc61..299fed5  main       -> origin/main

では、git fetchで取得した情報が保存されているか確認します。

% git branch -a
* main
  remotes/origin/main

現在いるmainブランチに米印がついていて、remotes/origin/mainに先程の変更内容が保存されています。


% git checkout remotes/origin/main
% ls
home.html   index.html

home.htmlがあることが確認できました。

メインブランチに切り替えてから、home.htmlをmergeします。

% git checkout main
% git merge origin/main
% ls
home.html   index.html



git pull

git pull origin main

fetchとmergeを一度にしてくれる。

gitの基本コマンド2 checkout, -- reset, commi --amend

ファイルの変更を取り消すgit checkout --

index.htmlに以下を追加

<p>この素晴らしい世界に祝福を!</p>

git statusで変更を確認すると変更部分が確認できます。

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   index.html


ではgit checkout --で変更部分を取り消しましょう

% git checkout -- index.html

そして

% git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

変更部分を取り消すことができました。

git checkout --はワークツリーの状態をステージに反映しています。



git reset git addでステージに追加しちゃったけどやっぱり追加したくないとき

git reset HEAD <ファイル名>

git reset HEAD <ディレクトリ名>

全変更を取り消したい場合は git reset HEAD .

このコマンドはステージに追加したものだけ削除します。git checkout --のようにローカルの変更分は変わらないので注意です。



git commit --amend 直前のコミットをやり直したい

index.htmlに以下を追記してgit add . とgit commitをします。

<p>この素晴らしい世界に祝福を</p>

ここでgit pushしようとするが、index.html内を変更したいと思った時にgit commit --amendを使うことができます。

index.htmlを書き直します。

<p>このすば<p>

そして再度git add .をします。そしてgit commit --amendをしてあげましょう。

git commit --amend -m 'このすばを追記'


git logで確認するとgit commitが反映されていることがわかります。

% git log
commit b6596078a33963f7787169c6b0f21c8a367da1c6 (HEAD -> main)
Author: 
Date:   Wed Nov 17 20:12:03 2021 +0900

    このすばを追記


注意することが一点リモートリポジトリにプッシュしたコミットはやり直したらダメ!

gitのコマンド確認 init status diff log

git init

まずはgit initコマンドを実行。空のgitリポジトリが作成されます。

% git init


中身を確認してみると、.gitという隠れフォルダが作成されています。

% ls -a
.       ..      .git


.gitファイルの中身を見てみると

% ls .git/
HEAD        config      description hooks       info        objects     refs
  • objects:リポジトリの本体。この中に圧縮ファイルやツリーファイルが保存されていく。

  • config:gitの設定ファイル。



git status

上の内容をgit add.します。 次にgit statusで内容を確認すると

% git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   index.html

するとcommitすべき変更があるよと表示されます。 それではcommitしてみましょう。commitが終わると

% git status
On branch master
nothing to commit, working tree clean

変更されたのでワークツリーとcommitの間に変更差分はないよとなります。このようにgit addやgit commitしたときに、どのファイルに対して行なっているか確認するくせが必要ですね。



変更差分が確認できるgit diff

index.htmlに以下を記載しgit diffで確認すると

<p>夏目友人帳</p>
% git diff
diff --git a/index.html b/index.html
index 04640d6..3f061a9 100644
--- a/index.html
+++ b/index.html
@@ -1,2 +1,3 @@
 <p>インデックス</p>
-<p>git status</p>
\ No newline at end of file
+<p>git status</p>
+<p>夏目友人帳</p>
\ No newline at end of file

+と書かれているところが変更部分になります。

それではgit add.コマンドを打ってから再度git diffコマンドで確認すると何も表示されなくなりました。git diffはワークツリーとステージ間の変更差分のみ表示してくれます。

% git diff


次にgit diff --stagedを入力するとステージとコミットの差分の変更を確認してくれます。

% git diff --staged
diff --git a/index.html b/index.html
index 04640d6..3f061a9 100644
--- a/index.html
+++ b/index.html
@@ -1,2 +1,3 @@
 <p>インデックス</p>
-<p>git status</p>
\ No newline at end of file
+<p>git status</p>
+<p>夏目友人帳</p>
\ No newline at end of file



変更履歴を確認するgit log

git logをすると今までのcommitの変更履歴を確認することができます。上から最新順に表示されます。

% git log

commit 69fcc617a5ea4fc79823927c59d4b0219d371839 (HEAD -> master)
Author: 
Date:   Wed Nov 17 19:16:15 2021 +0900

    git_diff(コミット名)

commit a0ba9495c735ec7e6191b502ddfaefc0729efbc4
Author:
Date:   Wed Nov 17 19:01:51 2021 +0900

    git_statusコマンド追記

Rails セレクトボックス

やりたいこと

railsにてセレクトボックス 機能をつけたい。セレクトボックスの内容はenumの値を表示。



調査

railsdoc.com



実装

main.rb

  enum stores: { seven: 0, lawson: 1, family: 2 }, _prefix: true

enumの値はセブン-イレブン、Lawson、FamilyMartとしている。

理想的な完成図は以下のようなセレクトボックス 。

✔️コンビニ名を選択してください
セブン-イレブン
Lawson
FamilyMart


今回、セレクトボックスを作成するに当たって、option_for_selectを採用しました。 option_for_selectは配列、ハッシュからオプションタグを生成するメソッドです。第一引数と第二引数に記載する内容が決められています。

  • 第1引数:選択肢にしたいものの配列/ハッシュ形式

  • 第2引数:デフォルト値を設定できるなど

今回はoption_for_selectの第一引数のみで実装します。


mains/new.html.erb

   <%= f.select :stores, options_for_select(Main.stores_i18n.invert), {include_blank: 'コンビニ名を選択'}, class: "food_create_select" %>


Main.stores_i18n.invertの部分は以前、記事を書いたことがありますので参考にしてください。

https://blog.hatena.ne.jp/takuya178/takuya178.hatenablog.com/edit?entry=13574176438028774642

Main.stores_i18n.invertでキーにenum_helpで日本語化された値が入り、バリューにenumで記載したseven,lawson,familyが入ってくれます。


次にinclude_blankなのですが、これは初期画面でセレクトボックスに表示したい文字を入れることができます。今回だと'コンビニ名を選択してください'を記載します。

renderとredirect_to

やりたいこと

renderとredirect_toについて理解したい。


Controller

controller

def new
  @board = Board.new
end

def create
  @board = Board.new(board_params)
  
  respond_to do |format|
    if @board.save
      format.html { redirect_to @board, notice: 'Success' }
   else
      format.html { render: new }
  end
end

private

def board_params
  params.require(:board).permit(:title, :body)
end



renderについて

format.html { render: new }

もし、入力された値の保存に失敗した場合はviewのnew.html.erbに遷移するという意味です。

ここで注意する事がnewアクションに行くわけではないという事です。つまり以下のnewアクションを介さないという事です。

def new
  @board = Board.new
end

じゃあ変数はどこで定義されたものを使っているのでしょうか?それはcreateアクションの@boardです。なのでcreateアクションの@boardがnewアクションを介さずにnew.html.erbに移動します。



redirect_toについて

 format.html { redirect_to @board, notice: 'Success' }

redirect_toは保存に成功した時にnewアクションの@boardを介します。なので@board = Board.newによりredirect_toされた後は入力された値が一旦リセットされます。



エラー文を表示したいとき

エラー文の表示はcreateアクション内の@boardに対してつけられるものです。 ここでrenderの部分をredirect_toとするとエラーメッセージ はどうなるでしょうか?

   else
      format.html { redirect_to new_board_path }
  end

renderの部分をredirect_toに変更してしまうとエラー文が表示されなくなります。これはredirect_toでnewアクション内の@boardを介してしまいエラー文が記載されるcreateアクション内の@boardを表示できないためです。