zpng 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,8 +6,8 @@ require 'pp'
6
6
  class ZPNG::CLI
7
7
 
8
8
  ACTIONS = {
9
- 'info' => 'General image info',
10
9
  'chunks' => 'Show file chunks (default)',
10
+ %w'i info' => 'General image info (default)',
11
11
  'ascii' => 'Try to display image as ASCII (works best with monochrome images)',
12
12
  'scanlines' => 'Show scanlines info',
13
13
  'palette' => 'Show palette'
@@ -22,7 +22,7 @@ class ZPNG::CLI
22
22
  @actions = []
23
23
  @options = { :verbose => 0 }
24
24
  optparser = OptionParser.new do |opts|
25
- opts.banner = "Usage: zpng [options]"
25
+ opts.banner = "Usage: zpng [options] filename.png"
26
26
 
27
27
  opts.on "-v", "--verbose", "Run verbosely (can be used multiple times)" do |v|
28
28
  @options[:verbose] += 1
@@ -32,12 +32,24 @@ class ZPNG::CLI
32
32
  end
33
33
 
34
34
  ACTIONS.each do |t,desc|
35
- opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ]
35
+ if t.is_a?(Array)
36
+ opts.on *[ "-#{t[0]}", "--#{t[1]}", desc, eval("lambda{ |_| @actions << :#{t[1]} }") ]
37
+ else
38
+ opts.on *[ "-#{t[0].upcase}", "--#{t}", desc, eval("lambda{ |_| @actions << :#{t} }") ]
39
+ end
36
40
  end
37
41
 
38
- opts.on "-E", "--extract-chunk id" do |id|
42
+ opts.on "-E", "--extract-chunk ID", "extract a single chunk" do |id|
39
43
  @actions << [:extract_chunk, id.to_i]
40
44
  end
45
+ opts.on "-U", "--unpack-imagedata", "unpack Image Data (IDAT) chunk(s), output to stdout" do
46
+ @actions << :unpack_imagedata
47
+ end
48
+
49
+ opts.on "-c", "--crop GEOMETRY", "crop image, {WIDTH}x{HEIGHT}+{X}+{Y},",
50
+ "puts results on stdout unless --ascii given" do |x|
51
+ @actions << [:crop, x]
52
+ end
41
53
  end
42
54
 
43
55
  if (argv = optparser.parse(@argv)).empty?
@@ -80,12 +92,25 @@ class ZPNG::CLI
80
92
  end
81
93
  end
82
94
 
95
+ def unpack_imagedata
96
+ print @img.imagedata
97
+ end
98
+
99
+ def crop geometry
100
+ unless geometry =~ /\A(\d+)x(\d+)\+(\d+)\+(\d+)\Z/i
101
+ STDERR.puts "[!] invalid geometry #{geometry.inspect}, must be WxH+X+Y, like 100x100+10+10"
102
+ exit 1
103
+ end
104
+ @img.crop! :width => $1.to_i, :height => $2.to_i, :x => $3.to_i, :y => $4.to_i
105
+ print @img.export unless @actions.include?(:ascii)
106
+ end
107
+
83
108
  def load_file fname
84
109
  @img = ZPNG::Image.new fname
85
110
  end
86
111
 
87
112
  def info
88
- puts "[.] image size #{@img.width}x#{@img.height}"
113
+ puts "[.] image size #{@img.width || '?'}x#{@img.height || '?'}"
89
114
  puts "[.] uncompressed imagedata size = #{@img.imagedata.size} bytes"
90
115
  puts "[.] palette = #{@img.palette}" if @img.palette
91
116
  end
@@ -1,7 +1,13 @@
1
1
  module ZPNG
2
2
  class Color < Struct.new(:r,:g,:b,:a)
3
3
 
4
+ def initialize *args
5
+ super
6
+ self.a ||= 0xff
7
+ end
8
+
4
9
  alias :alpha :a
10
+ def alpha= v; self.a = v; end
5
11
 
6
12
  BLACK = Color.new(0,0,0,0xff)
7
13
  WHITE = Color.new(0xff,0xff,0xff,0xff)
@@ -14,12 +20,36 @@ module ZPNG
14
20
  r == 0 && g == 0 && b == 0
15
21
  end
16
22
 
23
+ def transparent?
24
+ a == 0
25
+ end
26
+
17
27
  def to_grayscale
18
28
  (r+g+b)/3
19
29
  end
20
30
 
