Class: ZPNG::CLI

Inherits:
Object
  • Object
show all
Includes:
Hexdump
Defined in:
lib/zpng/cli.rb

Constant Summary collapse

DEFAULT_ACTIONS =
%w'info metadata chunks'

Instance Method Summary collapse

Methods included from Hexdump

dump, #hexdump

Constructor Details

#initialize(argv = ARGV) ⇒ CLI

Returns a new instance of CLI.



11
12
13
14
15
16
17
18
19
20
21
# File 'lib/zpng/cli.rb', line 11

def initialize argv = ARGV
  # hack #1: allow --chunk as well as --chunks
  @argv = argv.map{ |x| x.sub(/^--chunks?/,'--chunk(s)') }

  # 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
end

Instance Method Details

#_conditional_hexdump(data, v2 = 2) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/zpng/cli.rb', line 188

def _conditional_hexdump data, v2 = 2
  return unless data

  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

  elsif @options[:verbose] >= v2
    print Hexdump.dump(data){ |row| row.insert(0,"    ") }.gray
    puts
  end
end

#ansiObject



245
246
247
248
249
250
251
252
253
# File 'lib/zpng/cli.rb', line 245

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
end

#ansi256Object



255
256
257
258
259
260
261
262
263
264
# File 'lib/zpng/cli.rb', line 255

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

#asciiObject



234
235
236
237
238
239
240
241
242
243
# File 'lib/zpng/cli.rb', line 234

def ascii
  @img.height.times do |y|
    @img.width.times do |x|
      c = @img[x,y].to_ascii *[@options[:ascii_string]].compact
      c *= 2 if @options[:wide]
      print c
    end
    puts
  end
end

#chunks(idx = nil) ⇒ Object



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/zpng/cli.rb', line 207

def chunks idx=nil
  max_type_len = 0
  unless idx
    max_type_len = @img.chunks.map{ |x| x.type.to_s.size }.max
  end

  @img.chunks.each do |chunk|
    next if idx && chunk.idx != idx
    colored_type = chunk.type.ljust(max_type_len).magenta
    colored_crc =
      if chunk.crc == :no_crc # hack for BMP chunks (they have no CRC)
        ''
      elsif chunk.crc_ok?
        'CRC OK'.green
      else
        'CRC ERROR'.red
      end
    puts "[.] #{chunk.inspect(@options[:verbose]).sub(chunk.type, colored_type)} #{colored_crc}"

    if @options[:verbose] >= 3
      _conditional_hexdump(chunk.export(fix_crc: false))
    else
      _conditional_hexdump(chunk.data)
    end
  end
end

#colorsObject



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/zpng/cli.rb', line 297

def colors
  h=Hash.new(0)
  h2=Hash.new{ |k,v| k[v] = [] }
  @img.each_pixel do |c,x,y|
    h[c] += 1
    if h[c] < 6
      h2[c] << [x,y]
    end
  end

  xlen = @img.width.to_s.size
  ylen = @img.height.to_s.size

  h.sort_by{ |c,n| [n] + h2[c].first.reverse }.each do |c,n|
    printf "%6d : %s : ", n, c.inspect
    h2[c].each_with_index do |a,idx|
      print ";" if idx > 0
      if idx >= 4
        print " ..."
        break
      end
      printf " %*d,%*d", xlen, a[0], ylen, a[1]
    end
    puts
  end
end

#consoleObject



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/zpng/cli.rb', line 324

def console
  ARGV.clear # clear ARGV so IRB is not confused
  require 'irb'
  m0 = IRB.method(:setup)
  img = @img

  # override IRB.setup, called from IRB.start
  IRB.define_singleton_method :setup do |*args|
    m0.call *args
    conf[:IRB_RC] = Proc.new do |context|
      context.main.instance_variable_set '@img', img
      context.main.define_singleton_method(:img){ @img }
    end
  end

  puts "[.] img = ZPNG::Image.load(#{@fname.inspect})".gray
  IRB.start
end

#crop(geometry) ⇒ Object



135
136
137
138
139
140
141
142
# File 'lib/zpng/cli.rb', line 135

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
  @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

#extract_chunk(id) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/zpng/cli.rb', line 118

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
end

#infoObject



178
179
180
181
182
183
184
185
186
# File 'lib/zpng/cli.rb', line 178

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) if @options[:verbose] > 0
end

#load_file(fname) ⇒ Object



148
149
150
# File 'lib/zpng/cli.rb', line 148

def load_file fname
  @img = Image.load fname, :verbose => @options[:verbose]+1
end

#metadataObject



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/zpng/cli.rb', line 152

def 
  return if @img..empty?
  puts "[.] metadata:"
  @img..each do |k,v,h|
    if @options[:verbose] < 2
      if k.size > 512
        puts "[?] key too long (#{k.size}), truncated to 512 chars".yellow
        k = k[0,512] + "..."
      end
      if v.size > 512
        puts "[?] value too long (#{v.size}), truncated to 512 chars".yellow
        v = v[0,512] + "..."
      end
    end
    if h.keys.sort == [:keyword, :text]
      v.gsub!(/[\n\r]+/, "\n"+" "*19)
      printf "    %-12s : %s\n", k, v.gray
    else
      printf "    %s (%s: %s):", k, h[:language], h[:translated_keyword]
      v.gsub!(/[\n\r]+/, "\n"+" "*19)
      printf "\n%s%s\n", " "*19, v.gray
    end
  end
  puts
end

#paletteObject



282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/zpng/cli.rb', line 282

def palette
  if @img.palette
    pp @img.palette
    if @img.format == :bmp
      hexdump(@img.palette.data, :width => 4, :show_offset => false) do |row, offset|
        row.insert(0,"  color %4s:  " % "##{(offset/4)}")
      end
    else
      hexdump(@img.palette.data, :width => 3, :show_offset => false) do |row, offset|
        row.insert(0,"  color %4s:  " % "##{(offset/3)}")
      end
    end
  end
end

#rebuild(fname) ⇒ Object



144
145
146
# File 'lib/zpng/cli.rb', line 144

def rebuild fname
  File.binwrite(fname, @img.export)
end

#runObject



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/zpng/cli.rb', line 23

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(      "--colors", "Show colors used"){ @actions << :colors }

    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.on "-R", "--rebuild NEW_FILENAME", "rebuild image, useful in restoring borked images" do |x|
      @actions << [:rebuild, 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 '--ascii-string STRING', 'Use specific string to map pixels to ASCII characters' do |x|
      @options[:ascii_string] = x
      @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
    opts.on "-I", "--console", "opens IRB console with specified image loaded" do |v|
      @actions << :console
    end
  end

  if (argv = optparser.parse(@argv)).empty?
    puts optparser.help
    return
  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)
    end
    @fname = 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

#scanlinesObject



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/zpng/cli.rb', line 266

def scanlines
  @img.scanlines.each do |sl|
    p sl
    case @options[:verbose]
    when 1
      hexdump(sl.raw_data) if sl.raw_data
    when 2
      hexdump(sl.decoded_bytes)
    when 3..999
      hexdump(sl.raw_data) if sl.raw_data
      hexdump(sl.decoded_bytes)
      puts
    end
  end
end

#unpack_imagedataObject



131
132
133
# File 'lib/zpng/cli.rb', line 131

def unpack_imagedata
  print @img.imagedata
end