zpng 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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