zpng 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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