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 +1 -1
- data/lib/zpng/bmp/reader.rb +126 -12
- data/lib/zpng/chunk.rb +19 -0
- data/lib/zpng/cli.rb +15 -4
- data/lib/zpng/color.rb +72 -7
- data/lib/zpng/image.rb +77 -34
- data/lib/zpng/pixels.rb +17 -0
- data/lib/zpng/scan_line.rb +41 -14
- data/samples/bad/000000.png +0 -0
- data/samples/bad/b1.png +0 -0
- data/spec/bad_samples_spec.rb +47 -0
- data/spec/color_spec.rb +25 -0
- data/zpng.gemspec +5 -2
- metadata +28 -25
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.3
|
data/lib/zpng/bmp/reader.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
158
|
+
fhdr = BITMAPFILEHEADER.read(io)
|
72
159
|
# DIB Header immediately follows the Bitmap File Header
|
73
|
-
|
74
|
-
if
|
75
|
-
raise "dib_hdr_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 <<
|
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 = ((
|
85
|
-
# XXX hidden data in non-significant tail bits/bytes
|
172
|
+
row_size = ((ihdr.biBitCount*self.width+31)/32)*4
|
86
173
|
|
87
|
-
io.
|
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 -
|
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
|
data/lib/zpng/chunk.rb
CHANGED
@@ -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]
|
data/lib/zpng/cli.rb
CHANGED
@@ -134,7 +134,7 @@ module ZPNG
|
|
134
134
|
end
|
135
135
|
|
136
136
|
def load_file fname
|
137
|
-
@img = Image.load fname, :verbose =>
|
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
|
-
|
263
|
-
|
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
|
data/lib/zpng/color.rb
CHANGED
@@ -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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
data/lib/zpng/image.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
#
|
367
|
-
|
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
|
-
|
372
|
-
|
401
|
+
if options.fetch(:repack, true)
|
402
|
+
data = Zlib::Deflate.deflate(scanlines.map(&:export).join, options[:zlib_level])
|
373
403
|
|
374
|
-
|
375
|
-
|
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
|
-
|
378
|
-
|
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
|
-
|
381
|
-
|
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
|
data/lib/zpng/pixels.rb
CHANGED
@@ -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
|
data/lib/zpng/scan_line.rb
CHANGED
@@ -33,7 +33,7 @@ module ZPNG
|
|
33
33
|
end
|
34
34
|
if @filter = image.imagedata[@offset]
|
35
35
|
@filter = @filter.ord
|
36
|
-
|
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
|
-
|
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 =
|
253
|
+
s = raw
|
250
254
|
|
251
255
|
when FILTER_SUB # 1
|
252
256
|
s = "\x00" * (size-1)
|
253
|
-
s[0,bpp1] =
|
257
|
+
s[0,bpp1] = raw[0,bpp1] # TODO: optimize
|
254
258
|
bpp1.upto(size-2) do |i|
|
255
|
-
s.setbyte(i,
|
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,
|
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,
|
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,
|
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
|
-
|
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
|
-
|
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
|
data/samples/bad/b1.png
ADDED
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
|
data/spec/color_spec.rb
CHANGED
@@ -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
|
data/zpng.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "zpng"
|
8
|
-
s.version = "0.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-
|
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-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|