乐者为王

Do one thing, and do it well.

使用Rails进行图片处理

英文原文:https://www.sitepoint.com/image-processing-rails/

图片是任何应用程序的重要部分。从社交网络到简单的缺陷跟踪器,图片都扮演着重要的角色。不过,管理图片不是件容易的事情,需要提前做大量的规划。

在本文中,我将演示如何用Rails实现这些。我将向你展示如何处理你的图片,并在后台创建多个版本。我们还将看到如何通过压缩这些图片来提高页面的性能,而不损失质量。

起步

本教程中的示例在Rails 4.2上运行,使用MongoDB数据库,以及使用HAML渲染视图。不过,这里使用的代码片段应该能与任何版本的Rails兼容,尽管有轻微的配置差异。

配置环境

ImageMagick是POSIX系统图片处理库。如果你的系统没有安装ImageMagick,它可以用操作系统的软件包管理器安装。

在Ubuntu上:

1
2
3
sudo apt-get -y install imagemagick
sudo apt-get -y install libmagic-dev
sudo apt-get -y install libmagickwand-dev

在Mac OS X上,推荐使用Homebrew:

1
brew install imagemagick

现在,我们需要Ruby适配器来连接本地的ImageMagick库。我个人更偏爱MiniMagick,因为它是轻量级的,含有典型应用程序需要的几乎所有东西:

1
2
3
# Gemfile

gem 'mini_magick'

热身运动

在正式构建任何东西之前,让我们先试试MiniMagick的某些功能。打开Rails控制台(rails c)并运行以下命令:

1
2
3
4
5
6
7
8
9
# Open an image from a website

image = MiniMagick::Image.open("https://s3.amazonaws.com/StartupStockPhotos/20140808_StartupStockPhotos/85.jpg")

# Get the Image's width
image.width  # 4928

# Get the image's height
image.height  #3264

我的天哪,这图好大。让我们看看能否调整它的大小以适应我们的iPad:

1
2
3
4
5
image.resize "2048x1536"

# Now get the image's new width and height

p "Width is => #{image.width} and height is => #{image.height}"

调整速度很快,只是修改的文件存储在哪里?

1
image.path  # temp path

处理过的图片存储在临时路径中,并且随时可能被清理。要将其持久化到磁盘,只需要调用write方法即可:

1
image.write "public/uploads/test.jpg"

转换图片

你将要执行的最常用操作之一是将图片转换为不同的格式。MiniMagick使这非常简单:

1
2
image.format "png"
image.write "public/uploads/test.png"

你也可以在单个块中组合多个操作:

1
2
3
4
5
6
7
8
9
image.combine_options do |i|
  i.resize "2048x1536"
  i.flip
  i.rotate "-45"
  i.blur "0x15"
end
image.write "public/uploads/blur.png"

![Some weird result](blur.png)

好吧,在这里我做的有点过火。我要给自己辩解一下,我只是试图给你展示可以用MiniMagick做的所有酷的东西。

现在,让我们来看看如何将其与Rails应用程序相结合。

上传文件

Carrierwave是个奇妙的gem,它简化了Ruby中的文件上传。它还可以很好地与MiniMagick互动,使我们的生活更简单。

1
2
3
4
# Gemfile

gem 'carrierwave'
gem 'carrierwave-mongoid', :require => 'carrierwave/mongoid'

注意:如果你使用的是ActiveRecord或DataMapper,配置将略有不同,而Carrierwave官方文档会告诉你正确的方法。

批量获取所有这些gem:

1
bundle install

创建首个上传器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#app/uploaders/image_uploader.rb

class ImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this Uploader:
  storage :file
  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/images"
  end
end

这里的代码是不言自明的。storage:file指示服务器将图片存储在本地服务器上,store_dir指定存储位置。

由于这些文件是通过互联网发送的,因此最佳的做法是始终过滤进来的文件:

