zpng 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,18 +2,36 @@ module ZPNG
2
2
  class Pixels
3
3
  include Enumerable
4
4
 
5
- def initialize image
6
- @image = image
5
+ module ImageEnum
6
+ def each
7
+ @image.height.times do |y|
8
+ @image.width.times do |x|
9
+ yield @image[x,y]
10
+ end
11
+ end
12
+ end
7
13
  end
8
14
 
9
- def each
10
- @image.height.times do |y|
11
- @image.width.times do |x|
12
- yield @image[x,y]
15
+ module ScanLineEnum
16
+ def each
17
+ @scanline.width.times do |x|
18
+ yield @scanline[x]
13
19
  end
14
20
  end
15
21
  end
16
22
 
23
+ def initialize x
24
+ case x
25
+ when Image
26
+ @image = x
27
+ extend ImageEnum
28
+ when ScanLine
29
+ @scanline = x
30
+ extend ScanLineEnum
31
+ else raise ArgumentError, "don't know how to enumerate #{x.inspect}"
32
+ end
33
+ end
34
+
17
35
  def == other
18
36
  self.to_a == other.to_a
19
37
  end
@@ -0,0 +1,56 @@
1
+ module ZPNG
2
+ module ReadableStruct
3
+
4
+ def self.new fmt, *args
5
+ size = fmt.scan(/([a-z])(\d*)/i).map do |f,len|
6
+ [len.to_i, 1].max *
7
+ case f
8
+ when /[aAC]/ then 1
9
+ when 'v' then 2
10
+ when 'V','l' then 4
11
+ when 'Q' then 8
12
+ else raise "unknown fmt #{f.inspect}"
13
+ end
14
+ end.inject(&:+)
15
+
16
+ Struct.new( *args ).tap do |x|
17
+ x.const_set 'FORMAT', fmt
18
+ x.const_set 'SIZE', size
19
+ x.class_eval do
20
+ include InstanceMethods
21
+ end
22
+ x.extend ClassMethods
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+ # src can be IO or String, or anything that responds to :read or :unpack
28
+ def read src, size = nil
29
+ size ||= const_get 'SIZE'
30
+ data =
31
+ if src.respond_to?(:read)
32
+ src.read(size).to_s
33
+ elsif src.respond_to?(:unpack)
34
+ src
35
+ else
36
+ raise "[?] don't know how to read from #{src.inspect}"
37
+ end
38
+ if data.size < size
39
+ $stderr.puts "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
40
+ end
41
+ new(*data.unpack(const_get('FORMAT')))
42
+ end
43
+ end
44
+
45
+ module InstanceMethods
46
+ def pack
47
+ to_a.pack self.class.const_get('FORMAT')
48
+ end
49
+
50
+ def empty?
51
+ to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
52
+ end
53
+ end
54
+
55
+ end # ReadableStruct
56
+ end # ZPNG
@@ -10,7 +10,7 @@ module ZPNG
10
10
  attr_accessor :image, :idx, :filter, :offset, :bpp
11
11
  attr_writer :decoded_bytes
12
12
 
13
- def initialize image, idx
13
+ def initialize image, idx, params={}
14
14
  @image,@idx = image,idx
15
15
  @bpp = image.hdr.bpp
16
16
  raise "[!] zero bpp" if @bpp == 0
@@ -20,9 +20,10 @@ module ZPNG
20
20
  @BPP = (@bpp%8 == 0) && (@bpp>>3)
21
21
 
22
22
  if @image.new?
23
- @decoded_bytes = "\x00" * (size-1)
23
+ @size = params[:size]
24
+ @decoded_bytes = params[:decoded_bytes] || "\x00" * (size-1)
24
25
  @filter = FILTER_NONE
25
- @offset = idx*size
26
+ @offset = params[:offset] || idx*size
26
27
  else
27
28
  @offset =
28
29
  if image.interlaced?
@@ -45,23 +46,29 @@ module ZPNG
45
46
 
46
47
  # total scanline size in bytes, INCLUDING leading 'filter' byte
47
48
  def size
