zpng 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -10,4 +10,5 @@ group :development do
10
10
  gem "rspec", ">= 2.8.0"
11
11
  gem "bundler", ">= 1.0.0"
12
12
  gem "jeweler", "~> 1.8.4"
13
+ gem "what_methods"
13
14
  end
@@ -10,7 +10,7 @@ GEM
10
10
  rdoc
11
11
  json (1.7.5)
12
12
  rainbow (1.1.4)
13
- rake (10.0.2)
13
+ rake (10.0.3)
14
14
  rdoc (3.12)
15
15
  json (~> 1.4)
16
16
  rspec (2.12.0)
@@ -21,6 +21,7 @@ GEM
21
21
  rspec-expectations (2.12.0)
22
22
  diff-lcs (~> 1.1.3)
23
23
  rspec-mocks (2.12.0)
24
+ what_methods (1.0.1)
24
25
 
25
26
  PLATFORMS
26
27
  ruby
@@ -30,3 +31,4 @@ DEPENDENCIES
30
31
  jeweler (~> 1.8.4)
31
32
  rainbow
32
33
  rspec (>= 2.8.0)
34
+ what_methods
data/README.md CHANGED
@@ -16,7 +16,6 @@ Comparison
16
16
  ----------
17
17
  * supports `iTXt` (international text) chunks
18
18
  * full support of 16-bit color & alpha depth
19
- * correct 4bpp image decoding (as of 29-Dec-2012, ChunkyPNG had 1-bit error in 4bpp image decoding)
20
19
 
21
20
  Usage
22
21
  -----
@@ -31,6 +30,7 @@ Usage
31
30
 
32
31
  -S, --scanlines Show scanlines info
33
32
  -P, --palette Show palette
33
+ --colors Show colors used
34
34
  -E, --extract-chunk ID extract a single chunk
35
35
  -D, --imagedata dump unpacked Image Data (IDAT) chunk(s) to stdout
36
36
 
@@ -44,6 +44,7 @@ Usage
44
44
 
45
45
  -v, --verbose Run verbosely (can be used multiple times)
46
46
  -q, --quiet Silent any warnings (can be used multiple times)
47
+ -I, --console opens IRB console with specified image loaded
47
48
 
48
49
  ### Info
49
50
 
@@ -51,7 +52,7 @@ Usage
51
52
 
52
53
  [.] image size 35x35, 24bpp, COLOR_RGB
53
54
  [.] uncompressed imagedata size = 3710 bytes
54
- [.] <Chunk #00 IHDR size= 13, crc=91bb240e, width=35, color=2, interlace=0, depth=8, compression=0, height=35, filter=0> CRC OK
55
+ [.] <Chunk #00 IHDR size= 13, crc=91bb240e, color=2, compression=0, depth=8, filter=0, height=35, interlace=0, width=35> CRC OK
55
56
  [.] <Chunk #01 sRGB size= 1, crc=aece1ce9 > CRC OK
56
57
  [.] <Chunk #02 IDAT size= 399, crc=59790716 > CRC OK
57
58
  [.] <Chunk #03 IEND size= 0, crc=ae426082 > CRC OK
@@ -65,7 +66,7 @@ Usage
65
66
  01 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 |................|
66
67
  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 3678 bytes
67
68
 
68
- [.] <Chunk #00 IHDR size= 13, crc=91bb240e, width=35, idx=0, color=2, interlace=0, depth=8, compression=0, height=35, filter=0> CRC OK
69
+ [.] <Chunk #00 IHDR size= 13, crc=91bb240e, color=2, compression=0, depth=8, filter=0, height=35, idx=0, interlace=0, width=35> CRC OK
69
70
  00 00 00 23 00 00 00 23 08 02 00 00 00 |...#...#..... |
70
71
 
71
72
  [.] <Chunk #01 sRGB size= 1, crc=aece1ce9 > CRC OK
@@ -83,7 +84,7 @@ Usage
83
84
 
