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:
- 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)