Converting ASCII to an image

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.

• Possibly related: Generate fractals from bit patterns in ASCII – Ismael Miguel Aug 13 '15 at 8:49
• @IsmaelMiguel Not particularly. This is about converting a block of ASCII directly to an image; it doesn't have to be fractals. That's more relevant to my fractal generator question. – Nic Hartley Aug 13 '15 at 11:02
• Hence the "possibly". You want me to delete it and add the link on the other question (whatever it is)? – Ismael Miguel Aug 13 '15 at 11:08

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)