class PixelRaster::MagickConverter
Attributes
Public Class Methods
Parse command line arguments (ARGV
) and execute accordingly
See +img2pixel -h+ for a list of options.
# File lib/img2pixel.rb, line 14 def self.command_line(argv=ARGV, stdout=$stdout, stderr=$stderr) outfile = nil outdir = nil outmode = :both empty_suffix = '-empty' filled_suffix = '-filled' params = { type: :svg, bg_color: 'white' } if argv.length == 0 # no argument given argv << '-h' # give the help text end begin OptionParser.new do |p| p.accept(Symbol) { |s| s.downcase.to_sym } p.banner = "Convert image(s) to a pixel representation." p.separator "" p.separator "Usage: #{$0} [options] INFILE [INFILE…]" p.separator "" p.separator "For each INFILE two output files are written for the empty and filled version" p.separator "(but see --mode)." p.separator "" p.separator "With “-O OUTFILE” all images are output to OUTFILE." p.separator "Caveat: having several <svg>-elements concatenated in one file" p.separator "may not be supported by your viewer." p.separator "" p.separator "Options:" p.on_tail("-h", "--help", "Show this help message and exit.") do stdout.puts p exit end p.on("-V", "--version", "Show version information and exit.") do stdout.puts File.read(File.join(File.dirname(__FILE__),'..','VERSION')) exit end p.on('-O', '--output FILENAME', 'Write output to FILENAME (“-” for STDOUT).', 'If this option is given, all images', 'are written to the same file.', 'If this is given, “-d” is ignored.') do |filename| outfile = filename end p.on("-#", "--nr-of-colors N", Numeric, "Number of colors for the final image.") do |n| params[:nr_of_colors] = n end p.on('-c', '--bg-color COLOR', "Background-color for empty version.", "SVG color name. May be “none”.", "Ignored for --type tikz.", "Default: #{params[:bg_color]}") do |color| params[:bg_color] = color end p.on('-t', '--type TYPE', Symbol, 'The output type (svg or tikz).', "Default: #{params[:type]}") do |type| [:svg,:tikz].include?(type) or p.abort("unknown output type #{type}") params[:type] = type end p.on('-m', '--mode MODE', Symbol, 'Output mode (both, empty, filled).', "Default: #{outmode}") do |mode| [:both,:empty,:filled].include?(mode) or p.abort("unknown mode #{mode}") outmode = mode end p.on('-r', '--resize [SPEC]', 'Resize image to given dimensions.', 'See ImageMagick resize specification.') do |spec| params[:resize] = spec end p.on('-l','--light2dark', 'Color numbering direction.', 'Default: dark2light (unless 2 color imgs)') do params[:light2dark] = true end p.on('-L','--dark2light', 'Color numbering direction.', 'Default: dark2light, (unless 2 color imgs)') do params[:light2dark] = false end p.on('-d', '--dir DIR', 'Directory for output files.', 'Must exist and be writeable.', "Default: same as INFILE") do |dir| outdir = dir end end.parse!(argv) rescue OptionParser::ParseError => pe stderr.puts pe exit 22 # Errno::EINVAL::Errno (but may not exist on all platforms) end MagickConverter.new(**params) do |c| if outfile == '-' # Write everything to standard out argv.each do |infile| stdout << c.convert(infile, mode: :empty) unless outmode==:filled stdout << c.convert(infile, mode: :filled) unless outmode==:empty end elsif outfile # an outfile is given, so write everything in this file File.open(outfile, 'w') do |out| argv.each do |infile| out << c.convert(infile, mode: :empty) out << c.convert(infile, mode: :filled) end end else argv.each do |infile| # if no outfile is given we generate the filename from the infile base_out = File.join(outdir || File.dirname(infile), File.basename(infile, '.*')) empty_out = base_out + empty_suffix + '.' + params[:type].to_s filled_out = base_out + filled_suffix + '.' + params[:type].to_s unless outmode==:filled File.open(empty_out,"w") do |out| out << c.convert(infile, mode: :empty) end end unless outmode==:empty File.open(filled_out,"w") do |out| out << c.convert(infile, mode: :filled) end end end end end end
The converter is initialized with the constraints for the images to be processed.
Any image must have a color index (palette), this is enforced. It is not that useful to have more than 10 colors or more than 30x30 pixels, but this is only enforced if the corresponding parameters are given. @see read_image
@param nr_of_colors
maximum number of colors for the color index @param bg_color
background color for empty pixel image (svg) @param resize [String] resize string (see ImageMagick resize option) @param light2dark [Boolean] if true 0 is the lightest color @param type [:svg|:tikz]
# File lib/pixel_raster.rb, line 25 def initialize(nr_of_colors: nil, bg_color: 'white', resize: nil, light2dark: nil, type: :svg) @nr_of_colors = nr_of_colors @bg_color = bg_color @resize = resize @light2dark = light2dark @type = type yield(self) if block_given? end
Public Instance Methods
compute an ordered colormap for the image @param img [Magick::Image] the image @param light2dark [Boolean] whether to invert the color map
# File lib/pixel_raster.rb, line 164 def compute_colormap(img, light2dark: @light2dark) # we create a colormap sorted from dark to light colormap= {}; colors = img.color_histogram.map(&:first).sort_by(&:intensity) # at least for a black and white image having 0 as white and # 1 as black is more intuitive as you paint the pixels in black # which are set. This is different from the common internal # representation of images! # We therefore assume light2dark _unless_ it's a 2-color image # if it is not explicitely set: light2dark = colors.length == 2 if light2dark.nil? colors.reverse! if light2dark colors.each_with_index do |p,i| colormap[p]=i end return colormap end
Wrapper for the converter methods.
@param mode [:empty|:filled] the output mode @param type [:svg|:tikz] the output type @see image2tikz
@see image2svg
# File lib/pixel_raster.rb, line 78 def convert(filename, mode:, type: @type) img = read_image(filename) case type when :tikz image2tikz(img, prefix: mode==:empty ? 'n' : 'y') when :svg image2svg(img, mode: mode) else raise "unknown output type #{type}" end end
create a svg pixel representation of an image.
@param img [Magick::Image] the image @param mode [:empty|:filled] fill mode
# File lib/pixel_raster.rb, line 117 def image2svg(img, mode: :empty) colormap = compute_colormap(img) # ToDo: make these flexible: xw = 20 yh = 20 xwt = 10 yht = 15 # We create the SVG directly by string svg = %Q'<svg xmlns="http://www.w3.org/2000/svg"> <style> .pixel { stroke : black; stroke-width : 1; ' if mode == :empty svg << " fill: #{@bg_color}; " end svg << ' } ' if mode == :filled colormap.each do |k,v| color = k.to_color(Magick::AllCompliance,false, 8, true) textcolor = k.to_hsla[2] > 50 ? 'black' : 'white' svg << %Q{ .p#{v} { fill: #{color}; }\n} svg << %Q{ .t#{v} { fill: #{textcolor}; }\n} end end svg << %Q{ </style>\n} img.each_pixel do |p, c, r| cc = colormap[p] svg << %Q{ <rect x="#{c*xw}" y="#{r*yh}" width="#{xw}" height="#{yh}" class="pixel p#{cc}" />\n} svg << %Q{ <text x="#{c*xw+xwt}" y="#{r*yh+yht}" text-anchor="middle" class="t#{cc}">#{cc}</text>\n} end svg << "</svg>\n" svg end
create a tikz pixel representation of an image.
@param img [Magick::Image] the image @param prefix [String] node class prefix (to distinguish empty and filled mode)
# File lib/pixel_raster.rb, line 95 def image2tikz(img, prefix: "n") colormap = compute_colormap(img) tikz = " \\begin{tikzpicture}[yscale=-1] \\draw[step=1,help lines] (0,0) grid (#{img.columns},#{img.rows}); " img.each_pixel do |p, c, r| cc = colormap[p] tikz << " \\node[#{prefix}#{cc}] at (#{c+0.5},#{r+0.5}) {#{cc}};\n" end tikz << " \\end{tikzpicture} " tikz end
Reads an image using RMagick and adjusts size and palette. As we need an indexed image we check and create one if neccessary.
@see initialize @param filename read this file @return [Magick::Image] the (processed) image
# File lib/pixel_raster.rb, line 42 def read_image(filename) # We assume each file includes exactly one image image = Magick::Image.read(filename).first # on demand we resize the image # the API is kinda complicated here: # change_geometry computes the cols and rows from the given resize string # and yields the given block with these values if @resize image.change_geometry(@resize) do |cols, rows, i| image = i.resize!(cols, rows) end end # on demand we reduce the number of colors if @nr_of_colors image = image.quantize(@nr_of_colors) else # If the image does not have a palette/color index (and no number of # columns was given) we create one with 2 colors. unless image.palette? image = image.quantize(2) end end # finally we remove duplicate colors. image.compress_colormap! return image end