png 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ iʍ��/�7]L��;�OP�C��TZ3W��=�*�̒]}9���؃������o2�Њ�
2
+ 4��.���v��ud�L-f�ҹ�ns�%�<���STaXt�p[�+�W��_�}�wyәv�X�}�õ�e!{PEEAz��@�h�c�&^�,�Z˥[�`Ï7�yʈv��qK�^� �C)��{�5����
@@ -1,18 +1,60 @@
1
- *** 1.1.0 / 2007-03-26
2
-
3
- + 4 major enhancements:
4
- + Fixed and incorporated Dominik Barathon's optimizations.
5
- + Wrote inline methods for png_crc and png_join. Now about 15x faster overall.
6
- + Basic PNG loading.
7
- + Reoriented x/y origin to bottom left. This will break things!
8
- + 3 minor enhancements:
9
- + Awesome ascii art patches from Tom Werner: Canvas#inpsect, Canvas#to_s, Color#to_ascii.
10
- + Switched to Hoe.
11
- + PNG.pie_chart from png/pie.
12
- + 1 bug fix:
13
- + Fixed bug in PNG::Canvas#each.
14
-
15
- *** 1.0.0 / 2006-09-31
16
-
17
- + Birthday!
1
+ === 1.2.0 / 2009-06-23
2
+
3
+ * 26 minor enhancements:
4
+
5
+ * Add load_metadata flag to PNG.load to extract dimensions and depth.
6
+ * Add pure-ruby versions back for png_join and png_crc.
7
+ * Added Canvas#composite(canvas, x, y, style) with overwrite, underlay, overlay, and blend.
8
+ * Added Canvas#each, passes x, y, color.
9
+ * Added Canvas#extract.
10
+ * Added Color#|, Background color is effective false.
11
+ * Added Font#coordinates and cached each letter when extracted.
12
+ * Added PNG::load_file.
13
+ * Added reader tests.
14
+ * Added tests for PNG::Font.
15
+ * Added tests for PNG::load.
16
+ * Added to/from hsv methods to Color.
17
+ * Clean up tests to remove duplication.
18
+ * Cleaned up Color#to_ascii to make much easier to read and extend.
19
+ * Cleaned up a fair amount of code, removing as many raw literals as possible.
20
+ * Color#blend is a simple averaging algorithm now.
21
+ * Extended reader to include RGB as well, paving the way for grayscale.
22
+ * Fake support for RGB as well as RGBA (default alpha to 255).
23
+ * Made reader work again (was flipped and all sorts of broken).
24
+ * Split out reader.
25
+ * Switched to minitest.
26
+ * Time to refactor PNG::Font to use #extract and #composite...
27
+ * Updated Rakefile to new hoe capabilities.
28
+ * Updated manifest.
29
+ * metadata_only now cleaner.
30
+ * read_IHDR now returns height, width instead of a canvas.
31
+
32
+ * 3 bug fixes:
33
+
34
+ * Fixed example/lines and added text.
35
+ * Fixes for 1.9.
36
+ * Ignore color profile if it exists (fixes problems on osx screenshots).
37
+
38
+ === 1.1.0 / 2007-03-26
39
+
40
+ * 4 major enhancements:
41
+
42
+ * Fixed and incorporated Dominik Barathon's optimizations.
43
+ * Wrote inline methods for png_crc and png_join. Now about 15x faster overall.
44
+ * Basic PNG loading.
45
+ * Reoriented x/y origin to bottom left. This will break things!
46
+
47
+ * 3 minor enhancements:
48
+
49
+ * Awesome ascii art patches from Tom Werner: Canvas#inpsect, Canvas#to_s, Color#to_ascii.
50
+ * Switched to Hoe.
51
+ * PNG.pie_chart from png/pie.
52
+
53
+ * 1 bug fix:
54
+
55
+ * Fixed bug in PNG::Canvas#each.
56
+
57
+ === 1.0.0 / 2006-09-31
58
+
59
+ * Birthday!
18
60
 
@@ -6,5 +6,10 @@ example/lines.rb
6
6
  example/profile.rb
7
7
  example/profile_lines.rb
8
8
  lib/png.rb
9
+ lib/png/default_font.png
10
+ lib/png/font.rb
9
11
  lib/png/pie.rb
12
+ lib/png/reader.rb
10
13
  test/test_png.rb
14
+ test/test_png_font.rb
15
+ test/test_png_reader.rb
data/Rakefile CHANGED
@@ -1,17 +1,18 @@
1
- require 'hoe'
2
- require './lib/png.rb'
1
+ $: << "../../RubyInline/dev/lib"
2
+ $: << "../../hoe/dev/lib"
3
3
 
4
- Hoe.new 'png', PNG::VERSION do |s|
5
- s.rubyforge_name = 'seattlerb'
6
- s.author = ['Ryan Davis', 'Eric Hodel']
7
- s.email = 'support@zenspider.com'
4
+ require 'hoe'
8
5
 
9
- s.summary = 'An almost-pure-ruby PNG library'
10
- s.description = s.paragraphs_of('README.txt', 3..7).join("\n\n")
6
+ Hoe.add_include_dirs "../../hoe/dev/lib" # HACK remove
7
+ Hoe.add_include_dirs "../../RubyInline/dev/lib", "lib"
8
+ Hoe.plugin :seattlerb
9
+ Hoe.plugin :inline
11
10
 
12
- s.changes = s.paragraphs_of('History.txt', 0..1).join("\n\n")
11
+ Hoe.spec 'png' do
12
+ developer 'Ryan Davis', 'ryand-ruby@zenspider.com'
13
+ developer 'Eric Hodel', 'drbrain@segment7.net'
13
14
 
14
- s.extra_deps << ['RubyInline', '>= 3.5.0']
15
+ self.rubyforge_name = 'seattlerb'
15
16
  end
16
17
 
17
18
  # vim: syntax=Ruby
@@ -1,18 +1,9 @@
1
1
  #!/usr/local/bin/ruby -w
2
2
 
3
3
  require 'png'
4
+ require 'png/font'
4
5
 
5
- canvas = PNG::Canvas.new 1024, 1024, PNG::Color::White
6
-
7
- #canvas.each do |x, y|
8
- # case x
9
- # when y then
10
- # canvas.point(x, y, Color::Black)
11
- # when 50 then
12
- # canvas.point(x, y, Color::Background)
13
- # end
14
- # canvas.point(x, y, Color::Green) if y = 200
15
- #end
6
+ canvas = PNG::Canvas.new 201, 201, PNG::Color::White
16
7
 
17
8
  canvas.line 50, 50, 100, 50, PNG::Color::Blue
18
9
  canvas.line 50, 50, 50, 100, PNG::Color::Blue
@@ -22,7 +13,8 @@ canvas.line 100, 50, 200, 75, PNG::Color::Green
22
13
  canvas.line 0, 200, 200, 0, PNG::Color::Black
23
14
  canvas.line 0, 200, 150, 0, PNG::Color::Red
24
15
 
16
+ canvas.annotate 'Hello World', 10, 10
17
+
25
18
  png = PNG.new canvas
26
19
  png.save 'blah.png'
27
20
  `open blah.png`
28
-
data/lib/png.rb CHANGED
@@ -1,29 +1,26 @@
1
- begin; require 'rubygems'; rescue LoadError; end
1
+ # encoding: BINARY
2
+
3
+ require 'rubygems'
2
4
  require 'zlib'
3
5
  require 'inline'
4
6
 
5
- class String # :nodoc: # ZenTest SKIP
6
-
7
- ##
8
- # Calculates a CRC using the algorithm in the PNG specification.
7
+ unless "".respond_to? :getbyte then
8
+ class String
9
+ alias :getbyte :[]
10
+ end
11
+ end
9
12
 
13
+ class String # :nodoc: # ZenTest SKIP
10
14
  inline do |builder|
11
- if RUBY_VERSION < "1.8.6" then
12
- builder.prefix <<-EOM
13
- #define RSTRING_PTR(s) (RSTRING(s)->ptr)
14
- #define RSTRING_LEN(s) (RSTRING(s)->len)
15
- EOM
16
- end
17
-
18
15
  builder.c <<-EOM
19
16
  unsigned long png_crc() {
20
17
  static unsigned long crc[256];
21
18
  static char crc_table_computed = 0;
22
-
19
+
23
20
  if (! crc_table_computed) {
24
21
  unsigned long c;
25
22
  int n, k;
26
-
23
+
27
24
  for (n = 0; n < 256; n++) {
28
25
  c = (unsigned long) n;
29
26
  for (k = 0; k < 8; k++) {
@@ -35,8 +32,8 @@ class String # :nodoc: # ZenTest SKIP
35
32
  }
36
33
 
37
34
  unsigned long c = 0xffffffff;
38
- unsigned len = RSTRING_LEN(self);
39
- char * s = StringValuePtr(self);
35
+ unsigned len = RSTRING_LEN(self);
36
+ char * s = StringValuePtr(self);
40
37
  unsigned i;
41
38
 
42
39
  for (i = 0; i < len; i++) {
@@ -47,6 +44,30 @@ class String # :nodoc: # ZenTest SKIP
47
44
  }
48
45
  EOM
49
46
  end
47
+ rescue CompilationError => e
48
+ warn "COMPLIATION ERROR: #{e}"
49
+
50
+ unless defined? @@crc then
51
+ @@crc = Array.new(256)
52
+ 256.times do |n|
53
+ c = n
54
+ 8.times do
55
+ c = (c & 1 == 1) ? 0xedb88320 ^ (c >> 1) : c >> 1
56
+ end
57
+ @@crc[n] = c
58
+ end
59
+ end
60
+
61
+ ##
62
+ # Calculates a CRC using the algorithm in the PNG specification.
63
+
64
+ def png_crc()
65
+ c = 0xffffffff
66
+ each_byte do |b|
67
+ c = @@crc[(c^b) & 0xff] ^ (c >> 8)
68
+ end
69
+ return c ^ 0xffffffff
70
+ end
50
71
  end
51
72
 
52
73
  ##
@@ -71,47 +92,69 @@ end
71
92
  # canvas.line 50, 50, 100, 50, PNG::Color::Blue
72
93
  # png = PNG.new canvas
73
94
  # png.save 'blah.png'
95
+ #
96
+ # = TODO:
97
+ #
98
+ # + Get everything orinted entirely on [x,y,h,w] with x,y origin being
99
+ # bottom left.
74
100
 
75
101
  class PNG
76
-
77
- VERSION = '1.1.0'
102
+ VERSION = '1.2.0'
78
103
  SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
79
104
 
80
- inline do |builder|
81
- if RUBY_VERSION < "1.8.6" then
82
- builder.prefix <<-EOM
83
- #define RARRAY_PTR(s) (RARRAY(s)->ptr)
84
- #define RARRAY_LEN(s) (RARRAY(s)->len)
85
- EOM
86
- end
105
+ # Color Types:
106
+ GRAY = 0 # DEPTH = 1,2,4,8,16
107
+ RGB = 2 # DEPTH = 8,16
108
+ INDEXED = 3 # DEPTH = 1,2,4,8
109
+ GRAYA = 4 # DEPTH = 8,16
110
+ RGBA = 6 # DEPTH = 8,16
111
+
112
+ # Filter Types:
113
+ NONE = 0
114
+ SUB = 1
115
+ UP = 2
116
+ AVG = 3
117
+ PAETH = 4
118
+
119
+ begin
120
+ inline do |builder|
121
+ if RUBY_VERSION < "1.8.6" then
122
+ builder.prefix <<-EOM
123
+ #define RARRAY_PTR(s) (RARRAY(s)->ptr)
124
+ #define RARRAY_LEN(s) (RARRAY(s)->len)
125
+ EOM
126
+ end
87
127
 
88
- # C equivalent of:
89
- # @data.map { |row| "\0" + row.map { |p| p.values }.join }.join
90
- builder.c <<-EOM
91
- VALUE png_join() {
92
- int i, j;
93
- VALUE data = rb_iv_get(self, "@data");
94
- unsigned int data_len = RARRAY_LEN(data);
95
- unsigned int row_len = RARRAY_LEN(RARRAY_PTR(data)[0]);
96
- unsigned long size = data_len * (1 + (row_len * 4));
97
- char * result = malloc(size);
98
- unsigned long idx = 0;
99
- for (i = 0; i < data_len; i++) {
100
- VALUE row = RARRAY_PTR(data)[i];
101
- result[idx++] = 0;
102
- for (j = 0; j < row_len; j++) {
103
- VALUE color = RARRAY_PTR(row)[j];
104
- VALUE values = rb_iv_get(color, "@values");
105
- char * value = StringValuePtr(values);
106
- result[idx++] = value[0];
107
- result[idx++] = value[1];
108
- result[idx++] = value[2];
109
- result[idx++] = value[3];
110
- }
128
+ builder.c <<-EOM
129
+ VALUE png_join() {
130
+ int i, j;
131
+ VALUE data = rb_iv_get(self, "@data");
132
+ unsigned int data_len = RARRAY_LEN(data);
133
+ unsigned int row_len = RARRAY_LEN(RARRAY_PTR(data)[0]);
134
+ unsigned long size = data_len * (1 + (row_len * 4));
135
+ char * result = malloc(size);
136
+ unsigned long idx = 0;
137
+ for (i = 0; i < data_len; i++) {
138
+ VALUE row = RARRAY_PTR(data)[i];
139
+ result[idx++] = 0;
140
+ for (j = 0; j < row_len; j++) {
141
+ VALUE color = RARRAY_PTR(row)[j];
142
+ VALUE values = rb_iv_get(color, "@values");
143
+ char * value = StringValuePtr(values);
144
+ result[idx++] = value[0];
145
+ result[idx++] = value[1];
146
+ result[idx++] = value[2];
147
+ result[idx++] = value[3];
148
+ }
149
+ }
150
+ return rb_str_new(result, size);
111
151
  }
112
- return rb_str_new(result, size);
113
- }
114
- EOM
152
+ EOM
153
+ end
154
+ rescue CompilationError
155
+ def png_join
156
+ @data.map { |row| "\0" + row.map { |p| p.values }.join }.join
157
+ end
115
158
  end
116
159
 
117
160
  ##
@@ -121,116 +164,6 @@ class PNG
121
164
  [data.size, type, data, (type + data).png_crc].pack("Na*a*N")
122
165
  end
123
166
 
124
- def self.load(png)
125
- png = png.dup
126
- signature = png.slice! 0, 8
127
- raise ArgumentError, 'Invalid PNG signature' unless signature == SIGNATURE
128
-
129
- type, data = read_chunk png
130
-
131
- raise ArgumentError, 'Invalid PNG, no IHDR chunk' unless type == 'IHDR'
132
-
133
- canvas = read_IHDR data
134
- type, data = read_chunk png
135
- read_IDAT data, canvas
136
- type, data = read_chunk png
137
- raise 'oh no! IEND not next? crashing and burning!' unless type == 'IEND'
138
-
139
- new canvas
140
- end
141
-
142
- def self.check_crc(type, data, crc)
143
- return true if (type + data).png_crc == crc
144
- raise ArgumentError, "Invalid CRC encountered in #{type} chunk"
145
- end
146
-
147
- def self.paeth(a, b, c) # left, above, upper left
148
- p = a + b - c
149
- pa = (p - a).abs
150
- pb = (p - b).abs
151
- pc = (p - c).abs
152
-
153
- return a if pa <= pb && pa <= pc
154
- return b if pb <= pc
155
- c
156
- end
157
-
158
- def self.read_chunk(png)
159
- size, type = png.slice!(0, 8).unpack 'Na4'
160
- data, crc = png.slice!(0, size + 4).unpack "a#{size}N"
161
-
162
- check_crc type, data, crc
163
-
164
- return type, data
165
- end
166
-
167
- def self.read_IDAT(data, canvas)
168
- data = Zlib::Inflate.inflate(data).unpack 'C*'
169
- scanline_length = 4 * canvas.width + 1 # for filter
170
- row = 0
171
- until data.empty? do
172
- row_data = data.slice! 0, scanline_length
173
- filter = row_data.shift
174
- case filter
175
- when 0 then # None
176
- when 1 then # Sub
177
- row_data.each_with_index do |byte, index|
178
- left = index < 4 ? 0 : row_data[index - 4]
179
- row_data[index] = (byte + left) % 256
180
- #p [byte, left, row_data[index]]
181
- end
182
- when 2 then # Up
183
- row_data.each_with_index do |byte, index|
184
- col = index / 4
185
- upper = row == 0 ? 0 : canvas[col, row - 1].values[index % 4]
186
- row_data[index] = (upper + byte) % 256
187
- end
188
- when 3 then # Average
189
- row_data.each_with_index do |byte, index|
190
- col = index / 4
191
- upper = row == 0 ? 0 : canvas[col, row - 1].values[index % 4]
192
- left = index < 4 ? 0 : row_data[index - 4]
193
-
194
- row_data[index] = (byte + ((left + upper)/2).floor) % 256
195
- end
196
- when 4 then # Paeth
197
- left = upper = upper_left = nil
198
- row_data.each_with_index do |byte, index|
199
- col = index / 4
200
-
201
- left = index < 4 ? 0 : row_data[index - 4]
202
- if row == 0 then
203
- upper = upper_left = 0
204
- else
205
- upper = canvas[col, row - 1].values[index % 4]
206
- upper_left = col == 0 ? 0 :
207
- canvas[col - 1, row - 1].values[index % 4]
208
- end
209
-
210
- paeth = paeth left, upper, upper_left
211
- row_data[index] = (byte + paeth) % 256
212
- #p [byte, paeth, row_data[index]]
213
- end
214
- else
215
- raise ArgumentError, "Invalid filter algorithm #{filter}"
216
- end
217
-
218
- col = 0
219
- row_data.each_slice 4 do |slice|
220
- canvas[col, row] = PNG::Color.new(*slice)
221
- col += 1
222
- end
223
-
224
- row += 1
225
- end
226
- end
227
-
228
- def self.read_IHDR(data)
229
- width, height, *rest = data.unpack 'N2C5'
230
- raise ArgumentError, 'unsupported PNG file' unless rest == [8, 6, 0, 0, 0]
231
- return PNG::Canvas.new(height, width)
232
- end
233
-
234
167
  ##
235
168
  # Creates a new PNG object using +canvas+
236
169
 
@@ -245,7 +178,9 @@ class PNG
245
178
  # Writes the PNG to +path+.
246
179
 
247
180
  def save(path)
248
- File.open path, 'wb' do |f| f.write to_blob end
181
+ File.open path, 'wb' do |f|
182
+ f.write to_blob
183
+ end
249
184
  end
250
185
 
251
186
  ##
@@ -254,22 +189,29 @@ class PNG
254
189
  def to_blob
255
190
  blob = []
256
191
 
192
+ header = [@width, @height, @bits, RGBA, NONE, NONE, NONE]
193
+
257
194
  blob << SIGNATURE
258
- blob << PNG.chunk('IHDR', [@width, @height, @bits, 6, 0, 0, 0 ].pack("N2C5"))
259
- # 0 == filter type code "none"
260
- data = self.png_join
261
- blob << PNG.chunk('IDAT', Zlib::Deflate.deflate(data))
195
+ blob << PNG.chunk('IHDR', header.pack("N2C5"))
196
+ blob << PNG.chunk('IDAT', Zlib::Deflate.deflate(self.png_join))
262
197
  blob << PNG.chunk('IEND', '')
263
198
  blob.join
264
199
  end
265
200
 
266
201
  ##
267
- # RGBA colors
202
+ # A 32 bit RGBA color. Can be created from RGB or RGBA via #new,
203
+ # numeric value or hex string via #from, or HSV via #from_hsv.
268
204
 
269
205
  class Color
270
206
 
207
+ MAX=255
208
+
271
209
  attr_reader :values
272
210
 
211
+ ##
212
+ # Create a new color from a string or integer value. Can take an
213
+ # optional name as well.
214
+
273
215
  def self.from str, name = nil
274
216
  str = "%08x" % str if Integer === str
275
217
  colors = str.scan(/[\da-f][\da-f]/i).map { |n| n.hex }
@@ -280,7 +222,7 @@ class PNG
280
222
  ##
281
223
  # Creates a new color with values +red+, +green+, +blue+, and +alpha+.
282
224
 
283
- def initialize(red, green, blue, alpha, name = nil)
225
+ def initialize red, green, blue, alpha = MAX, name = nil
284
226
  @values = "%c%c%c%c" % [red, green, blue, alpha]
285
227
  @name = name
286
228
  end
@@ -303,44 +245,63 @@ class PNG
303
245
  White = Color.from 0xFFFFFFFF, "White"
304
246
  Yellow = Color.from 0xFFFF00FF, "Yellow"
305
247
 
306
- def ==(other) # :nodoc:
248
+ def == other # :nodoc:
307
249
  self.class === other and other.values == values
308
250
  end
309
251
 
252
+ alias :eql? :==
253
+
254
+ ##
255
+ # "Bitwise or" as applied to colors. Background color is
256
+ # considered false.
257
+
258
+ def | o
259
+ self == Background ? o : self
260
+ end
261
+
262
+ def hash # :nodoc:
263
+ self.values.hash
264
+ end
265
+
266
+ ##
267
+ # Return an array of RGB
268
+
269
+ def rgb # TODO: rgba?
270
+ return r, g, b
271
+ end
272
+
310
273
  ##
311
274
  # Red component
312
275
 
313
- def r; @values[0]; end
276
+ def r; @values.getbyte 0; end
314
277
 
315
278
  ##
316
279
  # Green component
317
280
 
318
- def g; @values[1]; end
281
+ def g; @values.getbyte 1; end
319
282
 
320
283
  ##
321
284
  # Blue component
322
285
 
323
- def b; @values[2]; end
286
+ def b; @values.getbyte 2; end
324
287
 
325
288
  ##
326
289
  # Alpha transparency component
327
290
 
328
- def a; @values[3]; end
291
+ def a; @values.getbyte 3; end
329
292
 
330
293
  ##
331
294
  # Blends +color+ into this color returning a new blended color.
332
295
 
333
- def blend(color)
334
- return Color.new((r * (0xFF - color.a) + color.r * color.a) >> 8,
335
- (g * (0xFF - color.a) + color.g * color.a) >> 8,
336
- (b * (0xFF - color.a) + color.b * color.a) >> 8,
337
- (a * (0xFF - color.a) + color.a * color.a) >> 8)
296
+ def blend color
297
+ return Color.new(((r + color.r) / 2), ((g + color.g) / 2),
298
+ ((b + color.b) / 2), ((a + color.a) / 2))
338
299
  end
339
300
 
340
301
  ##
341
302
  # Returns a new color with an alpha value adjusted by +i+.
342
303
 
343
- def intensity(i)
304
+ def intensity i
344
305
  return Color.new(r,g,b,(a*i) >> 8)
345
306
  end
346
307
 
@@ -358,24 +319,86 @@ class PNG
358
319
 
359
320
  def to_ascii
360
321
  return ' ' if a == 0x00
322
+
361
323
  brightness = (((r + g + b) / 3) * a) / 0xFF
362
- return '00' if brightness >= 0xc0
363
- return '++' if brightness >= 0x7F
364
- return ',,' if brightness >= 0x40
365
- return '..'
324
+
325
+ %w(.. ,, ++ 00)[brightness / 64]
366
326
  end
367
327
 
368
- def to_s
328
+ def to_s # :nodoc:
369
329
  if @name then
370
330
  @name
371
331
  else
372
332
  super
373
333
  end
374
334
  end
375
- end
335
+
336
+ ##
337
+ # Creates a new RGB color from HSV equivalent values.
338
+
339
+ def self.from_hsv h, s, v
340
+ r = g = b = v # gray
341
+ unless s == 0.0 then
342
+ h += 255 if h < 0
343
+ h = h / 255.0 * 6.0
344
+ s = s / 255.0
345
+ v = v / 255.0
346
+ i = h.floor
347
+ f = h - i
348
+ p = v * (1 - (s))
349
+ q = v * (1 - (s * (f)))
350
+ w = v * (1 - (s * (1-f)))
351
+ r, g, b = case i
352
+ when 0,6 then
353
+ [ v, w, p ]
354
+ when 1 then
355
+ [ q, v, p ]
356
+ when 2 then
357
+ [ p, v, w ]
358
+ when 3 then
359
+ [ p, q, v ]
360
+ when 4 then
361
+ [ w, p, v ]
362
+ when 5 then
363
+ [ v, p, q ]
364
+ else
365
+ raise [h, s, v, i, f, p, q, w].inspect
366
+ end
367
+ end
368
+ self.new((r * 255).round, (g * 255).round, (b * 255).round)
369
+ end
370
+
371
+ ##
372
+ # Returns HSV equivalent of the current color.
373
+
374
+ def to_hsv # errors = 54230 out of 255^3 are off by about 1 on r, g, or b
375
+ rgb = self.rgb
376
+ r, g, b = rgb
377
+ h, s, v = 0, 0, rgb.max
378
+
379
+ return h, s, v if v == 0
380
+
381
+ range = v - rgb.min
382
+ s = 255 * range / v
383
+
384
+ return h, s, v if s == 0
385
+
386
+ h = case v
387
+ when r then
388
+ 0x00 + 43 * (g - b) / range # 43 = 1/4 of 360 scaled to 255
389
+ when g then
390
+ 0x55 + 43 * (b - r) / range
391
+ else
392
+ 0xAA + 43 * (r - g) / range
393
+ end
394
+
395
+ return h.round, s.round, v.round
396
+ end
397
+ end # Color
376
398
 
377
399
  ##
378
- # PNG canvas
400
+ # A canvas used for drawing images. Origin is 0, 0 in the bottom
401
+ # left corner.
379
402
 
380
403
  class Canvas
381
404
 
@@ -394,7 +417,7 @@ class PNG
394
417
 
395
418
  attr_reader :data
396
419
 
397
- def initialize(width, height, background = Color::Background)
420
+ def initialize width, height, background = Color::Background
398
421
  @width = width
399
422
  @height = height
400
423
  @data = Array.new(@height) { |x| Array.new(@width, background) }
@@ -403,7 +426,7 @@ class PNG
403
426
  ##
404
427
  # Retrieves the color of the pixel at (+x+, +y+).
405
428
 
406
- def [](x, y)
429
+ def [] x, y
407
430
  raise "bad x value #{x} >= #{@width}" if x >= @width
408
431
  raise "bad y value #{y} >= #{@height}" if y >= @height
409
432
  @data[@height-y-1][x]
@@ -412,12 +435,59 @@ class PNG
412
435
  ##
413
436
  # Sets the color of the pixel at (+x+, +y+) to +color+.
414
437
 
415
- def []=(x, y, color)
438
+ def []= x, y, color
416
439
  raise "bad x value #{x} >= #{@width}" if x >= @width
417
440
  raise "bad y value #{y} >= #{@height}" if y >= @height
441
+ raise "bad color #{color.inspect}" unless color.kind_of? PNG::Color
418
442
  @data[@height-y-1][x] = color
419
443
  end
420
444
 
445
+ ##
446
+ # Composites another canvas onto self at the given (bottom left) coordinates.
447
+
448
+ def composite canvas, x, y, style = :overwrite
449
+ canvas.each do |x1, y1, color|
450
+ case style
451
+ when :overwrite then
452
+ self[x+x1, y+y1] = color
453
+ when :add, :underlay then
454
+ self[x+x1, y+y1] = self[x+x1, y+y1] | color
455
+ when :overlay then
456
+ self[x+x1, y+y1] = color | self[x+x1, y+y1]
457
+ when :blend then
458
+ self.point x+x1, y+y1, color
459
+ else
460
+ raise "unknown style for composite: #{style.inspect}"
461
+ end
462
+ end
463
+ end
464
+
465
+ ##
466
+ # Iterates over the canvas yielding x, y, and color.
467
+
468
+ def each
469
+ data.reverse.each_with_index do |row, y|
470
+ row.each_with_index do |color, x|
471
+ yield x, y, color
472
+ end
473
+ end
474
+ end
475
+
476
+ ##
477
+ # Create a new canvas copying a region of the current canvas
478
+
479
+ def extract x0, y0, x1, y1
480
+ canvas = Canvas.new(x1-x0+1, y1-y0+1)
481
+
482
+ (x0..x1).each_with_index do |x2, x3|
483
+ (y0..y1).each_with_index do |y2, y3|
484
+ canvas[x3, y3] = self[x2, y2]
485
+ end
486
+ end
487
+
488
+ canvas
489
+ end
490
+
421
491
  def inspect # :nodoc:
422
492
  '#<%s %dx%d>' % [self.class, @width, @height]
423
493
  end
@@ -512,5 +582,5 @@ class PNG
512
582
 
513
583
  return image.join
514
584
  end
515
- end
585
+ end # Canvas
516
586
  end