⚠️ Warning: This is a draft ⚠️

This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.

If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.

[[Category:Ruby]] ==The Code== Collecting all the Ruby code from [[:Category:Raster graphics operations]], so one can invoke: require 'raster_graphics'

Uses the [https://github.com/wvanbergen/chunky_png ChunkyPNG] pure-Ruby PNG library.

###########################################################################
# Represents an RGB[http://en.wikipedia.org/wiki/Rgb] 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

###########################################################################
# 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, :[]=

  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 = ["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)
    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_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

  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 {|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)
    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)
    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)
    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, 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 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 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


###########################################################################
# 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

==A Test Suite==

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

==An Image Viewer== {{libheader|Ruby/Tk}}

The ppmview.rb program is:

#!/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