Converting ASCII to an image

Posted on

Problem

So after I finished my fractal generator, I decided that text is ugly, at least compared to pictures, so I made a text-to-picture converter:

require 'chunky_png'
require 'etc'
require 'pathname'

class Array
  def chunk(chunk_size)
    self.each_with_index.with_object(Array.new(size / chunk_size) { [] }) do |(elt, ind), result|
      result[ind / chunk_size] << elt
    end
  end
end

raise ArgumentError, 'Wrong argument number. Run with -h to see usage.' unless ARGV.length % 2 == 1

filename = ARGV.pop

colors = Hash[
    ARGV.chunk(2).map do |(char, color)|
      color = ChunkyPNG::Color.from_hex(color)
      [char, color]
    end
]

text = []
text << $_.chomp.chars until $stdin.gets.nil?
row_length = text[0].length
raise ArgumentError, 'Input cannot be jagged' unless text.all? { |row| row.length == row_length }

image = ChunkyPNG::Image.new(row_length, text.length, ChunkyPNG::Color::WHITE)
text.each.with_index do |row, y|
  row.each_with_index do |char, x|
    raise ArgumentError, "No valid color provided for `#{char}`" if colors[char].nil?
    image[x, y] = colors[char]
  end
end

image.save(filename)

puts "Image saved to #{File.join(Dir.getwd, filename)}"

Note that it uses the ChunkyPNG gem, which I wholeheartedly recommend for any of your pixel-by-pixel image generation needs.

Since this script takes the input data via standard input, it’s meant to be used in a command like this:

ruby build_fractal.rb <fractal options> | ruby to_image.rb "X" 000 " " FFF fractal.png

which makes pretties like these.

I’m looking for advice specifically on:

  1. Efficiency: I want to spend as little time as possible drawing the image, so I can spend more time on generating the fractal.

That’s it. Since this is purely a utility script that, as far as I can tell, is complete, there’s no reason to worry about maintainability or code prettiness. I’m willing to sacrifice everything else to get every last bit of speed out of this.

Solution

class Array
  def chunk(chunk_size)
    self.each_with_index.with_object(Array.new(size / chunk_size) { [] }) do |(elt, ind), result|
      result[ind / chunk_size] << elt
    end
  end
end

You reinvented Enumerable#each_slice! It returns an enumerator, which is fine for your code. Simply write ARGV.each_slice(2).map do |char, color| instead of ARGV.chunk(2).map.

ARGV.length % 2 == 1

Change to ARGV.length.odd?.

'Run with -h to see usage.'

Doesn’t work.

colors = Hash[
    ARGV.each_slice(2).map do |char, color|
      color = ChunkyPNG::Color.from_hex(color)
      [char, color]
    end
]

I’d use to_h instead of Hash[] and inline color, to make the code shorter:

colors = ARGV.each_slice(2).map do |char, color|
  [char, ChunkyPNG::Color.from_hex(color)]
end.to_h
text << $_.chomp.chars until $stdin.gets.nil?

Instead of creating many arrays of characters, simply store the strings. Remove the call to .chars and iterate over a string using row.each_char.with_index. This makes the code a little faster.

raise ArgumentError, "No valid color provided for `#{char}`" if colors[char].nil?
image[x, y] = colors[char]

ChuckyPNG already throws an ArgumentError if the color is nil. You can remove your check for this, which may improve performance. (If you need to catch this error, you can wrap the loop with a begin-rescue block)

Leave a Reply

Your email address will not be published. Required fields are marked *