zpng 0.2.0 → 0.2.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.
@@ -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