在railscast看到用paperclip跟Jcrop(一个jquery的一个插件)来实现图片裁剪功能,看起来很简单 就顺便用在现在做的项目里面了。 结果杯具的花了一天多的时间来弄。

在上一篇

[/rails]

paperclip works on windows 中记录了怎么给一个model加avatar

100×100# VS 100×100>

在model中我们加入了一下的代码

has_attached_file :avatar, :styles => { :medium => "300x300>", :thumb => "100x100>" }

注意这里的在维度后面加了一个大于号,当时不是很理解。 google也没有找到答案,今天又仔细研究了一下, 100*100>的话呢,>是指按比例缩放的情况下最长边等于100. #就不是按比例缩放了, 如果是 100×100# 最后的图就是拉伸到100×100,不论之前的比例。

首先 在model里面把上面这句改成下面一句

has_attached_file :avatar, :style {:medium=>"150x150#", :large=>"500x500>"}

我们在页面上显示用户的头像是150*150的。
因为用户上传的图片的大小必然是各种各样的,所以我们这里先把用户给的图片变化成我们已知的大小, 这里的large style接下来就会用来被裁剪的图片。

Action

我们要实现的是, 当用户upload一个图片时转到一个叫crop的页面去进行裁剪。
所以需要修改controller里面的action,下面这段代码直接从railscast里面偷过来的。

def create
    @user = User.new(params[:user])
    if @user.save
      if params[:user][:avatar].blank?
        flash[:notice] = "Successfully created user."
        redirect_to @user
      else
        render :action => "crop"
      end
    else
      render :action => 'new'
    end
  end

  def update
    @user = User.find(params[:id])
    if @user.update_attributes(params[:user])
      if params[:user][:avatar].blank?
        flash[:notice] = "Successfully updated user."
        redirect_to @user
      else
        render :action => "crop"
      end
    else
      render :action => 'edit'
    end
  end

Jcrop选区

然后我们要新加一个crop的view,就是在upload一个图片后,跳转到用来图片剪裁的页面。 在这个页面中呢, 我们就要用到Jcrop这个plugin了,下载地址
把JavaScript,stylesheet一股脑都丢进这个文件里面, 当然为了保证Javascript在head里面呢,我们可以用 yield跟content_for 来做,请同学们自行动手尝试。(也可以到这里围观Ryan是怎么做的)

加入下列代码 依然是偷过来的,我自己的使用haml写的

    <% title "Crop Avatar" %>
    <% content_for (:head) do %>
    <%= stylesheet_link_tag "jquery.Jcrop" %>
    <%= javascript_include_tag "jquery.Jcrop.min" %>
    <script type="text/javascript">
     $(function() {
        $('#cropbox').Jcrop();
      });
    </script>
   <% end %>

  <%= image_tag @user.avatar.url(:large), :id => "cropbox" %>

通知paperclip选区范围

显然这只实现了一个在图片中选中一个区域的步骤,我们需要告诉paperclip我们选择了哪个区域。
回到model里 加入下面的代码

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

什么是attr_accessor, 字面上来看呢 就是attribute accessors,如果你熟悉java或者c++, whatever else. 这个就相当于getter跟setter的合体。
这里的四个值呢 分别是 crop_x 左上角的x坐标, crop_y 左上角的y坐标, crop_w 所选框的width,同理crop_h 所选框的height.

怎么把这些值传递给paperclip让它知道呢,首先想到的是要弄一个表单,为了不让这些值显示给亲爱的用户,所以我们使用hidden input.

继续偷代码

 <% form_for @user do |form| %>
    <% for attribute in [:crop_x, :crop_y, :crop_w, :crop_h] %>
      <%= form.text_field attribute, :id => attribute %>
     <% end %>
     <p><%= form.submit "Crop" %></p>
  <% end %>

这时候我们在Jcrop的调用中也要加一些配置了

$(function() {
  $('#cropbox').Jcrop({
    onChange: update_crop,
    onSelect: update_crop,
    setSelect: [0, 0, 500, 500],
    aspectRatio: 1
  });
});

function update_crop(coords) {
   $('#crop_x').val(coords.x);
   $('#crop_y').val(coords.y);
   $('#crop_w').val(coords.w);
   $('#crop_h').val(coords.h);
   }

