A note of a person who is learning programming, SakaTaQ

ロック好きのプログラミング学習

canvasで書いたデータをDBに保存(canvas +JavaScript + Rails)

canvas APIで描画したデータを保存する方法を学んだので実際に行ったことをアウトプットしていきます。

結論として、最初から保存される状態や形式、データ型などに拘らなければすぐ終わったのですが、随分と遠回りしてしまいました😅
最終的にはcanvasデータをバイナリ形式のままでDBに保存、読み込み編集できるというのと、画像としてPCに保存できるようにするということで2つに分けて実装することにしました。


バージョンについては前回のアプリをベースにしています。

Rails 5.2.3
rbenv 1.1.2
ruby 2.5.1

はじめに

スクール学習中に経験したのですが、jQueryRails上で反映させるにあたりgem 'turbolinks'の有無でローカル環境とデプロイ環境で動作が変わりました。
残したままでlink_toタグにdata: {'turbolinks' => false}を付与してリンク先で切っても、デプロイ環境ではエラーが出続けたり色々あったので、この辺りは大体切っています。
後は、application.jsのrequire部分のrails-ujsjQuery-ujs。この辺も競合したのでどっちかだけ残すように気をつけています。

canvasデータを画像としてPCへ保存

これは公式や個人記事でのソースがあったので色々参考にしながら実装しました。
割と決まった書き方になっていたのでそのまま写している部分もあります。

とりあえず「保存」というボタンを実装し、JavaScript(jQuery)でイベントを書いていく。

$('#canvas-submit').on('click', function(){
  // 処理用の関数などを書く
});

ボタンを押した時の処理をする関数をsaveCanvas()として関数を作成していく。

function saveCanvas(){
    let imageType = "image/png";
    let fileName = "sample.png";
    var base64 = cvs.toDataURL(imageType);
    var blob = base64toBlob(base64);
    var url = (window.URL || window.webkitURL);
    var dataUrl = url.createObjectURL(blob);
    var a = document.getElementById('canvas-submit');
    a.href = dataUrl;
    a.download = fileName;
  }

function base64toBlob(base64){
    var tmp = base64.split(',');
    var data = atob(tmp[1]);
    var mime = tmp[0].split(':')[1].split(';')[0];
    var buf = new Uint8Array(data.length);
    for (var i = 0; i < data.length; i++) {
      buf[i] = data.charCodeAt(i);
    }
    var blob = new Blob([buf.buffer], { type: mime });
    return blob;
  }

1つ目の関数について。1,2行目は変数宣言。これらは後で利用します。
3行目はcanvasタグのURLをbase64形式で取得している。
4行目は下記関数を呼び出しのコード。base64データををblobデータに変換している。
5行目はブラウザの種類?(Chrome, Safariなど)URLオブジェクトを作成した際に変数名が異なるので名前を統一する為に変数宣言して代入させている。
6行目はcreateObjectURL()という引数で指定されたオブジェクトを表すURLを生成するメソッドを5行目で宣言した変数urlに使用させているコード。引数には4行目で変換したblobデータを渡している。
最終的にaタグのリンクに持たせるダウンロード用のURLを作成している。
7行目ではクリックしたボタン要素を変数に代入。8、9行目はダウンロード用のURLの用意とファイル名をセット。
最終的には、クリックするとcanvasデータをPCでダウンロードするようになっている。

下側の関数では前述したとおりbase64データをblobデータに変換している。
1行目はカンマで分割してデータを配列で分けてtmpに代入、tmp[0]:データ形式(data:image/png;base64,)、tmp[1]:base64データ
2行目ではbase64のデータをatobメソッドでデコード。ランダムな文字列が並んでいるbase64データを引数に渡している。
3行目はtmp[0]の文字列(data:image/png;base64)からコンテンツタイプ(image/png)部分を取得し代入(=> image/png;base64 => image/png)
4行目では1文字ごとにUTF-16コードを表す 0から65535 の整数を取得、newなのでオブジェクトを生成してるのかな。
5行目からのfor文はcharCodeAt()メソッドで、与えられたインデックスに位置する文字の UTF-16 コードを表す 0 から 65535 の整数を返している。
この値はコンソールで表示させると配列で表示されるのは確認している。
その下の変数blobはblobオブジェクトを作成。引数にはこれまでで用意した変数を渡している。
最後は呼び出した関数base64toBlob()にreturnで値を返している。

参考にさせていただいた記事
HTMLCanvasElement.toBlob()
String.prototype.charCodeAt()
Blob()
Canvas上のイメージを画像ファイルとして保存する方法
Canvas に描いた画像を png などの形式の Blob に変換する方法

canvasデータをDBに画像ファイル(.pngとか)として保存させる

色々あって失敗。file_fieldに直接value渡そうとして、セキュリティ上の問題で対策されていて実装に失敗したり。最終的にたどり着いた記事
最終的には「そもそも各投稿毎に保存されて編集もできるようにする」という要件を満たす為に、画像として色々変換させてから保存させるのって読み込む時も面倒になるんじゃってことで断念。

DBにバイナリデータとして保存させる

再度読み込み編集、更新する物を変換しきってしまうのは面倒だと思ったので、Railsのデータ型を変えて保存させてしまった方が早いのでは?となり。
postsテーブルのimageカラムのデータ型をtextbinaryに変更するためのマイグレーションファイルの発行。

% rails g migration change_data_image_to_post

生成された中身。

class ChangeDataImageToPost < ActiveRecord::Migration[5.2]
  def change
  end