1
2
3
4
5
6
7
8
# app/uploaders/image_uploader.rb
...
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
  %w(jpg jpeg png gif)
end
...

此代码片段过滤掉未在此处指定的文件类型。这绝对不是万无一失的,但它可以作为抵御任何阻碍攻击的第一级过滤器。

将此上传器添加到我们的图片模型中:

1
2
3
4
5
6
7
8
9
10
11
# app/models/image.rb

class Image
  include Mongoid::Document
  include Mongoid::Timestamps
  include Mongoid::Paranoia
  include Mongoid::Attributes::Dynamic
  include Rails.application.routes.url_helpers

  mount_uploader :media, ImageUploader, mount_on: :media_filename
end

编辑image_uploader.rb去处理上传的图片:

1
2
3
4
5
6
# app/uploaders/image_uploader.rb

...
process :resize_to_fill => [200, 200]
process :convert => 'png'
...

尝试在Rails控制台创建一个新图片:

1
2
3
media = File.open("/Users/test/Desktop/image/jpg")
img = Image.new(:media => media)
img.save

上传的图片保存在store_dir目录。而且上传的图片将立即被处理并覆写为200x200的图片。我们不会为任何未来的编辑而保有原始文件的副本。为避免这种情况,需要创建文件的多个版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/uploaders/image_uploader.rb

...
version :thumb do
  process :resize_to_fit => [100, 100]
  process :convert => 'jpg'
end

version :cover   do
  process :resize_to_fit => [240, 180]
  process :convert => 'jpg'
end
...

除原始图片之外,还将创建2个新版本。检查Carrierwave创建的版本:

1
2
img.media.versions[:thumb]  # returns the thumb image instance
img.media.versions[:cover]  # returns the cover image instance

你注意到这些图片是瞬间生成的吗?这意味着图片转换发生在同一个线程中,并且执行被阻塞直到转换完成。在生产应用程序中,在同一个线程中创建图片的多个版本是不应该的。相反,我们应该有条件地处理这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/uploaders/image_uploader/rb

...
version :cover, :if => :is_live? do
  process :resize_to_fit => [240, 180]
  process :convert => 'jpg'
end

def is_live?(img = nil)
  @is_live
end

def is_live=(value)
  @is_live = value
end
...

现在,当我们尝试创建一个新图片时,封面版本将不会生成。我们可以在需要时通过简单的运行以下代码来手动触发:

1
2
3
img.media.is_live = true
img.save
img.media.recreate_versions! :cover

该代码也在前台运行,是个阻塞操作,但至少是推迟到最后可能的一刻。我们可以再进一步,通过Resque在后台运行这些代码:

1
2
3
4
5
6
7
8
9
10
# lib/resque/image_queue.rb
class ImageQueue
  @queue = :image_queue
  def self.perform(image_id)
    image = Image.find image_id
    img.media.is_live= true
    img.save
    img.media.recreate_versions! :cover
  end
end

然后,把它们放入队列:

1
Resque.enqueue(ImageQueue, img.id.to_s)

提高性能

重量级的图片往往会减慢网站的速度。减少页面大小的一种方法是压缩这些图片。Carrierwave Image Optimizer可以帮助我们轻松地压缩图片,而没有任何麻烦。

将其添加到你的Gemfile中:

1
gem 'carrierwave-imageoptimizer'

然后编辑image_uploader.rb文件:

1
2
3
4
5
6
7
# app/uploaders/image_uploader.rb

...
include CarrierWave::ImageOptimizer
process :optimize
process :quality => 100
...

这段代码压缩所有的图片,没有任何视觉损失。这种方式的做法是所有图片的元信息被剥离。平均而言,这减少了约20-30%的大小。

总结

图片处理是非常垂直的技术,我们几乎只触及表面。我们可以用它构建这么多酷的东西。我希望我已经用这篇文章引起你的兴趣。请在评论中分享你的想法。

Comments