这一集我们来用Paperclip和jQuery叫做Jcrop的插件在网页上裁剪图片。假设我们有一个Rails应用,比如一个论坛,允许用用户名注册,并且还可以用头像图片来展示自己。
用户选择上传的图片可以是任何大小,所以我们的应用将会调整大小并裁剪到100像素的正方形之内。
这个应用用Paperclip来自动裁剪和调整图片的大小,但是我们想让用户自己可以控制怎么去裁剪图片。我们可以用一些JavaScript来实现,我们这一集就要讲怎么来做。
如果你不熟悉Paperclip,在134集中涵盖了Paperclip的内容,你想理解怎么用Paperclip来添加附件到你的Model中,那一集是值得看一下的,并且我们这一集也是建立在那一集之上的。
有一些JavaScript图片裁剪的库可以用;我们将要用一个jQuery的插件,叫做Jcrop的库。如果你在使用Prototype和Scriptaculous来代替jQuery,JavaScript Image Cropper UI library提供类似的功能。
创建裁剪页面
在我们的应用中有一个User Model用了Paperclip。
class User < ActiveRecord::Base has_attached_file :avatar, :styles => { :small => "100x100#" } end
avatar属性有一个style将会把图片裁剪成100×100。我们要生成另外一个大一点的图片,显示给用户让他裁剪他的头像。我们可以给用户显示原始图片,但是原始图片可以是任意大小或长宽比,并且打乱了我们的程序布局。比较好的方式是添加另一个确定大小的样式的图片让用户来裁剪。
has_attached_file :avatar, :styles => { :small => "100x100#", :large => "500x500>" }
这个新的large样式将把图片调整到500×500像素之内,但我们在大小后面用了”>
”,这比用”#
”好,Paperclip将维持这个长宽比,所以图片不会被裁剪。
我们的目的是修改我们的应用,让用户注册并上传图片或修改头像的时候,他们直接去一个可以裁剪他们上传图片的页面。要实现这些我们需要修改UsersController
中的create
和update
action,并且新建一个叫crop
的新action。
我们将首先修改create
action。如果没有用户没有提供头像,将直接重定向到以前的操作页面,如果有图片上传了,我们将渲染crop
action。
def create @user = User.new(params[:user]) if @user.save flash[:notice] = "Successfully created user." if params[:user][:avatar].blank? redirect_to @user else render :action => 'crop' end else render :action => 'new' end end
同样的,当用户更新后如果有新图片上传我们要显示crop
视图。
def update @user = User.find(params[:id]) if @user.update_attributes(params[:user]) flash[:notice] = "Successfully updated user." if params[:user][:avatar].blank? redirect_to @user else render :action => 'crop' end else render :action => 'edit' end end
我们在/app/views/users/crop.html.erb
创建new视图文件。这个文件将需要包含Jcrop用到的css和JavaScript文件,就和包含jQuery库一样。在我们的应用布局文件里头部有一个yield
段,这个允许我们从视图文件向页面的head
段插入内容。我们已经包含了jQuery库,所以不用担心这点。
<head> <title><%= h(yield(:title) || "Untitled") %></title> <%= stylesheet_link_tag 'application' %> <%= javascript_include_tag 'jquery.1.3.2.min' %> <%= yield(:head) %> </head>
我们要给新页面一个标题,然后新建一个content_for
块来放我们模板里要传到head段的代码。这将由一个Jcrop的样式文件和JavaScript文件的链接构成,文件我们下载下来放到我们应用的适当位置,还有一些内联的JavaScript也要放在里面,这些JavaScript将是id为cropbox的元素拥有Jcrop的功能。
最后我们在页面的body中渲染图片本身,把它id
设为cropbox
,这样我们写的jQuery代码就可以用在图片上了。
<% 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" %>
如果现在我们编辑一个用户,并且上传一张新的图片,我们将被重定向到哪个新的crop
action,在那我们将看见那个大的500×500版本的图片有Jcrop的功能。当光标在图片上面的时候将变成十字线的样子,我们拖动覆盖图片的一部分,我们就选择了这部分。
现在我们能选择图片的一部分了,但是我们还有办法去裁剪它。要想裁剪我们还要给页面添加一个表单,表单包含裁剪图片高度和宽度的字段,除此之外还有相对于左上角的x,y坐标值,和和一个”crop”按钮。
头像是作为User的一个属性,所以这个新表单将会修改用户的信息。我们需要在User
模型中新建四个新属相来放裁剪区域的坐标。
class User < ActiveRecord::Base has_attached_file :avatar, :styles => { :small => "100x100#", :large => "500x500>" } attr_accessor :crop_x, :crop_y, :crop_w, :crop_h end
现在我们已经添加了新属性,我们可以创建表单了。我们通常使用隐藏字段来存裁剪区域的坐标,但是在开发我们的页面时候用文本框,是为了我们能看见那些值被存住了。表单的代码就直接放在crop视图的图片下面。
<% 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 %>
我们已经给表单中每个文本框一个id
,这样我们可以在JavaScript中更新这些字段的值。要在裁剪区域改变的时候去更新这些字段,我们要在Jcrop调用的时候添加一些参数。
$(function() { $('#cropbox').Jcrop({ onChange: update_crop, onSelect: update_crop, setSelect: [0, 0, 500, 500], aspectRatio: 1 }); });
Jcrop有两个主要的事件回调,onChange
和onSelect
,当选择改变或移动的时候被调用。我们指定一个回调函数,当这些事件发生时都会被调用。我们也定义另外两个参数:setSelect
,用来定义最初的的裁剪矩形,和aspectRatio
,用来定义裁剪矩形的长宽比,我们设为1,所以只有正方形可以被选择。
我们还需要写update_crop
回调函数。这是传递坐标对象的,从中能提取出x
, y
, width
和height
的值,并把它们传到适当的表单字段里。这个函数直接跟在上面函数的下面。
function update_crop(coords) { $('#crop_x').val(coords.x); $('#crop_y').val(coords.y); $('#crop_w').val(coords.w); $('#crop_h').val(coords.h); }
如果现在我们刷新这个页面,我们将会看到表单的字段都有表示但前裁剪区域的值。我们移动并调整裁剪区域的大小,这些文本框中的值将会改变。
当我们点击”crop”按钮的时候,User
模型被更新了。我们在裁剪后需要告诉Paperclip重新处理附件。我们可以用after_update
过滤器来实现,如果图片裁剪过过滤器将重新处理图片。
class User < ActiveRecord::Base has_attached_file :avatar, :styles => { :small => "100x100#", :large => "500x500>" } attr_accessor :crop_x, :crop_y, :crop_w, :crop_h 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 end
如果四个裁剪参数从表单传递过来,after_update
过滤器将被调用。如果有四个参数有值,Paperclip的reprocess!
方法将被调用。
我们还需要告诉Paperclip怎么去用表单传来的坐标重新处理图片。我们要写一个自定义的Paperclip processor,在那之前我们要在User
模型里指定processor。
has_attached_file :avatar, :styles => { :small => "100x100#", :large => "500x500>" }, :processors => [:cropper]
习惯上,我们把自定义的processor放在/lib下面的paperclip_processors
目录里面。我们要创建那个目录,并在里面新建一个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
自定义的Paperclip processor放在Paperclip
模块里面。我们的processor继承Paperclip
默认的processor,Thumbnail
processor。Thumbnail
有一个transformation_command
方法,我们要覆盖这个方法,因为我们要加上自己的命令参数。在我们的transformation_command
方法里,我们用正则表达式来检查存在的裁剪命令,并且用我们自己基于User
模型中属相的命令来替代。
选择图片中正确的部分
现在已经写完我们的processor,我们可以试试裁剪我们的图片了,看看运行起来是不是和预期的一样。我们选择上传图片中的车子并按下”crop”按钮。
结果基本正确,图片被裁剪了,但是不是在正确的位置。
这个没有正确是因为用户看见的图片是large
样式的,这个图片已经调整过大小了,裁剪坐标是给予这个图片的。Paperclip裁剪的图片是原始上传的图片,所以我们需要在这两个不同图片大小中补充,这样才能使图片正确裁剪。
我们可以用Paperclip的Geometry.from_file
方法来得到原始图片的尺寸。在我们的User
模型里,我们要写一个新的方法,在里面得到图片的尺寸,并存在一个示例变量里面。
def avatar_geometry(style = :original) @geometry ||= {} @geometry[style] ||= Paperclip::Geometry.from_file(avatar.path(style)) end
在crop
视图里面我们能修改当裁剪选择改变时设置表单值的JavaScript,这样就考虑到原始图片的尺寸了。我们计算原始图片宽度和裁剪过的图片的宽度的比例,并用这个比例把所有值计算出来。
function update_crop(coords) { var ratio = <%= @user.avatar_geometry(:original).width %> / <%= @user.avatar_geometry(:large).width %>; $('#crop_x').val(Math.floor(coords.x * ratio)); $('#crop_y').val(Math.floor(coords.y * ratio)); $('#crop_w').val(Math.floor(coords.w * ratio)); $('#crop_h').val(Math.floor(coords.h * ratio)); }
如果现在我们选择了车的前面并裁剪,图片的正确区域就被裁剪了。
添加预览图片
这一集的最后一件事就是我们要在裁剪页面添加一个预览图片,这样用户就可以确切的看到他们的头像是不是和点击crop按钮之前一样。
回到crop
视图里面,我们要在大图片和表单之间添加一个预览段落。在段落里还是显示大图但是用一个div来限定100×100像素大小,并且隐藏掉多出来的部分。现在显示坐标的文本框可以用隐藏的字段来替代了。
<%= image_tag @user.avatar.url(:large), :id => "cropbox" %> <h4>Preview</h4> <div style="width: 100px; height: 100px; overflow: hidden;"> <%= image_tag @user.avatar.url(:large), :id => "preview" %> </div> <% form_for @user do |form| %> <% for attribute in [:crop_x, :crop_y, :crop_w, :crop_h] %> <%= form.hidden_field attribute, :id => attribute %> <% end %> <p><%= form.submit "Crop" %></p> <% end %>
我们给预览图片一个id
叫preview
,这样我们可以在JavaScript里引用它。我们要修改回调函数,这样裁剪区域改变的时候就可以更新预览图片。
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.floor(coords.x * ratio)); $('#crop_y').val(Math.floor(coords.y * ratio)); $('#crop_w').val(Math.floor(coords.w * ratio)); $('#crop_h').val(Math.floor(coords.h * ratio)); }
裁剪区域一改变,预览图片就会跟着更新,也就是说,裁剪的正方形拖动或改变大小的时候用户就可以实时地看到他们的头像是什么样子。
这一集就到这了。如果你想进一步了解这些东西,你可以看一看jschwindt github上的rjcrop项目,它是一个Rails应用的例子,和我们这看到的差不多。