lib/zpng/cli.rb in zpng-0.2.0 vs lib/zpng/cli.rb in zpng-0.2.1

- old
+ new

@@ -1,193 +1,248 @@ require 'zpng' require 'optparse' -require 'hexdump' require 'pp' -class ZPNG::CLI +module ZPNG + class CLI + include Hexdump - ACTIONS = { - 'chunks' => 'Show file chunks (default)', - %w'i info' => 'General image info (default)', - 'scanlines' => 'Show scanlines info', - 'palette' => 'Show palette' - } - DEFAULT_ACTIONS = %w'info chunks' + DEFAULT_ACTIONS = %w'info metadata chunks' - def initialize argv = ARGV - @argv = argv - end + def initialize argv = ARGV + # hack #1: allow --chunk as well as --chunks + @argv = argv.map{ |x| x.sub(/^--chunks?/,'--chunk(s)') } - def run - @actions = [] - @options = { :verbose => 0 } - optparser = OptionParser.new do |opts| - opts.banner = "Usage: zpng [options] filename.png" - - opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v| - @options[:verbose] += 1 + # hack #2: allow --chunk(s) followed by a non-number, like "zpng --chunks fname.png" + @argv.each_cons(2) do |a,b| + if a == "--chunk(s)" && b !~ /^\d+$/ + a<<"=-1" + end end - opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v| - @options[:verbose] -= 1 - end + end - ACTIONS.each do |t,desc| - if t.is_a?(Array) - opts.on *[ "-#{t[0]}", "--#{t[1]}", desc, eval("lambda{ |_| @actions << :#{t[1]} }") ] - else - opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ] + def run + @actions = [] + @options = { :verbose => 0 } + optparser = OptionParser.new do |opts| + opts.banner = "Usage: zpng [options] filename.png" + opts.separator "" + + opts.on("-i", "--info", "General image info (default)"){ @actions << :info } + opts.on("-c", "--chunk(s) [ID]", Integer, "Show chunks (default) or single chunk by its #") do |id| + id = nil if id == -1 + @actions << [:chunks, id] end + opts.on("-m", "--metadata", "Show image metadata, if any (default)"){ @actions << :metadata } + + opts.separator "" + opts.on("-S", "--scanlines", "Show scanlines info"){ @actions << :scanlines } + opts.on("-P", "--palette", "Show palette"){ @actions << :palette } + + opts.on "-E", "--extract-chunk ID", Integer, "extract a single chunk" do |id| + @actions << [:extract_chunk, id] + end + opts.on "-D", "--imagedata", "dump unpacked Image Data (IDAT) chunk(s) to stdout" do + @actions << :unpack_imagedata + end + + opts.separator "" + opts.on "-C", "--crop GEOMETRY", "crop image, {WIDTH}x{HEIGHT}+{X}+{Y},", + "puts results on stdout unless --ascii given" do |x| + @actions << [:crop, x] + end + + opts.separator "" + opts.on "-A", '--ascii', 'Try to convert image to ASCII (works best with monochrome images)' do + @actions << :ascii + end + opts.on "-N", '--ansi', 'Try to display image as ANSI colored text' do + @actions << :ansi + end + opts.on "-2", '--256', 'Try to display image as 256-colored text' do + @actions << :ansi256 + end + opts.on "-W", '--wide', 'Use 2 horizontal characters per one pixel' do + @options[:wide] = true + end + + opts.separator "" + opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v| + @options[:verbose] += 1 + end + opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v| + @options[:verbose] -= 1 + end end - opts.on "-E", "--extract-chunk ID", "extract a single chunk" do |id| - @actions << [:extract_chunk, id.to_i] + if (argv = optparser.parse(@argv)).empty? + puts optparser.help + return end - opts.on "-D", "--imagedata", "dump unpacked Image Data (IDAT) chunk(s) to stdout" do - @actions << :unpack_imagedata - end - opts.separator "" - opts.on "-c", "--crop GEOMETRY", "crop image, {WIDTH}x{HEIGHT}+{X}+{Y},", - "puts results on stdout unless --ascii given" do |x| - @actions << [:crop, x] + @actions = DEFAULT_ACTIONS if @actions.empty? + + argv.each_with_index do |fname,idx| + if argv.size > 1 && @options[:verbose] >= 0 + puts if idx > 0 + puts "[.] #{fname}".color(:green) + end + @file_idx = idx + @file_name = fname + + @zpng = load_file fname + + @actions.each do |action| + if action.is_a?(Array) + self.send(*action) if self.respond_to?(action.first) + else + self.send(action) if self.respond_to?(action) + end + end end + rescue Errno::EPIPE + # output interrupt, f.ex. when piping output to a 'head' command + # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message + end - opts.separator "" - opts.on "-A", '--ascii', 'Try to convert image to ASCII (works best with monochrome images)' do - @actions << :ascii + def extract_chunk id + @img.chunks.each do |chunk| + if chunk.idx == id + case chunk + when Chunk::ZTXT + print chunk.text + else + print chunk.data + end + end end - opts.on "-N", '--ansi', 'Try to display image as ANSI colored text' do - @actions << :ansi - end - opts.on "-2", '--256', 'Try to display image as 256-colored text' do - @actions << :ansi256 - end - opts.on "-W", '--wide', 'Use 2 horizontal characters per one pixel' do - @options[:wide] = true - end end - if (argv = optparser.parse(@argv)).empty? - puts optparser.help - return + def unpack_imagedata + print @img.imagedata end - @actions = DEFAULT_ACTIONS if @actions.empty? - - argv.each_with_index do |fname,idx| - if argv.size > 1 && @options[:verbose] >= 0 - puts if idx > 0 - puts "[.] #{fname}".color(:green) + def crop geometry + unless geometry =~ /\A(\d+)x(\d+)\+(\d+)\+(\d+)\Z/i + STDERR.puts "[!] invalid geometry #{geometry.inspect}, must be WxH+X+Y, like 100x100+10+10" + exit 1 end - @file_idx = idx - @file_name = fname + @img.crop! :width => $1.to_i, :height => $2.to_i, :x => $3.to_i, :y => $4.to_i + print @img.export unless @actions.include?(:ascii) + end - @zpng = load_file fname + def load_file fname + @img = Image.load fname + end - @actions.each do |action| - if action.is_a?(Array) - self.send(*action) if self.respond_to?(action.first) + def metadata + return if @img.metadata.empty? + puts "[.] metadata:" + @img.metadata.each do |k,v,h| + if h.keys.sort == [:keyword, :text] + v.gsub!(/[\n\r]+/, "\n"+" "*18) + printf " %-11s : %s\n", k, v.gray else - self.send(action) if self.respond_to?(action) + printf " %s (%s: %s):", k, h[:language], h[:translated_keyword] + v.gsub!(/[\n\r]+/, "\n"+" "*18) + printf "\n%s%s\n", " "*18, v.gray end end + puts end - rescue Errno::EPIPE - # output interrupt, f.ex. when piping output to a 'head' command - # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message - end - def extract_chunk id - @img.chunks.each do |chunk| - if chunk.idx == id - case chunk - when ZPNG::Chunk::ZTXT - print chunk.text - else - print chunk.data - end + def info + color = %w'COLOR_GRAYSCALE COLOR_RGB COLOR_INDEXED COLOR_GRAY_ALPHA COLOR_RGBA'.find do |k| + @img.hdr.color == ZPNG.const_get(k) end + puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}, #{@img.bpp}bpp, #{color}" + puts "[.] palette = #{@img.palette}" if @img.palette + puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes" + _conditional_hexdump @img.imagedata, 3 end - end - def unpack_imagedata - print @img.imagedata - end + def _conditional_hexdump data, v2 = 2 + if @options[:verbose] <= 0 + # do nothing + elsif @options[:verbose] < v2 + sz = 0x20 + print Hexdump.dump(data[0,sz], + :show_offset => false, + :tail => data.size > sz ? " + #{data.size-sz} bytes\n" : "\n" + ){ |row| row.insert(0," ") }.gray + puts - def crop geometry - unless geometry =~ /\A(\d+)x(\d+)\+(\d+)\+(\d+)\Z/i - STDERR.puts "[!] invalid geometry #{geometry.inspect}, must be WxH+X+Y, like 100x100+10+10" - exit 1 + elsif @options[:verbose] >= v2 + print Hexdump.dump(data){ |row| row.insert(0," ") }.gray + puts + end end - @img.crop! :width => $1.to_i, :height => $2.to_i, :x => $3.to_i, :y => $4.to_i - print @img.export unless @actions.include?(:ascii) - end - def load_file fname - @img = ZPNG::Image.new fname - end + def chunks idx=nil + @img.chunks.each do |chunk| + next if idx && chunk.idx != idx + colored_type = chunk.type.magenta + colored_crc = chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red + puts "[.] #{chunk.inspect(@options[:verbose]).sub(chunk.type, colored_type)} #{colored_crc}" - def info - puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}, bpp=#{@img.bpp}" - puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes" - puts "[.] palette = #{@img.palette}" if @img.palette - end - - def chunks - @img.dump - end - - def ascii - @img.height.times do |y| - @img.width.times do |x| - c = @img[x,y].to_ascii - c *= 2 if @options[:wide] - print c + _conditional_hexdump(chunk.data) unless chunk.size == 0 end - puts end - end - def ansi - spc = @options[:wide] ? " " : " " - @img.height.times do |y| - @img.width.times do |x| - print spc.background(@img[x,y].to_ansi) + def ascii + @img.height.times do |y| + @img.width.times do |x| + c = @img[x,y].to_ascii + c *= 2 if @options[:wide] + print c + end + puts end - puts end - end - def ansi256 - require 'rainbow' - spc = @options[:wide] ? " " : " " - @img.height.times do |y| - @img.width.times do |x| - print spc.background(@img[x,y].to_html) + def ansi + spc = @options[:wide] ? " " : " " + @img.height.times do |y| + @img.width.times do |x| + print spc.background(@img[x,y].to_ansi) + end + puts end - puts end - end - def scanlines - @img.scanlines.each do |sl| - p sl - case @options[:verbose] - when 1 - Hexdump.dump(sl.raw_data) - when 2 - Hexdump.dump(sl.decoded_bytes) - when 3..999 - Hexdump.dump(sl.raw_data) - Hexdump.dump(sl.decoded_bytes) + def ansi256 + require 'rainbow' + spc = @options[:wide] ? " " : " " + @img.height.times do |y| + @img.width.times do |x| + print spc.background(@img[x,y].to_html) + end puts end end - end - def palette - if @img.palette - pp @img.palette - Hexdump.dump @img.palette.data, :width => 6*3 + def scanlines + @img.scanlines.each do |sl| + p sl + case @options[:verbose] + when 1 + hexdump(sl.raw_data) + when 2 + hexdump(sl.decoded_bytes) + when 3..999 + hexdump(sl.raw_data) + hexdump(sl.decoded_bytes) + puts + end + end + end + + def palette + if @img.palette + pp @img.palette + hexdump(@img.palette.data, :width => 3, :show_offset => false) do |row, offset| + row.insert(0," color %4s: " % "##{(offset/3)}") + end + end end end end