zpng 0.0.1

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