84
85
  # zpng --chunks qr_aux_chunks.png
85
86
 
86
- [.] <Chunk #00 IHDR size= 13, crc=36a28ef4, width=35, color=0, interlace=0, depth=1, compression=0, height=35, filter=0> CRC OK
87
+ [.] <Chunk #00 IHDR size= 13, crc=36a28ef4, color=0, compression=0, depth=1, filter=0, height=35, interlace=0, width=35> CRC OK
87
88
  [.] <Chunk #01 gAMA size= 4, crc=0bfc6105 > CRC OK
88
89
  [.] <Chunk #02 sRGB size= 1, crc=aece1ce9 > CRC OK
89
90
  [.] <Chunk #03 cHRM size= 32, crc=9cba513c > CRC OK
@@ -16,7 +16,6 @@ Comparison
16
16
  ----------
17
17
  * supports `iTXt` (international text) chunks
18
18
  * full support of 16-bit color & alpha depth
19
- * correct 4bpp image decoding (as of 29-Dec-2012, ChunkyPNG had 1-bit error in 4bpp image decoding)
20
19
 
21
20
  Usage
22
21
  -----
data/TODO CHANGED
@@ -1,3 +1,6 @@
1
+ [ ] benchmark accessing very last PNG scanline
2
+ [ ] use iostruct
3
+
1
4
  ways to hide info in PNG:
2
5
  * IHDR: longer than 13 bytes
3
6
  * IEND:
@@ -6,11 +9,13 @@ ways to hide info in PNG:
6
9
  * TEXT chunks
7
10
  * zTXT chunks
8
11
  * comp_method
12
+ * data after compressed data?
9
13
  * IDAT:
10
14
  * data after last scanline
11
15
  * last bits in scanline when bpp%8 != 0
12
16
  * PLTE:
13
17
  * raw letters
14
18
  * stegano
19
+ * many palette entries with same color => visually same pixels but, different color idx
15
20
  * custom chunks
16
21
  * crc
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1
1
+ 0.2.2
@@ -1,16 +1,26 @@
1
1
  require 'zlib'
2
2
  require 'stringio'
3
3
 
4
+ module ZPNG
5
+ class Exception < ::StandardError; end
6
+ class NotSupported < Exception; end
7
+ class ArgumentError < Exception; end
8
+ end
9
+
4
10
  require 'zpng/string_ext'
5
11
  require 'zpng/deep_copyable'
6
12
 
7
13
  require 'zpng/color'
8
14
  require 'zpng/block'
9
15
  require 'zpng/scan_line'
16
+ require 'zpng/scan_line/mixins'
10
17
  require 'zpng/chunk'
11
18
  require 'zpng/text_chunk'
12
- require 'zpng/image'
19
+ require 'zpng/readable_struct'
13
20
  require 'zpng/adam7_decoder'
14
21
  require 'zpng/hexdump'
15
22
  require 'zpng/metadata'
16
23
  require 'zpng/pixels'
