httpthumbnailer 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|