49hack

見習いエンジニアが魔法使いになるまで

ckeditor+carrierwaveで画像をS3にアップロードできるようにする

WYSIWYGといえばckeditor!ということで、ckeditorのインストールから画像のS3アップロードの設定までを書いていきます。

必要なgemをインストール

Gemfileに以下を追加し、bundleでインストールします。

# Gemfile
gem 'carrierwave',github: 'carrierwaveuploader/carrierwave'
gem "fog"
gem 'rmagick'
gem 'mini_magick'

今回は画像のアップローダーとしてcarrierwaveを使います。オススメですしおすし。

carrierwaveの場合、なぜかmini_magickがないとエラーになる鬼仕様なので画像処理にrmagickだけ使う場合もmini_magickが必要です。。

CKeditor + Carrierwave + RMagick Error · Issue #527 · galetahub/ckeditor

# ckeditor/lib/ckeditor/backend/carrierwave.rb
require 'mini_magick' # なんでmini_magick固定やねん!

module Ckeditor
  module Backend
    module CarrierWave
...

carrierwaveの設定を編集する

以下のコマンドを打つと、画像アップロードに必要なファイルの生成やルーティングの設定が行われます。

$ rails generate ckeditor:install --orm=active_record --backend=carrierwave

ちなみに、carrierwaveのほかにpaperclipやrefile、dragonfly(はじめて聞いた)にも対応してます。

carrierwaveをカスタマイズ

carrierwaveで画像をS3にアップロードするためにもろもろ設定が必要です。 この記事を参考にcarrierwave.rbを編集すればOKです。

carrierwaveを使ってS3に画像をアップロードする - 49hack

アップローダーをカスタマイズ

uploaderのストレージをS3用のfogに変更します。

# app/uploaders/ckeditor_picture_uploader.rb
class CkeditorPictureUploader < CarrierWave::Uploader::Base
    include Ckeditor::Backend::CarrierWave

    include CarrierWave::RMagick
    # include CarrierWave::MiniMagick
    # include CarrierWave::ImageScience

    storage :fog # ここ

また、url_contentを変更することで、取得する画像のURLを変更することが可能です。 CDNのURLに変更したりするといいのではないでしょうか〜。

# app/models/ckeditor/picture.rb
class Ckeditor::Picture < Ckeditor::Asset
    mount_uploader :data, CkeditorPictureUploader, :mount_on => :data_file_name

    # 画像アップロード時、または選択時にフォームにペーストされるURL
    def url_content
        ENV['CDN_URL'] +"/hoge/fuga/#{filename}"
    end

end

ckeditorの設定を編集する

config.jsを編集することでckeditorをカスタマイズできます。

ツールバーの表示項目が多ければconfig.toolbar = ...と記述すれば項目を限定することができます。詳しくはこちらをご覧ください。

Toolbar Configuration - CKEditor 4 Documentation

画像アップロードのボタン、タブを有効にするためには、config.filebrowserImageBrowseLinkUrlなどの記述が必要です。

設定は下記記事の内容をそのまま使用しました。CSRFトークンのもろもろの設定をしています。

使用 ckeditor gem - your site

