Raster graphics operations/Ruby

From Rosetta Code
Revision as of 19:34, 11 September 2009 by rosettacode>Glennj (accumulate Ruby's Raster graphics operations code)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Raster graphics operations/Ruby is an implementation of Raster graphics operations. Other implementations of Raster graphics operations.

The Code

Collecting all the Ruby code from Category:Raster graphics operations, so one can invoke: require 'raster_graphics' <lang ruby>###########################################################################

  1. Represents an RGB[1] colour.

class RGBColour

 # Red, green and blue values must fall in the range 0..255.
 def initialize(red, green, blue)
   ok = [red, green, blue].inject(true) {|ok,c| ok &= c.between?(0,255)}
   unless ok
     raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}"
   end
   @red, @green, @blue = red, green, blue
 end
 attr_reader :red, :green, :blue
 alias_method :r, :red
 alias_method :g, :green
 alias_method :b, :blue
 # Return the list of [red, green, blue] values.
 #     RGBColour.new(100,150,200).values # => [100, 150, 200]
 # call-seq:
 # values -> array
 #
 def values
   [@red, @green, @blue]
 end
 # Equality test: two RGBColour objects are equal if they have the same
 # red, green and blue values.
 # call-seq:
 #     ==(a_colour) -> true or false
 #
 def ==(a_colour)
   values == a_colour.values
 end
 # Comparison test: compares two RGBColour objects based on their #luminosity value
 # call-seq:
 #     <=>(a_colour) -> -1, 0, +1
 #
 def <=>(a_colour)
   self.luminosity <=> a_colour.luminosity
 end
 # Calculate a integer luminosity value, in the range 0..255
 #     RGBColour.new(100,150,200).luminosity # => 142
 # call-seq:
 #     luminosity -> int
 #
 def luminosity
   Integer(0.2126*@red + 0.7152*@green + 0.0722*@blue)
 end
 # Return a new RGBColour value where all the red, green, blue values are the
 # #luminosity value.
 #     RGBColour.new(100,150,200).to_grayscale.values # => [142, 142, 142]
 # call-seq:
 #     to_grayscale -> a_colour
 #
 def to_grayscale
   l = luminosity
   self.class.new(l, l, l)
 end
 # Return a new RGBColour object given an iteration value for the Pixmap.mandelbrot
 # method.
 def self.mandel_colour(i)
   self.new( 16*(i % 15), 32*(i % 7), 8*(i % 31) )
 end
 RED   = RGBColour.new(255,0,0)
 GREEN = RGBColour.new(0,255,0)
 BLUE  = RGBColour.new(0,0,255)
 YELLOW= RGBColour.new(255,255,0)
 BLACK = RGBColour.new(0,0,0)
 WHITE = RGBColour.new(255,255,255)

end

  1. A Pixel represents an (x,y) point in a Pixmap.

Pixel = Struct.new(:x, :y)

