2. CocoaMagick

So with no more delay, here is CocoaMagick, a single-file 300-line replacement for RMagick. The full source code is below, or you can download it from here. It’s loaded with hacks, but I think it’s fairly readable, and it illustrates a lot of great Ruby, Cocoa, and RubyCocoa features for image manipulation. All you need to use it is RubyCocoa (and gruff).  
Note: since I wrote this article, gruff has grown!  If anyone wants to help by updating CocoaMagick.rb for the lastest gruff, I’d be happy to incorporate your changes.  Otherwise, please follow this link to download the version of gruff that I used for this article.
CocoaMagick.rb [ruby]
# CocoaMagick.rb
#   Tim Burks / RubyCocoa Resources http://www.rubycocoa.com
#
# This is a quick-and-dirty replacement for RMagic written specifically 
# for Geoffrey Grosenbach's "gruff" graphing package.
# It uses RubyCocoa and the Cocoa API to draw the elements of gruff graphs.
# It is FAR from comprehensive, but works for nearly all of the test cases 
# distributed with gruff. Tested with gruff 0.1.2.  
#
# To use, change "require 'RMagick'"to "require 'CocoaMagick'" in gruff/base.rb.
# You also must have RubyCocoa installed, preferably the most recent version.
# This means you must also be running on a Mac, preferably with OS X 10.4 or later. 
# Due to a Cocoa limitation, this script must either be run by the user currently 
# logged into the console or as root.
#
# For help with RubyCocoa, visit "RubyCocoa Resources" at www.rubycocoa.com.
# For more on gruff, see http://nubyonrails.com/pages/gruff
# 
require 'osx/cocoa'

def log(str)
  puts str if false
end

class OSX::NSImage
  def writePNG(filename)
    bits = OSX::NSBitmapImageRep.alloc.initWithData(self.TIFFRepresentation)
    data = bits.representationUsingType_properties(OSX::NSPNGFileType, nil)
    data.writeToFile_atomically(filename, false)
  end
end

class OSX::NSColor
  def self.colorWithName(name) 
    log "colorWithName #{name}"
    if name[0..0] == "#"
      r = eval("0x"+name[1..2]) / 256.0
      g = eval("0x"+name[3..4]) / 256.0
      b = eval("0x"+name[5..6]) / 256.0
      colorWithDeviceRed_green_blue_alpha(r,g,b,1)
    elsif name == "transparent"
      OSX::NSColor.blackColor.colorWithAlphaComponent(0.0)
    elsif name == "grey"
      OSX::NSColor.colorWithDeviceRed_green_blue_alpha(0.5,0.5,0.5,1)
    else
      eval("OSX::NSColor.#{name}Color")
    end
  end
