lib/httpthumbnailer/plugin/thumbnailer.rb in httpthumbnailer-1.1.2 vs lib/httpthumbnailer/plugin/thumbnailer.rb in httpthumbnailer-1.2.0

- old
+ new

@@ -1,20 +1,20 @@ require 'RMagick' require 'forwardable' -# ImageMagick Image.mime_type is absolutely bunkers! It goes over file system to look for some strange files WTF?! -# Also it cannot be used for thumbnails since they are not yet rendered to desired format -# Here is stupid implementaiton module MetaData def width @image.columns end def height @image.rows end + # ImageMagick Image.mime_type is absolutely bunkers! It goes over file system to look for some strange files WTF?! + # Also it cannot be used for thumbnails since they are not yet rendered to desired format + # Here is stupid implementation def mime_type #TODO: how do I do it better? format = @format || @image.format mime = case format when 'JPG' then 'jpeg' @@ -92,11 +92,11 @@ @processing_methods = processing_methods end def thumbnail(spec) spec = spec.dup - # default backgraud is white + # default background is white spec.options['background-color'] = spec.options.fetch('background-color', 'white').sub(/^0x/, '#') width = spec.width == :input ? @image.columns : spec.width height = spec.height == :input ? @image.rows : spec.height @@ -132,11 +132,11 @@ @image.use do |image| yield self end end - def_delegators :@image, :destroy!, :destroyed? + def_delegators :@image, :destroy!, :destroyed?, :format include MetaData # We use base values since it might have been loaded with size hint and prescaled def width @@ -153,24 +153,36 @@ end end class Thumbnail include ClassLogging + extend Forwardable def initialize(image, format, options = {}) @image = image @format = format + @quality = (options['quality'] or default_quality(format)) @quality &&= @quality.to_i + + @interlace = (options['interlace'] or 'NoInterlace') + fail "unsupported interlace: #{@interlace}" unless Magick::InterlaceType.values.map(&:to_s).include? @interlace + @interlace = Magick.const_get @interlace.to_sym end + def_delegators :@image, :format + def data + # export class variables to local scope format = @format quality = @quality + interlace = @interlace + @image.to_blob do self.format = format self.quality = quality if quality + self.interlace = interlace end end include MetaData @@ -357,10 +369,30 @@ def set_limit(limit, value) old = Magick.limit_resource(limit, value) log.info "changed #{limit} limit from #{old} to #{value} bytes" old end + + def setup_default_methods + processing_method('crop') do |image, width, height, options| + image.resize_to_fill(width, height, (Float(options['float-x']) rescue 0.5), (Float(options['float-y']) rescue 0.5)) if image.columns != width or image.rows != height + end + + processing_method('fit') do |image, width, height, options| + image.resize_to_fit(width, height) if image.columns != width or image.rows != height + end + + processing_method('pad') do |image, width, height, options| + image.resize_to_fit(width, height).replace do |resize| + resize.render_on_background(options['background-color'], width, height, (Float(options['float-x']) rescue 0.5), (Float(options['float-y']) rescue 0.5)) + end if image.columns != width or image.rows != height + end + + processing_method('limit') do |image, width, height, options| + image.resize_to_fit(width, height) if image.columns > width or image.rows > height + end + end end def self.setup(app) Service.logger = app.logger_for(Service) InputImage.logger = app.logger_for(InputImage) @@ -370,27 +402,11 @@ limit_memory: app.settings[:limit_memory], limit_map: app.settings[:limit_map], limit_disk: app.settings[:limit_disk] ) - @@service.processing_method('crop') do |image, width, height, options| - image.resize_to_fill(width, height) if image.columns != width or image.rows != height - end - - @@service.processing_method('fit') do |image, width, height, options| - image.resize_to_fit(width, height) if image.columns != width or image.rows != height - end - - @@service.processing_method('pad') do |image, width, height, options| - image.resize_to_fit(width, height).replace do |resize| - resize.render_on_background(options['background-color'], width, height) - end if image.columns != width or image.rows != height - end - - @@service.processing_method('limit') do |image, width, height, options| - image.resize_to_fit(width, height) if image.columns > width or image.rows > height - end + @@service.setup_default_methods end def thumbnailer @@service end @@ -398,33 +414,43 @@ end class Magick::Image include Plugin::Thumbnailer::ImageProcessing - def render_on_background(background_color, width = nil, height = nil) - Magick::Image.new(width || self.columns, height || self.rows) { + def render_on_background(background_color, width = nil, height = nil, float_x = 0.5, float_y = 0.5) + # default to image size + width ||= self.columns + height ||= self.rows + + # make sure we have enough background to fit image on top of it + width = self.columns if width < self.columns + height = self.rows if height < self.rows + + Magick::Image.new(width, height) { begin self.background_color = background_color rescue ArgumentError raise Plugin::Thumbnailer::InvalidColorNameError.new(background_color) end self.depth = 8 }.replace do |background| - background.composite!(self, Magick::CenterGravity, Magick::OverCompositeOp) + background.composite!(self, *background.float_to_offset(self.columns, self.rows, float_x, float_y), Magick::OverCompositeOp) end end # non coping version - def resize_to_fill(ncols, nrows = nil, gravity = Magick::CenterGravity) - nrows ||= ncols - if ncols != columns or nrows != rows - scale = [ncols / columns.to_f, nrows / rows.to_f].max - resize(scale * columns + 0.5, scale * rows + 0.5).replace do |image| - image.crop(gravity, ncols, nrows, true) if ncols != columns or nrows != rows - end - else - crop(gravity, ncols, nrows, true) if ncols != columns or nrows != rows + def resize_to_fill(width, height = nil, float_x = 0.5, float_y = 0.5) + # default to square + height ||= width + + return if width == columns and height == rows + + scale = [width / columns.to_f, height / rows.to_f].max + + resize((scale * columns).ceil, (scale * rows).ceil).replace do |image| + next if width == image.columns and height == image.rows + image.crop(*image.float_to_offset(width, height, float_x, float_y), width, height, true) end end def downscale(f) sample(columns / f, rows / f) @@ -435,8 +461,24 @@ if columns / new_factor > max_width * 2 and rows / new_factor > max_height * 2 find_downscale_factor(max_width, max_height, factor * 2) else factor end + end + + def float_to_offset(float_width, float_height, float_x = 0.5, float_y = 0.5) + base_width = self.columns + base_height = self.rows + + x = ((base_width - float_width) * float_x).ceil + y = ((base_height - float_height) * float_y).ceil + + x = 0 if x < 0 + x = (base_width - float_width) if x > (base_width - float_width) + + y = 0 if y < 0 + y = (base_height - float_height) if y > (base_height - float_height) + + [x, y] end end