Crop image uploader with cropper and CarrierWave

I. Giới thiệu

Hẳn là mọi người không còn xa lạ gì với các ứng dụng cắt ảnh mỗi khi thay đổi ảnh đại diện của facebook hay google rồi đúng không ạ. Hôm nay em xin giới thiệu về ứng dụng cắt ảnh khi tải lên trong rails app sử dụng thư viện cropper và carrierWave.

Mọi người có thể xem các chức năng của thư viện cropper tại website: https://fengyuanchen.github.io/cropper/ Thư viện này có rất nhiều chức năng như xoay ảnh, zoom, cắt ảnh, nhưng trong bài viết này, em xin chỉ giới thiệu chức năng cắt ảnh của thư viện này.

II. Cài đặt

  1. Cài đặt gem cần thiết Trong Gemfile, thêm các gem sau:

    gem "carrierwave"
    gem "mini_magick"
    gem "cropper-rails"
    

    Chạy bundle install để cài đặt các gem này.

    Thêm vào application.js //= require cropper Thêm vào application.css *= require cropper

  2. Sử dụng Ví dụ ta có một model User với các thuộc tính name:string, avatat:string trong đó, avatar lưu hình ảnh được upload lên và chúng ta sẽ crop ảnh này trước khi lưu vào database.

  • Trong model, thêm các thuộc tính avatar_crop_x, avatar_crop_y, avatar_crop_w, avatar_crop_h để lưu các tọa độ của ảnh đã cắt.

    class User < ApplicationRecord
     attr_accessor :avatar_crop_x, :avatar_crop_y, :avatar_crop_w, :avatar_crop_h
     CROP_AVATAR = [:avatar_crop_x, :avatar_crop_y, :avatar_crop_w, :avatar_crop_h]
     validates :name, presence: true
     mount_uploader :avatar, AvatarUploader
    end
    

    Tất nhiên, để tải ảnh lên với carrierWave, phải thêm vào model User mount_uploader :avatar, AvatarUploader

  • Trong form new/edit user, ngoài các trường cần thiết, ta thêm các hidden field lưu các tọa độ cắt ảnh:

    <% User::CROP_AVATAR.each do |attr| %>
      <%= f.hidden_field attr.to_sym, id: "user_#{attr}" %>
    <% end %>
    

    Thêm html cho show popup:

    <div id="crop_avatar_popup" class="mfp-hide" style="display:none;">
     <div id="popup">
       <h2>Crop avatar</h2>
       <div class="crop_image">
         <img src="" id="user_avatar_crop" style="max-width: 100%;">
         <%= submit_tag "Crop", id: "submit_crop" %>
       </div>
     </div>
    </div>
    

    Chú ý: ảnh trong img tag sử dụng để crop ảnh cần phải thêm style="max-width: 100%;" cho thẻ img này.

  • Xử lý hoạt động cắt ảnh trong js: Mọi người có thể sử dụng cửa sổ modal để mở ảnh khi upload lên. Ngoài modal của bootstrap, ở đây, em sử dụng magnific-popup để mở cửa sổ modal dùng để cắt ảnh. Mọi người có thể tìm hiểu thêm về magnific-popup tại đây: https://github.com/joshuajansen/magnific-popup-rails http://dimsemenov.com/plugins/magnific-popup/documentation.html Trước hết, ta cần đoạn code js để đọc file ảnh mới upload:

    $(document).on("change", "input#user_avatar", function(event){
         var input = $(event.currentTarget);
         var file = input[0].files[0];
         var reader = new FileReader();
         var img_tag = input.parent().find("#preview_avatar").children("img");
         reader.onload = function(e){
           var img = new Image();
           img.src = e.target.result;
           img.onload = function() {
             open_crop_image_modal(img.src, img_tag);
           }
         };
         reader.readAsDataURL(file);
    });
    

    Trong đó, hàm open_crop_image_modal(img.src, imgtag) dùng để mở cửa sổ popup ảnh. Sau khi mở cửa sổ popup sử dụng magnific-popup, thêm src cho img tag từ ảnh đã input:

    $("#user_avatar_crop").attr("src", img);
    $(".cropper-canvas img, .cropper-view-box img").attr("src", img);
    

    Sau đó, sử dụng thư viện cropper để crop ảnh tại tag img có id là "user_avatar_crop":

    function cropImage(){
         var $crop_x = $("input#user_avatar_crop_x"),
           $crop_y = $("input#user_avatar_crop_y"),
           $crop_w = $("input#user_avatar_crop_w"),
           $crop_h = $("input#user_avatar_crop_h");
         $("#user_avatar_crop").cropper({
           aspectRatio: 1,
           background: false,
           zoomable: false,
           crop: function(data) {
             $crop_x.val(Math.round(data.x));
             $crop_y.val(Math.round(data.y));
             $crop_h.val(Math.round(data.height));
             $crop_w.val(Math.round(data.width));
           }
        });
    }
    

    Giải thích: Để sử dụng thư viện cropper và bắt đâuù cắt ảnh, sử dụng method $("#user_avatar_crop").cropper({}); Một số option của cropper:

    • aspectRatio: Đặc tả tỉ lệ giữa chiều ngang và chiều dọc của kích thức ảnh được cắt, ở đây, em set là 1 tức là ảnh được cắt sẽ có chiều ngang bằng chiều dọc, tức ảnh cắt có hình vuông.
    • background: mặc định sẽ là true, túc ảnh có thể được cắt cả phần bên ngoài của ảnh gốc và phần ngoài ảnh gốc đó được thay bằng 1màu background.
    • zoomable: mặc định là true, tức ảnh gốc có thể zoom out, zoom in và cắt bình thường.
    • crop: function(data){} : event crop ảnh, khi di chuyển khung crop hay thay đổi kích thước... trong khi cắt ảnh. Ở đây, em set các giá trị tọa độ x,y,height, width(tên tọa độ được cài mặc định, tương ứng với offset left, offset top, width, height của ảnh đã cắt) vào các input tương ứng trong form new/edit user.
    • Và rất nhiều option khác mọi người có thể xem thêm tại https://github.com/fengyuanchen/cropper

    Sau khi submit ảnh đã cắt, đóng cửa sổ popup lại và submit form như bình thường, như thế, params trả lên bây giờ sẽ bao gồm thêm các thuộc tính avatar_crop_x, avatar_crop_y, avatar_crop_w, avatar_crop_h với các giá trị tọa độ tương ứng.

  • Crop ảnh trong carrierWave: Trong file uploaders/avatar_uploader.rb định nghĩa hàm crop_image như sau:

    class AvatarUploader < CarrierWave::Uploader::Base
        include CarrierWave::MiniMagick
        process :crop_image
    
        storage :file
    
        version :medium do
          process resize_to_fit: [300, 300]
        end
    
        def extension_white_list
          %w(jpg jpeg gif png)
        end
    
        def crop_image
          if model.avatar_crop_x.present?
            manipulate! do |img|
              x = model.avatar_crop_x.to_i
              y = model.avatar_crop_y.to_i
              w = model.avatar_crop_w.to_i
              h = model.avatar_crop_h.to_i
              img.crop "#{w}x#{h}+#{x}+#{y}"
            end
          end
        end
    end
    

    Gọi process :crop_image, carrierWave sẽ check hàm crop_image và MiniMagick sử dụng hàm crop để cắt ảnh trước khi tạo các version và lưu vào các object. Và bây giờ, ảnh sau khi cắt đã được lưu thành công cho các user.

III. Tổng kết

Như vậy, em đã giới thiệu xong các bước cơ bản để tạo ứng dụng crop ảnh khi upload avatar cho các user. Mọi người có thể thử và áp dụng các chức năng khác của thư viện cropper cho ứng dụng của mình. Cảm ơn mọi người đã đọc bài viết, rất mong nhận được sự góp ý của mọi người ạ. Link project demo: https://github.com/phanbt58/crop_image