class PixelRaster::MagickConverter

Attributes

bg_color[R]
light2dark[R]
nr_of_colors[R]
resize[R]
type[R]

Public Class Methods

command_line(argv=ARGV, stdout=$stdout, stderr=$stderr) click to toggle source

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
new(nr_of_colors: nil, bg_color: 'white', resize: nil, light2dark: nil, type: :svg) { |self| ... } click to toggle source

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_colormap(img, light2dark: @light2dark) click to toggle source

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
convert(filename, mode:, type: @type) click to toggle source

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
image2svg(img, mode: :empty) click to toggle source

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
image2tikz(img, prefix: "n") click to toggle source

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
read_image(filename) click to toggle source

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