end

RailsのMigrationでbinary型を選択すると、MySQLを使用した場合はblob型で解釈されるとのこと。
また、binary型はdefaultでは64KB(65535Byte)までしか保存できないというのを目にしました。
canvasのデータが書き込まれる毎にサイズが増えていく、というのをコンソール上でBlob{size: 79345}のような形で確認。
簡単に64KBは超えてしまっていたのでmediumblob(最大16MB)に変えます。
下記のように書くとマイグレーション後にはdb/schema.rblimit: 16777215となっていることが確認できます。

class ChangeDataImageToPost < ActiveRecord::Migration[5.2]
  def change
    change_column :posts, :image, :binary, limit: 10.megabyte
  end
end

最初に発行した時にpostsと記入しなかったので、1行目複数形でないのが少し気になりましたが書き換えてしまうとマイグレーションできなかったので名前はこのまま。
change_column :posts, :image, :binary, limit: 10.megabyte
メソッド テーブル名 カラム名 データ型 オプションの順番で書かれている。
基本的な書き方についてや、メソッドの種類、オプション種類については一通り公式ドキュメントに書いてある。
Railsドキュメント/マイグレーションとは
細かくどういうことをするのかとかは説明の上手い人の記事見たりしながら自分のアプリ上で何が起こるか目にした方が早いです。

% rails db:migrate

でDBにマイグレーション情報を適用させる。適用できたかどうかは% rails db:migrateをする前後で

% rails db:migrate:status

で確認すれば差異がわかる。移行前は以下のような感じで表示される。

database: freehands_writer_development

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     20200521070549  Create posts
  down    20200602044245  Change data image to post

downはまだマイグレーションされてない状態。
% rails db:migrateした後に確認して二つともupになっていればOK。

参考にさせていただいた記事を貼っておきます
ActiveRecordでbinary型をblob以外の型にする
【Rails・MySQL】MySQLのデータ型とRailsのマイグレーションファイルのデータ定義の対応まとめ

バイナリデータを投稿できるようにしていく

送信ボタンを押した時にcanvasのデータを送信できるようにJavaScriptを書いていく。

// form.html.erb
// labelタグは必要なくなる + file_fieldから変更。
<%= form.hidden_field :image, class: 'hidden' , value: '' %>

//index.erb.html, show.erb.htmlなどのビューではimage_tagは外しておく。
$("#post_submit").on("click", function(e) {
  e.preventDefault();
  var base64 = cvs.toDataURL();
  $('#post_image').val(base64);
  $('#new_post').submit();
});

file_fieldではvalueを渡せなかったのでフォームヘルパーを変更し、hidden_fieldを使用。
念のためconsole.logなどでvalに挿入できたか確認→OK。
atobメソッドでデコードをしなかったらこんな感じ(つーか、blobにするとエンコードしないとRailsからエラー返されて表示すらできない)。
1行目のpreventDefault();がないとイベントが発生したらformの動作が勝手に進行してしまうのでここで止めてる...というくらいの感覚で使用してます。
一応確認として、引数eをconsole.logしてみるとjquery.fn.initが取得できた。押した時のDOM要素とイベント~.submitみたいなのが書いてあったので、これに対してpreventDefault()を使用することで止めているのが分かる。
最後に処理を止めているform要素のidに対して、submitメソッドを使用することでDBにデータ送信。
この流れでbase64データをDBに保存することができた。

投稿したデータを表示できるようにする

Base64形式で保存された物はimage_tagで、src属性に@post.imageを指定しておくだけで、そのまま画像として表示することができる。
表示できるデータがない場合は、alt: 'No Image'で何もデータがない時に表示させる文字を指定しておく。
DBからデータを読み込んだ際にJavaScriptを使ってcanvasに反映させるには、Railsの変数をJSに渡して使用する必要があるのではないかと思い、それを可能にするgem 'gon'を導入。

// どの環境においても使用するため、Gemfile最下部に記述
gem 'gon'

// 対象ディレクトリ上にcdして
% bundle install

// application.html.erb のheadタグ内で記述してgonを使って読み込みできるようにしておく
<%= include_gon %>

これでgem 'gon'を使用する準備はできたので、次は実際に繋ぐためのコードを書いていく。

def edit
  @post = Post.find(params[:id])
  gon.post_image = @post.image
end

各投稿のcanvasデータを編集するedit.html.erbに変遷した時、gonを使用してRailsで変数宣言。
変数名の前にgon.を書くことでこの変数をJavaScriptで使用することができる。

後はこの変数を使ってcanvas上にBase64の画像データを読み込ませるように記述していく。

var img = new Image();
img.src = gon.post_image;
img.onload = function(){
  conText.drawImage(img, 0, 0, 800, 600);
}

1行目では画像オブジェクトを生成し、2行目で先ほど表示させたようにsrc属性にBase64データを代入。
ここでRailsのgonを使用して定義した変数を使用している。
3行目~4行目で画像データををcanvasに設定している。引数は表示する画像、x, y, 大きさx, 大きさy。
0にしないと表示する場所が微妙に変わってしまうのと、大きさの数値を元の数値と合わせないと拡縮されてしまうので注意が必要。

参考にさせていただいた記事
【gem gon】Railsで定義した変数をさくっとJavascriptで使う
[JavaScript] Base64形式の画像データをcanvasに表示する


名前を間違えたり、gem 'gon'にたどり着くまで時間がかかった。
色々と可能性を感じるgem。また使える機会があれば!

したらな❗️ 👋