end

 module Magick
  class Draw
    attr_accessor :pointsize, :fill, :stroke, :font_weight, :gravity, :font, 
      :stroke_color, :fill_color, :stroke_width, :dasharray
    def stroke_and_fill(path)
      @stroke_color.colorWithAlphaComponent(@stroke_opacity).set
      path.stroke
      @fill_color.colorWithAlphaComponent(@fill_opacity).set
      path.fill
    end 
    def initialize
      @fill = "white"
      @pointsize = 18
      @stroke_opacity = 1
      @fill_opacity = 1
      @stroke_width = 1
      @stroke_color = OSX::NSColor.colorWithName("grey")
      @fill_color = OSX::NSColor.colorWithName("grey")
      @stack = []
      OSX::NSBezierPath.setDefaultLineJoinStyle(OSX::NSRoundLineJoinStyle)
      OSX::NSBezierPath.setDefaultLineWidth(1)
    end
    def push
      log "Draw.push"
      @stack.push({ # add more if needed
        :fill_color => @fill_color, 
        :stroke_color => @stroke_color, 
        :dasharray => @dasharray, 
        :dashcount => @dashcount,
        :stroke_width => @stroke_width
      })
      self
    end
    def pop
      log "Draw.pop"  
      hash = @stack.pop
      hash.keys.each {|key| self.instance_variable_set("@"+key.to_s, hash[key])}
      self
    end
    def scale(x,y)
      log "scale #{x} #{y}"
      @scalex = x
      @scaley = y
      self
    end
    def string_attributes
      attributes = OSX::NSMutableDictionary.alloc.initWithCapacity_(10)
      attributes.setObject_forKey(OSX::NSColor.colorWithName(@fill), 
        OSX.NSForegroundColorAttributeName)
      if @font_weight == BoldWeight
        attributes.setObject_forKey(OSX::NSFont.boldSystemFontOfSize(@pointsize), 
          OSX.NSFontAttributeName) 
      else
        attributes.setObject_forKey(OSX::NSFont.systemFontOfSize(@pointsize), 
          OSX.NSFontAttributeName) 
      end     
      attributes
    end  
    def get_type_metrics(a,text)
      log "get_type_metrics #{a} #{text.inspect}"
      attributes = string_attributes()
      metrics = TypeMetric.new
      size = OSX::NSString.stringWithString(text).sizeWithAttributes(attributes)
      metrics.width = size.width
      metrics
    end
    def annotate(image,width,height,x,y,text)
      log "annotate #{image} #{width} #{height} #{x} #{y} #{text}"
      attributes = string_attributes()      
      string = OSX::NSString.stringWithString(text)
      size = string.sizeWithAttributes(attributes) 
      if @gravity == EastGravity or @gravity == SouthEastGravity or @gravity == NorthEastGravity
        x = x + width - size.width/2
      elsif @gravity == CenterGravity or @gravity == NorthGravity or @gravity == SouthGravity
        x = x + width/2 - size.width/2 
      end
      if @gravity == NorthGravity or @gravity == NorthEastGravity or @gravity == NorthWestGravity
        y = y + height - size.height
      elsif @gravity == CenterGravity or @gravity == EastGravity or @gravity == WestGravity
        y = y + height/2 - size.height/2
      else
        y = y - size.height/2
      end
      string.drawAtPoint_withAttributes([x,image.height-y-size.height], attributes)
      self
    end
    def stroke(color)
      log "stroke #{color}"
      @stroke_color = OSX::NSColor.colorWithName(color)
      self
    end
    def fill(color)
      log "fill #{color}"
      @fill_color = OSX::NSColor.colorWithName(color)
      self
    end
    def rectangle(a,b,c,d)
      log "rectangle #{a} #{b} #{c} #{d}"
      @fill_color.set
      OSX::NSRectFill([a*@scalex,$height-d*@scalex,(c-a)*@scalex,(d-b)*@scaley])
      self
    end
    def stroke_width(a)
      log "stroke_width #{a}"
      @stroke_width = a
      OSX::NSBezierPath.setDefaultLineWidth(a*@scalex)
      self
    end
    def line(a,b,c,d)
      log "line #{a} #{b} #{c} #{d}"
      @stroke_color.set
      path = OSX::NSBezierPath.bezierPath
      path.setLineDash_count_phase(@dasharray, @dashcount, 0) if @dasharray
      path.moveToPoint([a*@scalex,$height-b*@scaley])
      path.lineToPoint([c*@scalex,$height-d*@scaley])
      path.stroke
      self
    end
    def stroke_opacity(opacity)
      log "stroke_opacity #{opacity}"
      @stroke_opacity = opacity
      self
    end
    def stroke_color(color)
      stroke(color)
      self
    end
    def fill_opacity(opacity)
      @fill_opacity = opacity
      self
    end
    def stroke_dasharray(a, b)
      log "stroke_dasharray #{a} #{b}"
      @dashcount = 2
      @dasharray = [a,b].pack('f2')
      self
    end
    def circle(cx,cy,px,py)
      log "circle #{cx} #{cy} #{px} #{py}"
      r = Math::sqrt((cx-px)*(cx-px) + (cy-py)*(cy-py))
      path = OSX::NSBezierPath.bezierPathWithOvalInRect(
        [(cx-r)*@scalex,$height-(cy+r)*@scaley,2*r*@scalex,2*r*@scaley])
      stroke_and_fill(path)
      self
    end
    def ellipse(x, y, w, h, as, ae)
      log "ellipse #{x} #{y} #{w} #{h} #{as} #{ae}"
      push
      stroke_width(1)
      @fill_color = @stroke_color   
      path = OSX::NSBezierPath.bezierPath
      center = [x*@scalex,$height-y*@scaley]
      path.moveToPoint(center)
      path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle(
        center,2*w*@scalex,-ae,-as)
      path.closePath
      stroke_and_fill(path)
      pop
      self
    end
    def polyline(*args)
      path = OSX::NSBezierPath.bezierPath
      path.moveToPoint([args[0]*@scalex,$height-args[1]*@scaley])
      i = 2
      while(i < args.length) 
        path.lineToPoint([args[i]*@scalex,$height-args[i+1]*@scaley])
        i = i + 2
      end
      stroke_and_fill(path)
      self
    end
    def polygon(*args)
      path = OSX::NSBezierPath.bezierPath
      path.moveToPoint([args[0]*@scalex,$height-args[1]*@scaley])
      i = 2
      while(i < args.length) 
        path.lineToPoint([args[i]*@scalex,$height-args[i+1]*@scaley])
        i = i + 2
      end
      path.closePath
      stroke_and_fill(path)
      self
    end
    def draw(a)
      log "draw #{a}"
      self
    end
  end
  class Image
    attr_accessor :image, :width, :height   
    def initialize(width, height, fill=nil)
      log "Image.initialize #{width} #{height} #{fill}"
      @width = width
      @height = height
      @image = OSX::NSImage.alloc.initWithSize([width,height])
      @image.lockFocus
      if fill == nil 
        OSX::NSColor.blackColor.set
        OSX::NSRectFill([0,0,width,height])
      elsif fill.class == Magick::GradientFill
        gradient = fill.image
        gradient.drawInRect_fromRect_operation_fraction([0,0,width,height], 
          [0,0,gradient.size.width,gradient.size.height], OSX::NSCompositeCopy, 1.0)
      else
        fill.drawInRect_fromRect_operation_fraction([0,0,width,height], 
          [0,0,fill.size.width,fill.size.height], OSX::NSCompositeCopy, 1.0)
      end
      $width = width
      $height = height
      self
    end
    def write(filename)
      log "Image.write #{filename}"
      @image.unlockFocus
      @image.writePNG(filename)
      self
    end
    def self.read(filename)
      log "Image.read #{filename}"
      image = OSX::NSImage.alloc.initWithContentsOfFile(filename)
      [Image.new(800,600,image)] 
    end
  end
  class GradientFill
    attr_accessor :image
    def initialize(a,b,c,d,e,f)
      log "GradientFill.initialize #{a} #{b} #{c} #{d} #{e} #{f}"     
      # There's no direct gradient fill support in Cocoa, so fake it
      resolution = 100  # increase this to reduce striping and increase run time...
      @image = OSX::NSImage.alloc.initWithSize([1,resolution])
      @image.lockFocus
      bottomColor = OSX::NSColor.colorWithName(f)
      topColor = OSX::NSColor.colorWithName(e)
      resolution.times {|i|
        bottomColor.blendedColorWithFraction_ofColor(i/(resolution - 1.0), topColor).set
        OSX::NSRectFill([0,i,1,i+1])
      }
      self
    end
  end
  class TypeMetric
    attr_accessor :width
    def initialize
      @width = 50 
    end
  end
  NormalWeight = 400
  BoldWeight = 700
  WestGravity = :west
  EastGravity = :east
  CenterGravity = :center
  SouthGravity = :south
  NorthGravity = :north
  SouthWestGravity = :southwest
  NorthWestGravity = :northwest
  NorthEastGravity = :northeast
  SouthEastGravity = :southeast
end

# this prevents a warning from AppKit
app = OSX::NSApplication.sharedApplication

Did you find an error? Is something missing? Post your comment or suggestion below!

Comments (1) post
  1. Updated Cocoamagick Sun Jul 22 12:33:18 +0000 2007

    here, I just finished this update I haven’t done much testing but the unit tests for gruff(svn) pass, I’ll polish it once I actually start integrating gruff in my app (translation: Warning! this is untested and probably has issues)

    http://ninjakitten.us/CocoaMagick.rb