Raster graphics operations/Ruby

From Rosetta Code

Ruby


The Code

Collecting all the Ruby code from Category:Raster graphics operations, so one can invoke: require 'raster_graphics'

Uses the ChunkyPNG pure-Ruby PNG library. Important to note won't work with `frozen_string_literal: true` uses mutable String operations.

<lang ruby>###########################################################################

  1. frozen_string_literal: false
  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) }
   raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}" unless ok
   @red = red
   @green = green
   @blue = blue
 end
 attr_reader :red, :green, :blue
 alias r red
 alias g green
 alias 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)
   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)
   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) && 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 get_pixel []
 def []=(x, y, colour)
   validate_pixel(x, y)
   @data[x][y] = colour
 end
 alias set_pixel []=
 def each_pixel
   if block_given?
     @height.times { |y| @width.times { |x| yield x, y } }
   else
     to_enum(:each_pixel)
   end
 end
 ###############################################
 # write to file/stream
 PIXMAP_FORMATS = %w[P3 P6].freeze # implemented output formats
 PIXMAP_BINARY_FORMATS = ['P6'].freeze # implemented output formats which are binary
 def write_ppm(ios, format = 'P6')
   raise NotImplementedError, "pixmap format #{format} has not been implemented" unless PIXMAP_FORMATS.include?(format)
   ios.puts format, "#{@width} #{@height}", '255'
   ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
   each_pixel do |x, y|
     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
 def save(filename, opts = { format: 'P6' })
   File.open(filename, 'w') do |f|
     write_ppm(f, opts[:format])
   end
 end
 alias 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
   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
   begin
     pipe.close
   rescue StandardError
     false
   end
 end
 def save_as_png(filename)
   require 'chunky_png'
   stream = StringIO.new(, 'r+')
   each_pixel { |x, y| stream << self[x, y].values.pack('ccc') }
   stream.seek(0)
   ChunkyPNG::Canvas.extend(ChunkyPNG::Canvas::StreamImporting)
   canvas = ChunkyPNG::Canvas.from_rgb_stream(width, height, stream)
   canvas.to_image.save(filename)
 end
 ###############################################
 # read from file/pipe
 def self.read_ppm(ios)
   format = ios.gets.chomp
   width, height = ios.gets.chomp.split.map(&:to_i)
   max_colour = ios.gets.chomp
   if !PIXMAP_FORMATS.include?(format) ||
      (width < 1) || (height < 1) ||
      (max_colour != '255')
     ios.close
     raise StandardError, "file '#{filename}' does not start with the expected header"
   end
   ios.binmode if PIXMAP_BINARY_FORMATS.include?(format)
   bitmap = new(width, height)
   bitmap.each_pixel do |x, y|
     # 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
   ios.close
   bitmap
 end
 def self.open(filename)
   read_ppm(File.open(filename, 'r'))
 end
 def self.open_from_jpeg(filename)
   raise ArgumentError, "#{filename} does not exists or is not readable." unless File.readable?(filename)
   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
     begin
       pipe.close
     rescue StandardError
       false
     end
   end
 end
 ###############################################
 # conversion methods
 def to_grayscale
   gray = self.class.new(@width, @height)
   each_pixel do |x, y|
     gray[x, y] = self[x, y].to_grayscale
   end
   gray
 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)
   each_pixel do |x, y|
     bw[x, y] = self[x, y].luminosity < median ? RGBColour::BLACK : RGBColour::WHITE
   end
   bw
 end
 def save_as_blackandwhite(filename)
   to_blackandwhite.save(filename)
 end
 ###############################################
 def draw_line(p1, p2, colour)
   validate_pixel(p1.x, p2.y)
   validate_pixel(p2.x, p2.y)
   x1 = p1.x
   y1 = p1.y
   x2 = p2.x
   y2 = 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.negative?
       y += ystep
       error += deltax
     end
   end
 end
 ###############################################
 def draw_line_antialised(p1, p2, colour)
   x1 = p1.x
   y1 = p1.y
   x2 = p2.x
   y2 = 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 += 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
     next unless 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
       %i[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
 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)
   radius += 1 if radius.even?
   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.negative?
           win_y = 0 if win_y.negative?
           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 magnify(factor)
   bigger = self.class.new(@width * factor, @height * factor)
   each_pixel do |x, y|
     colour = self[x, y]
     (x * factor..x * factor + factor - 1).each do |xx|
       (y * factor..y * factor + factor - 1).each do |yy|
         bigger[xx, yy] = colour
       end
     end
   end
   bigger
 end
 ###############################################
 def histogram
   histogram = Hash.new(0)
   each_pixel do |x, y|
     histogram[self[x, y].luminosity] += 1
   end
   histogram
 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) && (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.negative?
     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 enqueue push
 alias dequeue shift

end

class Numeric

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

end

class Integer

 def choose(k)
   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>