httpthumbnailer 1.2.0 → 1.3.0

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