// app/assets/javascripts/ckeditor/config.js
CKEDITOR.editorConfig = function (config) {
    // The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Link tab of Image dialog.
    config.filebrowserImageBrowseLinkUrl = "/ckeditor/pictures";

    // The location of an external file browser, that should be launched when "Browse Server" button is pressed in the Image dialog.
    config.filebrowserImageBrowseUrl = "/ckeditor/pictures";

    // The location of a script that handles file uploads in the Image dialog.
    config.filebrowserImageUploadUrl = "/ckeditor/pictures";
    // Rails CSRF token
    config.filebrowserParams = function(){
        var csrf_token, csrf_param, meta,
        metas = document.getElementsByTagName('meta'),
        params = new Object();

        for ( var i = 0 ; i < metas.length ; i++ ){
            meta = metas[i];

            switch(meta.name) {
                case "csrf-token":
                    csrf_token = meta.content;
                    break;
                case "csrf-param":
                    csrf_param = meta.content;
                    break;
                default:
                    continue;
            }
        }

        if (csrf_param !== undefined && csrf_token !== undefined) {
            params[csrf_param] = csrf_token;
        }

        return params;
    };

    config.addQueryString = function( url, params ){
        var queryString = [];

        if ( !params ) {
            return url;
        } else {
            for ( var i in params )
                queryString.push( i + "=" + encodeURIComponent( params[ i ] ) );
        }

        return url + ( ( url.indexOf( "?" ) != -1 ) ? "&" : "?" ) + queryString.join( "&" );
    };

    // Integrate Rails CSRF token into file upload dialogs (link, image, attachment and flash)
    CKEDITOR.on( 'dialogDefinition', function( ev ){
        // Take the dialog name and its definition from the event data.
        var dialogName = ev.data.name;
        var dialogDefinition = ev.data.definition;
        var content, upload;

        if (CKEDITOR.tools.indexOf(['link', 'image', 'attachment', 'flash'], dialogName) > -1) {
            content = (dialogDefinition.getContents('Upload') || dialogDefinition.getContents('upload'));
            upload = (content == null ? null : content.get('upload'));

            if (upload && upload.filebrowser && upload.filebrowser['params'] === undefined) {
                upload.filebrowser['params'] = config.filebrowserParams();
                upload.action = config.addQueryString(upload.action, upload.filebrowser['params']);
            }
        }
    });
}

I18nの設定も忘れずに

これがないとファイルブラウザでtranslation missingになってデザインが崩れちゃうのでご注意ください。

ja:
  ckeditor:
    page_title: 'CKEditorファイルマネージャー'
    confirm_delete: 'ファイルを削除しますか?'
    buttons:
      cancel: 'キャンセル'
      upload: 'アップロード'
      delete: '削除'
      next: '次へ'

まとめ

ckeditorの設定まわりで結構ハマりましたが(なんせドキュメントが見にくい。。)、これでいい感じにアップローダーも連携できたのでもうやりたい放題です。

やっぱCarrierwave使いやすいな〜。

参考

ckeditorのスタイルが本番環境で崩れる

Railsでよくある「開発環境では動くのに本番環境ではうまくいかない」パターン。

ckeditorが本番環境ではcssが 404 not found になってました。

以下の3行を加えてckeditorのアセットも一緒にプリコンパイルしてやればOKです。

