zpng 0.0.2 → 0.1.0

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