zpng 0.2.2 → 0.2.3

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.2
1
+ 0.2.3
@@ -1,5 +1,18 @@
1
1
  module ZPNG
2
2
  module BMP
3
+
4
+ class BITMAPFILEHEADER < ReadableStruct.new 'VvvV', #a2VvvV',
5
+ #:bfType,
6
+ :bfSize, # the size of the BMP file in bytes
7
+ :bfReserved1,
8
+ :bfReserved2,
9
+ :bfOffBits # imagedata offset
10
+
11
+ def inspect
12
+ "<" + super.partition(self.class.to_s.split('::').last)[1..-1].join
13
+ end
14
+ end
15
+
3
16
  class BITMAPINFOHEADER < ReadableStruct.new 'V3v2V6',
4
17
  :biSize, # BITMAPINFOHEADER::SIZE
5
18
  :biWidth,
@@ -21,13 +34,20 @@ module ZPNG
21
34
  class BmpHdrPseudoChunk < Chunk::IHDR
22
35
  def initialize bmp_hdr
23
36
  @bmp_hdr = bmp_hdr
24
- super(
37
+ h = {
25
38
  :width => bmp_hdr.biWidth,
26
39
  :height => bmp_hdr.biHeight.abs,
27
- :bpp => bmp_hdr.biBitCount,
28
40
  :type => 'BITMAPINFOHEADER',
29
41
  :crc => :no_crc # for CLI
30
- )
42
+ }
43
+ if bmp_hdr.biBitCount == 8
44
+ h[:color] = COLOR_INDEXED
45
+ h[:depth] = 8
46
+ else
47
+ h[:bpp] = bmp_hdr.biBitCount
48
+ end
49
+ super(h)
50
+ self.data = bmp_hdr.pack
31
51
  end
32
52
  def inspect *args
33
53
  @bmp_hdr.inspect
@@ -41,6 +61,73 @@ module ZPNG
41
61
  end
42
62
  end
43
63
 
64
+ class TypedBlock < Struct.new(:type, :size, :offset, :data)
65
+ def inspect
66
+ # string length of 16 is for alignment with BITMAP....HEADER chunks on
67
+ # zpng CLI output
68
+ "<%s size=%-5d (0x%-4x) offset=%-5d (0x%-4x)>" %
69
+ [type, size, size, offset, offset]
70
+ end
71
+ def pack; data; end
72
+ end
73
+
74
+ class BmpPseudoChunk < Chunk
75
+ def initialize struct
76
+ @struct = struct
77
+ type =
78
+ if struct.respond_to?(:type)
79
+ struct.type
80
+ else
81
+ struct.class.to_s.split('::').last
82
+ end
83
+
84
+ super(
85
+ #:size => struct.class.const_get('SIZE'),
86
+ :type => type,
87
+ :data => struct.pack,
88
+ :crc => :no_crc # for CLI
89
+ )
90
+ end
91
+ def inspect *args
92
+ @struct.inspect
93
+ end
94
+ def method_missing mname, *args
95
+ if @struct.respond_to?(mname)
96
+ @struct.send(mname,*args)
97
+ else
98
+ super
99
+ end
100
+ end
101
+ end
102
+
103
+ class BmpPaletteChunk < Chunk::PLTE
104
+ def initialize data
105
+ super(
106
+ :crc => :no_crc,
107
+ :data => data,
108
+ :type => 'PALETTE'
109
+ )
110
+ end
111
+
112
+ def [] idx
113
+ rgbx = @data[idx*4,4]
114
+ rgbx && Color.new(*rgbx.unpack('C4'))
115
+ end
116
+
117
+ def []= idx, color
118
+ @data ||= ''
119
+ @data[idx*4,4] = [color.r, color.g, color.b, color.a].pack('C4')
120
+ end
121
+
122
+ def ncolors
123
+ @data.to_s.size/4
124
+ end
125
+
126
+ def inspect *args
127
+ "<%s ncolors=%d>" % ["PALETTE", size/4]
128
+ end
129
+ end
130
+
44
131
  class Color < ZPNG::Color
45
132
  # BMP pixels are in perverted^w reverted order - BGR instead of RGB
46
133
  def initialize *a