24
+
25
+ require 'zpng/bmp/reader'
26
+ require 'zpng/image'
@@ -0,0 +1,105 @@
1
+ module ZPNG
2
+ module BMP
3
+ class BITMAPINFOHEADER < ReadableStruct.new 'V3v2V6',
4
+ :biSize, # BITMAPINFOHEADER::SIZE
5
+ :biWidth,
6
+ :biHeight,
7
+ :biPlanes,
8
+ :biBitCount,
9
+ :biCompression,
10
+ :biSizeImage,
11
+ :biXPelsPerMeter,
12
+ :biYPelsPerMeter,
13
+ :biClrUsed,
14
+ :biClrImportant
15
+
16
+ def inspect
17
+ "<" + super.partition(self.class.to_s.split('::').last)[1..-1].join
18
+ end
19
+ end
20
+
21
+ class BmpHdrPseudoChunk < Chunk::IHDR
22
+ def initialize bmp_hdr
23
+ @bmp_hdr = bmp_hdr
24
+ super(
25
+ :width => bmp_hdr.biWidth,
26
+ :height => bmp_hdr.biHeight.abs,
27
+ :bpp => bmp_hdr.biBitCount,
28
+ :type => 'BITMAPINFOHEADER',
29
+ :crc => :no_crc # for CLI
30
+ )
31
+ end
32
+ def inspect *args
33
+ @bmp_hdr.inspect
34
+ end
35
+ def method_missing mname, *args
36
+ if @bmp_hdr.respond_to?(mname)
37
+ @bmp_hdr.send(mname,*args)
38
+ else
39
+ super
40
+ end
41
+ end
42
+ end
43
+
44
+ class Color < ZPNG::Color
45
+ # BMP pixels are in perverted^w reverted order - BGR instead of RGB
46
+ def initialize *a
47
+ h = a.last.is_a?(Hash) ? a.pop : {}
48
+ case a.size
49
+ when 3
50
+ # BGR
51
+ super *a.reverse, h
52
+ when 4
53
+ # ABGR
54
+ super a[2], a[1], a[0], a[3], h
55
+ else
56
+ super
57
+ end
58
+ end
59
+ end
60
+
61
+ module ImageMixin
62
+ def imagedata
63
+ @imagedata ||= @scanlines.sort_by(&:offset).map(&:decoded_bytes).join
64
+ end
65
+ end
66
+
67
+ module Reader
68
+ # http://en.wikipedia.org/wiki/BMP_file_format
69
+
70
+ def _read_bmp io
71
+ filesize, reserved1, reserved2, imagedata_offset = io.read(4+2+2+4).unpack('VvvV')
72
+ # DIB Header immediately follows the Bitmap File Header
73
+ hdr = BITMAPINFOHEADER.read(io)
74
+ if hdr.biSize != BITMAPINFOHEADER::SIZE
75
+ raise "dib_hdr_size #{hdr.biSize} unsupported, want #{BITMAPINFOHEADER::SIZE}"
76
+ end
77
+
78
+ @new_image = true
79
+ @color_class = BMP::Color
80
+ @format = :bmp
81
+ @chunks << BmpHdrPseudoChunk.new(hdr)
82
+
83
+ # http://en.wikipedia.org/wiki/BMP_file_format#Pixel_storage
84
+ row_size = ((hdr.biBitCount*self.width+31)/32)*4
85
+ # XXX hidden data in non-significant tail bits/bytes
86
+
87
+ io.seek(imagedata_offset)
88
+
89
+ @scanlines = []
90
+ self.height.times do |idx|
91
+ offset = io.tell - imagedata_offset
92
+ data = io.read(row_size)
93
+ # BMP scanlines layout is upside-down
94
+ @scanlines.unshift ScanLine.new(self, self.height-idx-1,
95
+ :decoded_bytes => data,
96
+ :size => row_size,
97
+ :offset => offset
98
+ )
99
+ end
100
+
101
+ extend ImageMixin
102
+ end
103
+ end # Reader
104
+ end # BMP
105
+ end # ZPNG
@@ -45,7 +45,7 @@ module ZPNG
45
45
  def export
46
46
  @data = self.export_data # virtual
47
47
  @size = @data.size # XXX hmm.. is it always is?
48
- @crc = Zlib.crc32(data, Zlib.crc32(type))
48
+ fix_crc!
49
49
  [@size,@type].pack('Na4') + @data + [@crc].pack('N')
50
50
  end
51
51
 
@@ -66,6 +66,10 @@ module ZPNG
66
66
  expected_crc == crc
67
67
  end
68
68
 
69
+ def fix_crc!
70
+ @crc = Zlib.crc32(data, Zlib.crc32(type))
71
+ end
72
+
69
73
  class IHDR < Chunk
70
74
  attr_accessor :width, :height, :depth, :color, :compression, :filter, :interlace
71
75
 
