Nuestro propio procesador de imágenes utilizando Paperclip

  • por Juan Cortés

Un recurso básico que suelen tener las aplicaciones web es la posibilidad de agregar imágenes, ya sea como foto de perfil de usuario, compartir fotos, entre otros. Para estos casos, el usuario debe ser capaz de manejar su imagen previamente (¿se imaginan lo engorroso que sería tener que usar un editor de imágenes antes de subir una foto de perfil a Facebook? ¿O aplicar los filtros de Instagram por nuestra cuenta?)

En este caso, veremos como crear un procesador de imágenes que permita al usuario cortarla según las dimensiones que él quiera usando Rails 5 y Paperclip, para esto agregamos las gemas al Gemfile:

gem "paperclip", "~> 5.0.0"
gem "jcrop-rails-v2"

#app/assets/javascripts/application.js

//= require jquery.Jcrop

#app/assets/stylesheets/application.css

 *= require jquery.Jcrop

En este caso, utilizaremos la librería Jcrop, que a pesar de no haberse actualizado desde principios del 2014, permite mejorar el interfaz donde el usuario elige las dimensiones de corte para la imagen. Luego, suponiendo que el modelo User fue previamente creado, realizamos la siguiente migración que agregará las columnas necesarias al modelo de usuarios para agregar el avatar:

rails g paperclip user avatar

Hasta el momento deberiamos tener lo siguiente

#app/models/user.rb

class User < ApplicationRecord
  has_attached_file :avatar
  validates :avatar, attachment_presence: true
  validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\z/
end

#app/controllers/users_controller.rb

  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'Usuario creado' }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  def user_params
    params.require(:user).permit(:name, :avatar)
  end

Con esto listo, toca la parte interesante en la cual le daremos la posibilidad al usuario de elegir las dimensiones de su imagen, para ello debemos agregar las variables que nos indicarán donde cortar.

#app/models/user.rb

attr_accessor :crop_x, :crop_y, :crop_w, :crop_h

#app/controllers/users_controller.rb

  def user_params
    params.require(:user).permit(:name, :crop_x, :crop_y, :crop_h, :crop_w,
                                 :avatar)
  end

Estos parámetros, como han de suponer, por una parte indican en que punto en el eje 'x' e 'y'  se comenzará el corte y por otra, el alto y ancho que tendrá1. Luego de esto, tenemos que crear nuestro procesador, este tendrá la dirección lib/paperclip_processors/cropper.rb ya que Paperclip busca los procesadores ahí de forma predeterminada. 

module Paperclip
  class Cropper < Thumbnail
    def initialize(file, options = {}, attachment = nil)
      super
      if target.crop_w && target.crop_x
        @current_geometry.width  = target.crop_w
        @current_geometry.height = target.crop_h
      end
    end

    def target
      @attachment.instance
    end

    def transformation_command
      # call crop processing only if user inputs are there
      if target.crop_w && target.crop_x
        crop_command = [
          "-crop",
          "#{target.crop_w}x" \
            "#{target.crop_h}+" \
            "#{target.crop_x}+" \
            "#{target.crop_y}",
          "+repage"
        ]
        crop_command + super
      else
        super
      end
    end
  end
end

Algunos aspectos importantes sobre el procesador creado por nosotros son:

  • Debe estar en el módulo de Paperclip
  • Debe heredar del procesador de la gema, Thumbnail
  • El attachment es el objeto que se está subiendo y mediante él se puede acceder a la instancia a la cual se esta relacionando

Además, en nuestro modelo tenemos que especificar que nuestro avatar será procesado por Cropper:

#app/models/user.rb

has_attached_file :avatar, styles: { original: {} }, processors: [:cropper]

Es indispensable agregar estilos en conjunto al procesador, ya que Paperclip ignora todos los post-procesamientos en caso de que no hayan. Finalmente, agregamos la visualización de la imagen y el manejo con jcrop en el form donde estamos subiendo la imagen

#app/views/users/_form.html.erb

  <div id="avatar-preview">
    <img class="avatar-preview-tag" id=cropbox>
    <% for attribute in [:crop_x, :crop_y, :crop_w, :crop_h] %>
      <%= f.hidden_field attribute, id: attribute %>
    <% end %>
  </div>

#app/assets/javascripts/users.js

var jcrop_api = null;
$(document).on('turbolinks:load', function(){
  $('#user_avatar').on('change', function(event) {
    var files = event.target.files;
    var image = files[0]
    var reader = new FileReader();
    reader.onload = function(file) {
      var img = new Image();
      img.src = file.target.result;
      // remove css in case you change the avatar so it doesn't keep the other avatar styles
      $('.avatar-preview-tag').css('height', '');
      $('.avatar-preview-tag').css('width', '');
      if (jcrop_api !== null){
        jcrop_api.destroy()
      }
      $('.avatar-preview-tag').attr('src', img.src);
      $('#cropbox').Jcrop({
        onChange: update_crop,
        onSelect: update_crop,
        setSelect: [0, 0, 500, 300]
        // aspectRatio: 2  // Crop rectangle properties
      }, function(){
        jcrop_api = this;
      });
    }
    reader.readAsDataURL(image);
  function update_crop(coords) {
    $('#crop_x').val(coords.x);
    $('#crop_y').val(coords.y);
    $('#crop_w').val(coords.w);
    $('#crop_h').val(coords.h);
  }
  });
})

Con lo anterior se agrega una previsualización al estar subiendo la imagen para que el usuario pueda manejarla a su parecer. El resultado del procesador creado es similar al siguiente:

Código:

https://github.com/nnodes/crop_example

Referencias:
- Paperclip

- Thumbnail

- JCrop

https://www.viget.com/articles/manual-cropping-with-paperclip


1.- Es importante que los crops se permitan antes que el avatar para poder acceder a ellos en el procesador.