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