@@ -184,7 +188,7 @@ module ZPNG
184
188
  vars = instance_variables - [:@type, :@crc, :@data, :@size]
185
189
  vars -= [:@idx] if verbosity <= 0
186
190
  super.sub(/ *>$/,'') + ", " +
187
- vars.map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
191
+ vars.sort.map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
188
192
  join(", ") + ">"
189
193
  end
190
194
  end
@@ -37,6 +37,7 @@ module ZPNG
37
37
  opts.separator ""
38
38
  opts.on("-S", "--scanlines", "Show scanlines info"){ @actions << :scanlines }
39
39
  opts.on("-P", "--palette", "Show palette"){ @actions << :palette }
40
+ opts.on( "--colors", "Show colors used"){ @actions << :colors }
40
41
 
41
42
  opts.on "-E", "--extract-chunk ID", Integer, "extract a single chunk" do |id|
42
43
  @actions << [:extract_chunk, id]
@@ -72,6 +73,9 @@ module ZPNG
72
73
  opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
73
74
  @options[:verbose] -= 1
74
75
  end
76
+ opts.on "-I", "--console", "opens IRB console with specified image loaded" do |v|
77
+ @actions << :console
78
+ end
75
79
  end
76
80
 
77
81
  if (argv = optparser.parse(@argv)).empty?
@@ -86,8 +90,7 @@ module ZPNG
86
90
  puts if idx > 0
87
91
  puts "[.] #{fname}".color(:green)
88
92
  end
89
- @file_idx = idx
90
- @file_name = fname
93
+ @fname = fname
91
94
 
92
95
  @zpng = load_file fname
93
96
 
@@ -131,20 +134,30 @@ module ZPNG
131
134
  end
132
135
 
133
136
  def load_file fname
134
- @img = Image.load fname
137
+ @img = Image.load fname, :verbose => true
135
138
  end
136
139
 
137
140
  def metadata
138
141
  return if @img.metadata.empty?
139
142
  puts "[.] metadata:"
140
143
  @img.metadata.each do |k,v,h|
144
+ if @options[:verbose] < 2
145
+ if k.size > 512
146
+ puts "[?] key too long (#{k.size}), truncated to 512 chars".yellow
147
+ k = k[0,512] + "..."
148
+ end
149
+ if v.size > 512
150
+ puts "[?] value too long (#{v.size}), truncated to 512 chars".yellow
151
+ v = v[0,512] + "..."
152
+ end
153
+ end
141
154
  if h.keys.sort == [:keyword, :text]
142
- v.gsub!(/[\n\r]+/, "\n"+" "*18)
143
- printf " %-11s : %s\n", k, v.gray
155
+ v.gsub!(/[\n\r]+/, "\n"+" "*19)
156
+ printf " %-12s : %s\n", k, v.gray
144
157
  else
145
158
  printf " %s (%s: %s):", k, h[:language], h[:translated_keyword]
146
- v.gsub!(/[\n\r]+/, "\n"+" "*18)
147
- printf "\n%s%s\n", " "*18, v.gray
159
+ v.gsub!(/[\n\r]+/, "\n"+" "*19)
160
+ printf "\n%s%s\n", " "*19, v.gray
148
161
  end
149
162
  end
150
163
  puts
@@ -156,8 +169,8 @@ module ZPNG
156
169
  end
157
170
  puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}, #{@img.bpp}bpp, #{color}"
158
171
  puts "[.] palette = #{@img.palette}" if @img.palette
159
- puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes"
160
- _conditional_hexdump @img.imagedata, 3
172
+ puts "[.] uncompressed imagedata size = #{@img.imagedata_size} bytes"
173
+ _conditional_hexdump(@img.imagedata, 3) if @options[:verbose] > 0
161
174
  end
162
175
 
163
176
  def _conditional_hexdump data, v2 = 2
@@ -181,7 +194,14 @@ module ZPNG
181
194
  @img.chunks.each do |chunk|
182
195
  next if idx && chunk.idx != idx
