2. CocoaMagick
2.1 Presenting...
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.
2.2 CocoaMagick.rb
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
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