geometry.rb 3.46 KB
# This Geometry class was yanked from RMagick.  However, it lets ImageMagick handle the actual change_geometry.
# Use #new_dimensions_for to get new dimensions
# Used so I can use spiffy RMagick geometry strings with ImageScience
class Geometry
  # @ is removed until support for them is added
  FLAGS = ['', '%', '<', '>', '!']#, '@']
  RFLAGS = { '%' => :percent,
             '!' => :aspect,
             '<' => :>,
             '>' => :<,
             '@' => :area }

  attr_accessor :width, :height, :x, :y, :flag

  def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
    # Support floating-point width and height arguments so Geometry
    # objects can be used to specify Image#density= arguments.
    raise ArgumentError, "width must be >= 0: #{width}"   if width < 0
    raise ArgumentError, "height must be >= 0: #{height}" if height < 0
    @width  = width.to_f
    @height = height.to_f
    @x      = x.to_i
    @y      = y.to_i
    @flag   = flag
  end

  # Construct an object from a geometry string
  RE = /\A(\d*)(?:x(\d+)?)?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/

  def self.from_s(str)
    raise(ArgumentError, "no geometry string specified") unless str
  
    if m = RE.match(str)
      new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]])
    else
      raise ArgumentError, "invalid geometry format"
    end
  end

  # Convert object to a geometry string
  def to_s
    str = ''
    str << "%g" % @width if @width > 0
    str << 'x' if (@width > 0 || @height > 0)
    str << "%g" % @height if @height > 0
    str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
    str << FLAGS[@flag.to_i]
  end
  
  # attempts to get new dimensions for the current geometry string given these old dimensions.
  # This doesn't implement the aspect flag (!) or the area flag (@).  PDI
  def new_dimensions_for(orig_width, orig_height)
    new_width  = orig_width
    new_height = orig_height

    case @flag
      when :percent
        scale_x = @width.zero?  ? 100 : @width
        scale_y = @height.zero? ? @width : @height
        new_width    = scale_x.to_f * (orig_width.to_f  / 100.0)
        new_height   = scale_y.to_f * (orig_height.to_f / 100.0)
      when :aspect
        new_width = @width unless @width.nil?
        new_height = @height unless @height.nil?
      when :<, :>, nil
        scale_factor =
          if new_width.zero? || new_height.zero?
            1.0
          else
            if @width.nonzero? && @height.nonzero?
              [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min
            else
              @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f)
            end
          end
        new_width  = scale_factor * new_width.to_f
        new_height = scale_factor * new_height.to_f
        new_width  = orig_width  if @flag && orig_width.send(@flag,  new_width)
        new_height = orig_height if @flag && orig_height.send(@flag, new_height)
    end

    [new_width, new_height].collect! { |v| [v.round, 1].max }
  end
end

class Array
  # allows you to get new dimensions for the current array of dimensions with a given geometry string
  #
  #   [50, 64] / '40>' # => [40, 51]
  def /(geometry)
    raise ArgumentError, "Only works with a [width, height] pair" if size != 2
    raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry)
    geometry = Geometry.from_s(geometry) if geometry.is_a?(String)
    geometry.new_dimensions_for first, last
  end
end