# config/enviroments/production.rb
...
config.assets.precompile += Ckeditor.assets
config.assets.precompile += %w( ckeditor/* )
config.autoload_paths += %W(#{config.root}/app/models/ckeditor)
...

参考

rubyで unused variable と言われないように使わない変数にはアンダースコア(_)を使う

例えば、以下のように使わない変数が宣言されている場合警告が出ます。

array = [[1,2],[4,3],[2,1]]
array.sort{|(k1, v1), (k2, v2)| v2 <=> v1}
# 「assigned but unused variable - k1」と警告が出ます。

このように使わない変数にはアンダースコア_を使うと警告が出なくなります。

array = [[1,2],[4,3],[2,1]]
array.sort{|(_, v1), (_, v2)| v2 <=> v1}

また、ruby2.0からは単語の先頭にアンダースコア_をつけてあげればOKです。

array = [[1,2],[4,3],[2,1]]
array.sort{|(_k1, v1), (_k2, v2)| v2 <=> v1}

これは地味に便利。

参考

配列から重複した要素を取り除くのではなく取り出す

ただ単に重複した要素を取り除く場合は、uniqを使えばOKです。

a = [1,2,3,5,1,2,4]
a.uniq
=> [1, 2, 3, 4, 5]

今回は、重複した要素そのものを取り出したい、つまり、先ほどの配列aから[1,2]を抽出します。

a = [1,2,3,5,1,2,4]
a.uniq.map { | e | [e, a.count(e)] }.select { | _, c | c > 1 }.map{ |e, c| e }
=> [1, 2]

ちなみにrubyのバージョンは以下のとおりです。

ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin14.0]

参考

印刷するときだけ要素を非表示にする

印刷ページを別途用意するのもいいけど、ちょっとそれはめんどくさい。。

ってときに、css3のメディアクエリを使うと便利でした。

@media print{
    .no_print{
        display: none;
    }
}
<p>このパラグラフは印刷時には表示されるよ</p>
<p class="no_print">このパラグラフは印刷時には表示されないよ</p>

ちなみに、メディアクエリに使えるタイプはprintscreenで、max-widthとかいろいろ使ってレスポンシブにできるようです〜。なるほどなるほど。

@media screen and (min-width: 600px) and (max-width: 900px) {
    .hoge {
        background: #9CF;
    }
}

参考

Carrierwaveでアップロードした画像が回転して表示されてしまう問題を解決する

結構ハマりました。

画像アップロードの構成としては、

  1. CarrierwaveでS3にアップロード
  2. nginxでS3にリバースプロキシ
  3. small_lightで指定サイズにリサイズして表示

という流れをとっています。

アップロードした画像をS3から確認してみると正しい向きで表示されたのですが、nginxのsmall_lightを介すと回転して表示されました。。

そのため、最初はnginxのsmall_lightが原因と考えて調査していたのですが、なかなか該当記事が見つからず。。。small_lightのオプションでangleも指定できますが、そんなことしてないし。。

原因

画像のExifのOrientationがおかしいのが原因で、nginxを介した際にそれを考慮して回転されてたものと思われます。(なんでS3では回転されなかったんだろう。。古いタイプのスマートフォンで撮影した写真で起きるっぽい。。)

解決方法

CarrierwaveでOrientationを正しい値に修正してから画像をアップロードするようにします。

class ImageUploader < CarrierWave::Uploader::Base

    include CarrierWave::RMagick

    process :fix_rotate

    # アップロードした写真が回転してしまう問題に対応
    def fix_rotate
        manipulate! do |img|
            img = img.auto_orient
            img = yield(img) if block_given?
            img
        end
    end
end

参考

inputで画像ファイルを選択したときにその画像を表示する

ファイルを選択するまでは、てきとーにno photo画像をセットしておきます。

また、選択をキャンセルしたときはno photo画像がセットされます。

$(function(){
    function readImage(input) {
        if ( input.files && input.files[0] ) {
            var FR= new FileReader();
            FR.onload = function(e) {
                $('#image').attr( "src", e.target.result );
            };       
            FR.readAsDataURL( input.files[0] );
        }
        else {
            // ファイルが選択されてない
            $('#image').attr( "src", "no_photo.png" );
        }
    }

    $("#file").change(function(){
        readImage( this );
    });
});
<img id="image" src="no_photo.png">
<input id="file" name="photo" type="file">

参考

MacBook用スタンドKickflipが便利すぎてもうみんな買えばいいのに

f:id:paranishian:20150914121158j:plain

MBAのスタンドほしいなーとおもってたのですがイマイチいいのが見つからず困ってました。

で、知り合いのエンジニアにKickflipを教えてもらい、最高すぎてその場で即ポチりました。

MacBookにそのまま貼り付けるタイプなのでどこでも使えてちょー便利です。

対応サイズは13インチ/15インチ用の2種類ですが、、、、

安心して下さい。11インチでも使えますよ…!!

僕のMBAは11インチですが、13インチ用でも普通に使えます。ぴったり。 f:id:paranishian:20150914121331j:plain

横から見るとこんな感じ。5mmくらいしか厚さが変わらず、全然気になりません。 f:id:paranishian:20150914121508j:plain

立ててみます。通気性良し、熱がこもりません。 f:id:paranishian:20150914121459j:plain

どこでも立てれて普段使いも捗るしかなりイイ感じです。 f:id:paranishian:20150914122047j:plain

これがたったの3,000円ちょい。みんな買えばいいのに!

Carrierwaveで画像をリサイズする

RMagickを使って画像のリサイズを行います。

今回はサムネイル表示用にバージョンthumbもつくってみます。

gemのインストール

# Gemfile
gem 'carrierwave'
gem 'rmagick' # 今回のリサイズ処理に必要

リサイズ処理を記述

# app/uploaders/image_uploader.rb
...
class CommentImageUploader < CarrierWave::Uploader::Base

    # RMagickをincludeします   
    include CarrierWave::RMagick

    # 1200x900にリサイズします
    process resize_to_limit: [1200, 900]

    # バージョンを作成して、別のリサイズを指定することもできます
    version :thumb do
        process resize_to_limit: [480, 360]
    end
...

画像の表示

モデルはこんな感じなので、

# app/models/users.rb
class User < ActiveRecord::Base
    mount_uploader :avater, ImageUploader
...

画像はこんな感じで表示します。

# app/views/user/show.html.slim
...
    # 画像の表示
    = image_tag @user.avater.url
    # thumb画像の表示
    = image_tag @user.avater.thumb.url
...

画像のリサイズオプションについて

いくつかリサイズオプションがあるのですが、いまいち違いがわからず調べたところ、こちらの記事がかなりわかりやすかったです。

CarrierWave + RMagick 画像のリサイズをまとめてみました - 麺処 まつばCarrierWave + RMagick 画像のリサイズをまとめてみました - 麺処 まつば

こんにちは。麺処まつば副店長です。久々の投稿ですけど、もう色々気にしないことにしました。店長の視線がなんだって言うんですか。(キッ)さて副店長、先日中ずっとCa...

resize_to_fitresize_to_limitの違い

両方とも、元画像のアクペクト比を維持したままリサイズしますが、

resize_to_fit:画像が指定した大きさより小さい場合、指定した大きさまで拡大する

resize_to_limit:画像が指定した大きさより小さい場合、なにもしない

拡大してボケるのがいやなので今回はresize_to_limitを使いました。

参考

carrierwaveを使ってS3に画像をアップロードする

画像のアップロードにcarrierwaveを使ってみたのでメモがてらまとめます。 ほかにも、比較的実装が容易なpaperclipやcarrierwaveの後継であるrefileも使ってみましたが、保存先のディレクトリ指定、ファイル名変更、確認画面作成などの要件が満たせなかったので断念しました。

gemのインストール

Gemfileに必要なgemを記述してbundle installでインストールします。

# Gemfile
gem 'carrierwave',github: 'carrierwaveuploader/carrierwave'
gem "fog"
gem 'rmagick'

サーバー複数台構成でELB使ってる場合はキャッシュはローカルではなくS3にしましょう。例えば、確認画面に遷移したときに違うサーバーを見に行って該当ファイルがない!エラー!みたいな不具合が防げます。

キャッシュのS3対応をする場合、carrierwavegithubから取得する必要があります。

また、fogはS3へのアップロード、rmagickはサイズなどの画像情報取得に使います。

アップローダーの作成

アップローダーを作成します。

rails g uploader image
# => app/uploaders/image_uploader.rb

アップローダーのマウントとinitializerの設定

今回はUserモデルのavaterプロパティにマウントしてみます。

独自にfile_sizeのバリデーションも追加してます。

# app/models/user.rb
class User < ActiveRecord::Base
    mount_uploader :avater, ImageUploader

    validate :file_size

    # 5MB以上のファイルはUPLOADできないようにしてみる
    def file_size
        upload_limit = 5.megabytes.to_i
        if photo.file.size > upload_limit
            errors.add(:avater, "のサイズが大きすぎます。")
        end
    end
end
# /config/initializers/carrierwave.rb
CarrierWave.configure do |config|
    config.fog_credentials = {
        provider: 'AWS',
        aws_access_key_id: ****,
        aws_secret_access_key: ****,
        region: 'ap-northeast-1',  # Tokyoの場合
    }
    config.cache_storage = :fog # キャッシュにS3を指定

    # テストとかで同じとこにUPLOADされたくないのでバケットを分けます
    case Rails.env
    when 'production'
        config.fog_directory  = 'bucket_production'
    when 'staging'
        config.fog_directory  = 'bucket_staging'
    when 'development'
        config.fog_directory  = 'bucket_development'
    when 'test'
        config.fog_directory  = 'bucket_test'
    end
end

アップローダーをいろいろカスタマイズ

# app/uploaders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base

    # 画像のサイズとか取得するためにRMagickをinclude
    include CarrierWave::RMagick
    
    # before_createみたいなもの
    process :store_dimensions, :store_extension

    # ストレージにS3を指定
    storage :fog

    # 画像ごとに保存するディレクトリを変えたいのでオーバーライド
    def store_dir
        # 例えばidごとにディレクトリを分けてみる
        "avater/#{model.id}"
    end

    # ファイル名を書き換える
    def filename
        # 例えば avater_1.jpg みたいなファイル名にしてみる
        "avater_#{model.id}.#{file.extension}" if original_filename
    end

    # キャッシュ先のディレクトリを指定
    def cache_dir
        "cache"
    end

    # RMagickを使って画像の幅、高さを取得する
    def store_dimensions
        if file && model
            img = ::Magick::Image::read(file.file).first
            model.width = img.columns
            model.height = img.rows
        end
    end
    
    # 画像の拡張子を取得する
    def store_extension
        if file && model
            model.extension = file.extension
        end
    end
end

画像名に日本語が使えるようにする

デフォルトだとファイル名に日本語が使われている場合filenameが"____"に変換されてしまうのでinitializerに一行追加する。

# /config/initializers/carrierwave.rb
CarrierWave::SanitizedFile.sanitize_regexp = /[^[:word:]\.\-\+]/

確認画面をつくってみる

なにかとめんどくさい確認画面をつくります。(確認画面って日本だけの文化?海外の記事でもなかなか情報がなくて苦戦しました。。)

キャッシュをS3にしている場合、インスタンスをnewしてファイルをアタッチした時点でキャッシュに画像が保存されています。

***_cacheを使うことで再アップロードしなくてもファイルの情報を使いまわすことができます。これをファイルのバリデーションエラーや確認画面に利用します。

モデル.saveした時点で、画像がcache_dirから削除されstore_dirに保存されます。

すべて書くのが面倒なので一部だけ書きます。

# app/views/hoge/confirm.html.slim
...
    = form_for @user, url: "/path/to/create_user" do |f|
        = f.hidden_field avater_cache
        = image_tag @user.avater.url
        = f.submit "アップロード"
...
# app/controllers/hoge_controller.rb
...
    def confirm
        @user = User.new(user_params)
    end

    def create_user
        user = User.create(user_params)
    end

    private
    def user_params
        params.permit(
            ...
            :avater,
            :avater_cache
            ...
        )
    end
...

carrierwaveを使ってみて

今回、画像アップロード機能を実装するにあたり、paperclip→refile→carrierwaveの順で試してみました。

paperclipは実装がかなりカンタンでしたが、確認画面をつくるところで自前実装が必要だとわかり断念しました。。refileに関してはファイルがすべてprivateでしかUPLOADできなかったり?、まだまだ情報が少なかったので諦めました。笑

それに比べてcarrierwaveは自由度が高く、かつ情報を集めやすくて、とっても素敵なライブラリでした。

次回以降もお世話になろうと思います。

参考