zpng 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +5 -4
- data/README.md.tpl +0 -1
- data/TODO +5 -0
- data/VERSION +1 -1
- data/lib/zpng.rb +11 -1
- data/lib/zpng/bmp/reader.rb +105 -0
- data/lib/zpng/chunk.rb +6 -2
- data/lib/zpng/cli.rb +78 -12
- data/lib/zpng/image.rb +167 -61
- data/lib/zpng/pixels.rb +24 -6
- data/lib/zpng/readable_struct.rb +56 -0
- data/lib/zpng/scan_line.rb +94 -67
- data/lib/zpng/scan_line/mixins.rb +74 -0
- data/lib/zpng/string_ext.rb +3 -0
- data/samples/cats.png +0 -0
- data/samples/mouse.bmp +0 -0
- data/samples/mouse.png +0 -0
- data/samples/mouse17.bmp +0 -0
- data/samples/mouse17.png +0 -0
- data/spec/ascii_spec.rb +1 -1
- data/spec/bmp_spec.rb +20 -0
- data/spec/cli_spec.rb +12 -0
- data/spec/exception_spec.rb +9 -0
- data/spec/load_save_spec.rb +22 -0
- data/spec/modify_spec.rb +1 -1
- data/spec/spec_helper.rb +10 -0
- data/zpng.gemspec +16 -3
- metadata +30 -4
- data/spec/zpng_spec.rb +0 -7
data/lib/zpng/pixels.rb
CHANGED
@@ -2,18 +2,36 @@ module ZPNG
|
|
2
2
|
class Pixels
|
3
3
|
include Enumerable
|
4
4
|
|
5
|
-
|
6
|
-
|
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
|
-
|
10
|
-
|
11
|
-
@
|
12
|
-
yield @
|
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
|
data/lib/zpng/scan_line.rb
CHANGED
@@ -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
|
-
@
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
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
|
-
|
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,
|
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
|
-
|
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
|
95
|
+
b = decoded_bytes.getbyte(pos)
|
93
96
|
b = (b & (0xff-(mask<<shift))) | ((color_idx & mask) << shift)
|
94
|
-
decoded_bytes
|
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
|
143
|
+
def [] x
|
141
144
|
raw =
|
142
145
|
if @BPP
|
143
|
-
# 8, 16
|
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
|
-
|
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
|
-
|
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
|
-
|
236
|
-
|
237
|
-
|
238
|
-
s
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/zpng/string_ext.rb
CHANGED
data/samples/cats.png
ADDED
Binary file
|