@@ -68,27 +155,44 @@ module ZPNG
68
155
  # http://en.wikipedia.org/wiki/BMP_file_format
69
156
 
70
157
  def _read_bmp io
71
- filesize, reserved1, reserved2, imagedata_offset = io.read(4+2+2+4).unpack('VvvV')
158
+ fhdr = BITMAPFILEHEADER.read(io)
72
159
  # 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}"
160
+ ihdr = BITMAPINFOHEADER.read(io)
161
+ if ihdr.biSize != BITMAPINFOHEADER::SIZE
162
+ raise "dib_hdr_size #{ihdr.biSize} unsupported, want #{BITMAPINFOHEADER::SIZE}"
76
163
  end
77
164
 
78
165
  @new_image = true
79
166
  @color_class = BMP::Color
80
167
  @format = :bmp
81
- @chunks << BmpHdrPseudoChunk.new(hdr)
168
+ @chunks << BmpPseudoChunk.new(fhdr)
169
+ @chunks << BmpHdrPseudoChunk.new(ihdr)
82
170
 
83
171
  # 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
172
+ row_size = ((ihdr.biBitCount*self.width+31)/32)*4
86
173
 
87
- io.seek(imagedata_offset)
174
+ gap1_size = fhdr.bfOffBits - io.tell
175
+
176
+ STDERR.puts "[?] negative gap1=#{gap1_size}".red if gap1_size < 0
177
+
178
+ if ihdr.biBitCount == 8 && gap1_size >= 1024
179
+ # palette for 256-color BMP
180
+ data = io.read 1024
181
+ @chunks << BmpPaletteChunk.new(data)
182
+ gap1_size -= 1024
183
+ end
88
184
 
185
+ if gap1_size != 0
186
+ @chunks << BmpPseudoChunk.new(
187
+ TypedBlock.new("GAP1", gap1_size, io.tell, io.read(gap1_size))
188
+ )
189
+ #io.seek(fhdr.bfOffBits)
190
+ end
191
+
192
+ pos0 = io.tell
89
193
  @scanlines = []
90
194
  self.height.times do |idx|
91
- offset = io.tell - imagedata_offset
195
+ offset = io.tell - fhdr.bfOffBits
92
196
  data = io.read(row_size)
93
197
  # BMP scanlines layout is upside-down
94
198
  @scanlines.unshift ScanLine.new(self, self.height-idx-1,
@@ -98,6 +202,16 @@ module ZPNG
98
202
  )
99
203
  end
100
204
 
205
+ @chunks << BmpPseudoChunk.new(
206
+ TypedBlock.new("IMAGEDATA", io.tell - pos0, pos0, '')
207
+ )
208
+
209
+ unless io.eof?
210
+ @chunks << BmpPseudoChunk.new(
211
+ TypedBlock.new("GAP2", io.size - io.tell, io.tell, '')
212
+ )
213
+ end
214
+
101
215
  extend ImageMixin
102
216
  end
103
217
  end # Reader
@@ -210,6 +210,25 @@ module ZPNG
210
210
  @data.to_s.size/3
211
211
  end
212
212
 
213
+ # colors enumerator
214
+ def each
215
+ ncolors.times do |i|
216
+ yield self[i]
217
+ end
218
+ end
219
+
220
+ # colors enumerator with index
221
+ def each_with_index
222
+ ncolors.times do |i|
223
+ yield self[i], i
224
+ end
225
+ end
226
+
227
+ # convert to array of colors
228
+ def to_a
229
+ ncolors.times.map{ |i| self[i] }
230
+ end
231
+
213
232
  def index color
214
233
  ncolors.times do |i|
215
234
  c = self[i]
@@ -134,7 +134,7 @@ module ZPNG
134
134
  end
135
135
 
136
136
  def load_file fname
137
- @img = Image.load fname, :verbose => true
137
+ @img = Image.load fname, :verbose => @options[:verbose]+1
138
138
  end
139
139
 
140
140
  def metadata
@@ -191,9 +191,14 @@ module ZPNG
191
191
  end
192
192
 
193
193
  def chunks idx=nil
194
+ max_type_len = 0
195
+ unless idx
196
+ max_type_len = @img.chunks.map{ |x| x.type.to_s.size }.max
197
+ end
198
+
194
199
  @img.chunks.each do |chunk|
