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.
@@ -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