31
+ def self.from_grayscale value, alpha = nil
32
+ Color.new value,value,value, alpha
33
+ end
34
+
21
35
  def to_s
22
36
  "%02X%02X%02X" % [r,g,b]
23
37
  end
38
+
39
+ def to_i
40
+ ((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
41
+ end
42
+
43
+ def inspect
44
+ if r && g && b && a
45
+ "#<ZPNG::Color #%02x%02x%02x a=%d>" % [r,g,b,a]
46
+ else
47
+ rs = r ? "%02x" % r : "??"
48
+ gs = g ? "%02x" % g : "??"
49
+ bs = b ? "%02x" % b : "??"
50
+ as = a ? "%d" % a : "?"
51
+ "#<ZPNG::Color #%s%s%s%s a=%s>" % [rs,gs,bs,as]
52
+ end
53
+ end
24
54
  end
25
55
  end
@@ -5,7 +5,55 @@ module ZPNG
5
5
 
6
6
  PNG_HDR = "\x89PNG\x0d\x0a\x1a\x0a"
7
7
 
8
- def initialize x = nil
8
+ def initialize x
9
+ @chunks = []
10
+ case x
11
+ when IO
12
+ _from_string x.read
13
+ when String
14
+ _from_string x
15
+ when Hash
16
+ _from_hash x
17
+ else
18
+ raise "unsupported input data type #{x.class}"
19
+ end
20
+ if palette && hdr && hdr.depth
21
+ palette.max_colors = 2**hdr.depth
22
+ end
23
+ end
24
+
25
+ # load image from file
26
+ def self.load fname
27
+ open(fname,"rb") do |f|
28
+ Image.new(f)
29
+ end
30
+ end
31
+ alias :load_file :load
32
+
33
+ # save image to file
34
+ def save fname
35
+ File.open(fname,"wb"){ |f| f << export }
36
+ end
37
+
38
+ # flag that image is just created, and NOT loaded from file
39
+ # as in Rails' ActiveRecord::Base#new_record?
40
+ def new_image?
41
+ @new_image
42
+ end
43
+ alias :new? :new_image?
44
+
45
+ private
46
+
47
+ def _from_hash h
48
+ @new_image = true
49
+ @chunks << (@header = Chunk::IHDR.new(h))
50
+ if @header.palette_used?
51
+ @chunks << (@palette = Chunk::PLTE.new)
52
+ @palette[0] = h[:bg] || Color::BLACK # add default bg color
53
+ end
54
+ end
55
+
56
+ def _from_string x
9
57
  if x
10
58
  if x[PNG_HDR]
11
59
  # raw image data
@@ -23,7 +71,6 @@ module ZPNG
23
71
 
24
72
  io = StringIO.new(data)
25
73
  io.seek PNG_HDR.size
26
- @chunks = []
27
74
  while !io.eof?
28
75
  chunk = Chunk.from_stream(io)
29
76
  chunk.idx = @chunks.size
@@ -44,6 +91,7 @@ module ZPNG
44
91
  end
45
92
  end
46
93
 
94
+ public
47
95
  def dump
48
96
  @chunks.each do |chunk|
49
97
  puts "[.] #{chunk.inspect} #{chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red}"
@@ -58,13 +106,21 @@ module ZPNG
58
106
  @header && @header.height
59
107
  end
60
108
 
109
+ def grayscale?
110
+ @header && @header.grayscale?
111
+ end
112
+
61
113
  def imagedata
62
- if @header
63
- raise "only non-interlaced mode is supported for imagedata" if @header.interlace != 0
64
- else
65
- puts "[?] no image header, assuming non-interlaced RGB".yellow
66
- end
67
- @imagedata ||= Zlib::Inflate.inflate(@chunks.find_all{ |c| c.type == "IDAT" }.map(&:data).join)
114
+ @imagedata ||=
115
+ begin
116
+ if @header
117
+ raise "only non-interlaced mode is supported for imagedata" if @header.interlace != 0
118
+ else
119
+ puts "[?] no image header, assuming non-interlaced RGB".yellow
120
+ end
121
+ data = @chunks.find_all{ |c| c.is_a?(Chunk::IDAT) }.map(&:data).join
122
+ (data && data.size > 0) ? Zlib::Inflate.inflate(data) : ''
123
+ end
68
124
  end
69
125
 
70
126
  def [] x, y
@@ -72,30 +128,36 @@ module ZPNG
72
128
  end
73
129
 
74
130
  def []= x, y, newpixel
75
- # we must decode all scanlines before doing any modifications
76
- # or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
77
- _decode_all_scanlines unless @_all_scanlines_decoded
131
+ decode_all_scanlines
78
132
  scanlines[y][x] = newpixel
79
133
  end
80
134
 
81
- def _decode_all_scanlines
135
+ # we must decode all scanlines before doing any modifications
136
+ # or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
137
+ def decode_all_scanlines
138
+ return if @all_scanlines_decoded
139
+ @all_scanlines_decoded = true
82
140
  scanlines.each(&:decode!)
83
- @_all_scanlines_decoded = true
84
141
  end
85
142
 
86
143
  def scanlines
87
144
  @scanlines ||=
88
145
  begin
89
146
  r = []
90
- height.times do |i|
147
+ height.to_i.times do |i|
91
148
  r << ScanLine.new(self,i)
92
149
  end
150
+ r.delete_if(&:bad?)
93
151
  r
94
152
  end
95
153
  end
96
154
 
97
155
  def to_s h={}
98
- scanlines.map{ |l| l.to_s(h) }.join("\n")
156
+ if scanlines.any?
157
+ scanlines.map{ |l| l.to_s(h) }.join("\n")
158
+ else
159
+ super()
160
+ end
99
161
  end
100
162
 
101
163
  def extract_block x,y=nil,w=nil,h=nil
@@ -118,14 +180,55 @@ module ZPNG
118
180
  def export
119
181
  imagedata # fill @imagedata, if not already filled
120
182
 
121
- # delete redundant IDAT chunks
122
- first_idat = @chunks.find{ |c| c.type == 'IDAT' }
123
- @chunks.delete_if{ |c| c.type == 'IDAT' && c != first_idat }
183
+ # delete old IDAT chunks
184
+ @chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
124
185
 
125
186
  # fill first_idat @data with compressed imagedata
126
- first_idat.data = Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
187
+ @chunks << Chunk::IDAT.new(
188
+ :data => Zlib::Deflate.deflate(scanlines.map(&:export).join, 9)
189
+ )
190
+
191
+ # delete IEND chunk(s) b/c we just added a new chunk and IEND must be the last one
192
+ @chunks.delete_if{ |c| c.is_a?(Chunk::IEND) }
193
+
194
+ # add fresh new IEND
195
+ @chunks << Chunk::IEND.new
127
196
 
128
197
  PNG_HDR + @chunks.map(&:export).join
129
198
  end
199
+
200
+ # modifies this image
201
+ def crop! params
202
+ decode_all_scanlines
203
+
204
+ x,y,h,w = (params[:x]||0), (params[:y]||0), params[:height], params[:width]
205
+ raise "negative params not allowed" if [x,y,h,w].any?{ |x| x < 0 }
206
+
207
+ # adjust crop sizes if they greater than image sizes
208
+ h = self.height-y if (y+h) > self.height
209
+ w = self.width-x if (x+w) > self.width
210
+ raise "negative params not allowed (p2)" if [x,y,h,w].any?{ |x| x < 0 }
211
+
212
+ # delete excess scanlines at tail
213
+ scanlines[(y+h)..-1] = [] if (y+h) < scanlines.size
214
+
215
+ # delete excess scanlines at head
216
+ scanlines[0,y] = [] if y > 0
217
+
218
+ # crop remaining scanlines
219
+ scanlines.each{ |l| l.crop!(x,w) }
220
+
221
+ # modify header
222
+ hdr.height, hdr.width = h, w
223
+
224
+ # return self
225
+ self
226
+ end
227
+
228
+ # returns new image
229
+ def crop params
230
+ # deep copy first, then crop!
231
+ Marshal.load(Marshal.dump(self)).crop!(params)
232
+ end
130
233
  end
131
234
  end
@@ -1,3 +1,4 @@
1
+ #coding: binary
1
2
  module ZPNG
2
3
  class ScanLine
3
4
  FILTER_NONE = 0
@@ -12,17 +13,41 @@ module ZPNG
12
13
  @image,@idx = image,idx
13
14
  @bpp = image.hdr.bpp
14
15
  raise "[!] zero bpp" if @bpp == 0
15
- if @BPP = (@bpp%8 == 0) && (@bpp>>3)
16
- @offset = idx*(image.width*@BPP+1)
16
+
17
+ # Bytes Per Pixel, if bpp = 8, 16, 24, 32
18
+ # NULL otherwise
19
+ @BPP = (@bpp%8 == 0) && (@bpp>>3)
20
+
21
+ if @image.new?
22
+ @decoded_bytes = "\x00" * (size-1)
23
+ @filter = FILTER_NONE
17
24
  else
18
- @offset = idx*(image.width*@bpp/8.0+1).ceil
25
+ @offset = idx*size
26
+ if @filter = image.imagedata[@offset]
27
+ @filter = @filter.ord
28
+ else
29
+ STDERR.puts "[!] #{self.class}: ##@idx: no data at pos 0, scanline dropped".red
30
+ end
31
+ @offset += 1
32
+ end
33
+ end
34
+
35
+ # ScanLine is BAD if it has no filter
36
+ def bad?
37
+ !@filter
38
+ end
39
+
40
+ # total scanline size in bytes, INCLUDING leading 'filter' byte
41
+ def size
42
+ if @BPP
43
+ image.width*@BPP+1
44
+ else
45
+ (image.width*@bpp/8.0+1).ceil
19
46
  end
20
- @filter = image.imagedata[@offset].ord
21
- @offset += 1
22
47
  end
23
48
 
24
49
  def inspect
25
- "#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded]).
50
+ "#<ZPNG::ScanLine " + (instance_variables-[:@image, :@decoded, :@BPP]).
26
51
  map{ |var| "#{var.to_s.tr('@','')}=#{instance_variable_get(var)}" }.
27
52
  join(", ") + ">"
28
53
  end
@@ -34,6 +59,7 @@ module ZPNG
34
59
 
35
60
  @image.width.times.map do |i|
36
61
  px = decode_pixel(i)
62
+ # printf "[d] %08x %s %s\n", px.to_i, px.inspect, px.to_s unless px.black?
37
63
  px.white?? white : (px.black?? black : unknown)
38
64
  end.join
39
65
  end
@@ -43,40 +69,37 @@ module ZPNG
43
69
  end
44
70
 
45
71
  def []= x, newcolor
72
+ if image.hdr.palette_used?
73
+ color_idx = image.palette.find_or_add(newcolor)
74
+ raise "no color #{newcolor.inspect} in palette" unless color_idx
75
+ elsif image.grayscale?
76
+ color_idx = newcolor.to_grayscale
77
+ end
78
+
46
79
  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
80
+ when 1,2,4
81
+ pos = x*@bpp/8
82
+ b = decoded_bytes[pos].ord
83
+ mask = 2**@bpp-1
84
+ shift = 8-(x%(8/@bpp)+1)*@bpp
85
+ raise "invalid shift #{shift}" if shift < 0 || shift > 7
86
+
87
+ # printf "[d] %s x=%2d bpp=%d pos=%d mask=%08b shift=%d decoded_bytes=#{decoded_bytes.inspect}\n", self.to_s, x, @bpp, pos, mask, shift
88
+
89
+ b = (b & (0xff-(mask<<shift))) | ((color_idx & mask) << shift)
90
+ decoded_bytes[pos] = b.chr
91
+
69
92
  when 8
70
93
  if image.hdr.palette_used?
71
- decoded_bytes[x] = (image.palette.index(newcolor)).chr
94
+ decoded_bytes[x] = color_idx.chr
72
95
  else
73
96
  decoded_bytes[x] = ((newcolor.r + newcolor.g + newcolor.b)/3).chr
74
97
  end
75
98
  when 16
76
99
  if image.hdr.palette_used? && image.hdr.alpha_used?
77
- decoded_bytes[x*2] = (image.palette.index(newcolor)).chr
100
+ decoded_bytes[x*2] = color_idx.chr
78
101
  decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
79
- elsif image.hdr.grayscale? && image.hdr.alpha_used?
102
+ elsif image.grayscale? && image.hdr.alpha_used?
80
103
  decoded_bytes[x*2] = newcolor.to_grayscale.chr
81
104
  decoded_bytes[x*2+1] = (newcolor.alpha || 0xff).chr
82
105
  else
@@ -97,56 +120,51 @@ module ZPNG
97
120
  decoded_bytes[x*@BPP, @BPP]
98
121
  else
99
122
  # 1, 2 or 4 bits per pixel
100
- decoded_bytes[x*@bpp/8, (@bpp/8.0).ceil]
123
+ decoded_bytes[x*@bpp/8]
101
124
  end
102
125
 
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
126
+ color, alpha =
127
+ case @bpp
128
+ when 1,2,4
129
+ mask = 2**@bpp-1
130
+ shift = 8-(x%(8/@bpp)+1)*@bpp
131
+ raise "invalid shift #{shift}" if shift < 0 || shift > 7
132
+ [(raw.ord >> shift) & mask, nil]
133
+ when 8
134
+ [raw.ord, nil]
135
+ when 16
136
+ raw.unpack 'C2'
137
+ when 24
138
+ # RGB
139
+ return Color.new(*raw.unpack('C3'))
140
+ when 32
141
+ # RGBA
142
+ return Color.new(*raw.unpack('C4'))
129
143
  else
130
- raise "unexpected colormode #{colormode} for bpp #{@bpp}"
144
+ raise "unexpected bpp #{@bpp}"
131
145
  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}"
146
+
147
+ if image.grayscale?
148
+ if [1,2,4].include?(@bpp)
149
+ #color should be extended to a 8-bit range
150
+ if color%2 == 0
151
+ color <<= (8-@bpp)
152
+ else
153
+ (8-@bpp).times{ color = color*2 + 1 }
154
+ end
138
155
  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}"