195
200
  next if idx && chunk.idx != idx
196
- colored_type = chunk.type.magenta
201
+ colored_type = chunk.type.ljust(max_type_len).magenta
197
202
  colored_crc =
198
203
  if chunk.crc == :no_crc # hack for BMP chunks (they have no CRC)
199
204
  ''
@@ -259,8 +264,14 @@ module ZPNG
259
264
  def palette
260
265
  if @img.palette
261
266
  pp @img.palette
262
- hexdump(@img.palette.data, :width => 3, :show_offset => false) do |row, offset|
263
- row.insert(0," color %4s: " % "##{(offset/3)}")
267
+ if @img.format == :bmp
268
+ hexdump(@img.palette.data, :width => 4, :show_offset => false) do |row, offset|
269
+ row.insert(0," color %4s: " % "##{(offset/4)}")
270
+ end
271
+ else
272
+ hexdump(@img.palette.data, :width => 3, :show_offset => false) do |row, offset|
273
+ row.insert(0," color %4s: " % "##{(offset/3)}")
274
+ end
264
275
  end
265
276
  end
266
277
  end
@@ -14,7 +14,7 @@ module ZPNG
14
14
  @depth = h[:depth] || 8
15
15
 
16
16
  # default ALPHA = 0xff - opaque
17
- @a ||= h[:alpha] || (2**@depth-1)
17
+ @a ||= h[:alpha] || h[:a] || (2**@depth-1)
18
18
  end
19
19
 
20
20
  def a= a
@@ -88,12 +88,30 @@ module ZPNG
88
88
  [to_grayscale, alpha]
89
89
  end
90
90
 
91
- # from_grayscale level
92
- # from_grayscale level, :depth => 16
93
- # from_grayscale level, alpha
94
- # from_grayscale level, alpha, :depth => 16
95
- def self.from_grayscale value, *args
96
- Color.new value,value,value, *args
91
+ class << self
92
+ # from_grayscale level
93
+ # from_grayscale level, :depth => 16
94
+ # from_grayscale level, alpha
95
+ # from_grayscale level, alpha, :depth => 16
96
+ def from_grayscale value, *args
97
+ Color.new value,value,value, *args
98
+ end
99
+
100
+ # value: (String) "#ff00ff", "#f0f", "f0f", "eebbcc"
101
+ # alpha can be set via :alpha => N optional hash argument
102
+ def from_html value, *args
103
+ s = value.tr('#','')
104
+ case s.size
105
+ when 3
106
+ r,g,b = s.split('').map{ |x| x.to_i(16)*17 }
107
+ when 6
108
+ r,g,b = s.scan(/../).map{ |x| x.to_i(16) }
109
+ else
110
+ raise ArgumentError, "invalid HTML color #{s}"
111
+ end
112
+ Color.new r,g,b, *args
113
+ end
114
+ alias :from_css :from_html
97
115
  end
98
116
 
99
117
  ########################################################
@@ -198,6 +216,53 @@ module ZPNG
198
216
  r == 0 ? (c1.to_a <=> c2.to_a) : r
199
217
  end
200
218
 
219
+ # subtract other color from this one, returns new Color
220
+ def - c
221
+ op :-, c
222
+ end
223
+
224
+ # add other color to this one, returns new Color
225
+ def + c
226
+ op :+, c
227
+ end
228
+
229
+ # XOR this color with other one, returns new Color
230
+ def ^ c
231
+ op :^, c
232
+ end
233
+
234
+ # AND this color with other one, returns new Color
235
+ def & c
236
+ op :&, c
237
+ end
238
+
239
+ # OR this color with other one, returns new Color
240
+ def | c
241
+ op :|, c
242
+ end
243
+
244
+ # Op! op! op! Op!! Oppan Gangnam Style!!
245
+ def op op, c=nil
246
+ # XXX what to do with alpha?
247
+ max = 2**depth-1
248
+ if c
249
+ c = c.to_depth(depth)
250
+ Color.new(
251
+ @r.send(op, c.r) & max,
252
+ @g.send(op, c.g) & max,
253
+ @b.send(op, c.b) & max,
254
+ :depth => self.depth
255
+ )
256
+ else
257
+ Color.new(
258
+ @r.send(op) & max,
259
+ @g.send(op) & max,
260
+ @b.send(op) & max,
261
+ :depth => self.depth
262
+ )
263
+ end
264
+ end
265
+
201
266
  # for Array.uniq()