这里update_crop的方法呢就是用来更新选区。通过表单提交给model,这时候呢我们要通知model 我们需要来进行crop
在model中加入以下偷来的代码

 after_update :reprocess_avatar, :if => :cropping?

   def cropping?
     !crop_x.blank? && !crop_y.blank? && !crop_w.blank? && !crop_h.blank?
   end

   private
   def reprocess_avatar
     avatar.reprocess!
   end

上面这段代码是说什么呢, 通过crop_x, crop_y, crop_w,crop_h来判定是否进行croping,如果进行cropping的话就通知paperclip再reprocess一下。
然后就是要告诉paperclip该如何处理?

Reprocess

安装railscast上讲的,我们需要有在lib文件夹下,新建一个paperclip_processors的文件夹, 然后new以下新的文件cropper.rb 内容如下

module Paperclip
  class Cropper < Thumbnail
    def transformation_command
      if crop_command
        crop_command + super.sub(/ -crop \S+/, '')
      else
        super
      end
    end

    def crop_command
      target = @attachment.instance
      if target.cropping?
        " -crop '#{target.crop_w}x#{target.crop_h}+#{target.crop_x}+#{target.crop_y}'"
      end
    end
  end
end

这时候测试一下, 错误如下
uninitialized constant Paperclip::Cropper
检查一下,好像是因为lib文件夹下面的文件并没有自动载入, 一google才知道 rails3 取消了对lib文件的autoload. 既然不能自动我们就手动呗。到model文件里面加入下面两句

require 'lib/paperclip_processors/cropper.rb'
include Paperclip

再次运行,新的错误又来了,
undefined method `sub’ for ["-resize", "x170", "-crop", "170x170+9+0", "+repage"]:Array

看了railscast下面的comment,有人也有同样的问题,原因是新版本的paperclip改变了他处理命令的方式,这里有人给出了解决方式
在cropper文件里面分别替换成下面两行

crop_command + super.join(' ').sub(/ -crop \S+/, '').split(' ')
["-crop", "#{target.crop_w}x#{target.crop_h}+#{target.crop_x}+#{target.crop_y}"]

再运行下,理论上应该可以crop了。

校准

但是视乎裁剪的结果并不不准确,原因是我们用large style的图片来做选区,但其实是在original的图上裁剪的,所以我们需要在中间做一个转换。
我们需要知道original跟large的width跟height。在model里面再加入下面的代码

def avatar_geometry(style= :o riginal)
    @geometry ||= {}
    @geometry[style] ||= Paperclip::Geometry.from_file(avatar.path(style))
 end

把update_crop文件更新为下面这样

function update_crop(coords) {
 var ratio = <%= @user.avatar_geometry(:original).width %> / <%= @user.avatar_geometry(:large).width %>;
  $("#crop_x").val(Math.round(coords.x * ratio));
  $("#crop_y").val(Math.round(coords.y * ratio));
  $("#crop_w").val(Math.round(coords.w * ratio));
  $("#crop_h").val(Math.round(coords.h * ratio));
}
</script>

现在再一运行 一切正常啦。

add a preview

但是这样用户体验不够好啊,那再加入一个预览的吧
view文件里面

<h4>Preview</h4>
  <div style="width: 100px; height: 100px; overflow: hidden;">
  <%= image_tag @user.avatar.url(:large), :id => "preview" %>
</div>

javascrip里面继续更新一下update_crop文件

function update_crop(coords) {
	var rx = 100/coords.w;
	var ry = 100/coords.h;
	$('#preview').css({
		width: Math.round(rx * <%= @user.avatar_geometry(:large).width %>) + 'px',
		height: Math.round(ry * <%= @user.avatar_geometry(:large).height %>) + 'px',
		marginLeft: '-' + Math.round(rx * coords.x) + 'px',
		marginTop: '-' + Math.round(ry * coords.y) + 'px'
	});
  var ratio = <%= @user.avatar_geometry(:original).width %> / <%= @user.avatar_geometry(:large).width %>;
  $("#crop_x").val(Math.round(coords.x * ratio));
  $("#crop_y").val(Math.round(coords.y * ratio));
  $("#crop_w").val(Math.round(coords.w * ratio));
  $("#crop_h").val(Math.round(coords.h * ratio));
}

一切搞定。

悲催的我在写cropping?的时候, 少写了一个感叹号给crop_h.blank? debug了好久才找到。希望以后不要再犯这样的错误

n2148