48
- w =
49
- if image.interlaced?
50
- image.adam7.scanline_width(idx)
51
- else
52
- image.width
49
+ @size ||=
50
+ begin
51
+ if @BPP
52
+ width*@BPP+1
53
+ else
54
+ (width*@bpp/8.0+1).ceil
55
+ end
53
56
  end
54
- if @BPP
55
- w*@BPP+1
57
+ end
58
+
59
+ # scanline width in pixels
60
+ def width
61
+ if image.interlaced?
62
+ image.adam7.scanline_width(idx)
56
63
  else
57
- (w*@bpp/8.0+1).ceil
64
+ image.width
58
65
  end
59
66
  end
60
67
 
61
68
  def inspect
62
69
  if image.interlaced?
63
70
  "#<ZPNG::ScanLine idx=%-2d offset=%-3d width=%-2d size=%-2d bpp=%d filter=%d>" %
64
- [idx, offset, image.adam7.scanline_width(idx), size, bpp, filter]
71
+ [idx, offset, width, size, bpp, filter]
65
72
  else
66
73
  "#<ZPNG::ScanLine idx=%-2d offset=%-3d size=%-2d bpp=%d filter=%d>" %
67
74
  [idx, offset, size, bpp, filter]
@@ -70,14 +77,10 @@ module ZPNG
70
77
 
71
78
  def to_ascii *args
72
79
  @image.width.times.map do |i|
73
- decode_pixel(i).to_ascii(*args)
80
+ self[i].to_ascii(*args)
74
81
  end.join
75
82
  end
76
83
 
77
- def [] x
78
- decode_pixel(x)
79
- end
80
-
81
84
  def []= x, color
82
85
  case image.hdr.color
83
86
  when COLOR_INDEXED # ALLOWED_DEPTHS: 1, 2, 4, 8
@@ -89,9 +92,9 @@ module ZPNG
89
92
  raise "invalid shift #{shift}" if shift < 0 || shift > 7
90
93
 
91
94
  pos = x*@bpp/8
92
- b = decoded_bytes[pos].ord
95
+ b = decoded_bytes.getbyte(pos)
93
96
  b = (b & (0xff-(mask<<shift))) | ((color_idx & mask) << shift)
94
- decoded_bytes[pos] = b.chr
97
+ decoded_bytes.setbyte(pos, b)
95
98
  # TODO: transparency in TRNS
96
99
 
97
100
  when COLOR_GRAYSCALE # ALLOWED_DEPTHS: 1, 2, 4, 8, 16
@@ -137,10 +140,10 @@ module ZPNG
137
140
  end # case image.hdr.color
138
141
  end
139
142
 
140
- def decode_pixel x
143
+ def [] x
141
144
  raw =
142
145
  if @BPP
143
- # 8, 16 or 32 bits per pixel
146
+ # 8, 16, 24, 32, 48 bits per pixel
144
147
  decoded_bytes[x*@BPP, @BPP]
145
148
  else
146
149
  # 1, 2 or 4 bits per pixel
@@ -192,7 +195,12 @@ module ZPNG
192
195
  color =
193
196
  case @bpp
194
197
  when 24 # RGB 8 bits per sample = 24bpp
195
- Color.new(*raw.unpack('C3'))
198
+ if image.trns
199
+ extend Mixins::RGB24_TRNS
200
+ else
201
+ extend Mixins::RGB24
202
+ end
203
+ return self[x]
196
204
  when 48 # RGB 16 bits per sample = 48bpp
197
205
  Color.new(*raw.unpack('n3'), :depth => 16)
198
206
  else raise "COLOR_RGB unexpected bpp #@bpp"
@@ -213,7 +221,8 @@ module ZPNG
213
221
  when COLOR_RGBA # ALLOWED_DEPTHS: 8, 16
214
222
  case @bpp
215
223
  when 32 # RGBA 8-bit/sample
216
- return Color.new(*raw.unpack('C4'))
224
+ extend Mixins::RGBA32
225
+ return self[x]
217
226
  when 64 # RGBA 16-bit/sample
218
227
  return Color.new(*raw.unpack('n4'), :depth => 16 )
219
228
  else raise "COLOR_RGBA unexpected bpp #@bpp"
