zpng 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
1
  module ZPNG
2
2
  class Color
3
- attr_accessor :r, :g, :b, :a
4
- attr_accessor :depth, :alpha_depth
3
+ attr_accessor :r, :g, :b
4
+ attr_reader :a
5
+ attr_accessor :depth
5
6
 
6
7
  include DeepCopyable
7
8
 
@@ -9,16 +10,18 @@ module ZPNG
9
10
  h = a.last.is_a?(Hash) ? a.pop : {}
10
11
  @r,@g,@b,@a = *a
11
12
 
12
- # default ALPHA = 0xff - opaque
13
- @a ||= h[:alpha] || 0xff
14
-
15
13
  # default sample depth for r,g,b and alpha = 8 bits
16
14
  @depth = h[:depth] || 8
17
- @alpha_depth = h[:alpha_depth] || @depth
15
+
16
+ # default ALPHA = 0xff - opaque
17
+ @a ||= h[:alpha] || (2**@depth-1)
18
18
  end
19
19
 
20
- alias :alpha :a
21
- def alpha= a; @a=a; end
20
+ def a= a
21
+ @a = a || (2**@depth-1) # NULL alpha means fully opaque
22
+ end
23
+ alias :alpha :a
24
+ alias :alpha= :a=
22
25
 
23
26
  BLACK = Color.new(0 , 0, 0)
24
27
  WHITE = Color.new(255,255,255)
@@ -32,6 +35,8 @@ module ZPNG
32
35
  PURPLE= MAGENTA =
33
36
  Color.new(255, 0,255)
34
37
 
38
+ TRANSPARENT = Color.new(0,0,0,0)
39
+
35
40
  ANSI_COLORS = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
36
41
 
37
42
  #ASCII_MAP = %q_ .`,-:;~"!<+*^(LJ=?vctsxj12FuoCeyPSah5wVmXA4G9$OR0MQNW#&%@_
@@ -51,6 +56,7 @@ module ZPNG
51
56
 
52
57
  # euclidian distance - http://en.wikipedia.org/wiki/Euclidean_distance
53
58
  def euclidian other_color
59
+ # TODO: different depths
54
60
  r = (self.r.to_i - other_color.r.to_i)**2
55
61
  r += (self.g.to_i - other_color.g.to_i)**2
56
62
  r += (self.b.to_i - other_color.b.to_i)**2
@@ -58,7 +64,8 @@ module ZPNG
58
64
  end
59
65
 
60
66
  def white?
61
- r == 0xff && g == 0xff && b == 0xff
67
+ max = 2**depth-1
68
+ r == max && g == max && b == max
62
69
  end
63
70
 
64
71
  def black?
@@ -69,26 +76,51 @@ module ZPNG
69
76
  a == 0
70
77
  end
71
78
 
79
+ def opaque?
80
+ a.nil? || a == 2**depth-1
81
+ end
82
+
72
83
  def to_grayscale
73
84
  (r+g+b)/3
74
85
  end
75
86
 