183
196
  colored_type = chunk.type.magenta
184
- colored_crc = chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red
197
+ colored_crc =
198
+ if chunk.crc == :no_crc # hack for BMP chunks (they have no CRC)
199
+ ''
200
+ elsif chunk.crc_ok?
201
+ 'CRC OK'.green
202
+ else
203
+ 'CRC ERROR'.red
204
+ end
185
205
  puts "[.] #{chunk.inspect(@options[:verbose]).sub(chunk.type, colored_type)} #{colored_crc}"
186
206
 
187
207
  _conditional_hexdump(chunk.data) unless chunk.size == 0
@@ -225,11 +245,11 @@ module ZPNG
225
245
  p sl
226
246
  case @options[:verbose]
227
247
  when 1
228
- hexdump(sl.raw_data)
248
+ hexdump(sl.raw_data) if sl.raw_data
229
249
  when 2
230
250
  hexdump(sl.decoded_bytes)
231
251
  when 3..999
232
- hexdump(sl.raw_data)
252
+ hexdump(sl.raw_data) if sl.raw_data
233
253
  hexdump(sl.decoded_bytes)
234
254
  puts
235
255
  end
@@ -244,5 +264,51 @@ module ZPNG
244
264
  end
245
265
  end
246
266
  end
267
+
268
+ def colors
269
+ h=Hash.new(0)
270
+ h2=Hash.new{ |k,v| k[v] = [] }
271
+ @img.each_pixel do |c,x,y|
272
+ h[c] += 1
273
+ if h[c] < 6
274
+ h2[c] << [x,y]
275
+ end
276
+ end
277
+
278
+ xlen = @img.width.to_s.size
279
+ ylen = @img.height.to_s.size
280
+
281
+ h.sort_by{ |c,n| [n] + h2[c].first.reverse }.each do |c,n|
282
+ printf "%6d : %s : ", n, c.inspect
283
+ h2[c].each_with_index do |a,idx|
284
+ print ";" if idx > 0
285
+ if idx >= 4
286
+ print " ..."
287
+ break
288
+ end
289
+ printf " %*d,%*d", xlen, a[0], ylen, a[1]
290
+ end
291
+ puts
292
+ end
293
+ end
294
+
295
+ def console
296
+ ARGV.clear # clear ARGV so IRB is not confused
297
+ require 'irb'
298
+ m0 = IRB.method(:setup)
299
+ img = @img
300
+
301
+ # override IRB.setup, called from IRB.start
302
+ IRB.define_singleton_method :setup do |*args|
303
+ m0.call *args
304
+ conf[:IRB_RC] = Proc.new do |context|
305
+ context.main.instance_variable_set '@img', img
306
+ context.main.define_singleton_method(:img){ @img }
307
+ end
308
+ end
309
+
310
+ puts "[.] img = ZPNG::Image.load(#{@fname.inspect})".gray
311
+ IRB.start
312
+ end
247
313
  end
248
314
  end
@@ -1,38 +1,71 @@
1
+ require 'stringio'
2
+
1
3
  module ZPNG
2
4
  class Image
3
- attr_accessor :data, :header, :chunks, :scanlines, :imagedata
4
- alias :hdr :header
5
+ attr_accessor :chunks, :scanlines, :imagedata, :extradata, :format, :verbose
6
+
7
+ # now only for (limited) BMP support
8
+ attr_accessor :color_class
5
9
 
6
10
  include DeepCopyable
11
+ include BMP::Reader
7
12
 
8
- PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
13
+ PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a".force_encoding('binary')
14
+ BMP_HDR = "BM".force_encoding('binary')
9
15
 
10
- def initialize x
16
+ # possible input params:
17
+ # IO of opened image file
18
+ # String with image file already readed
19
+ # Hash of image parameters to create new blank image
20
+ def initialize x, h={}
11
21
  @chunks = []
