zpng 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,133 @@
1
+ module ZPNG
2
+ class Chunk
3
+ attr_accessor :size, :type, :data, :crc
4
+
5
+ def self.from_stream io
6
+ size, type = io.read(8).unpack('Na4')
7
+ io.seek(-8,IO::SEEK_CUR)
8
+ begin
9
+ if const_defined?(type.upcase)
10
+ klass = const_get(type.upcase)
11
+ klass.new(io)
12
+ else
13
+ Chunk.new(io)
14
+ end
15
+ rescue NameError
16
+ # invalid chunk type?
17
+ Chunk.new(io)
18
+ end
19
+ end
20
+
21
+ def initialize io
22
+ @size, @type = io.read(8).unpack('Na4')
23
+ @data = io.read(size)
24
+ @crc = io.read(4).to_s.unpack('N').first
25
+ end
26
+
27
+ def export
28
+ @data = self.export_data # virtual
29
+ @crc = Zlib.crc32(data, Zlib.crc32(type))
30
+ [@size,@type].pack('Na4') + @data + [@crc].pack('N')
31
+ end
32
+
33
+ def export_data
34
+ #STDERR.puts "[!] Chunk::#{type} must realize 'export_data' virtual method".yellow if @size != 0
35
+ @data
36
+ end
37
+
38
+ def inspect
39
+ size = @size ? sprintf("%5d",@size) : sprintf("%5s","???")
40
+ crc = @crc ? sprintf("%08x",@crc) : sprintf("%8s","???")
41
+ type = @type.to_s.gsub(/[^0-9a-z]/i){ |x| sprintf("\\x%02X",x.ord) }
42
+ sprintf("#<ZPNG::Chunk %4s size=%s, crc=%s >", type, size, crc)
43
+ end
44
+
45
+ def crc_ok?
46
+ expected_crc = Zlib.crc32(data, Zlib.crc32(type))
47
+ expected_crc == crc
48
+ end
49
+
50
+ class IHDR < Chunk
51
+ attr_accessor :width, :height, :depth, :color, :compression, :filter, :interlace
52
+
53
+ PALETTE_USED = 1
54
+ COLOR_USED = 2
55
+ ALPHA_USED = 4
56
+
57
+ COLOR_GRAYSCALE = 0 # Each pixel is a grayscale sample
58
+ COLOR_RGB = 2 # Each pixel is an R,G,B triple.
59
+ COLOR_INDEXED = 3 # Each pixel is a palette index; a PLTE chunk must appear.
60
+ COLOR_GRAY_ALPHA = 4 # Each pixel is a grayscale sample, followed by an alpha sample.
61
+ COLOR_RGBA = 6 # Each pixel is an R,G,B triple, followed by an alpha sample.
62
+
63
+ SAMPLES_PER_COLOR = {
64
+ COLOR_GRAYSCALE => 1,
65
+ COLOR_RGB => 3,
66
+ COLOR_INDEXED => 1,
67
+ COLOR_GRAY_ALPHA => 2,
68
+ COLOR_RGBA => 4
69
+ }
70
+
71
+ FORMAT = 'NNC5'
72
+
73
+ def initialize io
74
+ super
75
+ @width, @height, @depth, @color, @compression, @filter, @interlace = data.unpack(FORMAT)
76
+ end
77
+
78
+ def export_data
79
+ [@width, @height, @depth, @color, @compression, @filter, @interlace].pack(FORMAT)
80
+ end
81
+
82
+ # bits per pixel
83
+ def bpp
84
+ SAMPLES_PER_COLOR[@color] * depth
85
+ end
86
+
87
+ def color_used?
88
+ (@color & COLOR_USED) != 0
89
+ end
90
+
91
+ def grayscale?
92
+ !color_used?
93
+ end
94
+
95
+ def palette_used?
96
+ (@color & PALETTE_USED) != 0
97
+ end
98
+
99
+ def alpha_used?
100
+ (@color & ALPHA_USED) != 0
101
+ end
102
+
103
+ def inspect
104
+ super.sub(/ *>$/,'') + ", " +
105
+ (instance_variables-[:@type, :@crc, :@data, :@size]).
106
+ map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
107
+ join(", ") + ">"
108
+ end
109
+ end
110
+
111
+ class PLTE < Chunk
112
+ def [] idx
113
+ rgb = @data[idx*3,3]
114
+ rgb && ZPNG::Color.new(*rgb.split('').map(&:ord))
115
+ end
116
+
117
+ def ncolors
118
+ @size/3
119
+ end
120
+
121
+ def index color
122
+ ncolors.times do |i|
123
+ c = self[i]
124
+ return i if c.r == color.r && c.g == color.g && c.b == color.b
125
+ end
126
+ nil
127
+ end
128
+ end
129
+
130
+ class IEND < Chunk
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,90 @@
1
+ require 'zpng'
2
+ require 'optparse'
3
+ require 'hexdump'
4
+ require 'pp'
5
+
6
+ class ZPNG::CLI
7
+
8
+ ACTIONS = {
9
+ 'info' => 'General image info',
10
+ 'chunks' => 'Show file chunks (default)',
11
+ 'ascii' => 'Try to display image as ASCII (works best with monochrome images)',
12
+ 'scanlines' => 'Show scanlines info',
13
+ 'palette' => 'Show palette'
14
+ }
15
+ DEFAULT_ACTIONS = %w'info chunks'
16
+
17
+ def initialize argv = ARGV
18
+ @argv = argv
19
+ end
20
+
21
+ def run
22
+ @actions = []
23
+ @options = { :verbose => 0 }
24
+ optparser = OptionParser.new do |opts|
25
+ opts.banner = "Usage: zpng [options]"
26
+
27
+ opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
28
+ @options[:verbose] += 1
29
+ end
30
+ opts.on "-q", "--quiet", "Silent any warnings (can be used multiple times)" do |v|
31
+ @options[:verbose] -= 1
32
+ end
33
+
34
+ ACTIONS.each do |t,desc|
35
+ opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ]
36
+ end
37
+ end
38
+
39
+ if (argv = optparser.parse(@argv)).empty?
40
+ puts optparser.help
41
+ return
42
+ end
43
+
44
+ @actions = DEFAULT_ACTIONS if @actions.empty?
45
+
46
+ argv.each_with_index do |fname,idx|
47
+ @need_fname_header = (argv.size > 1)
48
+ @file_idx = idx
49
+ @file_name = fname
50
+
51
+ @zpng = load_file fname
52
+
53
+ @actions.each do |action|
54
+ self.send(action) if self.respond_to?(action)
55
+ end
56
+ end
57
+ rescue Errno::EPIPE
58
+ # output interrupt, f.ex. when piping output to a 'head' command
59
+ # prevents a 'Broken pipe - <STDOUT> (Errno::EPIPE)' message
60
+ end
61
+
62
+ def load_file fname
63
+ @img = ZPNG::Image.new fname
64
+ end
65
+
66
+ def info
67
+ puts "[.] image size #{@img.width}x#{@img.height}"
68
+ puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes"
69
+ puts "[.] palette = #{@img.palette}" if @img.palette
70
+ end
71
+
72
+ def chunks
73
+ @img.dump
74
+ end
75
+
76
+ def ascii
77
+ puts @img.to_s
78
+ end
79
+
80
+ def scanlines
81
+ pp @img.scanlines
82
+ end
83
+
84
+ def palette
85
+ if @img.palette
86
+ pp @img.palette
87
+ Hexdump.dump @img.palette.data, :width => 6*3
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,25 @@
1
+ module ZPNG
2
+ class Color < Struct.new(:r,:g,:b,:a)
3
+
4
+ alias :alpha :a
5
+
6
+ BLACK = Color.new(0,0,0,0xff)
7
+ WHITE = Color.new(0xff,0xff,0xff,0xff)
8
+
9
+ def white?
10
+ r == 0xff && g == 0xff && b == 0xff
11
+ end
12
+
13
+ def black?
14
+ r == 0 && g == 0 && b == 0
15
+ end
16
+
17
+ def to_grayscale
18
+ (r+g+b)/3
19
+ end
20
+
21
+ def to_s
22
+ "%02X%02X%02X" % [r,g,b]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,130 @@
1
+ module ZPNG
2
+ class Image
3
+ attr_accessor :data, :header, :chunks, :scanlines, :imagedata, :palette
4
+ alias :hdr :header
5
+
6
+ PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
7
+
8
+ def initialize x = nil
9
+ if x
10
+ if x[PNG_HDR]
11
+ # raw image data
12
+ @data = x
13
+ else
14
+ # filename
15
+ @data = File.binread(x)
16
+ end
17
+ end
18
+
19
+ d = data[0,PNG_HDR.size]
20
+ if d != PNG_HDR
21
+ puts "[!] first #{PNG_HDR.size} bytes must be #{PNG_HDR.inspect}, but got #{d.inspect}".red
22
+ end
23
+
24
+ io = StringIO.new(data)
25
+ io.seek PNG_HDR.size
26
+ @chunks = []
27
+ while !io.eof?
28
+ chunk = Chunk.from_stream(io)
29
+ @chunks << chunk
30
+ case chunk
31
+ when Chunk::IHDR
32
+ @header = chunk
33
+ when Chunk::PLTE
34
+ @palette = chunk
35
+ when Chunk::IEND
36
+ break
37
+ end
38
+ end
39
+ unless io.eof?
40
+ offset = io.tell
41
+ extradata = io.read
42
+ puts "[?] #{extradata.size} bytes of extra data after image end (IEND), offset = 0x#{offset.to_s(16)}".red
43
+ end
44
+ end
45
+
46
+ def dump
47
+ @chunks.each do |chunk|
48
+ puts "[.] #{chunk.inspect} #{chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red}"
49
+ end
50
+ end
51
+
52
+ def width
53
+ @header && @header.width
54
+ end
55
+
56
+ def height
57
+ @header && @header.height
58
+ end
59
+
60
+ def imagedata
61
+ if @header
62
+ raise "only non-interlaced mode is supported for imagedata" if @header.interlace != 0
63
+ else
64
+ puts "[?] no image header, assuming non-interlaced RGB".yellow
65
+ end
66
+ @imagedata ||= Zlib::Inflate.inflate(@chunks.find_all{ |c| c.type == "IDAT" }.map(&:data).join)
67
+ end
68
+
69
+ def [] x, y
70
+ scanlines[y][x]
71
+ end
72
+
73
+ def []= x, y, newpixel
74
+ # we must decode all scanlines before doing any modifications
75
+ # or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
76
+ _decode_all_scanlines unless @_all_scanlines_decoded
77
+ scanlines[y][x] = newpixel
78
+ end
79
+
80
+ def _decode_all_scanlines
81
+ scanlines.each(&:decode!)
82
+ @_all_scanlines_decoded = true
83
+ end
84
+
85
+ def scanlines
86
+ @scanlines ||=
87
+ begin
88
+ r = []
89
+ height.times do |i|
90
+ r << ScanLine.new(self,i)
91
+ end
92
+ r
93
+ end
94
+ end
95
+
96
+ def to_s h={}
97
+ scanlines.map{ |l| l.to_s(h) }.join("\n")
98
+ end
99
+
100
+ def extract_block x,y=nil,w=nil,h=nil
101
+ if x.is_a?(Hash)
102
+ Block.new(self,x[:x], x[:y], x[:width], x[:height])
103
+ else
104
+ Block.new(self,x,y,w,h)
105
+ end
106
+ end
107
+
108
+ def each_block bw,bh, &block
109
+ 0.upto(height/bh-1) do |by|
110
+ 0.upto(width/bw-1) do |bx|
111
+ b = extract_block(bx*bw, by*bh, bw, bh)
112
+ yield b
113
+ end
114
+ end
115
+ end
116
+
117
+ def export
118
+ imagedata # fill @imagedata, if not already filled
119
+
120
+ # delete redundant IDAT chunks
121
+ first_idat = @chunks.find{ |c| c.type == 'IDAT' }
122
+ @chunks.delete_if{ |c| c.type == 'IDAT' && c != first_idat }
123
+
124
+ # fill first_idat @data with compressed imagedata
125
+ first_idat.data = Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
126
+
127
+ PNG_HDR + @chunks.map(&:export).join
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,222 @@
1
+ module ZPNG
2
+ class ScanLine
3
+ FILTER_NONE = 0
4
+ FILTER_SUB = 1
5
+ FILTER_UP = 2
6
+ FILTER_AVERAGE = 3
7
+ FILTER_PAETH = 4
8
+
9
+ attr_accessor :image, :idx, :filter, :offset
10
+
11
+ def initialize image, idx
12
+ @image,@idx = image,idx
13
+ @bpp = image.hdr.bpp
14
+ raise "[!] zero bpp" if @bpp == 0
15
+ if @BPP = (@bpp%8 == 0) && (@bpp>>3)
16
+ @offset = idx*(image.width*@BPP+1)
17
+ else
18
+ @offset = idx*(image.width*@bpp/8.0+1).ceil
19
+ end
20
+ @filter = image.imagedata[@offset].ord
21
+ @offset += 1
22
+ end
23
+
24
+ def inspect
25
+ "#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded]).
26
+ map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
27
+ join(", ") + ">"
28
+ end
29
+
30
+ def to_s h={}
31
+ white = h[:white] || ' '
32
+ black = h[:black] || '#'
33
+ unknown = h[:unknown] || '?'
34
+
35
+ @image.width.times.map do |i|
36
+ px = decode_pixel(i)
37
+ px.white?? white : (px.black?? black : unknown)
38
+ end.join
39
+ end
40
+
41
+ def [] x
42
+ decode_pixel(x)
43
+ end
44
+
45
+ def []= x, newcolor
46
+ case @bpp
47
+ when 1
48
+ flag =
49
+ if image.hdr.palette_used?
50
+ idx = image.palette.index(newcolor)
51
+ raise "no color #{newcolor.inspect} in palette" unless idx
52
+ idx == 1
53
+ else
54
+ if newcolor.white?
55
+ true
56
+ elsif newcolor.black?
57
+ false
58
+ else
59
+ raise "1bpp pixel can only be WHITE or BLACK, got #{newcolor.inspect}"
60
+ end
61
+ end
62
+ if flag
63
+ # turn pixel on
64
+ decoded_bytes[x/8] = (decoded_bytes[x/8].ord | (1<<(7-(x%8)))).chr
65
+ else
66
+ # turn pixel off
67
+ decoded_bytes[x/8] = (decoded_bytes[x/8].ord & (0xff-(1<<(7-(x%8))))).chr
68
+ end
69
+ when 8
70
+ if image.hdr.palette_used?
71
+ decoded_bytes[x] = (image.palette.index(newcolor)).chr
72
+ else
73
+ decoded_bytes[x] = ((newcolor.r + newcolor.g + newcolor.b)/3).chr
74
+ end
75
+ when 16
76
+ if image.hdr.palette_used? && image.hdr.alpha_used?
77
+ decoded_bytes[x*2] = (image.palette.index(newcolor)).chr
78
+ decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
79
+ elsif image.hdr.grayscale? && image.hdr.alpha_used?
80
+ decoded_bytes[x*2] = newcolor.to_grayscale.chr
81
+ decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
82
+ else
83
+ raise "unexpected colormode #{image.hdr.inspect}"
84
+ end
85
+ when 24
86
+ decoded_bytes[x*3,3] = [newcolor.r, newcolor.g, newcolor.b].map(&:chr).join
87
+ when 32
88
+ decoded_bytes[x*4,4] = [newcolor.r, newcolor.g, newcolor.b, newcolor.a].map(&:chr).join
89
+ else raise "unsupported bpp #{@bpp}"
90
+ end
91
+ end
92
+
93
+ def decode_pixel x
94
+ raw =
95
+ if @BPP
96
+ # 8, 16 or 32 bits per pixel
97
+ decoded_bytes[x*@BPP, @BPP]
98
+ else
99
+ # 1, 2 or 4 bits per pixel
100
+ decoded_bytes[x*@bpp/8, (@bpp/8.0).ceil]
101
+ end
102
+
103
+ r = g = b = a = nil
104
+
105
+ colormode = image.hdr.color
106
+
107
+ if image.hdr.palette_used?
108
+ idx =
109
+ case @bpp
110
+ when 1
111
+ # needed for palette
112
+ (raw.ord & (1<<(7-(x%8)))) == 0 ? 0 : 1
113
+ when 8
114
+ raw.ord
115
+ when 16
116
+ raw[0].ord
117
+ else raise "unexpected bpp #{@bpp}"
118
+ end
119
+
120
+ return image.palette[idx]
121
+ end
122
+
123
+ case @bpp
124
+ when 1
125
+ r=g=b= (raw.ord & (1<<(7-(x%8)))) == 0 ? 0 : 0xff
126
+ when 8
127
+ if colormode == ZPNG::Chunk::IHDR::COLOR_GRAYSCALE
128
+ r=g=b= raw.ord
129
+ else
130
+ raise "unexpected colormode #{colormode} for bpp #{@bpp}"
131
+ end
132
+ when 16
133
+ if colormode == ZPNG::Chunk::IHDR::COLOR_GRAY_ALPHA
134
+ r=g=b= raw[0].ord
135
+ a = raw[1].ord
136
+ else
137
+ raise "unexpected colormode #{colormode} for bpp #{@bpp}"
138
+ end
139
+ when 24
140
+ r,g,b = raw.split('').map(&:ord)
141
+ when 32
142
+ r,g,b,a = raw.split('').map(&:ord)
143
+ else raise "unexpected bpp #{@bpp}"
144
+ end
145
+
146
+ Color.new(r,g,b,a)
147
+ end
148
+
149
+ def decoded_bytes
150
+ @decoded_bytes ||=
151
+ begin
152
+ # number of bytes per complete pixel, rounding up to one
153
+ bpp1 = (@bpp/8.0).ceil
154
+
155
+ # bytes in one scanline
156
+ nbytes = (image.width*@bpp/8.0).ceil
157
+
158
+ s = ''
159
+ nbytes.times do |i|
160
+ b0 = (i-bpp1) >= 0 ? s[i-bpp1] : nil
161
+ s[i] = decode_byte(i, b0, bpp1)
162
+ end
163
+ # print Hexdump.dump(s[0,16])
164
+ s
165
+ end
166
+ end
167
+
168
+ def decode!
169
+ decoded_bytes
170
+ true
171
+ end
172
+
173
+ def decode_byte x, b0, bpp1
174
+ raw = @image.imagedata[@offset+x]
175
+
176
+ unless raw
177
+ STDERR.puts "[!] not enough bytes at pos #{x} of scanline #@idx".red
178
+ raw = 0.chr
179
+ end
180
+
181
+ case @filter
182
+ when FILTER_NONE # 0
183
+ raw
184
+
185
+ when FILTER_SUB # 1
186
+ return raw unless b0
187
+ ((raw.ord + b0.ord) & 0xff).chr
188
+
189
+ when FILTER_UP # 2
190
+ return raw if @idx == 0
191
+ prev = @image.scanlines[@idx-1].decoded_bytes[x]
192
+ ((raw.ord + prev.ord) & 0xff).chr
193
+
194
+ when FILTER_AVERAGE # 3
195
+ prev = (b0 && b0.ord) || 0
196
+ prior = (@idx > 0) ? @image.scanlines[@idx-1].decoded_bytes[x].ord : 0
197
+ ((raw.ord + (prev + prior)/2) & 0xff).chr
198
+
199
+ when FILTER_PAETH # 4
200
+ pa = (b0 && b0.ord) || 0
201
+ pb = (@idx > 0) ? @image.scanlines[@idx-1].decoded_bytes[x].ord : 0
202
+ pc = (b0 && @idx > 0) ? @image.scanlines[@idx-1].decoded_bytes[x-bpp1].ord : 0
203
+ ((raw.ord + paeth_predictor(pa, pb, pc)) & 0xff).chr
204
+ else
205
+ raise "invalid ScanLine filter #{@filter}"
206
+ end
207
+ end
208
+
209
+ def paeth_predictor a,b,c
210
+ p = a + b - c
211
+ pa = (p - a).abs
212
+ pb = (p - b).abs
213
+ pc = (p - c).abs
214
+ (pa <= pb) ? (pa <= pc ? a : c) : (pb <= pc ? b : c)
215
+ end
216
+
217
+ def export
218
+ # we export in FILTER_NONE mode
219
+ "\x00" + decoded_bytes
220
+ end
221
+ end
222
+ end