httpthumbnailer 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/Gemfile +4 -3
- data/Gemfile.lock +12 -12
- data/README.md +242 -68
- data/Rakefile +8 -2
- data/VERSION +1 -1
- data/bin/httpthumbnailer +35 -7
- data/lib/httpthumbnailer/error_reporter.rb +4 -2
- data/lib/httpthumbnailer/ownership.rb +54 -0
- data/lib/httpthumbnailer/plugin.rb +87 -0
- data/lib/httpthumbnailer/plugin/thumbnailer.rb +22 -427
- data/lib/httpthumbnailer/plugin/thumbnailer/service.rb +163 -0
- data/lib/httpthumbnailer/plugin/thumbnailer/service/built_in_plugins.rb +134 -0
- data/lib/httpthumbnailer/plugin/thumbnailer/service/images.rb +295 -0
- data/lib/httpthumbnailer/plugin/thumbnailer/service/magick.rb +208 -0
- data/lib/httpthumbnailer/thumbnail_specs.rb +130 -37
- data/lib/httpthumbnailer/thumbnailer.rb +29 -11
- metadata +30 -81
- data/.rspec +0 -1
- data/features/httpthumbnailer.feature +0 -24
- data/features/identify.feature +0 -31
- data/features/step_definitions/httpthumbnailer_steps.rb +0 -159
- data/features/support/env.rb +0 -106
- data/features/support/test-large.jpg +0 -0
- data/features/support/test-transparent.png +0 -0
- data/features/support/test.jpg +0 -0
- data/features/support/test.png +0 -0
- data/features/support/test.txt +0 -1
- data/features/thumbnail.feature +0 -269
- data/features/thumbnails.feature +0 -158
- data/httpthumbnailer.gemspec +0 -121
- data/load_test/extralarge.jpg +0 -0
- data/load_test/large.jpg +0 -0
- data/load_test/large.png +0 -0
- data/load_test/load_test-374846090-1.1.0-rc1-identify-only.csv +0 -3
- data/load_test/load_test-374846090-1.1.0-rc1.csv +0 -11
- data/load_test/load_test-cd9679c.csv +0 -10
- data/load_test/load_test-v0.3.1.csv +0 -10
- data/load_test/load_test.jmx +0 -733
- data/load_test/medium.jpg +0 -0
- data/load_test/small.jpg +0 -0
- data/load_test/soak_test-ac0c6bcbe5e-broken-libjpeg-tatoos.csv +0 -11
- data/load_test/soak_test-cd9679c.csv +0 -10
- data/load_test/soak_test-f98334a-tatoos.csv +0 -11
- data/load_test/soak_test.jmx +0 -754
- data/load_test/tiny.jpg +0 -0
- data/load_test/v0.0.13-loading.csv +0 -7
- data/load_test/v0.0.13.csv +0 -7
- data/load_test/v0.0.14-no-optimization.csv +0 -10
- data/load_test/v0.0.14.csv +0 -10
- data/spec/image_processing_spec.rb +0 -148
- data/spec/plugin_thumbnailer_spec.rb +0 -318
- data/spec/spec_helper.rb +0 -14
- data/spec/support/square_even.png +0 -0
- data/spec/support/square_odd.png +0 -0
- data/spec/support/test_image.rb +0 -16
- data/spec/thumbnail_specs_spec.rb +0 -43
@@ -0,0 +1,163 @@
|
|
1
|
+
require_relative 'service/magick'
|
2
|
+
require_relative 'service/images'
|
3
|
+
require_relative 'service/built_in_plugins'
|
4
|
+
|
5
|
+
module Plugin
|
6
|
+
module Thumbnailer
|
7
|
+
class Service
|
8
|
+
include ClassLogging
|
9
|
+
|
10
|
+
extend Stats
|
11
|
+
def_stats(
|
12
|
+
:total_images_loaded,
|
13
|
+
:total_images_reloaded,
|
14
|
+
:total_images_downsampled,
|
15
|
+
:total_thumbnails_created,
|
16
|
+
:images_loaded,
|
17
|
+
:max_images_loaded,
|
18
|
+
:max_images_loaded_worker,
|
19
|
+
:total_images_created,
|
20
|
+
:total_images_destroyed,
|
21
|
+
:total_images_created_from_blob,
|
22
|
+
:total_images_created_initialize,
|
23
|
+
:total_images_created_initialize_copy,
|
24
|
+
:total_images_created_resize,
|
25
|
+
:total_images_created_crop,
|
26
|
+
:total_images_created_sample,
|
27
|
+
:total_images_created_blur_image,
|
28
|
+
:total_images_created_composite,
|
29
|
+
:total_images_created_rotate
|
30
|
+
)
|
31
|
+
|
32
|
+
def self.input_formats
|
33
|
+
Magick.formats.select do |name, mode|
|
34
|
+
mode.include? 'r'
|
35
|
+
end.keys.map(&:downcase)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.output_formats
|
39
|
+
Magick.formats.select do |name, mode|
|
40
|
+
mode.include? 'w'
|
41
|
+
end.keys.map(&:downcase)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.rmagick_version
|
45
|
+
Magick::Version
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.magick_version
|
49
|
+
Magick::Magick_version
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(options = {})
|
53
|
+
InputImage.logger = logger_for(InputImage)
|
54
|
+
Thumbnail.logger = logger_for(Thumbnail)
|
55
|
+
Magick::Image.logger = logger_for(Magick::Image)
|
56
|
+
|
57
|
+
@thumbnailing_methods = {}
|
58
|
+
@edits = {}
|
59
|
+
@options = options
|
60
|
+
@images_loaded = 0
|
61
|
+
|
62
|
+
log.info "initializing thumbnailer: #{self.class.rmagick_version} #{self.class.magick_version}"
|
63
|
+
|
64
|
+
set_limit(:area, options[:limit_area]) if options.member?(:limit_area)
|
65
|
+
set_limit(:memory, options[:limit_memory]) if options.member?(:limit_memory)
|
66
|
+
set_limit(:map, options[:limit_map]) if options.member?(:limit_map)
|
67
|
+
set_limit(:disk, options[:limit_disk]) if options.member?(:limit_disk)
|
68
|
+
|
69
|
+
Magick.trace_proc = lambda do |which, description, id, method|
|
70
|
+
case which
|
71
|
+
when :c
|
72
|
+
Service.stats.incr_images_loaded
|
73
|
+
@images_loaded += 1
|
74
|
+
Service.stats.max_images_loaded = Service.stats.images_loaded if Service.stats.images_loaded > Service.stats.max_images_loaded
|
75
|
+
Service.stats.max_images_loaded_worker = @images_loaded if @images_loaded > Service.stats.max_images_loaded_worker
|
76
|
+
Service.stats.incr_total_images_created
|
77
|
+
case method
|
78
|
+
when :from_blob
|
79
|
+
Service.stats.incr_total_images_created_from_blob
|
80
|
+
when :initialize
|
81
|
+
Service.stats.incr_total_images_created_initialize
|
82
|
+
when :initialize_copy
|
83
|
+
Service.stats.incr_total_images_created_initialize_copy
|
84
|
+
when :resize
|
85
|
+
Service.stats.incr_total_images_created_resize
|
86
|
+
when :resize!
|
87
|
+
Service.stats.incr_total_images_created_resize
|
88
|
+
when :crop
|
89
|
+
Service.stats.incr_total_images_created_crop
|
90
|
+
when :crop!
|
91
|
+
Service.stats.incr_total_images_created_crop
|
92
|
+
when :sample
|
93
|
+
Service.stats.incr_total_images_created_sample
|
94
|
+
when :blur_image
|
95
|
+
Service.stats.incr_total_images_created_blur_image
|
96
|
+
when :composite
|
97
|
+
Service.stats.incr_total_images_created_composite
|
98
|
+
when :rotate
|
99
|
+
Service.stats.incr_total_images_created_rotate
|
100
|
+
else
|
101
|
+
log.warn "uncounted image creation method: #{method}"
|
102
|
+
end
|
103
|
+
when :d
|
104
|
+
Service.stats.decr_images_loaded
|
105
|
+
@images_loaded -= 1
|
106
|
+
Service.stats.incr_total_images_destroyed
|
107
|
+
end
|
108
|
+
log.debug{"image event: #{which}, #{description}, #{id}, #{method}: loaded images: #{Service.stats.images_loaded}"}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def load(io, options = {}, &block)
|
113
|
+
blob = io.read
|
114
|
+
|
115
|
+
old_memory_limit = nil
|
116
|
+
borrowed_memory_limit = nil
|
117
|
+
if options.member?(:limit_memory)
|
118
|
+
borrowed_memory_limit = options[:limit_memory].borrow(options[:limit_memory].limit, 'image magick')
|
119
|
+
old_memory_limit = set_limit(:memory, borrowed_memory_limit)
|
120
|
+
end
|
121
|
+
|
122
|
+
InputImage.from_blob(blob, @thumbnailing_methods, @edits, options, &block)
|
123
|
+
ensure
|
124
|
+
if old_memory_limit
|
125
|
+
set_limit(:memory, old_memory_limit)
|
126
|
+
options[:limit_memory].return(borrowed_memory_limit, 'image magick')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def thumbnailing_method(method, &impl)
|
131
|
+
log.info "adding thumbnailing method: #{method}"
|
132
|
+
@thumbnailing_methods[method] = impl
|
133
|
+
end
|
134
|
+
|
135
|
+
def edit(name, &impl)
|
136
|
+
log.info "adding edit: #{name}(#{impl.parameters.drop(1).reverse.drop(2).reverse.map{|p| p.last.to_s}.join(', ')})"
|
137
|
+
@edits[name] = impl
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_limit(limit, value)
|
141
|
+
old = Magick.limit_resource(limit, value)
|
142
|
+
log.info "changed #{limit} limit from #{old} to #{value} bytes"
|
143
|
+
old
|
144
|
+
end
|
145
|
+
|
146
|
+
def load_plugin(plugin_context)
|
147
|
+
plugin_context.thumbnailing_methods.each do |name, block|
|
148
|
+
thumbnailing_method(name, &block)
|
149
|
+
end
|
150
|
+
|
151
|
+
plugin_context.edits.each do |name, block|
|
152
|
+
edit(name, &block)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def setup_built_in_plugins
|
157
|
+
log.info("loading built in plugins")
|
158
|
+
load_plugin(self.class.built_in_plugin)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Plugin
|
2
|
+
module Thumbnailer
|
3
|
+
class Service
|
4
|
+
def self.built_in_plugin
|
5
|
+
PluginContext.new do
|
6
|
+
thumbnailing_method('crop') do |image, width, height, options|
|
7
|
+
image.resize_to_fill(width, height, float!('float-x', options['float-x'], 0.5), float!('float-y', options['float-y'], 0.5)) if image.width != width or image.height != height
|
8
|
+
end
|
9
|
+
|
10
|
+
thumbnailing_method('fit') do |image, width, height, options|
|
11
|
+
image.resize_to_fit(width, height) if image.width != width or image.height != height
|
12
|
+
end
|
13
|
+
|
14
|
+
thumbnailing_method('pad') do |image, width, height, options|
|
15
|
+
image.resize_to_fit(width, height).get do |resize|
|
16
|
+
resize.render_on_background(options['background-color'], width, height, float!('float-x', options['float-x'], 0.5), float!('float-y', options['float-y'], 0.5))
|
17
|
+
end if image.width != width or image.height != height
|
18
|
+
end
|
19
|
+
|
20
|
+
thumbnailing_method('limit') do |image, width, height, options|
|
21
|
+
image.resize_to_fit(width, height) if image.width > width or image.height > height
|
22
|
+
end
|
23
|
+
|
24
|
+
edit('resize_crop') do |image, width, height, options, thumbnail_spec|
|
25
|
+
width = float!('width', width)
|
26
|
+
height = float!('height', height)
|
27
|
+
|
28
|
+
image.resize_to_fill(width, height, ufloat!('float-x', options['float-x'], 0.5), ufloat!('float-y', options['float-y'], 0.5)) if image.width != width or image.height != height
|
29
|
+
end
|
30
|
+
|
31
|
+
edit('resize_fit') do |image, width, height, options, thumbnail_spec|
|
32
|
+
width = float!('width', width)
|
33
|
+
height = float!('height', height)
|
34
|
+
|
35
|
+
image.resize_to_fit(width, height) if image.width != width or image.height != height
|
36
|
+
end
|
37
|
+
|
38
|
+
edit('resize_limit') do |image, width, height, options, thumbnail_spec|
|
39
|
+
width = float!('width', width)
|
40
|
+
height = float!('height', height)
|
41
|
+
|
42
|
+
image.resize_to_fit(width, height) if image.width > width or image.height > height
|
43
|
+
end
|
44
|
+
|
45
|
+
edit('rotate') do |image, angle, options, thumbnail_spec|
|
46
|
+
angle = float!('angle', angle)
|
47
|
+
next image if angle % 360 == 0
|
48
|
+
image.with_background_color(options['background-color'] || thumbnail_spec.options['background-color']) do
|
49
|
+
image.rotate(angle)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
edit('crop') do |image, x, y, width, height, options, thumbnail_spec|
|
54
|
+
x, y, width, height = normalize_region(
|
55
|
+
float!('x', x),
|
56
|
+
float!('y', y),
|
57
|
+
float!('width', width),
|
58
|
+
float!('height', height)
|
59
|
+
)
|
60
|
+
|
61
|
+
next image if [x, y, width, height] == [0.0, 0.0, 1.0, 1.0]
|
62
|
+
|
63
|
+
image.crop(
|
64
|
+
*image.rel_to_px_box(x, y, width, height),
|
65
|
+
true
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
edit('pixelate') do |image, box_x, box_y, box_width, box_height, options, thumbnail_spec|
|
70
|
+
x, y, width, height = normalize_region(
|
71
|
+
float!('box_x', box_x),
|
72
|
+
float!('box_y', box_y),
|
73
|
+
float!('box_width', box_width),
|
74
|
+
float!('box_height', box_height)
|
75
|
+
)
|
76
|
+
size = ufloat!('size', options['size'], 0.01)
|
77
|
+
|
78
|
+
image.pixelate_region(
|
79
|
+
*image.rel_to_px_box(x, y, width, height),
|
80
|
+
image.rel_to_diagonal(size)
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
edit('blur') do |image, box_x, box_y, box_width, box_height, options, thumbnail_spec|
|
85
|
+
x, y, width, height = normalize_region(
|
86
|
+
float!('box_x', box_x),
|
87
|
+
float!('box_y', box_y),
|
88
|
+
float!('box_width', box_width),
|
89
|
+
float!('box_height', box_height)
|
90
|
+
)
|
91
|
+
|
92
|
+
radius = ufloat!('radius', options['radius'], 0.0) # auto
|
93
|
+
sigma = ufloat!('sigma', options['sigma'], 0.01)
|
94
|
+
|
95
|
+
radius = image.rel_to_diagonal(radius)
|
96
|
+
sigma = image.rel_to_diagonal(sigma)
|
97
|
+
|
98
|
+
if radius > 50
|
99
|
+
log.warn "limiting effective radius from #{radius} down to 50"
|
100
|
+
radius = 50
|
101
|
+
end
|
102
|
+
|
103
|
+
if sigma > 50
|
104
|
+
log.warn "limiting effective sigma from #{sigma} down to 50"
|
105
|
+
sigma = 50
|
106
|
+
end
|
107
|
+
|
108
|
+
image.blur_region(
|
109
|
+
*image.rel_to_px_box(x, y, width, height),
|
110
|
+
radius, sigma
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
edit('rectangle') do |image, box_x, box_y, box_width, box_height, options, thumbnail_spec|
|
115
|
+
x, y, width, height = normalize_region(
|
116
|
+
float!('box_x', box_x),
|
117
|
+
float!('box_y', box_y),
|
118
|
+
float!('box_width', box_width),
|
119
|
+
float!('box_height', box_height)
|
120
|
+
)
|
121
|
+
|
122
|
+
color = options['color'] || 'black'
|
123
|
+
|
124
|
+
image.render_rectangle(
|
125
|
+
*image.rel_to_px_box(x, y, width, height),
|
126
|
+
color
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,295 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Plugin
|
4
|
+
module Thumbnailer
|
5
|
+
class Service
|
6
|
+
module MimeType
|
7
|
+
# ImageMagick Image.mime_type is absolutely bunkers! It goes over file system to look for some strange files WTF?!
|
8
|
+
# Also it cannot be used for thumbnails since they are not yet rendered to desired format
|
9
|
+
# Here is stupid implementation
|
10
|
+
def mime_type
|
11
|
+
#TODO: how do I do it better?
|
12
|
+
mime = case format
|
13
|
+
when 'JPG' then 'jpeg'
|
14
|
+
else format.downcase
|
15
|
+
end
|
16
|
+
"image/#{mime}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class InputImage
|
21
|
+
UpscaledError = Class.new RuntimeError
|
22
|
+
|
23
|
+
include ClassLogging
|
24
|
+
include PerfStats
|
25
|
+
extend PerfStats
|
26
|
+
extend Forwardable
|
27
|
+
|
28
|
+
def initialize(image, thumbnailing_methods, edits)
|
29
|
+
@image = image
|
30
|
+
@thumbnailing_methods = thumbnailing_methods
|
31
|
+
@edits = edits
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_blob(blob, thumbnailing_methods, edits, options = {}, &block)
|
35
|
+
mw = options[:max_width]
|
36
|
+
mh = options[:max_height]
|
37
|
+
|
38
|
+
begin
|
39
|
+
image = measure "loading original image" do
|
40
|
+
image = measure "loading image form blob" do
|
41
|
+
begin
|
42
|
+
images =
|
43
|
+
if mw and mh
|
44
|
+
measure "loading image form blob with size hint", "#{mw}x#{mh}" do
|
45
|
+
log.info "using max size hint of: #{mw}x#{mh}"
|
46
|
+
Magick::Image.from_blob(blob) do |info|
|
47
|
+
# actual hint is 2x the max thumbnail dimensions so we don't loose too much quality
|
48
|
+
define('jpeg', 'size', "#{mw*2}x#{mh*2}")
|
49
|
+
define('jbig', 'size', "#{mw*2}x#{mh*2}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
else
|
53
|
+
measure "loading image form blob without size hint" do
|
54
|
+
Magick::Image.from_blob(blob)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
begin
|
58
|
+
image = images.shift
|
59
|
+
begin
|
60
|
+
if image.columns > image.base_columns or image.rows > image.base_rows
|
61
|
+
log.warn "input image got upscaled from: #{image.base_columns}x#{image.base_rows} to #{image.columns}x#{image.rows}"
|
62
|
+
if not options[:no_upscale_fix]
|
63
|
+
raise UpscaledError if options[:reload]
|
64
|
+
measure "downsampling input image to base size", "#{image.base_columns}x#{image.base_rows}" do
|
65
|
+
log.warn "downsampling input image to base size: #{image.base_columns}x#{image.base_rows}"
|
66
|
+
image = image.get do |image|
|
67
|
+
image.sample(image.base_columns, image.base_rows)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
image
|
73
|
+
rescue
|
74
|
+
image.destroy!
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
ensure
|
78
|
+
images.each do |other|
|
79
|
+
other.destroy!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
rescue UpscaledError
|
83
|
+
log.warn "reloading input image without max size hint!"
|
84
|
+
Service.stats.incr_total_images_reloaded
|
85
|
+
mw = mh = nil
|
86
|
+
retry
|
87
|
+
end
|
88
|
+
end
|
89
|
+
image.get do |image|
|
90
|
+
blob = nil
|
91
|
+
|
92
|
+
log.info "loaded image: #{image.inspect.strip}"
|
93
|
+
Service.stats.incr_total_images_loaded
|
94
|
+
|
95
|
+
# clean up the image
|
96
|
+
image.strip!
|
97
|
+
image.properties do |key, value|
|
98
|
+
log.debug "deleting user propertie '#{key}'"
|
99
|
+
image[key] = nil
|
100
|
+
end
|
101
|
+
image
|
102
|
+
end.get do |image|
|
103
|
+
if mw and mh and not options[:no_downsample]
|
104
|
+
f = image.find_downsample_factor(mw, mh)
|
105
|
+
if f > 1
|
106
|
+
measure "downsampling", image.inspect.strip do
|
107
|
+
image = image.downsample(f)
|
108
|
+
log.info "downsampled image by factor of #{f}: #{image.inspect.strip}"
|
109
|
+
Service.stats.incr_total_images_downsampled
|
110
|
+
image
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
image.get do |image|
|
117
|
+
yield self.new(image, thumbnailing_methods, edits)
|
118
|
+
true # make sure it is destroyed
|
119
|
+
end
|
120
|
+
rescue Magick::ImageMagickError => error
|
121
|
+
raise ImageTooLargeError, error if error.message =~ /cache resources exhausted/
|
122
|
+
raise UnsupportedMediaTypeError, error
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def thumbnail!(spec, &block)
|
127
|
+
# it is OK if the image get's destroyed in the process
|
128
|
+
@image.get do |image|
|
129
|
+
_thumbnail(image, spec, &block)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def thumbnail(spec, &block)
|
134
|
+
# we don't want to destory the input image after we have generated the thumbnail so we can generate another one
|
135
|
+
@image.borrow do |image|
|
136
|
+
_thumbnail(image, spec, &block)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def _thumbnail(image, spec)
|
141
|
+
spec = spec.dup
|
142
|
+
# default background is white
|
143
|
+
spec.options['background-color'] = spec.options.fetch('background-color', 'white').sub(/^0x/, '#')
|
144
|
+
|
145
|
+
width = spec.width == :input ? @image.columns : spec.width
|
146
|
+
height = spec.height == :input ? @image.rows : spec.height
|
147
|
+
image_format = spec.format == :input ? @image.format : spec.format
|
148
|
+
|
149
|
+
raise ZeroSizedImageError.new(width, height) if width == 0 or height == 0
|
150
|
+
|
151
|
+
begin
|
152
|
+
measure "generating thumbnail to spec", spec do
|
153
|
+
image.get do |image|
|
154
|
+
if image.alpha?
|
155
|
+
measure "rendering image on background", image.inspect.strip do
|
156
|
+
log.info 'image has alpha, rendering on background'
|
157
|
+
image.render_on_background(spec.options['background-color'])
|
158
|
+
end
|
159
|
+
else
|
160
|
+
image
|
161
|
+
end
|
162
|
+
end.get do |image|
|
163
|
+
spec.edits.each do |edit|
|
164
|
+
log.debug "applying edit '#{edit}'"
|
165
|
+
image = image.get do |image|
|
166
|
+
measure "edit", edit do
|
167
|
+
edit_image(image, edit.name, *edit.args, edit.options, spec)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
image
|
172
|
+
end.get do |image|
|
173
|
+
log.debug "thumbnailing with method '#{spec.method} #{width}x#{height} #{spec.options}'"
|
174
|
+
measure "thumbnailing with method", "#{spec.method} #{width}x#{height} #{spec.options}" do
|
175
|
+
thumbnail_image(image, spec.method, width, height, spec.options)
|
176
|
+
end
|
177
|
+
end.get do |image|
|
178
|
+
if image.alpha?
|
179
|
+
measure "rendering thumbnail on background", image.inspect.strip do
|
180
|
+
log.info 'thumbnail has alpha, rendering on background'
|
181
|
+
image.render_on_background(spec.options['background-color'])
|
182
|
+
end
|
183
|
+
else
|
184
|
+
image
|
185
|
+
end
|
186
|
+
end.get do |image|
|
187
|
+
Service.stats.incr_total_thumbnails_created
|
188
|
+
yield Thumbnail.new(image, image_format, spec.options)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
rescue Magick::ImageMagickError => error
|
192
|
+
raise ImageTooLargeError, error.message if error.message =~ /cache resources exhausted/
|
193
|
+
raise
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def edit_image(image, name, *args, options, spec)
|
198
|
+
impl = @edits[name] or raise UnsupportedEditError, name
|
199
|
+
|
200
|
+
# make sure we pass as many args as expected (filling with nil)
|
201
|
+
args_no = impl.arity - 3 # for image, optioins and spec
|
202
|
+
args = args.dup
|
203
|
+
args.fill(nil, (args.length)...args_no)
|
204
|
+
if args.length > args_no
|
205
|
+
log.warn "extra arguments to edit '#{name}': #{args[args_no..-1].join(', ')}"
|
206
|
+
args = args[0...args_no]
|
207
|
+
end
|
208
|
+
|
209
|
+
ret = impl.call(image, *args, options, spec)
|
210
|
+
|
211
|
+
fail "edit '#{name}' returned '#{ret.class.name}' - expecting nil or Magick::Image" unless ret.nil? or ret.kind_of? Magick::Image
|
212
|
+
ret or image
|
213
|
+
rescue PluginContext::PluginArgumentError => error
|
214
|
+
raise EditArgumentError.new(name, error.message)
|
215
|
+
end
|
216
|
+
|
217
|
+
def thumbnail_image(image, method, width, height, options)
|
218
|
+
impl = @thumbnailing_methods[method] or raise UnsupportedMethodError, method
|
219
|
+
ret = impl.call(image, width, height, options)
|
220
|
+
fail "thumbnailing method '#{name}' returned '#{ret.class.name}' - expecting nil or Magick::Image" unless ret.nil? or ret.kind_of? Magick::Image
|
221
|
+
ret or image
|
222
|
+
rescue PluginContext::PluginArgumentError => error
|
223
|
+
raise ThumbnailArgumentError.new(method, error.message)
|
224
|
+
end
|
225
|
+
|
226
|
+
def_delegators :@image, :format, :width, :height
|
227
|
+
|
228
|
+
include MimeType
|
229
|
+
|
230
|
+
# We use base values since it might have been loaded with size hint and prescaled
|
231
|
+
def width
|
232
|
+
@image.base_columns
|
233
|
+
end
|
234
|
+
|
235
|
+
def height
|
236
|
+
@image.base_rows
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
class Thumbnail
|
241
|
+
include ClassLogging
|
242
|
+
extend Forwardable
|
243
|
+
include PerfStats
|
244
|
+
|
245
|
+
def initialize(image, format, options = {})
|
246
|
+
@image = image
|
247
|
+
@format = format
|
248
|
+
|
249
|
+
@quality = (options['quality'] or default_quality(format))
|
250
|
+
@quality &&= @quality.to_i
|
251
|
+
|
252
|
+
@interlace = (options['interlace'] or 'NoInterlace')
|
253
|
+
fail "unsupported interlace: #{@interlace}" unless Magick::InterlaceType.values.map(&:to_s).include? @interlace
|
254
|
+
@interlace = Magick.const_get @interlace.to_sym
|
255
|
+
end
|
256
|
+
|
257
|
+
attr_reader :format
|
258
|
+
def_delegators :@image, :width, :height
|
259
|
+
|
260
|
+
#def_delegators :@image, :format
|
261
|
+
|
262
|
+
def data
|
263
|
+
# export class variables to local scope
|
264
|
+
format = @format
|
265
|
+
quality = @quality
|
266
|
+
interlace = @interlace
|
267
|
+
|
268
|
+
measure "to blob", "#{@format} (quality: #{@quality} interlace: #{@interlace})" do
|
269
|
+
@image.to_blob do
|
270
|
+
self.format = format
|
271
|
+
self.quality = quality if quality
|
272
|
+
self.interlace = interlace
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
include MimeType
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
def default_quality(format)
|
282
|
+
case format
|
283
|
+
when /png/i
|
284
|
+
95 # max zlib compression, adaptive filtering (photo)
|
285
|
+
when /jpeg|jpg/i
|
286
|
+
85
|
287
|
+
else
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|