Rails / できるだけ高速に画像をアップロードする
このブログでも画像をアプロードする機能は実装していて、とくに不便を感じていなかったのですが、CANPATHのユーザーから要望があったので考え直すことにした。
今までのアップローダー
こんな感じです。
ポップアップウインドウ的なものが出てきて、画像を選択するかドラッグ&ドロップすると自動的にアップロードがはじまって、終了するとサムネイルが現れる。→ 選択すると記事内に挿入される。
これはこれでシンプルで気に入っているんだけど、問題もある。
アップロード中にプログレスバーは出るんだけど、
それはアップロードの経過を示すもので、100%完了しているように見えても
「アップロード後にサーバー側でサムネと大きなサイズの2種類作って保存する」作業が残っているため、そのまま待たされてしまう。
大きなサイズの画像を同時にアップすると
なかなか終わらなくて「使えない!」と思われてしまうらしい。
7MBの写真がガンガンUPされるのは結構つらいなー、ということで考えた。
そもそも巨大なファイルをアップロードしない
CANPATHでの写真はブログ記事に使われるような小さなものだから、
7MBなんていらないわけで、アップロード前にユーザーが縮小してくれればいいのに…
という開発側の甘えを実現する方法 → クライアントサイドで縮小
ファイル選択
↓
ブラウザ上で縮小
↓
アップロード
これなら7MBが500KBくらいになってとても効率的!
実装
アップロードにはこれ、
carrierwaveuploader/carrierwave · GitHub
あと以下のプラグインを使っています。
blueimp/jQuery-File-Upload · GitHub
blueimp/JavaScript-Load-Image · GitHub
実装したコードではないですが、だいたいこんな感じ。
HTML
html
1
2
3
4
5
6
7
<div class="image_widget">
<div class="manipulation">
<a href="#" class="upload"></a>
</div>
</div>
javascript (coffee)
coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
widget = $('.image_widget')
manipulation = $('.image_widget').find('.manipulation')
#initialize
widget.fileupload(
singleFileUploads: true,
autoUpload: false,
)
addImage = (area, img) ->
# --- いろいろ画像処理 --- #
area.prepend(img)
widget.on 'fileuploadadd', (e, data) ->
file = data.files[0]
types = /(\.|\/)(gif|jpe?g|png)$/i
unless types.test(file.type) || types.test(file.name)
alert("#{file.name} はアップロードできないファイル形式です。")
return false
loadImage(file, (img) ->
addImage(manipulation, img)
, { maxWidth: 750, canvas: true }
)
manipulation.on 'click', '.upload', (e) ->
e.preventDefault()
img = manipulation.find('canvas').get(0)
if img.toBlob
img.toBlob( (blob) ->
fd = new FormData()
fd.append('image[url]', blob) #carrierwaveのmount_uploader
fd.append('image[user_id]', currentUserId) #その他送りたい
fd.append('image[その他のデータ]', etc) #いろんな情報をお好みで
$.ajax(
type: 'POST',
url: '/images', #'images#create'(POST)を想定してます
data: fd,
processData: false, #これ大事
contentType: false #これ大事
beforeSend: ->
sending = 'sending...'
manipulation.html(sending)
).done( (data) ->
#完了した後のいろいろ
)
)
fileuploadで画像読み込みを監視して、
読み込まれたらloadImageで画像を縮小してcanvasに描画。
ユーザーのクリックでblobオブジェクトをFormDataを使って送信。
CANPATHでは
せっかくクライアントで縮小させるなら、回転とか切り抜きとかできると最高だなと思って、そういうのも入れました。
画像を読み込むとサムネを表示してます。丸いボタンで右回りに回転することが出来ます。
実際にはもうひとつ大きな画像をcanvasに読み込んでいて…
編集ボタンを押すと、大きな方を表示。切り抜けます。
アップロードが終わるとcanvasは消され、サーバーからサムネだけが返ってきます。
最新のchrome, firefox, safariとかandroidとかiPhoneとかモダンなやつでは動いている様子。IEは知りません…w
ちょっとテスト運用して問題なさそうだったらこちらに切り替えようと思います。
参考
blueimpのいろいろ
Basic plugin · blueimp/jQuery-File-Upload Wiki · GitHub
blueimp/JavaScript-Canvas-to-Blob · GitHub
JavaScript Load Image
RailsでCanvasに描かれたイメージを送信する方法
Canvas Images and Rails - Blog @ RohitRox
例では使ってないですが、切り抜き機能も付け加えたので。
Jcrop Manual - Deep Liquid
contentType: false がなんで必要なのか
javascript - How to send FormData objects with Ajax-requests in jQuery? - Stack Overflow
例にはないですが、回転も実装してみたので。
JavaScript-Load-Image/load-image-orientation.js at master · blueimp/JavaScript-Load-Image · GitHub
Exif Orientation Page
Rotate image clockwise or anticlockwise inside a div using javascript - Stack Overflow