
#==============================================================================#
# $Id: image.rb,v 1.13 2004/01/17 08:15:43 yuya Exp $
# $Source: /cvsroot/gruby/gruby/lib/grb/image.rb,v $
#==============================================================================#

require 'GD'
require 'grb/color'
require 'grb/stroke'
require 'grb/fill'
require 'grb/polygon'
require 'grb/font'
require 'grb/math_ex'

#==============================================================================#

module GRb

  class Image

    def initialize(image)
      @image = image
    end

    attr_reader :image

    def self.create(width, height, truecolor = false)
      if truecolor
        image = GD::Image.newTrueColor(width, height)
        image.filledRectangle(0, 0, width - 1, height - 1, image.colorAllocate(255, 255, 255))
      else
        image = GD::Image.new(width, height)
        image.colorAllocate(255, 255, 255)
      end

      return self.new(image)
    end

    def self.create_and_write_png_file(width, height, truecolor, filepath)
      image = self.create(width, height, truecolor)
      yield(image)
      image.write_png_file(filepath)
    end

    def self.new_from_png_file(filepath)
      return File.open(filepath) { |file|
        self.new(GD::Image.newFromPng(file))
      }
    end

    def self.new_from_jpeg_file(filepath)
      return File.open(filepath) { |file|
        self.new(GD::Image.newFromJpeg(file))
      }
    end

    def image2gd(image)
      case image
      when GRb::Image then return image.image
      when GD::Image  then return image
      else raise TypeError, "GRb::Image/GD::Image required"
      end
    end
    protected :image2gd

    # NOT TESTED:
    def interlace
      return @image.interlace
    end

    # NOT TESTED:
    def interlace=(value)
      @image.interlace = value
    end

    def truecolor?
      return @image.is_trueColor?
    end

    def width
      return @image.width
    end

    def height
      return @image.height
    end

    def destroy
      @image.destroy
    end

    # NOT TESTED:
    def transparent(color)
      return @image.transparent(color)
    end

    # NOT TESTED:
    def rgb(color)
      return @image.rgb(color)
    end

    # NOT TESTED:
    def color(red, green, blue)
      return self.color_resolve(red, green, blue)
    end

    # NOT TESTED:
    def color_allocate(red, green, blue)
      return @image.colorAllocate(red, green, blue)
    end

    # NOT TESTED:
    def color_closest(red, green, blue)
      return @image.colorClosest(red, green, blue)
    end

    # NOT TESTED:
    def color_deallocate(color)
      return @image.colorDeallocate(color)
    end

    # NOT TESTED:
    def color_exact(red, green, blue)
      return @image.colorExact
    end

    def color_resolve(red, green, blue)
      return @image.colorResolve(red, green, blue)
    end

    # NOT TESTED:
    def colors_total
      return @image.colorsTotal
    end

    def copy(image, x, y, sx, sy, dx, dy)
      @image.copy(image2gd(image), x, y, sx, sy, dx, dy)
    end

    def copy_merge(image, x, y, sx, sy, dx, dy, percent)
      @image.copyMerge(image2gd(image), x, y, sx, sy, dx, dy, percent)
    end

    # NOT TESTED:
    def copy_merge_gray(image, x, y, sx, sy, dx, dy, percent)
      @image.copyMergeGray(image2gd(image), x, y, sx, sy, dx, dy, percent)
    end

    # NOT TESTED:
    def copy_resampled(image, x, y, sx, sy, dx, dy, sdx, sdy)
      @image.copyResampled(image2gd(image), x, y, sx, sy, dx, dy, sdx, sdy)
    end

    # NOT TESTED:
    def copy_resized(image, x, y, sx, sy, dx, dy, sdx, sdy)
      @image.copyResized(image2gd(image), x, y, sx, sy, dx, dy, sdx, sdy)
    end

    def write_png(io)
      @image.png(io)
    end

    def write_png_file(filepath)
      File.open(filepath, 'wb') { |file|
        self.write_png(file)
      }
    end

    # NOT TESTED:
    def write_jpeg(io, quality = -1)
      @image.jpeg(io, quality)
    end

    # NOT TESTED:
    def write_jpeg_file(filepath, quality = -1)
      File.open(filepath, 'wb') { |file|
        self.write_jpeg(file, quality)
      }
    end

    def set(obj)
      case obj
      when GRb::Color  then obj.set(self)
      when GRb::Stroke then obj.set(self)
      when GRb::Fill   then obj.set(self)
      else raise(ArgumentError, 'invalid object type')
      end
    end

    def set_brush(brush)
      @image.setBrush(brush)
    end

    def set_style(*style)
      @image.setStyle(*style)
    end

    # NOT TESTED:
    def get_pixel(x, y)
      return @image.getPixel(x, y)
    end

    def set_pixel(x, y, color)
      @image.setPixel(x, y, color.set(self))
    end

    def pset(x, y, color)
      self.set_pixel(x, y, color)
    end

    def rect(x1, y1, x2, y2, line = nil, fill = nil)
      xx1, xx2 = [x1, x2].sort
      yy1, yy2 = [y1, y2].sort

      filler = Fill.new_from(fill)

      if filler.draw?
        layer = Layer.new(self, filler.opacity, filler.color.invert)
        layer.image.filledRectangle(xx1, yy1, xx2, yy2, layer.set(filler))
        layer.merge
      end

      stroke = Stroke.new_from(line)

      if stroke.draw?
        layer = Layer.new(self, stroke.opacity, stroke.color.invert)
        layer.image.rectangle(xx1, yy1, xx2, yy2, layer.set(stroke))
        layer.merge
      end
    end

    def line(x1, y1, x2, y2, line)
      stroke = Stroke.new_from(line)

      if stroke.draw?
        layer = Layer.new(self, stroke.opacity, stroke.color.invert)
        layer.image.line(x1, y1, x2, y2, layer.set(stroke))
        layer.merge
      end
    end

    def circle(cx, cy, rx, ry, line = nil, fill = nil)
      self.arc(cx, cy, rx, ry, 0, 360, line, fill)
    end

    def arc(cx, cy, rx, ry, deg1, deg2, line = nil, fill = nil)
      filler = Fill.new_from(fill)

      if filler.draw?
        layer = Layer.new(self, filler.opacity, filler.color.invert)
        layer.image.filledArc(cx, cy, rx, ry, deg1, deg2, layer.set(filler), 0)
        layer.merge
      end

      stroke = Stroke.new_from(line)

      if stroke.draw?
        layer = Layer.new(self, stroke.opacity, stroke.color.invert)
        layer.image.arc(cx, cy, rx, ry, deg1, deg2, layer.set(stroke))
        layer.merge
      end
    end

    def polygon(poly, line = nil, fill = nil)
      filler = Fill.new_from(fill)

      if filler.draw?
        layer = Layer.new(self, filler.opacity, filler.color.invert)
        layer.image.filledPolygon(poly.to_polygon, layer.set(filler))
        layer.merge
      end

      stroke = Stroke.new_from(line)

      if stroke.draw?
        layer = Layer.new(self, stroke.opacity, stroke.color.invert)
        layer.image.polygon(poly.to_polygon, layer.set(stroke))
        layer.merge
      end
    end

    def string(font, x, y, angle, color, text, align = :left, valign = :top)
      font.draw(@image, x, y, angle, self.set(color), text, align, valign)
    end

    def fill(x, y, fill, border = nil)
      if border
        @image.fillToBorder(x, y, self.set(border), self.set(fill))
      else
        @image.fill(x, y, self.set(fill))
      end
    end

    def gradation(_x1, _y1, _x2, _y2, start_color, end_color, angle = 0, opacity = 100)
      x, xx = [_x1, _x2].sort
      y, yy = [_y1, _y2].sort
      dx    = xx - x + 1
      dy    = yy - y + 1

      w1    = MathEx.sin(angle) * dx
      w2    = MathEx.sin(angle) * dy
      delta = (MathEx.sin(angle) * dx).abs + (MathEx.cos(angle) * dy).abs

      case angle % 360
      when 0..90
        x1 = (MathEx.cos(angle + 270) * w1).floor
        y1 = (MathEx.sin(angle + 270) * w1).floor
        x2 = (MathEx.cos(angle)       * w2).floor + dx
        y2 = (MathEx.sin(angle)       * w2).floor
      when 90..180
        x1 = (MathEx.cos(angle + 180) * w2).floor + dx
        y1 = (MathEx.sin(angle + 180) * w2).floor + dy
        x2 = (MathEx.cos(angle + 270) * w1).floor
        y2 = (MathEx.sin(angle + 270) * w1).floor + dy
      when 180..270
        x1 = (MathEx.cos(angle + 90)  * w1).floor + dx
        y1 = (MathEx.sin(angle + 90)  * w1).floor + dy
        x2 = (MathEx.cos(angle + 180) * w2).floor
        y2 = (MathEx.sin(angle + 180) * w2).floor + dy
      when 270..360
        x1 = (MathEx.cos(angle)       * w2).floor
        y1 = (MathEx.sin(angle)       * w2).floor
        x2 = (MathEx.cos(angle + 90)  * w1).floor + dx
        y2 = (MathEx.sin(angle + 90)  * w1).floor
      else raise 'bug?'
      end

      delta_r = start_color.r - end_color.r
      delta_g = start_color.g - end_color.g
      delta_b = start_color.b - end_color.b

      raito_r = delta_r.to_f / delta.to_f
      raito_g = delta_g.to_f / delta.to_f
      raito_b = delta_b.to_f / delta.to_f

      raito_x = MathEx.cos(angle + 90)
      raito_y = MathEx.sin(angle + 90)

      frame = GD::Image.new(dx, dy)

      (0..delta.ceil).each { |i|
        red   = [[(start_color.r - raito_r * i).to_i, 0].max, 255].min
        green = [[(start_color.g - raito_g * i).to_i, 0].max, 255].min
        blue  = [[(start_color.b - raito_b * i).to_i, 0].max, 255].min

        xx1, xx2 = (x1 + raito_x * i).floor, (x2 + raito_x * i).floor
        yy1, yy2 = (y1 + raito_y * i).floor, (y2 + raito_y * i).floor

        brush = GD::Image.new(2, 2)
        brush.colorAllocate(red, green, blue)
        frame.setBrush(brush)
        frame.line(xx1, yy1, xx2, yy2, GD::Brushed)
        brush.destroy
      }

      case opacity
      when 0     then #nop
      when 1..99 then frame.copyMerge(@image, x, y, 0, 0, dx, dy, opacity)
      when 100   then frame.copy(@image, x, y, 0, 0, dx, dy)
      else raise ArgumentError, 'opacity must be between 0 and 100'
      end

      frame.destroy
    end

  end # Image

end # GRb

#==============================================================================#

require 'grb/layer'

#==============================================================================#
#==============================================================================#