202
267
  def hash
203
268
  self.to_i
@@ -8,6 +8,9 @@ module ZPNG
8
8
  attr_accessor :color_class
9
9
 
10
10
  include DeepCopyable
11
+ alias :clone :deep_copy
12
+ alias :dup :deep_copy
13
+
11
14
  include BMP::Reader
12
15
 
13
16
  PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a".force_encoding('binary')
@@ -19,6 +22,7 @@ module ZPNG
19
22
  # Hash of image parameters to create new blank image
20
23
  def initialize x, h={}
21
24
  @chunks = []
25
+ @extradata = []
22
26
  @color_class = Color
23
27
  @format = :png
24
28
  @verbose =
@@ -73,8 +77,8 @@ module ZPNG
73
77
  end
74
78
 
75
79
  # save image to file
76
- def save fname
77
- File.open(fname,"wb"){ |f| f << export }
80
+ def save fname, options={}
81
+ File.open(fname,"wb"){ |f| f << export(options) }
78
82
  end
79
83
 
80
84
  # flag that image is just created, and NOT loaded from file
@@ -132,8 +136,8 @@ module ZPNG
132
136
 
133
137
  unless io.eof?
134
138
  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
139
+ @extradata << io.read
140
+ puts "[?] #{@extradata.last.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red if @verbose >= 1
137
141
  end
138
142
  end
139
143
 
@@ -235,6 +239,50 @@ module ZPNG
235
239
  data_chunks.map(&:data).join
236
240
  end
237
241
  end
242
+
243
+ # unpack zlib,
244
+ # on errors keep going and try to return maximum possible data
245
+ def _safe_inflate data
246
+ zi = Zlib::Inflate.new
247
+ pos = 0; r = ''
248
+ begin
249
+ # save some memory by not using String#[] when not necessary
250
+ r << zi.inflate(pos==0 ? data : data[pos..-1])
251
+ if zi.total_in < data.size
252
+ @extradata << data[zi.total_in..-1]
253
+ puts "[?] #{@extradata.last.size} bytes of extra data after zlib stream".red if @verbose >= 1
254
+ end
255
+ # decompress OK
256
+ rescue Zlib::BufError
257
+ # tried to decompress, but got EOF - need more data
258
+ puts "[!] #{$!.inspect}".red if @verbose >= -1
259
+ # collect any remaining data in decompress buffer
260
+ r << zi.flush_next_out
261
+ rescue Zlib::DataError
262
+ puts "[!] #{$!.inspect}".red if @verbose >= -1
263
+ #p [pos, zi.total_in, zi.total_out, data.size, r.size]
264
+ r << zi.flush_next_out
265
+ # XXX TODO try to skip error and continue
266
+ # printf "[d] pos=%d/%d t_in=%d t_out=%d bytes_ok=%d\n".gray, pos, data.size,
267
+ # zi.total_in, zi.total_out, r.size
268
+ # if pos < zi.total_in
269
+ # pos = zi.total_in
270
+ # else
271
+ # pos += 1
272
+ # end
273
+ # pos = 0
274
+ # retry if pos < data.size
275
+ rescue Zlib::NeedDict
276
+ puts "[!] #{$!.inspect}".red if @verbose >= -1
277
+ # collect any remaining data in decompress buffer
278
+ r << zi.flush_next_out
279
+ end
280
+
281
+ r == "" ? nil : r
282
+ ensure
283
+ zi.close if zi && !zi.closed?
284
+ end
285
+
238
286
  public
239
287
 
240
288
  def imagedata
@@ -242,8 +290,7 @@ module ZPNG
242
290
  begin
243
291
  puts "[?] no image header, assuming non-interlaced RGB".yellow unless header
244
292
  data = _imagedata
245
- #check_zlib_extradata data
246
- (data && data.size > 0) ? Zlib::Inflate.inflate(data) : ''
293
+ (data && data.size > 0) ? _safe_inflate(data) : ''
247
294
  end