@@ -229,15 +238,55 @@ module ZPNG
229
238
  #raise if caller.size > 50
230
239
  @decoded_bytes ||=
231
240
  begin
241
+ imagedata = @image.imagedata
242
+
232
243
  # number of bytes per complete pixel, rounding up to one
233
244
  bpp1 = (@bpp/8.0).ceil
234
245
 
235
- s = ''
236
- (size-1).times do |i|
237
- b0 = (i-bpp1) >= 0 ? s[i-bpp1] : nil
238
- s[i] = decode_byte(i, b0, bpp1)
246
+ case @filter
247
+
248
+ when FILTER_NONE # 0
249
+ s = imagedata[@offset+1, size-1]
250
+
251
+ when FILTER_SUB # 1
252
+ s = "\x00" * (size-1)
253
+ s[0,bpp1] = imagedata[@offset+1,bpp1]
254
+ bpp1.upto(size-2) do |i|
255
+ s.setbyte(i, imagedata.getbyte(@offset+i+1) + s.getbyte(i-bpp1))
256
+ end
257
+
258
+ when FILTER_UP # 2
259
+ s = "\x00" * (size-1)
260
+ 0.upto(size-2) do |i|
261
+ s.setbyte(i, imagedata.getbyte(@offset+i+1) + prev_scanline_byte(i))
262
+ end
263
+
264
+ when FILTER_AVERAGE # 3
265
+ s = "\x00" * (size-1)
266
+ 0.upto(bpp1-1) do |i|
267
+ s.setbyte(i, imagedata.getbyte(@offset+i+1) + prev_scanline_byte(i)/2)
268
+ end
269
+ 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
+ )
273
+ end
274
+
275
+ when FILTER_PAETH # 4
276
+ s = "\x00" * (size-1)
277
+ 0.upto(bpp1-1) do |i|
278
+ s.setbyte(i, imagedata.getbyte(@offset+i+1) + prev_scanline_byte(i))
279
+ end
280
+ bpp1.upto(size-2) do |i|
281
+ s.setbyte(i,
282
+ imagedata.getbyte(@offset+i+1) +
283
+ paeth_predictor(s.getbyte(i-bpp1), prev_scanline_byte(i), prev_scanline_byte(i-bpp1))
284
+ )
285
+ end
286
+
287
+ else raise "invalid ScanLine filter #{@filter}"
239
288
  end
240
- # print Hexdump.dump(s[0,16])
289
+
241
290
  s
242
291
  end
243
292
  end
@@ -254,49 +303,16 @@ module ZPNG
254
303
  private
255
304
 
256
305
  def prev_scanline_byte x
306
+ # defining instance methods gives 10-15% speed boost
257
307
  if image.interlaced?
258
- # When the image is interlaced, each pass of the interlace pattern is
259
- # treated as an independent image for filtering purposes
260
- image.adam7.pass_start?(@idx) ? 0 : image.scanlines[@idx-1].decoded_bytes[x].ord
308
+ extend Mixins::Interlaced
261
309
  elsif @idx > 0
262
- image.scanlines[@idx-1].decoded_bytes[x].ord
263
- else
264
- 0
265
- end
266
- end
267
-
268
- def decode_byte x, b0, bpp1
269
- raw = @image.imagedata[@offset+x+1]
270
-
271
- unless raw
272
- STDERR.puts "[!] #{self.class}: ##@idx: no data at pos #{x}".red
273
- raw = 0.chr
274
- end
275
-
276
- case @filter
277
- when FILTER_NONE # 0
278
- raw
279
-
280
- when FILTER_SUB # 1
281
- return raw unless b0
282
- ((raw.ord + b0.ord) & 0xff).chr
283
-
284
- when FILTER_UP # 2
285
- ((raw.ord + prev_scanline_byte(x)) & 0xff).chr
286
-
287
- when FILTER_AVERAGE # 3
288
- prev = (b0 && b0.ord) || 0
289
- prior = prev_scanline_byte(x)
290
- ((raw.ord + (prev + prior)/2) & 0xff).chr
291
-
292
- when FILTER_PAETH # 4
293
- pa = (b0 && b0.ord) || 0
294
- pb = prev_scanline_byte(x)
295
- pc = b0 ? prev_scanline_byte(x-bpp1) : 0
296
- ((raw.ord + paeth_predictor(pa, pb, pc)) & 0xff).chr
310
+ extend Mixins::NotFirstLine
297
311
  else