class Pixmap

 def initialize(width, height)
   @width = width
   @height = height
   @data = fill(RGBColour::WHITE)
 end
 attr_reader :width, :height
 def fill(colour)
   @data = Array.new(@width) {Array.new(@height, colour)}
 end
 def validate_pixel(x,y)
   unless x.between?(0, @width-1) and y.between?(0, @height-1)
     raise ArgumentError, "requested pixel (#{x}, #{y}) is outside dimensions of this bitmap"
   end
 end
 ###############################################
 def [](x,y)
   validate_pixel(x,y)
   @data[x][y]
 end
 alias_method :get_pixel, :[]
 def []=(x,y,colour)
   validate_pixel(x,y)
   @data[x][y] = colour
 end
 alias_method :set_pixel, :[]=
 ###############################################
 # write to file/stream
 PIXMAP_FORMATS = ["P3", "P6"]   # implemented output formats
 PIXMAP_BINARY_FORMATS = ["P6"]  # implemented output formats which are binary
 def write_ppm(ios, format="P6")
   if not PIXMAP_FORMATS.include?(format)
     raise NotImplementedError, "pixmap format #{format} has not been implemented" 
   end
   ios.puts format, "#{@width} #{@height}", "255"
   ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
   @height.times do |y|
     @width.times do |x|
       case format
       when "P3" then ios.print @data[x][y].values.join(" "),"\n"
       when "P6" then ios.print @data[x][y].values.pack('C3')
       end
     end
   end
 end
 def save(filename, opts={:format=>"P6"})
   File.open(filename, 'w') do |f|
     write_ppm(f, opts[:format])
   end
 end
 alias_method :write, :save
 def print(opts={:format=>"P6"})
   write_ppm($stdout, opts[:format])
 end
 def save_as_jpeg(filename, quality=75)
   # using the ImageMagick convert tool
   begin
     pipe = IO.popen("convert ppm:- -quality #{quality} jpg:#{filename}", 'w')
     write_ppm(pipe)
   rescue SystemCallError => e
     warn "problem writing data to 'convert' utility -- does it exist in your $PATH?"
   ensure
     pipe.close rescue false
   end
 end
 ###############################################
 # read from file/pipe
 def self.read_ppm(ios)
   format = ios.gets.chomp
   width, height = ios.gets.chomp.split.map {|n| n.to_i }
   max_colour = ios.gets.chomp
   if (not PIXMAP_FORMATS.include?(format)) or 
       width < 1 or height < 1 or
       max_colour != '255'
   then
     ios.close
     raise StandardError, "file '#{filename}' does not start with the expected header"
   end
   ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
   bitmap = self.new(width, height)
   height.times do |y|
     width.times do |x|
       # read 3 bytes
       red, green, blue = case format
         when 'P3' then ios.gets.chomp.split
         when 'P6' then ios.read(3).unpack('C3')
       end
       bitmap[x,y] = RGBColour.new(red, green, blue)
     end
   end
   ios.close
   bitmap
 end
 def self.open(filename)
   read_ppm(File.open(filename, 'r'))
 end
 def self.open_from_jpeg(filename)
   unless File.readable?(filename)
     raise ArgumentError, "#{filename} does not exists or is not readable."
   end
   begin
     pipe = IO.popen("convert jpg:#{filename} ppm:-", 'r')
     read_ppm(pipe)
   rescue SystemCallError => e
     warn "problem reading data from 'convert' utility -- does it exist in your $PATH?"
   ensure
     pipe.close rescue false
   end
 end
 ###############################################
 # conversion methods
 def to_grayscale
   gray = self.class.new(@width, @height)
   @width.times do |x|
     @height.times do |y|
       gray[x,y] = self[x,y].to_grayscale
     end
   end
   gray
 end
 ###############################################
 def draw_line(p1, p2, colour)
   validate_pixel(p1.x, p2.y)
   validate_pixel(p2.x, p2.y)
   x1, y1 = p1.x, p1.y
   x2, y2 = p2.x, p2.y

   steep = (y2 - y1).abs > (x2 - x1).abs
   if steep
     x1, y1 = y1, x1
     x2, y2 = y2, x2
   end
   if x1 > x2
     x1, x2 = x2, x1
     y1, y2 = y2, y1
   end
   deltax = x2 - x1
   deltay = (y2 - y1).abs
   error = deltax / 2
   ystep = y1 < y2 ? 1 : -1

   y = y1
   x1.upto(x2) do |x|
     pixel = steep ? [y,x] : [x,y]
     self[*pixel] = colour
     error -= deltay
     if error < 0
       y += ystep
       error += deltax
     end
   end
 end
 ###############################################
 def draw_line_antialised(p1, p2, colour)
   x1, y1 = p1.x, p1.y
   x2, y2 = p2.x, p2.y

   steep = (y2 - y1).abs > (x2 - x1).abs
   if steep
     x1, y1 = y1, x1
     x2, y2 = y2, x2
   end
   if x1 > x2
     x1, x2 = x2, x1
     y1, y2 = y2, y1
   end
   deltax = x2 - x1
   deltay = (y2 - y1).abs
   gradient = 1.0 * deltay / deltax

   # handle the first endpoint
   xend = x1.round
   yend = y1 + gradient * (xend - x1)
   xgap = (x1 + 0.5).rfpart
   xpxl1 = xend
   ypxl1 = yend.truncate
   put_colour(xpxl1, ypxl1, colour, steep, yend.rfpart * xgap)
   put_colour(xpxl1, ypxl1 + 1, colour, steep, yend.fpart * xgap)
   itery = yend + gradient

   # handle the second endpoint
   xend = x2.round
   yend = y2 + gradient * (xend - x2)
   xgap = (x2 + 0.5).rfpart
   xpxl2 = xend
   ypxl2 = yend.truncate
   put_colour(xpxl2, ypxl2, colour, steep, yend.rfpart * xgap)
   put_colour(xpxl2, ypxl2 + 1, colour, steep, yend.fpart * xgap)

   # in between
   (xpxl1 + 1).upto(xpxl2 - 1).each do |x|
     put_colour(x, itery.truncate, colour, steep, itery.rfpart)
     put_colour(x, itery.truncate + 1, colour, steep, itery.fpart)
     itery = itery + gradient
   end
 end
 def put_colour(x, y, colour, steep, c)
   x, y = y, x if steep
   self[x, y] = anti_alias(colour, self[x, y], c)
 end
 def anti_alias(new, old, ratio)
   blended = new.values.zip(old.values).map {|n, o| (n*ratio + o*(1.0 - ratio)).round}
   RGBColour.new(*blended)
 end
 ###############################################
 def draw_circle(pixel, radius, colour)
   validate_pixel(pixel.x, pixel.y)

   self[pixel.x, pixel.y + radius] = colour
   self[pixel.x, pixel.y - radius] = colour
   self[pixel.x + radius, pixel.y] = colour
   self[pixel.x - radius, pixel.y] = colour

   f = 1 - radius
   ddF_x = 1
   ddF_y = -2 * radius
   x = 0
   y = radius
   while x < y
     if f >= 0
       y -= 1
       ddF_y += 2
       f += ddF_y
     end
     x += 1
     ddF_x += 2
     f += ddF_x
     self[pixel.x + x, pixel.y + y] = colour
     self[pixel.x + x, pixel.y - y] = colour
     self[pixel.x - x, pixel.y + y] = colour
     self[pixel.x - x, pixel.y - y] = colour
     self[pixel.x + y, pixel.y + x] = colour
     self[pixel.x + y, pixel.y - x] = colour
     self[pixel.x - y, pixel.y + x] = colour
     self[pixel.x - y, pixel.y - x] = colour
   end
 end
 ###############################################
 def flood_fill(pixel, new_colour)
   current_colour = self[pixel.x, pixel.y]
   queue = Queue.new
   queue.enqueue(pixel)
   until queue.empty?
     p = queue.dequeue
     if self[p.x, p.y] == current_colour
       west = find_border(p, current_colour, :west)
       east = find_border(p, current_colour, :east)
       draw_line(west, east, new_colour)
       q = west
       while q.x <= east.x
         [:north, :south].each do |direction|
           n = neighbour(q, direction)
           queue.enqueue(n) if self[n.x, n.y] == current_colour
         end
         q = neighbour(q, :east)
       end
     end
   end
 end
 def neighbour(pixel, direction)
   case direction
   when :north then Pixel[pixel.x, pixel.y - 1]
   when :south then Pixel[pixel.x, pixel.y + 1]
   when :east  then Pixel[pixel.x + 1, pixel.y]
   when :west  then Pixel[pixel.x - 1, pixel.y]
   end
 end
 def find_border(pixel, colour, direction)
   nextp = neighbour(pixel, direction)
   while self[nextp.x, nextp.y] == colour
     pixel = nextp
     nextp = neighbour(pixel, direction)
   end
   pixel
 end
 ###############################################
 def median_filter(radius=3)
   if radius.even?
     radius += 1
   end
   filtered = self.class.new(@width, @height)


   $stdout.puts "processing #{@height} rows"
   pb = ProgressBar.new(@height) if $DEBUG
   @height.times do |y|
     @width.times do |x|
       window = []
       (x - radius).upto(x + radius).each do |win_x|
         (y - radius).upto(y + radius).each do |win_y|
           win_x = 0 if win_x < 0
           win_y = 0 if win_y < 0
           win_x = @width-1 if win_x >= @width
           win_y = @height-1 if win_y >= @height
           window << self[win_x, win_y]
         end
       end
       # median
       filtered[x, y] = window.sort[window.length / 2]
     end
     pb.update(y) if $DEBUG
   end
   pb.close if $DEBUG
   filtered
 end
 ###############################################
 def histogram
   histogram = Hash.new(0)
   @height.times do |y|
     @width.times do |x|
       histogram[self[x,y].luminosity] += 1
     end
   end
   histogram 
 end
 def to_blackandwhite
   hist = histogram
   # find the median luminosity
   median = nil
   sum = 0
   hist.keys.sort.each do |lum|
     sum += hist[lum]
     if sum > @height * @width / 2
       median = lum
       break
     end
   end
   # create the black and white image
   bw = self.class.new(@width, @height)
   @height.times do |y|
     @width.times do |x|
       bw[x,y] = self[x,y].luminosity < median ? RGBColour::BLACK : RGBColour::WHITE
     end
   end
   bw
 end
 def save_as_blackandwhite(filename)
   to_blackandwhite.save(filename)
 end
 ###############################################
 def draw_bezier_curve(points, colour)
   # ensure the points are increasing along the x-axis
   points = points.sort_by {|p| [p.x, p.y]}
   xmin = points[0].x
   xmax = points[-1].x
   increment = 2
   prev = points[0]
   ((xmin + increment) .. xmax).step(increment) do |x|
     t = 1.0 * (x - xmin) / (xmax - xmin)
     p = Pixel[x, bezier(t, points).round]
     draw_line(prev, p, colour)
     prev = p
   end
 end
 # the generalized n-degree Bezier summation
 def bezier(t, points)
   n = points.length - 1
   points.each_with_index.inject(0.0) do |sum, (point, i)|
     sum += n.choose(i) * (1-t)**(n - i) * t**i * point.y
   end
 end

 ###############################################
 def self.mandelbrot(width, height)
   mandel = Pixmap.new(width,height)
   pb = ProgressBar.new(width) if $DEBUG
   width.times do |x|
     height.times do |y|
       x_ish = Float(x - width*11/15) / (width/3)
       y_ish = Float(y - height/2) / (height*3/10)
       mandel[x,y] = RGBColour.mandel_colour(mandel_iters(x_ish, y_ish))
     end
     pb.update(x) if $DEBUG
   end
   pb.close if $DEBUG
   mandel
 end
 def self.mandel_iters(cx,cy)
   x = y = 0.0
   count = 0
   while Math.hypot(x,y) < 2 and count < 255
     x, y = (x**2 - y**2 + cx), (2*x*y + cy)
     count += 1
   end
   count
 end 
 ###############################################
 # Apply a convolution kernel to a whole image
 def convolute(kernel)
   newimg = Pixmap.new(@width, @height)
   pb = ProgressBar.new(@width) if $DEBUG
   @width.times do |x|
     @height.times do |y|
       apply_kernel(x, y, kernel, newimg)
     end
     pb.update(x) if $DEBUG
   end
   pb.close if $DEBUG
   newimg
 end
 # Applies a convolution kernel to produce a single pixel in the destination
 def apply_kernel(x, y, kernel, newimg)
   x0 = [0, x-1].max
   y0 = [0, y-1].max
   x1 = x
   y1 = y
   x2 = [@width-1, x+1].min
   y2 = [@height-1, y+1].min

   r = g = b = 0.0
   [x0, x1, x2].zip(kernel).each do |xx, kcol|
     [y0, y1, y2].zip(kcol).each do |yy, k|
       r += k * self[xx,yy].r
       g += k * self[xx,yy].g
       b += k * self[xx,yy].b