248
295
  end
249
296
 
@@ -255,21 +302,6 @@ module ZPNG
255
302
  end
256
303
  end
257
304
 
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
305
  # # try to get imagedata size in bytes, w/o storing entire decompressed
274
306
  # # stream in memory. used in bin/zpng
275
307
  # # result: less memory used on big images, but speed gain near 1-2% in best case,
@@ -362,23 +394,34 @@ module ZPNG
362
394
  end
363
395
  end
364
396
 
365
- def export
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
- )
397
+ def export options = {}
398
+ # allow :zlib_level => nil
399
+ options[:zlib_level] = 9 unless options.key?(:zlib_level)
370
400
 
371
- # delete old IDAT chunks
372
- @chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
401
+ if options.fetch(:repack, true)
402
+ data = Zlib::Deflate.deflate(scanlines.map(&:export).join, options[:zlib_level])
373
403
 
374
- # add newly created IDAT
375
- @chunks << idat
404
+ idats = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }
405
+ case idats.size
406
+ when 0
407
+ # add new IDAT
408
+ @chunks << Chunk::IDAT.new( :data => data )
409
+ when 1
410
+ idats[0].data = data
411
+ else
412
+ idats[0].data = data
413
+ # delete other IDAT chunks
414
+ @chunks -= idats[1..-1]
415
+ end
416
+ end
376
417
 
377
- # delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
378
- @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
418
+ unless @chunks.last.is_a?(Chunk::IEND)
419
+ # delete old IEND chunk(s) b/c IEND must be the last one
420
+ @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
379
421
 
380
- # add fresh new IEND
381
- @chunks << Chunk::IEND.new
422
+ # add fresh new IEND
423
+ @chunks << Chunk::IEND.new
424
+ end
382
425
 
383
426
  PNG_HDR + @chunks.map(&:export).join
384
427
  end
@@ -10,6 +10,23 @@ module ZPNG
10
10
  end
11
11
  end
12
12
  end
13
+
14
+ def keep_if otherwise = Color::TRANSPARENT
15
+ @image.height.times do |y|
16
+ @image.width.times do |x|
17
+ @image[x,y] = otherwise unless yield(@image[x,y])
18
+ end
19
+ end
20
+ end
21
+
22
+ def filter!
23
+ @image.height.times do |y|
24
+ @image.width.times do |x|
25
+ @image[x,y] = yield(@image[x,y])
26
+ end
27
+ end
28
+ end
29
+ alias :map! :filter!
13
30
  end
14
31
 
15
32
  module ScanLineEnum
@@ -33,7 +33,7 @@ module ZPNG
33
33
  end
34
34
  if @filter = image.imagedata[@offset]
35
35
  @filter = @filter.ord
36
- else
36
+ elsif @image.verbose >= -1
37
37
  STDERR.puts "[!] #{self.class}: ##@idx: no data at pos 0, scanline dropped".red
38
38
  end
39
39
  end
@@ -235,10 +235,14 @@ module ZPNG
235
235
  end
236
236
 
237
237
  def decoded_bytes
238
- #raise if caller.size > 50
239
238
  @decoded_bytes ||=
240
239
  begin
241
- imagedata = @image.imagedata
240
+ raw = @image.imagedata[@offset+1, size-1]
241
+ if raw.size < size-1
242
+ # handle broken images when data ends in the middle of scanline
243
+ raw += "\x00" * (size-1-raw.size)
244
+ end
245
+ # TODO: check if converting raw to array would give any speedup
242
246
 
243
247
  # number of bytes per complete pixel, rounding up to one
244
248
  bpp1 = (@bpp/8.0).ceil
@@ -246,40 +250,38 @@ module ZPNG
246
250
  case @filter
247
251
 
248
252
  when FILTER_NONE # 0
249
- s = imagedata[@offset+1, size-1]
253
+ s = raw
250
254
 
251
255
  when FILTER_SUB # 1
252
256
  s = "\x00" * (size-1)
253
- s[0,bpp1] = imagedata[@offset+1,bpp1]
257
+ s[0,bpp1] = raw[0,bpp1] # TODO: optimize
254
258
  bpp1.upto(size-2) do |i|