298
- raise "invalid ScanLine filter #{@filter}"
312
+ extend Mixins::FirstLine
299
313
  end
314
+ # call newly created method
315
+ prev_scanline_byte x
300
316
  end
301
317
 
302
318
  def paeth_predictor a,b,c
@@ -309,6 +325,7 @@ module ZPNG
309
325
 
310
326
  public
311
327
  def crop! x, w
328
+ @size = nil # unmemoize self size b/c it's changed after crop
312
329
  if @BPP
313
330
  # great, crop is byte-aligned! :)
314
331
  decoded_bytes[0,x*@BPP] = ''
@@ -356,5 +373,15 @@ module ZPNG
356
373
  # we export in FILTER_NONE mode
357
374
  FILTER_NONE.chr + decoded_bytes
358
375
  end
376
+
377
+ def each_pixel
378
+ width.times do |i|
379
+ yield self[i], i
380
+ end
381
+ end
382
+
383
+ def pixels
384
+ Pixels.new(self)
385
+ end
359
386
  end
360
387
  end
@@ -0,0 +1,74 @@
1
+ module ZPNG
2
+ class ScanLine
3
+ module Mixins
4
+
5
+ # scanline decoding
6
+
7
+ module Interlaced
8
+ def prev_scanline_byte x
9
+ # When the image is interlaced, each pass of the interlace pattern is
10
+ # treated as an independent image for filtering purposes
11
+ image.adam7.pass_start?(@idx) ? 0 : image.scanlines[@idx-1].decoded_bytes.getbyte(x)
12
+ end
13
+ end
14
+
15
+ module NotFirstLine
16
+ def prev_scanline_byte x
17
+ image.scanlines[@idx-1].decoded_bytes.getbyte(x)
18
+ end
19
+ end
20
+
21
+ module FirstLine
22
+ def prev_scanline_byte x
23
+ 0
24
+ end
25
+ end
26
+
27
+ # pixel access
28
+
29
+ # RGB 8 bits per sample = 24bpp
30
+ module RGB24
31
+ def [] x
32
+ t = x*3
33
+ # color_class is for (limited) BMP support
34
+ image.color_class.new(
35
+ decoded_bytes.getbyte(t),
36
+ decoded_bytes.getbyte(t+1),
37
+ decoded_bytes.getbyte(t+2)
38
+ )
39
+ end
40
+ end
41
+
42
+ # if image has tRNS chunk - 10% slower than RGB24
43
+ module RGB24_TRNS
44
+ def [] x
45
+ t = x*3
46
+ # color_class is for (limited) BMP support
47
+ color = image.color_class.new(
48
+ decoded_bytes.getbyte(t),
49
+ decoded_bytes.getbyte(t+1),
50
+ decoded_bytes.getbyte(t+2)
51
+ )
52
+ color.alpha = image._alpha_color(color)
53
+ color
54
+ end
55
+ end
56
+
57
+ # RGBA 8 bits per sample = 32bpp
58
+ module RGBA32
59
+ def [] x
60
+ # substring => 1.50s on 270_000 pixels
61
+ # getbyte(s) => 1.25s on 270_000 pixels
62
+ t = x*4
63
+ image.color_class.new(
64
+ decoded_bytes.getbyte(t),
65
+ decoded_bytes.getbyte(t+1),
66
+ decoded_bytes.getbyte(t+2),
67
+ decoded_bytes.getbyte(t+3)
68
+ )
69
+ end
70
+ end
71
+
72
+ end # Mixins
73
+ end # ScanLine
74
+ end # ZPNG
@@ -6,6 +6,9 @@ class String
6
6
  define_method color do
7
7
  color(color)
8
8
  end
9
+ define_method "bright_#{color}" do
10
+ color(color).bright
11
+ end
9
12
  end
10
13
  end
11
14
 
Binary file