22
+ @color_class = Color
23
+ @format = :png
24
+ @verbose =
25
+ case h[:verbose]
26
+ when true; 1
27
+ when false; 0
28
+ else h[:verbose].to_i
29
+ end
30
+
12
31
  case x
13
32
  when IO
14
- _from_string x.read
33
+ _from_io x
15
34
  when String
16
- _from_string x
35
+ _from_io StringIO.new(x)
17
36
  when Hash
18
37
  _from_hash x
19
38
  else
20
- raise "unsupported input data type #{x.class}"
39
+ raise NotSupported, "unsupported input data type #{x.class}"
21
40
  end
22
41
  if palette && hdr && hdr.depth
23
42
  palette.max_colors = 2**hdr.depth
24
43
  end
25
44
  end
26
45
 
46
+ def inspect
47
+ "#<ZPNG::Image " +
48
+ %w'width height bpp chunks scanlines'.map do |k|
49
+ v = case (v = send(k))
50
+ when Array
51
+ "[#{v.size} entries]"
52
+ when String
53
+ v.size > 40 ? "[#{v.bytesize} bytes]" : v.inspect
54
+ else v.inspect
55
+ end
56
+ "#{k}=#{v}"
57
+ end.compact.join(", ") + ">"
58
+ end
59
+
27
60
  def adam7
28
61
  @adam7 ||= Adam7Decoder.new(self)
29
62
  end
30
63
 
31
64
  class << self
32
65
  # load image from file
33
- def load fname
66
+ def load fname, h={}
34
67
  open(fname,"rb") do |f|
35
- self.new(f)
68
+ self.new(f,h)
36
69
  end
37
70
  end
38
71
  alias :load_file :load
@@ -55,8 +88,8 @@ module ZPNG
55
88
 
56
89
  def _from_hash h
57
90
  @new_image = true
58
- @chunks << (@header = Chunk::IHDR.new(h))
59
- if @header.palette_used?
91
+ @chunks << Chunk::IHDR.new(h)
92
+ if header.palette_used?
60
93
  if h.key?(:palette)
61
94
  if h[:palette]
62
95
  @chunks << h[:palette]
@@ -71,41 +104,36 @@ module ZPNG
71
104
  end
72
105
  end
73
106
 
74
- def _from_string x
75
- if x
76
- if PNG_HDR.size.times.all?{ |i| x[i].ord == PNG_HDR[i].ord } # encoding error workaround
77
- # raw image data
78
- @data = x
79
- elsif File.exist?(x)
80
- # filename
81
- @data = File.binread(x)
82
- else
83
- raise "Don't know what #{x.inspect} is"
84
- end
85
- end
86
-
87
- d = data[0,PNG_HDR.size]
88
- if d != PNG_HDR
89
- puts "[!] first #{PNG_HDR.size} bytes must be #{PNG_HDR.inspect}, but got #{d.inspect}".red
90
- end
91
-
92
- io = StringIO.new(data)
93
- io.seek PNG_HDR.size
107
+ def _read_png io
94
108
  while !io.eof?
95
109
  chunk = Chunk.from_stream(io)
96
110
  chunk.idx = @chunks.size
97
111
  @chunks << chunk
98
- case chunk
99
- when Chunk::IHDR
100
- @header = chunk
101
- when Chunk::IEND
102
- break
112
+ break if chunk.is_a?(Chunk::IEND)
113
+ end
114
+ end
115
+
116
+ def _from_io io
117
+ # Puts ios into binary mode.
118
+ # Once a stream is in binary mode, it cannot be reset to nonbinary mode.
119
+ io.binmode
120
+
121
+ hdr = io.read(BMP_HDR.size)
122
+ if hdr == BMP_HDR
123
+ _read_bmp io
124
+ else
125
+ hdr << io.read(PNG_HDR.size - BMP_HDR.size)
126
+ if hdr == PNG_HDR
127
+ _read_png io
128
+ else
129
+ raise NotSupported, "Unsupported header #{hdr.inspect} in #{io.inspect}"
103
130
  end