76
- def self.from_grayscale value, alpha_or_hash = nil
77
- Color.new value,value,value, alpha_or_hash
87
+ def to_gray_alpha
88
+ [to_grayscale, alpha]
89
+ end
90
+
91
+ # from_grayscale level
92
+ # from_grayscale level, :depth => 16
93
+ # from_grayscale level, alpha
94
+ # from_grayscale level, alpha, :depth => 16
95
+ def self.from_grayscale value, *args
96
+ Color.new value,value,value, *args
97
+ end
98
+
99
+ ########################################################
100
+ # simple conversions
101
+
102
+ def to_i
103
+ ((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
78
104
  end
79
105
 
80
106
  def to_s
81
107
  "%02X%02X%02X" % [r,g,b]
82
108
  end
83
109
 
110
+ def to_a
111
+ [r, g, b, a]
112
+ end
113
+
84
114
  ########################################################
115
+ # complex conversions
85
116
 
86
- # try to convert to pseudographics
117
+ # try to convert to one pseudographics ASCII character
87
118
  def to_ascii map=ASCII_MAP
88
119
  #p self
89
120
  map[self.to_grayscale*(map.size-1)/(2**@depth-1), 1]
90
121
  end
91
122
 
123
+ # convert to ANSI color name
92
124
  def to_ansi
93
125
  return to_depth(8).to_ansi if depth != 8
94
126
  a = ANSI_COLORS.map{|c| self.class.const_get(c.to_s.upcase) }
@@ -96,6 +128,7 @@ module ZPNG
96
128
  ANSI_COLORS[a.index(a.min)]
97
129
  end
98
130
 
131
+ # HTML/CSS color in notation like #33aa88
99
132
  def to_css
100
133
  return to_depth(8).to_css if depth != 8
101
134
  "#%02X%02X%02X" % [r,g,b]
@@ -104,30 +137,22 @@ module ZPNG
104
137
 
105
138
  ########################################################
106
139
 
107
- def to_i
108
- ((a||0) << 24) + ((r||0) << 16) + ((g||0) << 8) + (b||0)
109
- end
110
-
111
140
  # change bit depth, return new Color
112
141
  def to_depth new_depth
113
- c = Color.new :depth => new_depth
142
+ return self if depth == new_depth
143
+
144
+ color = Color.new :depth => new_depth
114
145
  if new_depth > self.depth
115
- %w'r g b'.each do |part|
116
- color = self.send(part)
117
- if color%2 == 0
118
- color <<= (new_depth-self.depth)
119
- else
120
- (new_depth-self.depth).times{ color = color*2 + 1 }
121
- end
122
- c.send("#{part}=", color)
146
+ %w'r g b a'.each do |part|
147
+ color.send("#{part}=", (2**new_depth-1)/(2**depth-1)*self.send(part))
123
148
  end
124
149
  else
125
150
  # new_depth < self.depth
126
- %w'r g b'.each do |part|
127
- c.send("#{part}=", self.send(part)>>(self.depth-new_depth))
151
+ %w'r g b a'.each do |part|
152
+ color.send("#{part}=", self.send(part)>>(self.depth-new_depth))
128
153
  end
129
154
  end
130
- c
155
+ color
131
156
  end
132
157
 
133
158
  def inspect
@@ -136,16 +161,46 @@ module ZPNG
136
161
  s << " r=" + (r ? "%04x" % r : "????")
137
162
  s << " g=" + (g ? "%04x" % g : "????")
138
163
  s << " b=" + (b ? "%04x" % b : "????")
164
+ s << " alpha=%04x" % alpha if alpha && alpha != 0xffff
139
165
  else
140
166
  s << " #"
141
167
  s << (r ? "%02x" % r : "??")
142
168
  s << (g ? "%02x" % g : "??")
143
169
  s << (b ? "%02x" % b : "??")
170
+ s << " alpha=%02x" % alpha if alpha && alpha != 0xff
144
171
  end
145
- s << " a=#{a}" if a && alpha_depth != 0
146
172
  s << " depth=#{depth}" if depth != 8
147
- s << " alpha_depth=#{alpha_depth}" if alpha_depth != 8 && alpha_depth != 0
148
173
  s << ">"
149
174
  end
175
+
176
+ # compare with other color
177
+ def == c
178
+ return false unless c.is_a?(Color)
179
+ c1,c2 =
180
+ if self.depth > c.depth
181
+ [self, c.to_depth(self.depth)]
182
+ else
183
+ [self.to_depth(c.depth), c]
184
+ end
185
+ c1.r == c2.r && c1.g == c2.g && c1.b == c2.b && c1.a == c2.a
186
+ end
187
+ alias :eql? :==
188
+
189
+ # compare with other color
190
+ def <=> c
191
+ c1,c2 =
192
+ if self.depth > c.depth
193
+ [self, c.to_depth(self.depth)]
194
+ else
195
+ [self.to_depth(c.depth), c]
196
+ end
197
+ r = c1.to_grayscale <=> c2.to_grayscale
198
+ r == 0 ? (c1.to_a <=> c2.to_a) : r
199
+ end
200
+
201
+ # for Array.uniq()
202
+ def hash
203
+ self.to_i
204
+ end
150
205
  end
151
206
  end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module ZPNG
4
+ module Hexdump
5
+
6
+ def hexdump *args, &block
7
+ print Hexdump.dump(*args, &block)
8
+ end
9
+
10
+ class << self
11
+ def dump data, h = {}
12
+ offset = h[:offset] || 0
13
+ add = h[:add] || 0
14
+ size = h[:size] || (data.size-offset)
15
+ tail = h[:tail] || "\n"
16
+ width = h[:width] || 0x10 # row width, in bytes
17
+
18
+ h[:show_offset] = true unless h.key?(:show_offset)
19
+ h[:dedup] = true unless h.key?(:dedup)
20
+
21
+ size = data.size-offset if size+offset > data.size
22
+
23
+ r = ''; prevhex = ''; c = nil; prevdup = false
24
+ while true
25
+ ascii = ''; hex = ''
26
+ width.times do |i|
27
+ hex << ' ' if i%8==0 && i>0
28
+ if c = ((size > 0) && data[offset+i])
29
+ hex << "%02x " % c.ord
30
+ ascii << ((32..126).include?(c.ord) ? c : '.')
31
+ else
32
+ hex << ' '
33
+ ascii << ' '
34
+ end
35
+ size-=1
36
+ end
37
+
38
+ if h[:dedup] && hex == prevhex
39
+ row = "*"
40
+ yield(row, offset+add, ascii) if block_given?
41
+ r << row << "\n" unless prevdup
42
+ prevdup = true
43
+ else
44
+ row = (h[:show_offset] ? ("%08x: " % (offset + add)) : '') + hex + ' |' + ascii + "|"
45
+ yield(row, offset+add, ascii) if block_given?
46
+ r << row << "\n"
47
+ prevdup = false
48
+ end
49
+
50
+ offset += width
51
+ prevhex = hex
52
+ break if size <= 0
53
+ end
54
+ if h[:show_offset] && prevdup
55
+ row = "%08x: " % (offset + add)
56
+ yield(row) if block_given?
57
+ r << row
58
+ end
59
+ r.chomp + tail
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ if $0 == __FILE__
66
+ h = {}
67
+ case ARGV.size
68
+ when 0
69
+ puts "gimme fname [offset] [size]"
70
+ exit
71
+ when 1
72
+ fname = ARGV[0]
73
+ when 2
74
+ fname = ARGV[0]
75
+ h[:offset] = ARGV[1].to_i
76
+ when 3
77
+ fname = ARGV[0]
78
+ h[:offset] = ARGV[1].to_i
79
+ h[:size] = ARGV[2].to_i
80
+ end
81
+ File.open(fname,"rb") do |f|
82
+ f.seek h[:offset] if h[:offset]
83
+ @data = f.read(h[:size])
84
+ end
85
+ puts ZPNG::Hexdump.dump(@data)
86
+ end
@@ -1,6 +1,6 @@
1
1
  module ZPNG
2
2
  class Image
3
- attr_accessor :data, :header, :chunks, :scanlines, :imagedata, :palette
3
+ attr_accessor :data, :header, :chunks, :scanlines, :imagedata
4
4
  alias :hdr :header
5
5
 
6
6
  include DeepCopyable
@@ -28,13 +28,16 @@ module ZPNG
28
28
  @adam7 ||= Adam7Decoder.new(self)
29
29
  end
30
30
 
31
- # load image from file
32
- def self.load fname
33
- open(fname,"rb") do |f|
34
- Image.new(f)
31
+ class << self
32
+ # load image from file
33
+ def load fname
34
+ open(fname,"rb") do |f|
35
+ self.new(f)
36
+ end
35
37
  end
38
+ alias :load_file :load
39
+ alias :from_file :load # as in ChunkyPNG
36
40
  end
37
- alias :load_file :load
38
41
 
39
42
  # save image to file
40
43
  def save fname
@@ -52,10 +55,19 @@ module ZPNG
52
55
 
53
56
  def _from_hash h
54
57
  @new_image = true
55
- @chunks << (@header = Chunk::IHDR.new(h))
58
+ @chunks << (@header = Chunk::IHDR.new(h))
56
59
  if @header.palette_used?
57
- @chunks << (@palette = Chunk::PLTE.new)
58
- @palette[0] = h[:background] || h[:bg] || Color::BLACK # add default bg color
60
+ if h.key?(:palette)
61
+ if h[:palette]
62
+ @chunks << h[:palette]
63
+ else
64
+ # :palette => nil
65
+ # assume palette will be added later
66
+ end
67
+ else
68
+ @chunks << Chunk::PLTE.new
69
+ palette[0] = h[:background] || h[:bg] || Color::BLACK # add default bg color
70
+ end
59
71
  end
60
72
  end
61
73
 
@@ -86,8 +98,6 @@ module ZPNG
86
98
  case chunk
87
99
  when Chunk::IHDR
88
100
  @header = chunk
89
- when Chunk::PLTE
90
- @palette = chunk
91
101
  when Chunk::IEND
92
102
  break
93
103
  end
@@ -100,12 +110,58 @@ module ZPNG
100
110
  end
101
111
 
102
112
  public
103
- def dump
104
- @chunks.each do |chunk|
105
- puts "[.] #{chunk.inspect} #{chunk.crc_ok? ? 'CRC OK'.green : 'CRC ERROR'.red}"
106
- end
113
+
114
+ # internal helper method for color types 0 (grayscale) and 2 (truecolor)
115
+ def _alpha_color color
116
+ return nil unless trns
117
+
118
+ # For color type 0 (grayscale), the tRNS chunk contains a single gray level value, stored in the format:
119
+ #
120
+ # Gray: 2 bytes, range 0 .. (2^bitdepth)-1
121
+ #
122
+ # For color type 2 (truecolor), the tRNS chunk contains a single RGB color value, stored in the format:
123
+ #
124
+ # Red: 2 bytes, range 0 .. (2^bitdepth)-1
125
+ # Green: 2 bytes, range 0 .. (2^bitdepth)-1
126
+ # Blue: 2 bytes, range 0 .. (2^bitdepth)-1
127
+ #
128
+ # (If the image bit depth is less than 16, the least significant bits are used and the others are 0)
129
+ # Pixels of the specified gray level are to be treated as transparent (equivalent to alpha value 0);
130
+ # all other pixels are to be treated as fully opaque ( alpha = (2^bitdepth)-1 )
131
+
132
+ @alpha_color ||=
133
+ case hdr.color
134
+ when COLOR_GRAYSCALE
135
+ v = trns.data.unpack('n')[0] & (2**hdr.depth-1)
136
+ Color.from_grayscale(v, :depth => hdr.depth)
137
+ when COLOR_RGB
138
+ a = trns.data.unpack('n3').map{ |v| v & (2**hdr.depth-1) }
139
+ Color.new(*a, :depth => hdr.depth)
140
+ else
141
+ raise "color2alpha only intended for GRAYSCALE & RGB color modes"
142
+ end
143
+
144
+ color == @alpha_color ? 0 : (2**hdr.depth-1)
145
+ end
146
+
147
+ public
148
+
149
+ ###########################################################################
150
+ # chunks access
151
+
152
+ def trns
153
+ # not used "@trns ||= ..." here b/c it will call find() each time of there's no TRNS chunk
154
+ defined?(@trns) ? @trns : (@trns=@chunks.find{ |c| c.is_a?(Chunk::TRNS) })
107
155
  end
108
156
 
157
+ def plte
158
+ @plte ||= @chunks.find{ |c| c.is_a?(Chunk::PLTE) }
159
+ end
160
+ alias :palette :plte
161
+
162
+ ###########################################################################
163
+ # image attributes
164
+
109
165
  def bpp
110
166
  @header && @header.bpp
111
167
  end
@@ -139,6 +195,10 @@ module ZPNG
139
195
  end
140
196
  end
141
197
 
198
+ def metadata
199
+ @metadata ||= Metadata.new(self)
200
+ end
201
+
142
202
  def [] x, y
143
203
  x,y = adam7.convert_coords(x,y) if interlaced?
144
204
  scanlines[y][x]
@@ -153,7 +213,7 @@ module ZPNG
153
213
  # we must decode all scanlines before doing any modifications
154
214
  # or scanlines decoded AFTER modification of UPPER ones will be decoded wrong
155
215
  def decode_all_scanlines
156
- return if @all_scanlines_decoded
216
+ return if @all_scanlines_decoded || new_image?
157
217
  @all_scanlines_decoded = true
158
218
  scanlines.each(&:decode!)
159
219
  end
@@ -201,7 +261,8 @@ module ZPNG
201
261
  end
202
262
 
203
263
  def export
204
- imagedata # fill @imagedata, if not already filled
264
+ # fill @imagedata, if not already filled
265
+ imagedata unless new_image?
205
266
 
206
267
  # delete old IDAT chunks
207
268
  @chunks.delete_if{ |c| c.is_a?(Chunk::IDAT) }
@@ -255,6 +316,16 @@ module ZPNG
255
316
  deep_copy.crop!(params)
256
317
  end
257
318
 
319
+ def pixels
320
+ Pixels.new(self)
321
+ end
322
+
323
+ def == other_image
324
+ width == other_image.width &&
325
+ height == other_image.height &&
326
+ pixels == other_image.pixels
327
+ end
328
+
258
329
  def each_pixel &block
259
330
  height.times do |y|
260
331
  width.times do |x|
@@ -267,22 +338,29 @@ module ZPNG
267
338
  # OR returns self if no need to deinterlace
268
339
  def deinterlace
269
340
  return self unless interlaced?
270
- require 'pp'
271
- pp chunks
272
341
 
273
342
  # copy all but 'interlace' header params
274
343
  h = Hash[*%w'width height depth color compression filter'.map{ |k| [k.to_sym, hdr.send(k)] }.flatten]
275
- new_img = Image.new h
344
+
345
+ # don't auto-add palette chunk
346
+ h[:palette] = nil
347
+
348
+ # create new img
349
+ new_img = self.class.new h
350
+
351
+ # copy all but hdr/imagedata/end chunks
276
352
  chunks.each do |chunk|
277
353
  next if chunk.is_a?(Chunk::IHDR)
278
354
  next if chunk.is_a?(Chunk::IDAT)
279
355
  next if chunk.is_a?(Chunk::IEND)
280
356
  new_img.chunks << chunk.deep_copy
281
357
  end
358
+
359
+ # pixel-by-pixel copy
282
360
  each_pixel do |c,x,y|
283
361
  new_img[x,y] = c
284
362
  end
285
- p new_img.scanlines
363
+
286
364
  new_img
287
365
  end
288
366
  end