156
+ Color.from_grayscale(color, alpha)
157
+ elsif image.palette
158
+ color = image.palette[color]
159
+ color.alpha = alpha
160
+ color
161
+ else
162
+ raise "cannot decode color"
144
163
  end
145
-
146
- Color.new(r,g,b,a)
147
164
  end
148
165
 
149
166
  def decoded_bytes
167
+ raise if caller.size > 50
150
168
  @decoded_bytes ||=
151
169
  begin
152
170
  # number of bytes per complete pixel, rounding up to one
@@ -170,11 +188,12 @@ module ZPNG
170
188
  true
171
189
  end
172
190
 
191
+ private
173
192
  def decode_byte x, b0, bpp1
174
193
  raw = @image.imagedata[@offset+x]
175
194
 
176
195
  unless raw
177
- STDERR.puts "[!] not enough bytes at pos #{x} of scanline #@idx".red
196
+ STDERR.puts "[!] #{self.class}: ##@idx: no data at pos #{x}".red
178
197
  raw = 0.chr
179
198
  end
180
199
 
@@ -214,9 +233,54 @@ module ZPNG
214
233
  (pa <= pb) ? (pa <= pc ? a : c) : (pb <= pc ? b : c)
215
234
  end
216
235
 
236
+ public
237
+ def crop! x, w
238
+ if @BPP
239
+ # great, crop is byte-aligned! :)
240
+ decoded_bytes[0,x*@BPP] = ''
241
+ decoded_bytes[w*@BPP..-1] = ''
242
+ else
243
+ # oh, no we have to shift bits in a whole line :(
244
+ case @bpp
245
+ when 1,2,4
246
+ cut_bits_head = @bpp*x
247
+ if cut_bits_head > 8
248
+ # cut whole head bytes
249
+ decoded_bytes[0,cut_bits_head/8] = ''
250
+ end
251
+ cut_bits_head %= 8
252
+ if cut_bits_head > 0
253
+ # bit-shift all remaining bytes
254
+ (w*@bpp/8.0).ceil.times do |i|
255
+ decoded_bytes[i] = ((
256
+ (decoded_bytes[i].ord<<cut_bits_head) |
257
+ (decoded_bytes[i+1].ord>>(8-cut_bits_head))
258
+ ) & 0xff).chr
259
+ end
260
+ end
261
+
262
+ new_width_bits = w*@bpp
263
+ diff = decoded_bytes.size*8 - new_width_bits
264
+ raise if diff < 0
265
+ if diff > 8
266
+ # cut whole tail bytes
267
+ decoded_bytes[(new_width_bits/8.0).ceil..-1] = ''
268
+ end
269
+ diff %= 8
270
+ if diff > 0
271
+ # zero tail bits of last byte
272
+ decoded_bytes[-1] = (decoded_bytes[-1].ord & (0xff-(2**diff)+1)).chr
273
+ end
274
+
275
+ else
276
+ raise "unexpected bpp=#@bpp"
277
+ end
278
+ end
279
+ end
280
+
217
281
  def export
218
282
  # we export in FILTER_NONE mode
219
- "\x00" + decoded_bytes
283
+ FILTER_NONE.chr + decoded_bytes
220
284
  end
221
285
  end
222
286
  end