104
131
  end
132
+
105
133
  unless io.eof?
106
- offset = io.tell
107
- extradata = io.read
108
- puts "[?] #{extradata.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red
134
+ offset = io.tell
135
+ @extradata = io.read
136
+ puts "[?] #{@extradata.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red if @verbose >= 1
109
137
  end
110
138
  end
111
139
 
@@ -138,7 +166,7 @@ module ZPNG
138
166
  a = trns.data.unpack('n3').map{ |v| v & (2**hdr.depth-1) }
139
167
  Color.new(*a, :depth => hdr.depth)
140
168
  else
141
- raise "color2alpha only intended for GRAYSCALE & RGB color modes"
169
+ raise Exception, "color2alpha only intended for GRAYSCALE & RGB color modes"
142
170
  end
143
171
 
144
172
  color == @alpha_color ? 0 : (2**hdr.depth-1)
@@ -149,6 +177,12 @@ module ZPNG
149
177
  ###########################################################################
150
178
  # chunks access
151
179
 
180
+ def ihdr
181
+ @ihdr ||= @chunks.find{ |c| c.is_a?(Chunk::IHDR) }
182
+ end
183
+ alias :header :ihdr
184
+ alias :hdr :ihdr
185
+
152
186
  def trns
153
187
  # not used "@trns ||= ..." here b/c it will call find() each time of there's no TRNS chunk
154
188
  defined?(@trns) ? @trns : (@trns=@chunks.find{ |c| c.is_a?(Chunk::TRNS) })
@@ -163,51 +197,119 @@ module ZPNG
163
197
  # image attributes
164
198
 
165
199
  def bpp
166
- @header && @header.bpp
200
+ ihdr && @ihdr.bpp
167
201
  end
168
202
 
169
203
  def width
170
- @header && @header.width
204
+ ihdr && @ihdr.width
171
205
  end
172
206
 
173
207
  def height
174
- @header && @header.height
208
+ ihdr && @ihdr.height
175
209
  end
176
210
 
177
211
  def grayscale?
178
- @header && @header.grayscale?
212
+ ihdr && @ihdr.grayscale?
179
213
  end
180
214
 
181
215
  def interlaced?
182
- @header && @header.interlace != 0
216
+ ihdr && @ihdr.interlace != 0
183
217
  end
184
218
 
185
219
  def alpha_used?
186
- @header && @header.alpha_used?
220
+ ihdr && @ihdr.alpha_used?
221
+ end
222
+
223
+ private
224
+ def _imagedata
225
+ data_chunks = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }
226
+ case data_chunks.size
227
+ when 0
228
+ # no imagedata chunks ?!
229
+ nil
230
+ when 1
231
+ # a single chunk - save memory and return a reference to its data
232
+ data_chunks[0].data
233
+ else
234
+ # multiple data chunks - join their contents
235
+ data_chunks.map(&:data).join
236
+ end
187
237
  end
238
+ public
188
239
 
189
240
  def imagedata
190
241
  @imagedata ||=
191
242
  begin
192
- puts "[?] no image header, assuming non-interlaced RGB".yellow unless @header
193
- data = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }.map(&:data).join
243
+ puts "[?] no image header, assuming non-interlaced RGB".yellow unless header
244
+ data = _imagedata
245
+ #check_zlib_extradata data
194
246
  (data && data.size > 0) ? Zlib::Inflate.inflate(data) : ''
195
247
  end
196
248
  end
197
249
 
