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.
Files changed (57) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +4 -3
  3. data/Gemfile.lock +12 -12
  4. data/README.md +242 -68
  5. data/Rakefile +8 -2
  6. data/VERSION +1 -1
  7. data/bin/httpthumbnailer +35 -7
  8. data/lib/httpthumbnailer/error_reporter.rb +4 -2
  9. data/lib/httpthumbnailer/ownership.rb +54 -0
  10. data/lib/httpthumbnailer/plugin.rb +87 -0
  11. data/lib/httpthumbnailer/plugin/thumbnailer.rb +22 -427
  12. data/lib/httpthumbnailer/plugin/thumbnailer/service.rb +163 -0
  13. data/lib/httpthumbnailer/plugin/thumbnailer/service/built_in_plugins.rb +134 -0
  14. data/lib/httpthumbnailer/plugin/thumbnailer/service/images.rb +295 -0
  15. data/lib/httpthumbnailer/plugin/thumbnailer/service/magick.rb +208 -0
  16. data/lib/httpthumbnailer/thumbnail_specs.rb +130 -37
  17. data/lib/httpthumbnailer/thumbnailer.rb +29 -11
  18. metadata +30 -81
  19. data/.rspec +0 -1
  20. data/features/httpthumbnailer.feature +0 -24
  21. data/features/identify.feature +0 -31
  22. data/features/step_definitions/httpthumbnailer_steps.rb +0 -159
  23. data/features/support/env.rb +0 -106
  24. data/features/support/test-large.jpg +0 -0
  25. data/features/support/test-transparent.png +0 -0
  26. data/features/support/test.jpg +0 -0
  27. data/features/support/test.png +0 -0
  28. data/features/support/test.txt +0 -1
  29. data/features/thumbnail.feature +0 -269
  30. data/features/thumbnails.feature +0 -158
  31. data/httpthumbnailer.gemspec +0 -121
  32. data/load_test/extralarge.jpg +0 -0
  33. data/load_test/large.jpg +0 -0
  34. data/load_test/large.png +0 -0
  35. data/load_test/load_test-374846090-1.1.0-rc1-identify-only.csv +0 -3
  36. data/load_test/load_test-374846090-1.1.0-rc1.csv +0 -11
  37. data/load_test/load_test-cd9679c.csv +0 -10
  38. data/load_test/load_test-v0.3.1.csv +0 -10
  39. data/load_test/load_test.jmx +0 -733
  40. data/load_test/medium.jpg +0 -0
  41. data/load_test/small.jpg +0 -0
  42. data/load_test/soak_test-ac0c6bcbe5e-broken-libjpeg-tatoos.csv +0 -11
  43. data/load_test/soak_test-cd9679c.csv +0 -10
  44. data/load_test/soak_test-f98334a-tatoos.csv +0 -11
  45. data/load_test/soak_test.jmx +0 -754
  46. data/load_test/tiny.jpg +0 -0
  47. data/load_test/v0.0.13-loading.csv +0 -7
  48. data/load_test/v0.0.13.csv +0 -7
  49. data/load_test/v0.0.14-no-optimization.csv +0 -10
  50. data/load_test/v0.0.14.csv +0 -10
  51. data/spec/image_processing_spec.rb +0 -148
  52. data/spec/plugin_thumbnailer_spec.rb +0 -318
  53. data/spec/spec_helper.rb +0 -14
  54. data/spec/support/square_even.png +0 -0
  55. data/spec/support/square_odd.png +0 -0
  56. data/spec/support/test_image.rb +0 -16
  57. 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
+