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/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -10,7 +10,7 @@ GEM
|
|
10
10
|
rdoc
|
11
11
|
json (1.7.5)
|
12
12
|
rainbow (1.1.4)
|
13
|
-
rake (10.0.
|
13
|
+
rake (10.0.3)
|
14
14
|
rdoc (3.12)
|
15
15
|
json (~> 1.4)
|
16
16
|
rspec (2.12.0)
|
@@ -21,6 +21,7 @@ GEM
|
|
21
21
|
rspec-expectations (2.12.0)
|
22
22
|
diff-lcs (~> 1.1.3)
|
23
23
|
rspec-mocks (2.12.0)
|
24
|
+
what_methods (1.0.1)
|
24
25
|
|
25
26
|
PLATFORMS
|
26
27
|
ruby
|
@@ -30,3 +31,4 @@ DEPENDENCIES
|
|
30
31
|
jeweler (~> 1.8.4)
|
31
32
|
rainbow
|
32
33
|
rspec (>= 2.8.0)
|
34
|
+
what_methods
|
data/README.md
CHANGED
@@ -16,7 +16,6 @@ Comparison
|
|
16
16
|
----------
|
17
17
|
* supports `iTXt` (international text) chunks
|
18
18
|
* full support of 16-bit color & alpha depth
|
19
|
-
* correct 4bpp image decoding (as of 29-Dec-2012, ChunkyPNG had 1-bit error in 4bpp image decoding)
|
20
19
|
|
21
20
|
Usage
|
22
21
|
-----
|
@@ -31,6 +30,7 @@ Usage
|
|
31
30
|
|
32
31
|
-S, --scanlines Show scanlines info
|
33
32
|
-P, --palette Show palette
|
33
|
+
--colors Show colors used
|
34
34
|
-E, --extract-chunk ID extract a single chunk
|
35
35
|
-D, --imagedata dump unpacked Image Data (IDAT) chunk(s) to stdout
|
36
36
|
|
@@ -44,6 +44,7 @@ Usage
|
|
44
44
|
|
45
45
|
-v, --verbose Run verbosely (can be used multiple times)
|
46
46
|
-q, --quiet Silent any warnings (can be used multiple times)
|
47
|
+
-I, --console opens IRB console with specified image loaded
|
47
48
|
|
48
49
|
### Info
|
49
50
|
|
@@ -51,7 +52,7 @@ Usage
|
|
51
52
|
|
52
53
|
[.] image size 35x35, 24bpp, COLOR_RGB
|
53
54
|
[.] uncompressed imagedata size = 3710 bytes
|
54
|
-
[.] <Chunk #00 IHDR size= 13, crc=91bb240e,
|
55
|
+
[.] <Chunk #00 IHDR size= 13, crc=91bb240e, color=2, compression=0, depth=8, filter=0, height=35, interlace=0, width=35> CRC OK
|
55
56
|
[.] <Chunk #01 sRGB size= 1, crc=aece1ce9 > CRC OK
|
56
57
|
[.] <Chunk #02 IDAT size= 399, crc=59790716 > CRC OK
|
57
58
|
[.] <Chunk #03 IEND size= 0, crc=ae426082 > CRC OK
|
@@ -65,7 +66,7 @@ Usage
|
|
65
66
|
01 ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 |................|
|
66
67
|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + 3678 bytes
|
67
68
|
|
68
|
-
[.] <Chunk #00 IHDR size= 13, crc=91bb240e,
|
69
|
+
[.] <Chunk #00 IHDR size= 13, crc=91bb240e, color=2, compression=0, depth=8, filter=0, height=35, idx=0, interlace=0, width=35> CRC OK
|
69
70
|
00 00 00 23 00 00 00 23 08 02 00 00 00 |...#...#..... |
|
70
71
|
|
71
72
|
[.] <Chunk #01 sRGB size= 1, crc=aece1ce9 > CRC OK
|
@@ -83,7 +84,7 @@ Usage
|
|
83
84
|
|
84
85
|
# zpng --chunks qr_aux_chunks.png
|
85
86
|
|
86
|
-
[.] <Chunk #00 IHDR size= 13, crc=36a28ef4,
|
87
|
+
[.] <Chunk #00 IHDR size= 13, crc=36a28ef4, color=0, compression=0, depth=1, filter=0, height=35, interlace=0, width=35> CRC OK
|
87
88
|
[.] <Chunk #01 gAMA size= 4, crc=0bfc6105 > CRC OK
|
88
89
|
[.] <Chunk #02 sRGB size= 1, crc=aece1ce9 > CRC OK
|
89
90
|
[.] <Chunk #03 cHRM size= 32, crc=9cba513c > CRC OK
|
data/README.md.tpl
CHANGED
data/TODO
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
[ ] benchmark accessing very last PNG scanline
|
2
|
+
[ ] use iostruct
|
3
|
+
|
1
4
|
ways to hide info in PNG:
|
2
5
|
* IHDR: longer than 13 bytes
|
3
6
|
* IEND:
|
@@ -6,11 +9,13 @@ ways to hide info in PNG:
|
|
6
9
|
* TEXT chunks
|
7
10
|
* zTXT chunks
|
8
11
|
* comp_method
|
12
|
+
* data after compressed data?
|
9
13
|
* IDAT:
|
10
14
|
* data after last scanline
|
11
15
|
* last bits in scanline when bpp%8 != 0
|
12
16
|
* PLTE:
|
13
17
|
* raw letters
|
14
18
|
* stegano
|
19
|
+
* many palette entries with same color => visually same pixels but, different color idx
|
15
20
|
* custom chunks
|
16
21
|
* crc
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.2
|
data/lib/zpng.rb
CHANGED
@@ -1,16 +1,26 @@
|
|
1
1
|
require 'zlib'
|
2
2
|
require 'stringio'
|
3
3
|
|
4
|
+
module ZPNG
|
5
|
+
class Exception < ::StandardError; end
|
6
|
+
class NotSupported < Exception; end
|
7
|
+
class ArgumentError < Exception; end
|
8
|
+
end
|
9
|
+
|
4
10
|
require 'zpng/string_ext'
|
5
11
|
require 'zpng/deep_copyable'
|
6
12
|
|
7
13
|
require 'zpng/color'
|
8
14
|
require 'zpng/block'
|
9
15
|
require 'zpng/scan_line'
|
16
|
+
require 'zpng/scan_line/mixins'
|
10
17
|
require 'zpng/chunk'
|
11
18
|
require 'zpng/text_chunk'
|
12
|
-
require 'zpng/
|
19
|
+
require 'zpng/readable_struct'
|
13
20
|
require 'zpng/adam7_decoder'
|
14
21
|
require 'zpng/hexdump'
|
15
22
|
require 'zpng/metadata'
|
16
23
|
require 'zpng/pixels'
|
24
|
+
|
25
|
+
require 'zpng/bmp/reader'
|
26
|
+
require 'zpng/image'
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module ZPNG
|
2
|
+
module BMP
|
3
|
+
class BITMAPINFOHEADER < ReadableStruct.new 'V3v2V6',
|
4
|
+
:biSize, # BITMAPINFOHEADER::SIZE
|
5
|
+
:biWidth,
|
6
|
+
:biHeight,
|
7
|
+
:biPlanes,
|
8
|
+
:biBitCount,
|
9
|
+
:biCompression,
|
10
|
+
:biSizeImage,
|
11
|
+
:biXPelsPerMeter,
|
12
|
+
:biYPelsPerMeter,
|
13
|
+
:biClrUsed,
|
14
|
+
:biClrImportant
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"<" + super.partition(self.class.to_s.split('::').last)[1..-1].join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class BmpHdrPseudoChunk < Chunk::IHDR
|
22
|
+
def initialize bmp_hdr
|
23
|
+
@bmp_hdr = bmp_hdr
|
24
|
+
super(
|
25
|
+
:width => bmp_hdr.biWidth,
|
26
|
+
:height => bmp_hdr.biHeight.abs,
|
27
|
+
:bpp => bmp_hdr.biBitCount,
|
28
|
+
:type => 'BITMAPINFOHEADER',
|
29
|
+
:crc => :no_crc # for CLI
|
30
|
+
)
|
31
|
+
end
|
32
|
+
def inspect *args
|
33
|
+
@bmp_hdr.inspect
|
34
|
+
end
|
35
|
+
def method_missing mname, *args
|
36
|
+
if @bmp_hdr.respond_to?(mname)
|
37
|
+
@bmp_hdr.send(mname,*args)
|
38
|
+
else
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Color < ZPNG::Color
|
45
|
+
# BMP pixels are in perverted^w reverted order - BGR instead of RGB
|
46
|
+
def initialize *a
|
47
|
+
h = a.last.is_a?(Hash) ? a.pop : {}
|
48
|
+
case a.size
|
49
|
+
when 3
|
50
|
+
# BGR
|
51
|
+
super *a.reverse, h
|
52
|
+
when 4
|
53
|
+
# ABGR
|
54
|
+
super a[2], a[1], a[0], a[3], h
|
55
|
+
else
|
56
|
+
super
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ImageMixin
|
62
|
+
def imagedata
|
63
|
+
@imagedata ||= @scanlines.sort_by(&:offset).map(&:decoded_bytes).join
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
module Reader
|
68
|
+
# http://en.wikipedia.org/wiki/BMP_file_format
|
69
|
+
|
70
|
+
def _read_bmp io
|
71
|
+
filesize, reserved1, reserved2, imagedata_offset = io.read(4+2+2+4).unpack('VvvV')
|
72
|
+
# DIB Header immediately follows the Bitmap File Header
|
73
|
+
hdr = BITMAPINFOHEADER.read(io)
|
74
|
+
if hdr.biSize != BITMAPINFOHEADER::SIZE
|
75
|
+
raise "dib_hdr_size #{hdr.biSize} unsupported, want #{BITMAPINFOHEADER::SIZE}"
|
76
|
+
end
|
77
|
+
|
78
|
+
@new_image = true
|
79
|
+
@color_class = BMP::Color
|
80
|
+
@format = :bmp
|
81
|
+
@chunks << BmpHdrPseudoChunk.new(hdr)
|
82
|
+
|
83
|
+
# http://en.wikipedia.org/wiki/BMP_file_format#Pixel_storage
|
84
|
+
row_size = ((hdr.biBitCount*self.width+31)/32)*4
|
85
|
+
# XXX hidden data in non-significant tail bits/bytes
|
86
|
+
|
87
|
+
io.seek(imagedata_offset)
|
88
|
+
|
89
|
+
@scanlines = []
|
90
|
+
self.height.times do |idx|
|
91
|
+
offset = io.tell - imagedata_offset
|
92
|
+
data = io.read(row_size)
|
93
|
+
# BMP scanlines layout is upside-down
|
94
|
+
@scanlines.unshift ScanLine.new(self, self.height-idx-1,
|
95
|
+
:decoded_bytes => data,
|
96
|
+
:size => row_size,
|
97
|
+
:offset => offset
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
extend ImageMixin
|
102
|
+
end
|
103
|
+
end # Reader
|
104
|
+
end # BMP
|
105
|
+
end # ZPNG
|
data/lib/zpng/chunk.rb
CHANGED
@@ -45,7 +45,7 @@ module ZPNG
|
|
45
45
|
def export
|
46
46
|
@data = self.export_data # virtual
|
47
47
|
@size = @data.size # XXX hmm.. is it always is?
|
48
|
-
|
48
|
+
fix_crc!
|
49
49
|
[@size,@type].pack('Na4') + @data + [@crc].pack('N')
|
50
50
|
end
|
51
51
|
|
@@ -66,6 +66,10 @@ module ZPNG
|
|
66
66
|
expected_crc == crc
|
67
67
|
end
|
68
68
|
|
69
|
+
def fix_crc!
|
70
|
+
@crc = Zlib.crc32(data, Zlib.crc32(type))
|
71
|
+
end
|
72
|
+
|
69
73
|
class IHDR < Chunk
|
70
74
|
attr_accessor :width, :height, :depth, :color, :compression, :filter, :interlace
|
71
75
|
|
@@ -184,7 +188,7 @@ module ZPNG
|
|
184
188
|
vars = instance_variables - [:@type, :@crc, :@data, :@size]
|
185
189
|
vars -= [:@idx] if verbosity <= 0
|
186
190
|
super.sub(/ *>$/,'') + ", " +
|
187
|
-
vars.map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
|
191
|
+
vars.sort.map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
|
188
192
|
join(", ") + ">"
|
189
193
|
end
|
190
194
|
end
|
data/lib/zpng/cli.rb
CHANGED
@@ -37,6 +37,7 @@ module ZPNG
|
|
37
37
|
opts.separator ""
|
38
38
|
opts.on("-S", "--scanlines", "Show scanlines info"){ @actions << :scanlines }
|
39
39
|
opts.on("-P", "--palette", "Show palette"){ @actions << :palette }
|
40
|
+
opts.on( "--colors", "Show colors used"){ @actions << :colors }
|
40
41
|
|
41
42
|
opts.on "-E", "--extract-chunk ID", Integer, "extract a single chunk" do |id|
|
42
43
|
@actions << [:extract_chunk, id]
|
@@ -72,6 +73,9 @@ module ZPNG
|
|
72
73
|
opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
|
73
74
|
@options[:verbose] -= 1
|
74
75
|
end
|
76
|
+
opts.on "-I", "--console", "opens IRB console with specified image loaded" do |v|
|
77
|
+
@actions << :console
|
78
|
+
end
|
75
79
|
end
|
76
80
|
|
77
81
|
if (argv = optparser.parse(@argv)).empty?
|
@@ -86,8 +90,7 @@ module ZPNG
|
|
86
90
|
puts if idx > 0
|
87
91
|
puts "[.] #{fname}".color(:green)
|
88
92
|
end
|
89
|
-
@
|
90
|
-
@file_name = fname
|
93
|
+
@fname = fname
|
91
94
|
|
92
95
|
@zpng = load_file fname
|
93
96
|
|
@@ -131,20 +134,30 @@ module ZPNG
|
|
131
134
|
end
|
132
135
|
|
133
136
|
def load_file fname
|
134
|
-
@img = Image.load fname
|
137
|
+
@img = Image.load fname, :verbose => true
|
135
138
|
end
|
136
139
|
|
137
140
|
def metadata
|
138
141
|
return if @img.metadata.empty?
|
139
142
|
puts "[.] metadata:"
|
140
143
|
@img.metadata.each do |k,v,h|
|
144
|
+
if @options[:verbose] < 2
|
145
|
+
if k.size > 512
|
146
|
+
puts "[?] key too long (#{k.size}), truncated to 512 chars".yellow
|
147
|
+
k = k[0,512] + "..."
|
148
|
+
end
|
149
|
+
if v.size > 512
|
150
|
+
puts "[?] value too long (#{v.size}), truncated to 512 chars".yellow
|
151
|
+
v = v[0,512] + "..."
|
152
|
+
end
|
153
|
+
end
|
141
154
|
if h.keys.sort == [:keyword, :text]
|
142
|
-
v.gsub!(/[\n\r]+/, "\n"+" "*
|
143
|
-
printf " %-
|
155
|
+
v.gsub!(/[\n\r]+/, "\n"+" "*19)
|
156
|
+
printf " %-12s : %s\n", k, v.gray
|
144
157
|
else
|
145
158
|
printf " %s (%s: %s):", k, h[:language], h[:translated_keyword]
|
146
|
-
v.gsub!(/[\n\r]+/, "\n"+" "*
|
147
|
-
printf "\n%s%s\n", " "*
|
159
|
+
v.gsub!(/[\n\r]+/, "\n"+" "*19)
|
160
|
+
printf "\n%s%s\n", " "*19, v.gray
|
148
161
|
end
|
149
162
|
end
|
150
163
|
puts
|
@@ -156,8 +169,8 @@ module ZPNG
|
|
156
169
|
end
|
157
170
|
puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}, #{@img.bpp}bpp, #{color}"
|
158
171
|
puts "[.] palette = #{@img.palette}" if @img.palette
|
159
|
-
puts "[.] uncompressed imagedata size = #{@img.
|
160
|
-
_conditional_hexdump
|
172
|
+
puts "[.] uncompressed imagedata size = #{@img.imagedata_size} bytes"
|
173
|
+
_conditional_hexdump(@img.imagedata, 3) if @options[:verbose] > 0
|
161
174
|
end
|
162
175
|
|
163
176
|
def _conditional_hexdump data, v2 = 2
|
@@ -181,7 +194,14 @@ module ZPNG
|
|
181
194
|
@img.chunks.each do |chunk|
|
182
195
|
next if idx && chunk.idx != idx
|
183
196
|
colored_type = chunk.type.magenta
|
184
|
-
colored_crc
|
197
|
+
colored_crc =
|
198
|
+
if chunk.crc == :no_crc # hack for BMP chunks (they have no CRC)
|
199
|
+
''
|
200
|
+
elsif chunk.crc_ok?
|
201
|
+
'CRC OK'.green
|
202
|
+
else
|
203
|
+
'CRC ERROR'.red
|
204
|
+
end
|
185
205
|
puts "[.] #{chunk.inspect(@options[:verbose]).sub(chunk.type, colored_type)} #{colored_crc}"
|
186
206
|
|
187
207
|
_conditional_hexdump(chunk.data) unless chunk.size == 0
|
@@ -225,11 +245,11 @@ module ZPNG
|
|
225
245
|
p sl
|
226
246
|
case @options[:verbose]
|
227
247
|
when 1
|
228
|
-
hexdump(sl.raw_data)
|
248
|
+
hexdump(sl.raw_data) if sl.raw_data
|
229
249
|
when 2
|
230
250
|
hexdump(sl.decoded_bytes)
|
231
251
|
when 3..999
|
232
|
-
hexdump(sl.raw_data)
|
252
|
+
hexdump(sl.raw_data) if sl.raw_data
|
233
253
|
hexdump(sl.decoded_bytes)
|
234
254
|
puts
|
235
255
|
end
|
@@ -244,5 +264,51 @@ module ZPNG
|
|
244
264
|
end
|
245
265
|
end
|
246
266
|
end
|
267
|
+
|
268
|
+
def colors
|
269
|
+
h=Hash.new(0)
|
270
|
+
h2=Hash.new{ |k,v| k[v] = [] }
|
271
|
+
@img.each_pixel do |c,x,y|
|
272
|
+
h[c] += 1
|
273
|
+
if h[c] < 6
|
274
|
+
h2[c] << [x,y]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
xlen = @img.width.to_s.size
|
279
|
+
ylen = @img.height.to_s.size
|
280
|
+
|
281
|
+
h.sort_by{ |c,n| [n] + h2[c].first.reverse }.each do |c,n|
|
282
|
+
printf "%6d : %s : ", n, c.inspect
|
283
|
+
h2[c].each_with_index do |a,idx|
|
284
|
+
print ";" if idx > 0
|
285
|
+
if idx >= 4
|
286
|
+
print " ..."
|
287
|
+
break
|
288
|
+
end
|
289
|
+
printf " %*d,%*d", xlen, a[0], ylen, a[1]
|
290
|
+
end
|
291
|
+
puts
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def console
|
296
|
+
ARGV.clear # clear ARGV so IRB is not confused
|
297
|
+
require 'irb'
|
298
|
+
m0 = IRB.method(:setup)
|
299
|
+
img = @img
|
300
|
+
|
301
|
+
# override IRB.setup, called from IRB.start
|
302
|
+
IRB.define_singleton_method :setup do |*args|
|
303
|
+
m0.call *args
|
304
|
+
conf[:IRB_RC] = Proc.new do |context|
|
305
|
+
context.main.instance_variable_set '@img', img
|
306
|
+
context.main.define_singleton_method(:img){ @img }
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
puts "[.] img = ZPNG::Image.load(#{@fname.inspect})".gray
|
311
|
+
IRB.start
|
312
|
+
end
|
247
313
|
end
|
248
314
|
end
|
data/lib/zpng/image.rb
CHANGED
@@ -1,38 +1,71 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
1
3
|
module ZPNG
|
2
4
|
class Image
|
3
|
-
attr_accessor :
|
4
|
-
|
5
|
+
attr_accessor :chunks, :scanlines, :imagedata, :extradata, :format, :verbose
|
6
|
+
|
7
|
+
# now only for (limited) BMP support
|
8
|
+
attr_accessor :color_class
|
5
9
|
|
6
10
|
include DeepCopyable
|
11
|
+
include BMP::Reader
|
7
12
|
|
8
|
-
PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
|
13
|
+
PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a".force_encoding('binary')
|
14
|
+
BMP_HDR = "BM".force_encoding('binary')
|
9
15
|
|
10
|
-
|
16
|
+
# possible input params:
|
17
|
+
# IO of opened image file
|
18
|
+
# String with image file already readed
|
19
|
+
# Hash of image parameters to create new blank image
|
20
|
+
def initialize x, h={}
|
11
21
|
@chunks = []
|
22
|
+
@color_class = Color
|
23
|
+
@format = :png
|
24
|
+
@verbose =
|
25
|
+
case h[:verbose]
|
26
|
+
when true; 1
|
27
|
+
when false; 0
|
28
|
+
else h[:verbose].to_i
|
29
|
+
end
|
30
|
+
|
12
31
|
case x
|
13
32
|
when IO
|
14
|
-
|
33
|
+
_from_io x
|
15
34
|
when String
|
16
|
-
|
35
|
+
_from_io StringIO.new(x)
|
17
36
|
when Hash
|
18
37
|
_from_hash x
|
19
38
|
else
|
20
|
-
raise "unsupported input data type #{x.class}"
|
39
|
+
raise NotSupported, "unsupported input data type #{x.class}"
|
21
40
|
end
|
22
41
|
if palette && hdr && hdr.depth
|
23
42
|
palette.max_colors = 2**hdr.depth
|
24
43
|
end
|
25
44
|
end
|
26
45
|
|
46
|
+
def inspect
|
47
|
+
"#<ZPNG::Image " +
|
48
|
+
%w'width height bpp chunks scanlines'.map do |k|
|
49
|
+
v = case (v = send(k))
|
50
|
+
when Array
|
51
|
+
"[#{v.size} entries]"
|
52
|
+
when String
|
53
|
+
v.size > 40 ? "[#{v.bytesize} bytes]" : v.inspect
|
54
|
+
else v.inspect
|
55
|
+
end
|
56
|
+
"#{k}=#{v}"
|
57
|
+
end.compact.join(", ") + ">"
|
58
|
+
end
|
59
|
+
|
27
60
|
def adam7
|
28
61
|
@adam7 ||= Adam7Decoder.new(self)
|
29
62
|
end
|
30
63
|
|
31
64
|
class << self
|
32
65
|
# load image from file
|
33
|
-
def load fname
|
66
|
+
def load fname, h={}
|
34
67
|
open(fname,"rb") do |f|
|
35
|
-
self.new(f)
|
68
|
+
self.new(f,h)
|
36
69
|
end
|
37
70
|
end
|
38
71
|
alias :load_file :load
|
@@ -55,8 +88,8 @@ module ZPNG
|
|
55
88
|
|
56
89
|
def _from_hash h
|
57
90
|
@new_image = true
|
58
|
-
@chunks <<
|
59
|
-
if
|
91
|
+
@chunks << Chunk::IHDR.new(h)
|
92
|
+
if header.palette_used?
|
60
93
|
if h.key?(:palette)
|
61
94
|
if h[:palette]
|
62
95
|
@chunks << h[:palette]
|
@@ -71,41 +104,36 @@ module ZPNG
|
|
71
104
|
end
|
72
105
|
end
|
73
106
|
|
74
|
-
def
|
75
|
-
if x
|
76
|
-
if PNG_HDR.size.times.all?{ |i| x[i].ord == PNG_HDR[i].ord } # encoding error workaround
|
77
|
-
# raw image data
|
78
|
-
@data = x
|
79
|
-
elsif File.exist?(x)
|
80
|
-
# filename
|
81
|
-
@data = File.binread(x)
|
82
|
-
else
|
83
|
-
raise "Don't know what #{x.inspect} is"
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
d = data[0,PNG_HDR.size]
|
88
|
-
if d != PNG_HDR
|
89
|
-
puts "[!] first #{PNG_HDR.size} bytes must be #{PNG_HDR.inspect}, but got #{d.inspect}".red
|
90
|
-
end
|
91
|
-
|
92
|
-
io = StringIO.new(data)
|
93
|
-
io.seek PNG_HDR.size
|
107
|
+
def _read_png io
|
94
108
|
while !io.eof?
|
95
109
|
chunk = Chunk.from_stream(io)
|
96
110
|
chunk.idx = @chunks.size
|
97
111
|
@chunks << chunk
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
112
|
+
break if chunk.is_a?(Chunk::IEND)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def _from_io io
|
117
|
+
# Puts ios into binary mode.
|
118
|
+
# Once a stream is in binary mode, it cannot be reset to nonbinary mode.
|
119
|
+
io.binmode
|
120
|
+
|
121
|
+
hdr = io.read(BMP_HDR.size)
|
122
|
+
if hdr == BMP_HDR
|
123
|
+
_read_bmp io
|
124
|
+
else
|
125
|
+
hdr << io.read(PNG_HDR.size - BMP_HDR.size)
|
126
|
+
if hdr == PNG_HDR
|
127
|
+
_read_png io
|
128
|
+
else
|
129
|
+
raise NotSupported, "Unsupported header #{hdr.inspect} in #{io.inspect}"
|
103
130
|
end
|
104
131
|
end
|
132
|
+
|
105
133
|
unless io.eof?
|
106
|
-
offset
|
107
|
-
extradata = io.read
|
108
|
-
puts "[?] #{extradata.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red
|
134
|
+
offset = io.tell
|
135
|
+
@extradata = io.read
|
136
|
+
puts "[?] #{@extradata.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red if @verbose >= 1
|
109
137
|
end
|
110
138
|
end
|
111
139
|
|
@@ -138,7 +166,7 @@ module ZPNG
|
|
138
166
|
a = trns.data.unpack('n3').map{ |v| v & (2**hdr.depth-1) }
|
139
167
|
Color.new(*a, :depth => hdr.depth)
|
140
168
|
else
|
141
|
-
raise "color2alpha only intended for GRAYSCALE & RGB color modes"
|
169
|
+
raise Exception, "color2alpha only intended for GRAYSCALE & RGB color modes"
|
142
170
|
end
|
143
171
|
|
144
172
|
color == @alpha_color ? 0 : (2**hdr.depth-1)
|
@@ -149,6 +177,12 @@ module ZPNG
|
|
149
177
|
###########################################################################
|
150
178
|
# chunks access
|
151
179
|
|
180
|
+
def ihdr
|
181
|
+
@ihdr ||= @chunks.find{ |c| c.is_a?(Chunk::IHDR) }
|
182
|
+
end
|
183
|
+
alias :header :ihdr
|
184
|
+
alias :hdr :ihdr
|
185
|
+
|
152
186
|
def trns
|
153
187
|
# not used "@trns ||= ..." here b/c it will call find() each time of there's no TRNS chunk
|
154
188
|
defined?(@trns) ? @trns : (@trns=@chunks.find{ |c| c.is_a?(Chunk::TRNS) })
|
@@ -163,51 +197,119 @@ module ZPNG
|
|
163
197
|
# image attributes
|
164
198
|
|
165
199
|
def bpp
|
166
|
-
|
200
|
+
ihdr && @ihdr.bpp
|
167
201
|
end
|
168
202
|
|
169
203
|
def width
|
170
|
-
|
204
|
+
ihdr && @ihdr.width
|
171
205
|
end
|
172
206
|
|
173
207
|
def height
|
174
|
-
|
208
|
+
ihdr && @ihdr.height
|
175
209
|
end
|
176
210
|
|
177
211
|
def grayscale?
|
178
|
-
|
212
|
+
ihdr && @ihdr.grayscale?
|
179
213
|
end
|
180
214
|
|
181
215
|
def interlaced?
|
182
|
-
|
216
|
+
ihdr && @ihdr.interlace != 0
|
183
217
|
end
|
184
218
|
|
185
219
|
def alpha_used?
|
186
|
-
|
220
|
+
ihdr && @ihdr.alpha_used?
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
def _imagedata
|
225
|
+
data_chunks = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }
|
226
|
+
case data_chunks.size
|
227
|
+
when 0
|
228
|
+
# no imagedata chunks ?!
|
229
|
+
nil
|
230
|
+
when 1
|
231
|
+
# a single chunk - save memory and return a reference to its data
|
232
|
+
data_chunks[0].data
|
233
|
+
else
|
234
|
+
# multiple data chunks - join their contents
|
235
|
+
data_chunks.map(&:data).join
|
236
|
+
end
|
187
237
|
end
|
238
|
+
public
|
188
239
|
|
189
240
|
def imagedata
|
190
241
|
@imagedata ||=
|
191
242
|
begin
|
192
|
-
puts "[?] no image header, assuming non-interlaced RGB".yellow unless
|
193
|
-
data =
|
243
|
+
puts "[?] no image header, assuming non-interlaced RGB".yellow unless header
|
244
|
+
data = _imagedata
|
245
|
+
#check_zlib_extradata data
|
194
246
|
(data && data.size > 0) ? Zlib::Inflate.inflate(data) : ''
|
195
247
|
end
|
196
248
|
end
|
197
249
|
|
250
|
+
def imagedata_size
|
251
|
+
if new_image?
|
252
|
+
@scanlines.map(&:size).inject(&:+)
|
253
|
+
else
|
254
|
+
imagedata.size
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
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
|
+
# # try to get imagedata size in bytes, w/o storing entire decompressed
|
274
|
+
# # stream in memory. used in bin/zpng
|
275
|
+
# # result: less memory used on big images, but speed gain near 1-2% in best case,
|
276
|
+
# # and 2x slower in worst case because imagedata decoded 2 times
|
277
|
+
# def imagedata_size
|
278
|
+
# if @imagedata
|
279
|
+
# # already decompressed
|
280
|
+
# @imagedata.size
|
281
|
+
# else
|
282
|
+
# zi = nil
|
283
|
+
# @imagedata_size ||=
|
284
|
+
# begin
|
285
|
+
# zi = Zlib::Inflate.new(Zlib::MAX_WBITS)
|
286
|
+
# io = StringIO.new(_imagedata)
|
287
|
+
# while !io.eof? && !zi.finished?
|
288
|
+
# n = zi.inflate(io.read(16384))
|
289
|
+
# end
|
290
|
+
# zi.finish unless zi.finished?
|
291
|
+
# zi.total_out
|
292
|
+
# ensure
|
293
|
+
# zi.close if zi && !zi.closed?
|
294
|
+
# end
|
295
|
+
# end
|
296
|
+
# end
|
297
|
+
|
198
298
|
def metadata
|
199
299
|
@metadata ||= Metadata.new(self)
|
200
300
|
end
|
201
301
|
|
202
302
|
def [] x, y
|
303
|
+
# extracting this check into a module => +1-2% speed
|
203
304
|
x,y = adam7.convert_coords(x,y) if interlaced?
|
204
305
|
scanlines[y][x]
|
205
306
|
end
|
206
307
|
|
207
|
-
def []= x, y,
|
308
|
+
def []= x, y, newcolor
|
309
|
+
# extracting these checks into a module => +1-2% speed
|
208
310
|
decode_all_scanlines
|
209
311
|
x,y = adam7.convert_coords(x,y) if interlaced?
|
210
|
-
scanlines[y][x] =
|
312
|
+
scanlines[y][x] = newcolor
|
211
313
|
end
|
212
314
|
|
213
315
|
# we must decode all scanlines before doing any modifications
|
@@ -261,16 +363,16 @@ module ZPNG
|
|
261
363
|
end
|
262
364
|
|
263
365
|
def export
|
264
|
-
#
|
265
|
-
|
366
|
+
# XXX creating new IDAT must be BEFORE deleting old IDAT chunks
|
367
|
+
idat = Chunk::IDAT.new(
|
368
|
+
:data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
|
369
|
+
)
|
266
370
|
|
267
371
|
# delete old IDAT chunks
|
268
372
|
@chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
|
269
373
|
|
270
|
-
#
|
271
|
-
@chunks <<
|
272
|
-
:data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
|
273
|
-
)
|
374
|
+
# add newly created IDAT
|
375
|
+
@chunks << idat
|
274
376
|
|
275
377
|
# delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
|
276
378
|
@chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
|
@@ -286,12 +388,12 @@ module ZPNG
|
|
286
388
|
decode_all_scanlines
|
287
389
|
|
288
390
|
x,y,h,w = (params[:x]||0), (params[:y]||0), params[:height], params[:width]
|
289
|
-
raise "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
|
391
|
+
raise ArgumentError, "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
|
290
392
|
|
291
393
|
# adjust crop sizes if they greater than image sizes
|
292
394
|
h = self.height-y if (y+h) > self.height
|
293
395
|
w = self.width-x if (x+w) > self.width
|
294
|
-
raise "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
|
396
|
+
raise ArgumentError, "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
|
295
397
|
|
296
398
|
# delete excess scanlines at tail
|
297
399
|
scanlines[(y+h)..-1] = [] if (y+h) < scanlines.size
|
@@ -321,9 +423,13 @@ module ZPNG
|
|
321
423
|
end
|
322
424
|
|
323
425
|
def == other_image
|
324
|
-
|
325
|
-
|
326
|
-
|
426
|
+
return false unless other_image.is_a?(Image)
|
427
|
+
return false if width != other_image.width
|
428
|
+
return false if height != other_image.height
|
429
|
+
each_pixel do |c,x,y|
|
430
|
+
return false if c != other_image[x,y]
|
431
|
+
end
|
432
|
+
true
|
327
433
|
end
|
328
434
|
|
329
435
|
def each_pixel &block
|