250
+ def imagedata_size
251
+ if new_image?
252
+ @scanlines.map(&:size).inject(&:+)
253
+ else
254
+ imagedata.size
255
+ end
256
+ end
257
+
258
+ # # check for extradata in zlib datastream after the end of compressed stream
259
+ # def check_zlib_extradata data
260
+ # zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
261
+ # io = StringIO.new(data)
262
+ # while !io.eof? && !zi.finished?
263
+ # zi.inflate(io.read(16384))
264
+ # end
265
+ # zi.finish unless zi.finished?
266
+ # if data.size != zi.total_in
267
+ # p [data.size, zi.total_in, zi.total_out]
268
+ # raise
269
+ # end
270
+ # zi.close if zi && !zi.closed?
271
+ # end
272
+
273
+ # # try to get imagedata size in bytes, w/o storing entire decompressed
274
+ # # stream in memory. used in bin/zpng
275
+ # # result: less memory used on big images, but speed gain near 1-2% in best case,
276
+ # # and 2x slower in worst case because imagedata decoded 2 times
277
+ # def imagedata_size
278
+ # if @imagedata
279
+ # # already decompressed
280
+ # @imagedata.size
281
+ # else
282
+ # zi = nil
283
+ # @imagedata_size ||=
284
+ # begin
285
+ # zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
286
+ # io = StringIO.new(_imagedata)
287
+ # while !io.eof? && !zi.finished?
288
+ # n = zi.inflate(io.read(16384))
289
+ # end
290
+ # zi.finish unless zi.finished?
291
+ # zi.total_out
292
+ # ensure
293
+ # zi.close if zi && !zi.closed?
294
+ # end
295
+ # end
296
+ # end
297
+
198
298
  def metadata
199
299
  @metadata ||= Metadata.new(self)
200
300
  end
201
301
 
202
302
  def [] x, y
303
+ # extracting this check into a module => +1-2% speed
203
304
  x,y = adam7.convert_coords(x,y) if interlaced?
204
305
  scanlines[y][x]
205
306
  end
206
307
 
207
- def []= x, y, newpixel
308
+ def []= x, y, newcolor
309
+ # extracting these checks into a module => +1-2% speed
208
310
  decode_all_scanlines
209
311
  x,y = adam7.convert_coords(x,y) if interlaced?
210
- scanlines[y][x] = newpixel
312
+ scanlines[y][x] = newcolor
211
313
  end
212
314
 
213
315
  # we must decode all scanlines before doing any modifications
@@ -261,16 +363,16 @@ module ZPNG
261
363
  end
262
364
 
263
365
  def export
264
- # fill @imagedata, if not already filled
265
- imagedata unless new_image?
366
+ # XXX creating new IDAT must be BEFORE deleting old IDAT chunks
367
+ idat = Chunk::IDAT.new(
368
+ :data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
369
+ )
266
370
 
267
371
  # delete old IDAT chunks
268
372
  @chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
269
373
 
270
- # fill first_idat @data with compressed imagedata
271
- @chunks << Chunk::IDAT.new(
272
- :data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
273
- )
374
+ # add newly created IDAT
375
+ @chunks << idat
274
376
 
275
377
  # delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
276
378
  @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
@@ -286,12 +388,12 @@ module ZPNG
286
388
  decode_all_scanlines
287
389
 
288
390
  x,y,h,w = (params[:x]||0), (params[:y]||0), params[:height], params[:width]
289
- raise "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
391
+ raise ArgumentError, "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
290
392
 
291
393
  # adjust crop sizes if they greater than image sizes
292
394
  h = self.height-y if (y+h) > self.height
293
395
  w = self.width-x if (x+w) > self.width
294
- raise "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
396
+ raise ArgumentError, "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
295
397
 
296
398
  # delete excess scanlines at tail
297
399
  scanlines[(y+h)..-1] = [] if (y+h) < scanlines.size
@@ -321,9 +423,13 @@ module ZPNG
321
423
  end
322
424
 
323
425
  def == other_image
324
- width == other_image.width &&
325
- height == other_image.height &&
326
- pixels == other_image.pixels
426
+ return false unless other_image.is_a?(Image)
427
+ return false if width != other_image.width
428
+ return false if height != other_image.height
429
+ each_pixel do |c,x,y|
430
+ return false if c != other_image[x,y]
431
+ end
432
+ true
327
433
  end
328
434
 
329
435
  def each_pixel &block