thoughtbot/paperclip — pull request #1360 by oggy (2013-10-18 17:43:24+00:00)
Title: Support generating multiple styles at once

This patch adds an optional "generation" step that occurs *after* processing the original file, *before* the result is handed to the processor pipeline for non-original styles. In picture form:

Current paperclip:

```
+------> style1 --> [style1 file]
/ processors
/
[original] -> original --------+---------> style2 --> [style2 file]
processors \ processors
\
+------> style3 --> [style3 file]
processors
```

With this patch:

```
+------> style1 --> [style1 file]
/ processors
/
[original] -> original -> generator -----> style2 --> [style2 file]
processors \ processors
\
+------> style3 --> [style3 file]
processors
```

Unlike the processors -- which each take 1 input file and produce 1 output file -- a generator takes 1 input file and produces up to N output files (N = number of styles). If any styles are missing from the output of the generator, the processed original file is simply passed on to the ungenerated style's processors (as if the generator was the identity transform for these styles).

In code, a generator is simply a processor which is specified via a :generator option to the attachment. The generator's `options` will be a hash of style name to style options, and `make` should return a hash of style name to result file.

## Motivation

ImageMagick has the capability of generating multiple output files, and doing so can be much more efficient than doing them one at a time. The reasons should be pretty intuitive:

* The OS spawns 1 process, not N.
* The input file only has to be read from disk once, not N times.
* Initial processing steps common to multiple styles can be performed once, not N times.

## Benchmarks

$ ./bench.rb
Rehearsal --------------------------------------------
FastUser 0.070000 0.050000 1.050000 ( 1.081597)
SlowUser 0.050000 0.040000 1.400000 ( 1.428589)
----------------------------------- total: 2.450000sec

user system total real
FastUser 0.050000 0.050000 1.030000 ( 1.085033)
SlowUser 0.040000 0.050000 1.420000 ( 1.451335)

$ cat bench.rb
#!/usr/bin/env ruby

require 'benchmark'
require_relative './config/environment'

Benchmark.bmbm do |x|
x.report('FastUser') { open('input.jpg') { |f| FastUser.create(photo: f) } }
x.report('SlowUser') { open('input.jpg') { |f| SlowUser.create(photo: f) } }
end

$ cat app/models/fast_user.rb
class FastUser < ActiveRecord::Base
set_table_name 'users'
attr_accessible :photo
has_attached_file :photo, styles: {a: '100x100', b: '200x200', c: '400x400'}, generator: :photo_generator
end

$ cat app/models/slow_user.rb
class SlowUser < ActiveRecord::Base
set_table_name 'users'
attr_accessible :photo
has_attached_file :photo,
styles: {a: '100x100', b: '200x200', c: '400x400'},
convert_options: {all: "-strip -quality 80"}
end

$ cat lib/paperclip_processors/photo_generator.rb
require 'shellwords'

module Paperclip
class PhotoGenerator < Processor
def make
command = <<-EOS
convert #{file.path.shellescape}[0] -auto-orient -strip -quality 80
( +clone -resize '100x100^' -write #{results[:a].path.shellescape} +delete )
( +clone -resize '200x200^' -write #{results[:b].path.shellescape} +delete )
-resize '300x300^' #{results[:c].path.shellescape}
EOS
Paperclip.run command.shellsplit.shelljoin
results
end

def results
@results ||= options.inject({}) do |results, (name, style)|
results[name] = Tempfile.new(['paperclip', '.jpg']).tap(&:binmode)
results
end
end
end
end

So this chops ~1/4 off the processing time. The input file used here was a 1.3MB, 2048x1536 JPG. Savings would increase with larger input files, when generating more styles, or with pipelines with more common leading steps.