end

   end
   newimg[x,y] = RGBColour.new(luma(r), luma(g), luma(b))
 end
 # Function for clamping values to those that we can use with colors
 def luma(value)
   if value < 0
     0
   elsif value > 255
     255
   else
     value
   end
 end

end


  1. Utilities

class ProgressBar

 def initialize(max)
   $stdout.sync = true
   @progress_max = max
   @progress_pos = 0
   @progress_view = 68
   $stdout.print "[#{'-'*@progress_view}]\r["
 end
 def update(n)
   new_pos = n * @progress_view/@progress_max
   if new_pos > @progress_pos
     @progress_pos = new_pos 
     $stdout.print '='
   end
 end
 def close
   $stdout.puts '=]'
 end

end

class Queue < Array

 alias_method :enqueue, :push
 alias_method :dequeue, :shift

end

class Numeric

 def fpart
   self - self.truncate
 end
 def rfpart
   1.0 - self.fpart
 end

end

class Integer

 def choose(k)
   self.factorial / (k.factorial * (self - k).factorial)
 end
 def factorial
   (2 .. self).reduce(1, :*)
 end

end </lang>

A Test Suite

<lang ruby>def display_pixmap(filename)

 puts "displaying #{filename}"
 system "./ppmview.rb #{filename} &"

