zpng 0.2.2 → 0.2.3
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/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
|