255
- s.setbyte(i, imagedata.getbyte(@offset+i+1) + s.getbyte(i-bpp1))
259
+ s.setbyte(i, raw.getbyte(i) + s.getbyte(i-bpp1))
256
260
  end
257
261
 
258
262
  when FILTER_UP # 2
259
263
  s = "\x00" * (size-1)
260
264
  0.upto(size-2) do |i|
261
- s.setbyte(i, imagedata.getbyte(@offset+i+1) + prev_scanline_byte(i))
265
+ s.setbyte(i, raw.getbyte(i) + prev_scanline_byte(i))
262
266
  end
263
267
 
264
268
  when FILTER_AVERAGE # 3
265
269
  s = "\x00" * (size-1)
266
270
  0.upto(bpp1-1) do |i|
267
- s.setbyte(i, imagedata.getbyte(@offset+i+1) + prev_scanline_byte(i)/2)
271
+ s.setbyte(i, raw.getbyte(i) + prev_scanline_byte(i)/2)
268
272
  end
269
273
  bpp1.upto(size-2) do |i|
270
- s.setbyte(i,
271
- imagedata.getbyte(@offset+i+1) + (s.getbyte(i-bpp1) + prev_scanline_byte(i))/2
272
- )
274
+ s.setbyte(i, raw.getbyte(i) + (s.getbyte(i-bpp1) + prev_scanline_byte(i))/2)
273
275
  end
274
276
 
275
277
  when FILTER_PAETH # 4
276
278
  s = "\x00" * (size-1)
277
279
  0.upto(bpp1-1) do |i|
278
- s.setbyte(i, imagedata.getbyte(@offset+i+1) + prev_scanline_byte(i))
280
+ s.setbyte(i, raw.getbyte(i) + prev_scanline_byte(i))
279
281
  end
280
282
  bpp1.upto(size-2) do |i|
281
283
  s.setbyte(i,
282
- imagedata.getbyte(@offset+i+1) +
284
+ raw.getbyte(i) +
283
285
  paeth_predictor(s.getbyte(i-bpp1), prev_scanline_byte(i), prev_scanline_byte(i-bpp1))
284
286
  )
285
287
  end
@@ -300,6 +302,25 @@ module ZPNG
300
302
  @offset ? @image.imagedata[@offset, size] : ''
301
303
  end
302
304
 
305
+ def raw_data= data
306
+ if data.size == size
307
+ @image.imagedata[@offset, size] = data
308
+ else
309
+ raise Exception, "raw data size must be #{size}, got #{data.size}"
310
+ end
311
+ end
312
+
313
+ # set raw byte data at specified offset
314
+ # modifies @image, resets @decoded_bytes
315
+ def raw_set offset, value
316
+ @decoded_bytes = nil
317
+ value = value.ord if value.is_a?(String)
318
+ if offset == 0
319
+ @filter = value # XXX possible bugs with Singleton Modules
320
+ end
321
+ @image.imagedata.setbyte(@offset+offset, value)
322
+ end
323
+
303
324
  private
304
325
 
305
326
  def prev_scanline_byte x
@@ -371,7 +392,13 @@ module ZPNG
371
392
 
372
393
  def export
373
394
  # we export in FILTER_NONE mode
374
- FILTER_NONE.chr + decoded_bytes
395
+ # [] is for preventing spare tail bytes that can break scanlines sequence
396
+ if @decoded_bytes
397
+ FILTER_NONE.chr + decoded_bytes[0,size-1]
398
+ else
399
+ # scanline was never decoded => export it as-is to save memory & CPU
400
+ @image.imagedata[@offset, size]
401
+ end
375
402
  end
376
403
 
377
404
  def each_pixel