end

if $0 == __FILE__

 old_debug = $DEBUG
 $DEBUG = true
 # for testing
 class Pixmap
   def ==(a_bitmap)
     return false if @width != a_bitmap.width or @height != a_bitmap.height
     @width.times {|x| @height.times {|y| 
       return false if not self[x,y] == (a_bitmap[x,y])
     }}
     true
   end
 end
 require 'test/unit'
 class TestRGBColour < Test::Unit::TestCase
   def test_init
     color = RGBColour.new(0,100,200)
     assert_equal(100, color.g)
   end
   def test_constants
     assert_equal([255,0,0], [RGBColour::RED.r,RGBColour::RED.g,RGBColour::RED.b])
     assert_equal([0,255,0], [RGBColour::GREEN.r,RGBColour::GREEN.g,RGBColour::GREEN.b])
     assert_equal([0,0,255], [RGBColour::BLUE.r,RGBColour::BLUE.g,RGBColour::BLUE.b])
   end
   def test_error
     color = RGBColour.new(0,100,200)
     assert_raise(ArgumentError) {RGBColour.new(0,0,256)}
   end
 end
 class TestPixmap < Test::Unit::TestCase
   def setup
     @w = 20
     @h = 30
     @bitmap = Pixmap.new(@w,@h)
   end
   def test_init
     assert_equal(@w, @bitmap.width)
     assert_equal(@h, @bitmap.height)
     assert_equal(RGBColour::WHITE, @bitmap.get_pixel(10,10))
   end
   def test_fill
     @bitmap.fill(RGBColour::RED)
     assert_equal(255,@bitmap[10,10].red)
     assert_equal(0,@bitmap[10,10].green)
     assert_equal(0,@bitmap[10,10].blue)
   end
   def test_get_pixel
     assert_equal(@bitmap[5,6], @bitmap.get_pixel(5,6))
     assert_raise(ArgumentError) {@bitmap[100,100]}
   end
   def test_grayscale
     @bitmap.fill(RGBColour::BLUE)
     @bitmap.height.times {|y| [9,10,11].each {|x| @bitmap[x,y]=RGBColour::GREEN}}
     @bitmap.width.times  {|x| [14,15,16].each {|y| @bitmap[x,y]=RGBColour::GREEN}}
     @bitmap.save('testcross.ppm')
     Pixmap.open('testcross.ppm').to_grayscale.save('testgray.ppm')
   end
   def test_save
     @bitmap.fill(RGBColour::BLUE)
     filename = 'test.ppm'
     @bitmap.save(filename)
     expected_size = 3 + (@w.to_s.length + 1 + @h.to_s.length + 1) + 4 + (@w * @h * 3)
     assert_equal(expected_size, File.size(filename))
   end 
   def test_open
     @bitmap.fill(RGBColour::RED)
     @bitmap.set_pixel(10,15, RGBColour::WHITE)
     filename = 'test.ppm'
     @bitmap.save(filename)
     new = Pixmap.open(filename)
     assert(@bitmap == new)
   end
 end
 # a green cross on a blue background
 colour_bitmap = Pixmap.new(20, 30)
 colour_bitmap.fill(RGBColour::BLUE)
 colour_bitmap.height.times {|y| [9,10,11].each {|x| colour_bitmap[x,y]=RGBColour::GREEN}}
 colour_bitmap.width.times  {|x| [14,15,16].each {|y| colour_bitmap[x,y]=RGBColour::GREEN}}
 colour_bitmap.save('testcross.ppm')
 display_pixmap 'testcross.ppm'
 Pixmap.open('testcross.ppm').to_grayscale.save('testgray.ppm')
 image = Pixmap.open('testcross.ppm')
 image.save_as_jpeg('testcross.jpg')
 #image.print(:format => "P3")
 bitmap = Pixmap.open_from_jpeg('testcross.jpg')
 savefile = 'testcross_from_jpeg.ppm'
 bitmap.save(savefile)
 display_pixmap savefile
 bitmap = Pixmap.new(500, 500)
 bitmap.fill(RGBColour::BLUE)
 10.step(430, 60) do |a|
   bitmap.draw_line(Pixel[10, 10], Pixel[490,a], RGBColour::YELLOW)
   bitmap.draw_line(Pixel[10, 10], Pixel[a,490], RGBColour::YELLOW)
 end
 bitmap.draw_line(Pixel[10, 10], Pixel[490,490], RGBColour::YELLOW)
 savefile = 'testlines4.ppm'
 bitmap.save(savefile)
 display_pixmap savefile
 bitmap = Pixmap.new(30, 30)
 bitmap.draw_circle(Pixel[14,14], 12, RGBColour::BLACK)
 savefile = 'testcircle.ppm'
 bitmap.save(savefile)
 display_pixmap savefile
 bitmap = Pixmap.new(300, 300)
 bitmap.draw_circle(Pixel[149,149], 120, RGBColour::BLACK)
 bitmap.draw_circle(Pixel[200,100], 40, RGBColour::BLACK)
 bitmap.flood_fill(Pixel[140,160], RGBColour::BLUE)
 savefile = 'testflood.ppm'
 bitmap.save(savefile)
 display_pixmap savefile
 bitmap = Pixmap.new(500, 500)
 bitmap.fill(RGBColour::BLUE)
 10.step(430, 60) do |a|
   bitmap.draw_line_antialised(Pixel[10, 10], Pixel[490,a], RGBColour::YELLOW)
   bitmap.draw_line_antialised(Pixel[10, 10], Pixel[a,490], RGBColour::YELLOW)
 end
 bitmap.draw_line_antialised(Pixel[10, 10], Pixel[490,490], RGBColour::YELLOW)
 bitmap.save('testantialias.ppm')
 display_pixmap 'testantialias.ppm'
 file = 'teapot.ppm'
 display_pixmap file
 bitmap = Pixmap.open(file)
 # test new grayscale
 savefile = 'teapotgray.ppm'
 gray = bitmap.to_grayscale
 gray.save(savefile)
 display_pixmap savefile
 #
 savefile = 'testfiltered.ppm'
 filtered = bitmap.median_filter
 filtered.save(savefile)
 display_pixmap savefile
 file = 'teapot.ppm'
 savefile = 'teapotbw.ppm'
 display_pixmap file
 Pixmap.open(file).save_as_blackandwhite(savefile)
 display_pixmap savefile
 bitmap = Pixmap.new(400, 400)
 points = [
   Pixel[40,100], Pixel[100,350], Pixel[150,50], 
   Pixel[150,150], Pixel[350,250], Pixel[250,250]
 ]
 points.each {|p| bitmap.draw_circle(p, 3, RGBColour::RED)}
 bitmap.draw_bezier_curve(points, RGBColour::BLUE)
 savefile = 'testbezier.ppm'
 bitmap.save(savefile)
 display_pixmap savefile
 savefile = 'testmandel.ppm'
 Pixmap.mandelbrot(500,500).save(savefile)
 display_pixmap savefile
 
 # Demonstration code using the teapot image from Tk's widget demo
 teapot = Pixmap.open('teapot.ppm')
 [ ['Emboss',  [[-2.0, -1.0, 0.0],  [-1.0, 1.0, 1.0],  [0.0, 1.0, 2.0]]], 
   ['Sharpen', [[-1.0, -1.0, -1.0], [-1.0, 9.0, -1.0], [-1.0, -1.0, -1.0]]], 
   ['Blur',    [[0.1111,0.1111,0.1111],[0.1111,0.1111,0.1111],[0.1111,0.1111,0.1111]]],
 ].each do |label, kernel|
   savefile = 'test' + label.downcase + '.ppm'
   teapot.convolute(kernel).save(savefile)
   display_pixmap savefile
 end
 $DEBUG = old_debug 

end </lang>

An Image Viewer

Library: Ruby/Tk

The ppmview.rb program is: <lang ruby>#!/usr/bin/ruby

require 'tk'

if ARGV.empty?

 $stderr.puts "usage: #{File.basename($0)} imagefile"
 exit 1

end

filename = ARGV.shift unless File.readable?(filename)

 raise ArgumentError, "can't read file '#{filename}'"

end

root = TkRoot.new('title' => File.basename(filename)) label = TkLabel.new(root) {image TkPhotoImage.new('file' => filename)} label.pack Tk.mainloop </lang>