Rails / できるだけ高速に画像をアップロードする

Shunsuke Sawada

このブログでも画像をアプロードする機能は実装していて、とくに不便を感じていなかったのですが、CANPATHのユーザーから要望があったので考え直すことにした。

今までのアップローダー

Screen Shot 2014-10-20 at 10.55.04 AM

こんな感じです。
ポップアップウインドウ的なものが出てきて、画像を選択するかドラッグ&ドロップすると自動的にアップロードがはじまって、終了するとサムネイルが現れる。→ 選択すると記事内に挿入される。
  
これはこれでシンプルで気に入っているんだけど、問題もある。
アップロード中にプログレスバーは出るんだけど、
それはアップロードの経過を示すもので、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に読み込んでいて…
Screen Shot 2014-10-20 at 11.46.37 AM
  
編集ボタンを押すと、大きな方を表示。切り抜けます。
Screen Shot 2014-10-20 at 11.47.07 AM
  
アップロードが終わるとcanvasは消され、サーバーからサムネだけが返ってきます。
Screen Shot 2014-10-20 at 11.47.45 AM
  
最新の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

84
Shunsuke Sawada

おすすめの記事

Rails / AjaxでHTMLが返ってきてしまう時の対処法
与えられた時間はたった1日 --ウェブサービス企画から公開まで--
7
[もっとみる...] / [See more...] をjQueryで実装する(開閉できるタイプ)