Binary file
Binary file
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'zpng/cli'
3
+
4
+ each_sample("bad/*.png") do |fname|
5
+ describe fname do
6
+ before(:all) do
7
+ @img = ZPNG::Image.load(fname, :verbose => -2)
8
+ end
9
+
10
+ it "returns dimensions" do
11
+ lambda{
12
+ @img.width
13
+ @img.height
14
+ }.should_not raise_error
15
+ end
16
+
17
+ it "should access 1st pixel" do
18
+ @img[0,0].should be_instance_of(ZPNG::Color)
19
+ end
20
+
21
+ describe "CLI" do
22
+ it "shows info & chunks" do
23
+ orig_stdout, out = $stdout, ""
24
+ begin
25
+ $stdout = StringIO.new(out)
26
+ lambda { ZPNG::CLI.new([fname, "-qqq"]).run }.should_not raise_error
27
+ ensure
28
+ $stdout = orig_stdout
29
+ end
30
+ out.should include("#{@img.width}x#{@img.height}")
31
+ end
32
+
33
+ it "shows scanlines" do
34
+ orig_stdout, out = $stdout, ""
35
+ begin
36
+ $stdout = StringIO.new(out)
37
+ lambda { ZPNG::CLI.new([fname, "-qqq", "--scanlines"]).run }.should_not raise_error
38
+ ensure
39
+ $stdout = orig_stdout
40
+ end
41
+ sl = out.scan(/scanline/i)
42
+ sl.size.should > 0
43
+ sl.size.should == @img.scanlines.size
44
+ end
45
+ end
46
+ end
47
+ end
@@ -47,4 +47,29 @@ describe ZPNG::Color do
47
47
  [c3,c2,c1].sort.should == [c1,c2,c3]
48
48
  [c1,c3,c2].sort.should == [c1,c2,c3]
49
49
  end
50
+
51
+ describe "#from_html" do
52
+ it "should understand short notation" do
53
+ ZPNG::Color.from_html('#ff1133').should == ZPNG::Color.new(0xff,0x11,0x33)
54
+ end
55
+ it "should understand long notation" do
56
+ ZPNG::Color.from_html('#f13').should == ZPNG::Color.new(0xff,0x11,0x33)
57
+ end
58
+ it "should understand short notation w/o '#'" do
59
+ ZPNG::Color.from_html('ff1133').should == ZPNG::Color.new(0xff,0x11,0x33)
60
+ end
61
+ it "should understand long notation w/o '#'" do
62
+ ZPNG::Color.from_html('f13').should == ZPNG::Color.new(0xff,0x11,0x33)
63
+ end
64
+ it "should set alpha" do
65
+ ZPNG::Color.from_html('f13', :alpha => 0x11).should ==
66
+ ZPNG::Color.new(0xff,0x11,0x33, 0x11)
67
+
68
+ ZPNG::Color.from_html('#f13', :a => 0x44).should ==
69
+ ZPNG::Color.new(0xff,0x11,0x33, 0x44)
70
+
71
+ ZPNG::Color.from_html('f13').should_not ==
72
+ ZPNG::Color.new(0xff,0x11,0x33, 0x11)
73
+ end
74
+ end
50
75
  end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "zpng"
8
- s.version = "0.2.2"
8
+ s.version = "0.2.3"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Andrey \"Zed\" Zaikin"]
12
- s.date = "2013-01-13"
12
+ s.date = "2013-01-24"
13
13
  s.email = "zed.0xff@gmail.com"
14
14
  s.executables = ["zpng"]
15
15
  s.extra_rdoc_files = [
@@ -49,6 +49,8 @@ Gem::Specification.new do |s|
49
49
  "lib/zpng/text_chunk.rb",
50
50
  "misc/chars.png",
51
51
  "misc/gen_ascii_map.rb",
52
+ "samples/bad/000000.png",
53
+ "samples/bad/b1.png",
52
54
  "samples/captcha_4bpp.png",
53
55
  "samples/cats.png",
54
56
  "samples/itxt.png",
@@ -68,6 +70,7 @@ Gem::Specification.new do |s|
68
70
  "spec/adam7_spec.rb",
69
71
  "spec/alpha_spec.rb",
70
72
  "spec/ascii_spec.rb",
73
+ "spec/bad_samples_spec.rb",
71
74
  "spec/bmp_spec.rb",
72
75
  "spec/cli_spec.rb",
73
76
  "spec/color_spec.rb",
metadata CHANGED
@@ -1,96 +1,96 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zpng
3
3
  version: !ruby/object:Gem::Version
4
+ version: 0.2.3
4
5
  prerelease:
5
- version: 0.2.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Andrey "Zed" Zaikin
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-13 00:00:00.000000000 Z
12
+ date: 2013-01-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- prerelease: false
16
- type: :runtime
17
15
  name: rainbow
18
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
19
18
  requirements:
20
19
  - - ! '>='
21
20
  - !ruby/object:Gem::Version
22
21
  version: '0'
23
- none: false
22
+ type: :runtime
23
+ prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
25
26
  requirements:
26
27
  - - ! '>='
27
28
  - !ruby/object:Gem::Version
28
29
  version: '0'
29
- none: false
30
30
  - !ruby/object:Gem::Dependency
31
- prerelease: false
32
- type: :development
33
31
  name: rspec
34
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
35
34
  requirements:
36
35
  - - ! '>='
37
36
  - !ruby/object:Gem::Version
38
37
  version: 2.8.0
39
- none: false
38
+ type: :development
39
+ prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
41
42
  requirements:
42
43
  - - ! '>='
43
44
  - !ruby/object:Gem::Version
44
45
  version: 2.8.0
45
- none: false
46
46
  - !ruby/object:Gem::Dependency
47
- prerelease: false
48
- type: :development
49
47
  name: bundler
50
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
51
50
  requirements:
52
51
  - - ! '>='
53
52
  - !ruby/object:Gem::Version
54
53
  version: 1.0.0
55
- none: false
54
+ type: :development
55
+ prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
57
58
  requirements:
58
59
  - - ! '>='
59
60
  - !ruby/object:Gem::Version
60
61
  version: 1.0.0
61
- none: false
62
62
  - !ruby/object:Gem::Dependency
63
- prerelease: false
64
- type: :development
65
63
  name: jeweler
66
64
  requirement: !ruby/object:Gem::Requirement
65
+ none: false
67
66
  requirements:
68
67
  - - ~>
69
68
  - !ruby/object:Gem::Version
70
69
  version: 1.8.4
71
- none: false
70
+ type: :development
71
+ prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
73
74
  requirements:
74
75
  - - ~>
75
76
  - !ruby/object:Gem::Version
76
77
  version: 1.8.4
77
- none: false
78
78
  - !ruby/object:Gem::Dependency
79
- prerelease: false
80
- type: :development
81
79
  name: what_methods
82
80
  requirement: !ruby/object:Gem::Requirement
81
+ none: false
83
82
  requirements:
84
83
  - - ! '>='
85
84
  - !ruby/object:Gem::Version
86
85
  version: '0'
87
- none: false
86
+ type: :development
87
+ prerelease: false
88
88
  version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
89
90
  requirements:
90
91
  - - ! '>='
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
- none: false
94
94
  description:
95
95
  email: zed.0xff@gmail.com
96
96
  executables:
@@ -132,6 +132,8 @@ files:
132
132
  - lib/zpng/text_chunk.rb
133
133
  - misc/chars.png
134
134
  - misc/gen_ascii_map.rb
135
+ - samples/bad/000000.png
136
+ - samples/bad/b1.png
135
137
  - samples/captcha_4bpp.png
136
138
  - samples/cats.png
137
139
  - samples/itxt.png
@@ -151,6 +153,7 @@ files:
151
153
  - spec/adam7_spec.rb
152
154
  - spec/alpha_spec.rb
153
155
  - spec/ascii_spec.rb
156
+ - spec/bad_samples_spec.rb
154
157
  - spec/bmp_spec.rb
155
158
  - spec/cli_spec.rb
156
159
  - spec/color_spec.rb
@@ -177,20 +180,20 @@ rdoc_options: []
177
180
  require_paths:
178
181
  - lib
179
182
  required_ruby_version: !ruby/object:Gem::Requirement
183
+ none: false
180
184
  requirements:
181
185
  - - ! '>='
182
186
  - !ruby/object:Gem::Version
183
187
  version: '0'
184
188
  segments:
185
189
  - 0
186
- hash: 3712335013458038737
187
- none: false
190
+ hash: 377147211466559287
188
191
  required_rubygems_version: !ruby/object:Gem::Requirement
192
+ none: false
189
193
  requirements:
190
194
  - - ! '>='
191
195
  - !ruby/object:Gem::Version
192
196
  version: '0'
193
- none: false
194
197
  requirements: []
195
198
  rubyforge_project:
196
199
  rubygems_version: 1.8.24