png 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
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!
18
+
@@ -1,5 +1,10 @@
1
+ History.txt
1
2
  Manifest.txt
2
- README
3
+ README.txt
3
4
  Rakefile
4
5
  example/lines.rb
6
+ example/profile.rb
7
+ example/profile_lines.rb
5
8
  lib/png.rb
9
+ lib/png/pie.rb
10
+ test/test_png.rb
@@ -0,0 +1,65 @@
1
+ = PNG
2
+
3
+ * http://seattlerb.rubyforge.org/
4
+
5
+ == DESCRIPTION
6
+
7
+ PNG is an almost-pure-ruby PNG library. It lets you write a PNG
8
+ without any C libraries.
9
+
10
+ == FEATURES
11
+
12
+ * Very simple interface.
13
+ * Outputs simple PNG files with ease.
14
+ * Basic PNG reader as well (someday it might do compositing and the like!).
15
+ * Almost pure ruby, does require a compiler.
16
+
17
+ == SYNOPSYS
18
+
19
+ require 'png'
20
+
21
+ canvas = PNG::Canvas.new 200, 200
22
+
23
+ # Set a point to a color
24
+ canvas[100, 100] = PNG::Color::Black
25
+
26
+ # draw an anti-aliased line
27
+ canvas.line 50, 50, 100, 50, PNG::Color::Blue
28
+
29
+ png = PNG.new canvas
30
+ png.save 'blah.png'
31
+
32
+ == REQUIREMENTS
33
+
34
+ + C compiler
35
+ + RubyInline
36
+ + Hoe
37
+
38
+ == INSTALL
39
+
40
+ + sudo gem install -y png
41
+
42
+ == LICENSE
43
+
44
+ (The MIT License)
45
+
46
+ Copyright (c) 2006-2007 Ryan Davis, Eric Hodel, Zen Spider Software
47
+
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of this software and associated documentation files (the
50
+ "Software"), to deal in the Software without restriction, including
51
+ without limitation the rights to use, copy, modify, merge, publish,
52
+ distribute, sublicense, and/or sell copies of the Software, and to
53
+ permit persons to whom the Software is furnished to do so, subject to
54
+ the following conditions:
55
+
56
+ The above copyright notice and this permission notice shall be
57
+ included in all copies or substantial portions of the Software.
58
+
59
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
60
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
61
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
62
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
63
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
64
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
65
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile CHANGED
@@ -1,56 +1,17 @@
1
- require 'rubygems'
2
- require 'rake'
3
- require 'rake/testtask'
4
- require 'rake/rdoctask'
5
- require 'rake/gempackagetask'
1
+ require 'hoe'
2
+ require './lib/png.rb'
6
3
 
7
- $VERBOSE = nil
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'
8
8
 
9
- spec = Gem::Specification.new do |s|
10
- s.name = 'png'
11
- s.version = '1.0.0'
12
- s.summary = 'A pure ruby PNG library'
13
- s.description = 'png allows you to write a PNG file without any C libraries.'
14
- s.author = 'Ryan Davis'
15
- s.email = 'ryand-ruby@zenspider.com'
9
+ s.summary = 'An almost-pure-ruby PNG library'
10
+ s.description = s.paragraphs_of('README.txt', 3..7).join("\n\n")
16
11
 
17
- s.has_rdoc = true
18
- s.files = File.read('Manifest.txt').split($/)
19
- s.require_path = 'lib'
20
- end
21
-
22
- desc 'Run tests'
23
- task :default => [ :test ]
24
-
25
- Rake::TestTask.new('test') do |t|
26
- t.libs << 'test'
27
- t.pattern = 'test/test_*.rb'
28
- t.verbose = true
29
- end
30
-
31
- desc 'Update Manifest.txt'
32
- task :update_manifest do
33
- sh "find . -type f | sed -e 's%./%%' | egrep -v 'svn|swp|~' | egrep -v '^(doc|pkg)/' | sort > Manifest.txt"
34
- end
35
-
36
- desc 'Generate RDoc'
37
- Rake::RDocTask.new :rdoc do |rd|
38
- rd.rdoc_dir = 'doc'
39
- rd.rdoc_files.add 'lib', 'README'
40
- rd.main = 'README'
41
- rd.options << '-d' if `which dot` =~ /\/dot/
42
- rd.options << '-t png'
43
- end
12
+ s.changes = s.paragraphs_of('History.txt', 0..1).join("\n\n")
44
13
 
45
- desc 'Build Gem'
46
- Rake::GemPackageTask.new spec do |pkg|
47
- pkg.need_tar = true
14
+ s.extra_deps << ['RubyInline', '>= 3.5.0']
48
15
  end
49
16
 
50
- desc 'Clean up'
51
- task :clean => [ :clobber_rdoc, :clobber_package ]
52
-
53
- desc 'Clean up'
54
- task :clobber => [ :clean ]
55
-
56
17
  # vim: syntax=Ruby
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'png'
4
4
 
5
- canvas = PNG::Canvas.new 1024, 1024, PNG::Color::Black
5
+ canvas = PNG::Canvas.new 1024, 1024, PNG::Color::White
6
6
 
7
7
  #canvas.each do |x, y|
8
8
  # case x
@@ -17,9 +17,10 @@ canvas = PNG::Canvas.new 1024, 1024, PNG::Color::Black
17
17
  canvas.line 50, 50, 100, 50, PNG::Color::Blue
18
18
  canvas.line 50, 50, 50, 100, PNG::Color::Blue
19
19
  canvas.line 100, 50, 150, 100, PNG::Color::Blue
20
- canvas.line 100, 50, 125, 100, PNG::Color::Green # currently wrong
21
- canvas.line 100, 50, 200, 75, PNG::Color::Green # currently wrong
20
+ canvas.line 100, 50, 125, 100, PNG::Color::Green
21
+ canvas.line 100, 50, 200, 75, PNG::Color::Green
22
22
  canvas.line 0, 200, 200, 0, PNG::Color::Black
23
+ canvas.line 0, 200, 150, 0, PNG::Color::Red
23
24
 
24
25
  png = PNG.new canvas
25
26
  png.save 'blah.png'
@@ -0,0 +1,16 @@
1
+ require 'png'
2
+
3
+ class PNGProfile
4
+
5
+ def draw
6
+ canvas = PNG::Canvas.new 400, 400
7
+ png = PNG.new canvas
8
+ png.to_blob
9
+ end
10
+
11
+ end
12
+
13
+ pp = PNGProfile.new
14
+
15
+ 10.times do pp.draw end
16
+
@@ -0,0 +1,37 @@
1
+ require 'png'
2
+
3
+ class PNGProfileLine
4
+
5
+ COLORS = [
6
+ PNG::Color::Red,
7
+ PNG::Color::Orange,
8
+ PNG::Color::Yellow,
9
+ PNG::Color::Green,
10
+ PNG::Color::Blue,
11
+ PNG::Color::Purple,
12
+ ]
13
+
14
+ def draw
15
+ line = 0
16
+ canvas = PNG::Canvas.new 100, 100
17
+
18
+ 0.step 99, 10 do |x|
19
+ canvas.line x, 0, 99 - x, 99, COLORS[line % 6]
20
+ line += 1
21
+ end
22
+
23
+ 0.step 99, 10 do |y|
24
+ canvas.line 0, y, 99, y, COLORS[line % 6]
25
+ line += 1
26
+ end
27
+
28
+ canvas
29
+ end
30
+
31
+ end
32
+
33
+ ppl = PNGProfileLine.new
34
+
35
+ 5.times do ppl.draw end
36
+ #PNG.new(ppl.draw).save 'x.png'
37
+
data/lib/png.rb CHANGED
@@ -1,33 +1,56 @@
1
+ begin; require 'rubygems'; rescue LoadError; end
1
2
  require 'zlib'
3
+ require 'inline'
2
4
 
3
- class String # :nodoc:
5
+ class String # :nodoc: # ZenTest SKIP
4
6
 
5
7
  ##
6
8
  # Calculates a CRC using the algorithm in the PNG specification.
7
9
 
8
- def png_crc
9
- unless defined? @@crc then
10
- @@crc = Array.new(256)
11
- 256.times do |n|
12
- c = n
13
- 8.times do
14
- c = (c & 1 == 1) ? 0xedb88320 ^ (c >> 1) : c >> 1
15
- end
16
- @@crc[n] = c
17
- end
10
+ 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
18
16
  end
19
17
 
20
- c = 0xffffffff
21
- each_byte do |b|
22
- c = @@crc[(c^b) & 0xff] ^ (c >> 8)
23
- end
24
- return c ^ 0xffffffff
18
+ builder.c <<-EOM
19
+ unsigned long png_crc() {
20
+ static unsigned long crc[256];
21
+ static char crc_table_computed = 0;
22
+
23
+ if (! crc_table_computed) {
24
+ unsigned long c;
25
+ int n, k;
26
+
27
+ for (n = 0; n < 256; n++) {
28
+ c = (unsigned long) n;
29
+ for (k = 0; k < 8; k++) {
30
+ c = (c & 1) ? 0xedb88320L ^ (c >> 1) : c >> 1;
31
+ }
32
+ crc[n] = c;
33
+ }
34
+ crc_table_computed = 1;
35
+ }
36
+
37
+ unsigned long c = 0xffffffff;
38
+ unsigned len = RSTRING_LEN(self);
39
+ char * s = StringValuePtr(self);
40
+ unsigned i;
41
+
42
+ for (i = 0; i < len; i++) {
43
+ c = crc[(c ^ s[i]) & 0xff] ^ (c >> 8);
44
+ }
45
+
46
+ return c ^ 0xffffffff;
47
+ }
48
+ EOM
25
49
  end
26
-
27
50
  end
28
51
 
29
52
  ##
30
- # A pure Ruby Portable Network Graphics (PNG) writer.
53
+ # An almost-pure-ruby Portable Network Graphics (PNG) writer.
31
54
  #
32
55
  # http://www.libpng.org/pub/png/spec/1.2/
33
56
  #
@@ -42,7 +65,7 @@ end
42
65
  # = Example
43
66
  #
44
67
  # require 'png'
45
- #
68
+ #
46
69
  # canvas = PNG::Canvas.new 200, 200
47
70
  # canvas[100, 100] = PNG::Color::Black
48
71
  # canvas.line 50, 50, 100, 50, PNG::Color::Blue
@@ -51,6 +74,46 @@ end
51
74
 
52
75
  class PNG
53
76
 
77
+ VERSION = '1.1.0'
78
+ SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
79
+
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
87
+
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
+ }
111
+ }
112
+ return rb_str_new(result, size);
113
+ }
114
+ EOM
115
+ end
116
+
54
117
  ##
55
118
  # Creates a PNG chunk of type +type+ that contains +data+.
56
119
 
@@ -58,6 +121,116 @@ class PNG
58
121
  [data.size, type, data, (type + data).png_crc].pack("Na*a*N")
59
122
  end
60
123
 
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
+
61
234
  ##
62
235
  # Creates a new PNG object using +canvas+
63
236
 
@@ -72,15 +245,22 @@ class PNG
72
245
  # Writes the PNG to +path+.
73
246
 
74
247
  def save(path)
75
- File.open(path, "w") do |f|
76
- f.write [137, 80, 78, 71, 13, 10, 26, 10].pack("C*") # PNG signature
77
- f.write PNG.chunk('IHDR',
78
- [ @height, @width, @bits, 6, 0, 0, 0 ].pack("N2C5"))
79
- # 0 == filter type code "none"
80
- data = @data.map { |row| [0] + row.map { |p| p.values } }.flatten
81
- f.write PNG.chunk('IDAT', Zlib::Deflate.deflate(data.pack("C*"), 9))
82
- f.write PNG.chunk('IEND', '')
83
- end
248
+ File.open path, 'wb' do |f| f.write to_blob end
249
+ end
250
+
251
+ ##
252
+ # Raw PNG data
253
+
254
+ def to_blob
255
+ blob = []
256
+
257
+ 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))
262
+ blob << PNG.chunk('IEND', '')
263
+ blob.join
84
264
  end
85
265
 
86
266
  ##
@@ -90,28 +270,42 @@ class PNG
90
270
 
91
271
  attr_reader :values
92
272
 
273
+ def self.from str, name = nil
274
+ str = "%08x" % str if Integer === str
275
+ colors = str.scan(/[\da-f][\da-f]/i).map { |n| n.hex }
276
+ colors << name
277
+ self.new(*colors)
278
+ end
279
+
93
280
  ##
94
281
  # Creates a new color with values +red+, +green+, +blue+, and +alpha+.
95
282
 
96
- def initialize(red, green, blue, alpha)
97
- @values = [red, green, blue, alpha]
283
+ def initialize(red, green, blue, alpha, name = nil)
284
+ @values = "%c%c%c%c" % [red, green, blue, alpha]
285
+ @name = name
98
286
  end
99
287
 
100
288
  ##
101
289
  # Transparent white
102
290
 
103
- Background = Color.new 0xFF, 0xFF, 0xFF, 0x00
104
-
105
- White = Color.new 0xFF, 0xFF, 0xFF, 0xFF
106
- Black = Color.new 0x00, 0x00, 0x00, 0xFF
107
- Gray = Color.new 0x7F, 0x7F, 0x7F, 0xFF
108
-
109
- Red = Color.new 0xFF, 0x00, 0x00, 0xFF
110
- Orange = Color.new 0xFF, 0xA5, 0x00, 0xFF
111
- Yellow = Color.new 0xFF, 0xFF, 0x00, 0xFF
112
- Green = Color.new 0x00, 0xFF, 0x00, 0xFF
113
- Blue = Color.new 0x00, 0x00, 0xFF, 0xFF
114
- Purple = Color.new 0XFF, 0x00, 0xFF, 0xFF
291
+ Background = Color.from 0x00000000, "Transparent"
292
+ Black = Color.from 0x000000FF, "Black"
293
+ Blue = Color.from 0x0000FFFF, "Blue"
294
+ Brown = Color.from 0x996633FF, "Brown"
295
+ Bubblegum = Color.from 0xFF66FFFF, "Bubblegum"
296
+ Cyan = Color.from 0x00FFFFFF, "Cyan"
297
+ Gray = Color.from 0x7F7F7FFF, "Gray"
298
+ Green = Color.from 0x00FF00FF, "Green"
299
+ Magenta = Color.from 0xFF00FFFF, "Magenta"
300
+ Orange = Color.from 0xFF7F00FF, "Orange"
301
+ Purple = Color.from 0x7F007FFF, "Purple"
302
+ Red = Color.from 0xFF0000FF, "Red"
303
+ White = Color.from 0xFFFFFFFF, "White"
304
+ Yellow = Color.from 0xFFFF00FF, "Yellow"
305
+
306
+ def ==(other) # :nodoc:
307
+ self.class === other and other.values == values
308
+ end
115
309
 
116
310
  ##
117
311
  # Red component
@@ -147,13 +341,37 @@ class PNG
147
341
  # Returns a new color with an alpha value adjusted by +i+.
148
342
 
149
343
  def intensity(i)
150
- return Color.new(r,b,g,(a*i) >> 8)
344
+ return Color.new(r,g,b,(a*i) >> 8)
151
345
  end
152
346
 
153
347
  def inspect # :nodoc:
154
- "#<%s %02x %02x %02x %02x>" % [self.class, *@values]
348
+ if @name then
349
+ "#<%s %s>" % [self.class, @name]
350
+ else
351
+ "#<%s %02x %02x %02x %02x>" % [self.class, r, g, b, a]
352
+ end
155
353
  end
156
354
 
355
+ ##
356
+ # An ASCII representation of this color, almost suitable for making ASCII
357
+ # art!
358
+
359
+ def to_ascii
360
+ return ' ' if a == 0x00
361
+ 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 '..'
366
+ end
367
+
368
+ def to_s
369
+ if @name then
370
+ @name
371
+ else
372
+ super
373
+ end
374
+ end
157
375
  end
158
376
 
159
377
  ##
@@ -176,39 +394,32 @@ class PNG
176
394
 
177
395
  attr_reader :data
178
396
 
179
- def initialize(height, width, background = Color::White)
180
- @height = height
397
+ def initialize(width, height, background = Color::Background)
181
398
  @width = width
182
- @data = Array.new(@width) { |x| Array.new(@height) { background } }
399
+ @height = height
400
+ @data = Array.new(@height) { |x| Array.new(@width, background) }
183
401
  end
184
402
 
185
403
  ##
186
404
  # Retrieves the color of the pixel at (+x+, +y+).
187
405
 
188
406
  def [](x, y)
189
- raise "bad x value #{x} >= #{@height}" if x >= @height
190
- raise "bad y value #{y} >= #{@width}" if y >= @width
191
- @data[y][x]
407
+ raise "bad x value #{x} >= #{@width}" if x >= @width
408
+ raise "bad y value #{y} >= #{@height}" if y >= @height
409
+ @data[@height-y-1][x]
192
410
  end
193
411
 
194
412
  ##
195
413
  # Sets the color of the pixel at (+x+, +y+) to +color+.
196
414
 
197
415
  def []=(x, y, color)
198
- raise "bad x value #{x} >= #{@height}" if x >= @height
199
- raise "bad y value #{y} >= #{@width}" if y >= @width
200
- @data[y][x] = color
416
+ raise "bad x value #{x} >= #{@width}" if x >= @width
417
+ raise "bad y value #{y} >= #{@height}" if y >= @height
418
+ @data[@height-y-1][x] = color
201
419
  end
202
420
 
203
- ##
204
- # Iterates over each pixel in the canvas.
205
-
206
- def each
207
- @data.each_with_index do |row, y|
208
- row.each_with_index do |pixel, x|
209
- yield x, y, color
210
- end
211
- end
421
+ def inspect # :nodoc:
422
+ '#<%s %dx%d>' % [self.class, @width, @height]
212
423
  end
213
424
 
214
425
  ##
@@ -224,9 +435,10 @@ class PNG
224
435
  # http://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm
225
436
 
226
437
  def line(x0, y0, x1, y1, color)
438
+ y0, y1, x0, x1 = y1, y0, x1, x0 if y0 > y1
227
439
  dx = x1 - x0
228
440
  sx = dx < 0 ? -1 : 1
229
- dx *= sx # TODO: abs?
441
+ dx *= sx
230
442
  dy = y1 - y0
231
443
 
232
444
  # 'easy' cases
@@ -245,7 +457,7 @@ class PNG
245
457
  end
246
458
 
247
459
  if dx == dy then
248
- Range.new(*[x0,x1].sort).each do |x|
460
+ x0.step(x1, sx) do |x|
249
461
  point(x, y0, color)
250
462
  y0 += 1
251
463
  end
@@ -259,7 +471,7 @@ class PNG
259
471
  e = (dx << 16) / dy
260
472
  (y0...y1-1).each do |i|
261
473
  e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF
262
- x0 = x0 + sx if (e_acc <= e_acc_temp)
474
+ x0 = x0 + sx if (e_acc <= e_acc_temp)
263
475
  w = 0xFF-(e_acc >> 8)
264
476
  point(x0, y0, color.intensity(w))
265
477
  y0 = y0 + 1
@@ -271,7 +483,7 @@ class PNG
271
483
 
272
484
  # horizontal displacement
273
485
  e = (dy << 16) / dx
274
- (x0...(x1-sx)).each do |i|
486
+ (dx - 1).downto(0) do |i|
275
487
  e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xFFFF
276
488
  y0 += 1 if (e_acc <= e_acc_temp)
277
489
  w = 0xFF-(e_acc >> 8)
@@ -282,7 +494,23 @@ class PNG
282
494
  point(x1, y1, color)
283
495
  end
284
496
 
285
- end
497
+ ##
498
+ # Returns an ASCII representation of this image
286
499
 
287
- end
500
+ def to_s
501
+ image = []
502
+ scale = (@width / 39) + 1
503
+
504
+ @data.each_with_index do |row, x|
505
+ next if x % scale != 0
506
+ row.each_with_index do |color, y|
507
+ next if y % scale != 0
508
+ image << color.to_ascii
509
+ end
510
+ image << "\n"
511
+ end
288
512
 
513
+ return image.join
514
+ end
515
+ end
516
+ end
@@ -0,0 +1,47 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'png'
4
+
5
+ class PNG
6
+ FULL = 360.0
7
+ HALF = FULL / 2
8
+
9
+ def self.angle(x, y)
10
+ return 0 if x == 0 and y == 0
11
+ rad_to_deg = 180.0 / Math::PI
12
+ (Math.atan2(-y, x) * rad_to_deg + 90) % 360
13
+ end
14
+
15
+ ##
16
+ # Makes a pie chart you can pass to PNG.new:
17
+ #
18
+ # png = PNG.new pie_chart(250, 0.30)
19
+ # png.save "pie.png"
20
+ # system 'open pie.png'
21
+
22
+ def self.pie_chart(diameter, pct_green,
23
+ good_color=PNG::Color::Green, bad_color=PNG::Color::Red)
24
+ diameter += 1 if diameter % 2 == 0
25
+ radius = (diameter / 2.0).to_i
26
+ pct_in_deg = FULL * pct_green
27
+ rad_to_deg = HALF / Math::PI
28
+
29
+ canvas = PNG::Canvas.new(diameter, diameter)
30
+
31
+ (-radius..radius).each do |x|
32
+ (-radius..radius).each do |y|
33
+ magnitude = Math.sqrt(x*x + y*y)
34
+ if magnitude <= radius then
35
+ angle = PNG.angle(x, y)
36
+ color = ((angle <= pct_in_deg) ? good_color : bad_color)
37
+
38
+ rx, ry = x+radius, y+radius
39
+
40
+ canvas[ rx, ry ] = color
41
+ end
42
+ end
43
+ end
44
+
45
+ canvas
46
+ end
47
+ end
@@ -0,0 +1,423 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'png'
4
+ require 'png/pie'
5
+
6
+ class TestPng < Test::Unit::TestCase
7
+
8
+ def setup
9
+ @canvas = PNG::Canvas.new 5, 10, PNG::Color::White
10
+ @png = PNG.new @canvas
11
+
12
+ @IHDR_length = "\000\000\000\r"
13
+ @IHDR_crc = "\2152\317\275"
14
+ @IHDR_crc_value = @IHDR_crc.unpack('N').first
15
+ @IHDR_data = "\000\000\000\n\000\000\000\n\b\006\000\000\000"
16
+ @IHDR_chunk = "#{@IHDR_length}IHDR#{@IHDR_data}#{@IHDR_crc}"
17
+
18
+ @blob = <<-EOF.unpack('m*').first
19
+ iVBORw0KGgoAAAANSUhEUgAAAAUAAAAKCAYAAAB8OZQwAAAAD0lEQVR4nGP4
20
+ jwUwDGVBALuJxzlQugpEAAAAAElFTkSuQmCC
21
+ EOF
22
+ end
23
+
24
+ def test_class_check_crc
25
+ assert PNG.check_crc('IHDR', @IHDR_data, @IHDR_crc_value)
26
+ end
27
+
28
+ def test_class_check_crc_exception
29
+ begin
30
+ PNG.check_crc('IHDR', @IHDR_data, @IHDR_crc_value + 1)
31
+ rescue ArgumentError => e
32
+ assert_equal "Invalid CRC encountered in IHDR chunk", e.message
33
+ else
34
+ flunk "exception wasn't raised"
35
+ end
36
+ end
37
+
38
+ def test_class_chunk
39
+ chunk = PNG.chunk 'IHDR', [10, 10, 8, 6, 0, 0, 0 ].pack('N2C5')
40
+ assert_equal @IHDR_chunk, chunk
41
+ end
42
+
43
+ def test_class_chunk_empty
44
+ chunk = PNG.chunk 'IHDR'
45
+ expected = "#{0.chr * 4}IHDR#{["IHDR".png_crc].pack 'N'}"
46
+ assert_equal expected, chunk
47
+ end
48
+
49
+ def test_to_blob
50
+ assert_equal @blob, @png.to_blob
51
+ end
52
+
53
+ def test_save
54
+ path = "blah.png"
55
+ @png.save(path)
56
+ assert_equal @blob, File.read(path)
57
+ ensure
58
+ assert_equal 1, File.unlink(path)
59
+ end
60
+
61
+ class TestCanvas < Test::Unit::TestCase
62
+
63
+ def setup
64
+ @canvas = PNG::Canvas.new 5, 10, PNG::Color::White
65
+ end
66
+
67
+ def test_index
68
+ assert_equal PNG::Color::White, @canvas[1, 2]
69
+ assert_same @canvas[1, 2], @canvas.data[1][2]
70
+ end
71
+
72
+ def test_index_tall
73
+ @canvas = PNG::Canvas.new 2, 4, PNG::Color::White
74
+ @canvas[ 0, 0] = PNG::Color::Black
75
+ @canvas[ 0, 3] = PNG::Color::Background
76
+ @canvas[ 1, 0] = PNG::Color::Yellow
77
+ @canvas[ 1, 3] = PNG::Color::Blue
78
+
79
+ expected = " ,,\n0000\n0000\n..++\n"
80
+
81
+ assert_equal expected, @canvas.to_s
82
+ end
83
+
84
+ def test_index_wide
85
+ @canvas = PNG::Canvas.new 4, 2, PNG::Color::White
86
+ @canvas[ 0, 0] = PNG::Color::Black
87
+ @canvas[ 3, 0] = PNG::Color::Background
88
+ @canvas[ 0, 1] = PNG::Color::Yellow
89
+ @canvas[ 3, 1] = PNG::Color::Blue
90
+
91
+ expected = "++0000,,\n..0000 \n"
92
+
93
+ assert_equal expected, @canvas.to_s
94
+ end
95
+
96
+ def test_index_bad_x
97
+ begin
98
+ @canvas[6, 1]
99
+ rescue => e
100
+ assert_equal "bad x value 6 >= 5", e.message
101
+ else
102
+ flunk "didn't raise"
103
+ end
104
+ end
105
+
106
+ def test_index_bad_y
107
+ begin
108
+ @canvas[1, 11]
109
+ rescue => e
110
+ assert_equal "bad y value 11 >= 10", e.message
111
+ else
112
+ flunk "didn't raise"
113
+ end
114
+ end
115
+
116
+ def test_index_equals
117
+ @canvas[1, 2] = PNG::Color::Red
118
+ assert_equal PNG::Color::Red, @canvas[1, 2]
119
+ assert_same @canvas[1, 2], @canvas.data[7][1]
120
+
121
+ expected = "
122
+ 0000000000
123
+ 0000000000
124
+ 0000000000
125
+ 0000000000
126
+ 0000000000
127
+ 0000000000
128
+ 0000000000
129
+ 00,,000000
130
+ 0000000000
131
+ 0000000000".strip + "\n"
132
+ actual = @canvas.to_s
133
+ assert_equal expected, actual
134
+ end
135
+
136
+ def test_index_equals_bad_x
137
+ begin
138
+ @canvas[6, 1] = PNG::Color::Red
139
+ rescue => e
140
+ assert_equal "bad x value 6 >= 5", e.message
141
+ else
142
+ flunk "didn't raise"
143
+ end
144
+ end
145
+
146
+ def test_index_equals_bad_y
147
+ begin
148
+ @canvas[1, 11] = PNG::Color::Red
149
+ rescue => e
150
+ assert_equal "bad y value 11 >= 10", e.message
151
+ else
152
+ flunk "didn't raise"
153
+ end
154
+ end
155
+
156
+ # def test_point
157
+ # raise NotImplementedError, 'Need to write test_point'
158
+ # end
159
+
160
+ def test_inspect
161
+ assert_equal "#<PNG::Canvas 5x10>", @canvas.inspect
162
+ end
163
+
164
+ def test_point
165
+ assert_equal PNG::Color.new(0xfe, 0x00, 0xfe, 0xfe),
166
+ @canvas.point(0, 0, PNG::Color::Magenta)
167
+ # flunk "this doesn't test ANYTHING"
168
+ end
169
+
170
+ def test_line
171
+ @canvas.line 0, 9, 4, 0, PNG::Color::Black
172
+
173
+ expected = <<-EOF
174
+ ..00000000
175
+ ,,00000000
176
+ 00,,000000
177
+ 00..000000
178
+ 00++++0000
179
+ 0000..0000
180
+ 0000++++00
181
+ 000000..00
182
+ 000000,,00
183
+ 00000000..
184
+ EOF
185
+
186
+ assert_equal expected, @canvas.to_s
187
+ end
188
+
189
+ def test_positive_slope_line
190
+ @canvas.line 0, 0, 4, 9, PNG::Color::Black
191
+
192
+ expected = <<-EOF
193
+ 00000000..
194
+ 00000000,,
195
+ 000000,,00
196
+ 000000..00
197
+ 0000++++00
198
+ 0000..0000
199
+ 00++++0000
200
+ 00..000000
201
+ 00,,000000
202
+ ..00000000
203
+ EOF
204
+
205
+ assert_equal expected, @canvas.to_s
206
+ end
207
+
208
+ def util_ascii_art(width, height)
209
+ (("0" * width * 2) + "\n") * height
210
+ end
211
+
212
+ def test_to_s_normal
213
+ @canvas = PNG::Canvas.new 5, 10, PNG::Color::White
214
+ expected = util_ascii_art(5, 10)
215
+ assert_equal expected, @canvas.to_s
216
+ end
217
+
218
+ def test_to_s_wide
219
+ @canvas = PNG::Canvas.new 250, 10, PNG::Color::White
220
+ expected = util_ascii_art(36, 2) # scaled
221
+ assert_equal expected, @canvas.to_s
222
+ end
223
+
224
+ def test_to_s_tall
225
+ @canvas = PNG::Canvas.new 10, 250, PNG::Color::White
226
+ expected = util_ascii_art(10, 250)
227
+ assert_equal expected, @canvas.to_s
228
+ end
229
+
230
+ def test_to_s_huge
231
+ @canvas = PNG::Canvas.new 250, 250, PNG::Color::White
232
+ expected = util_ascii_art(36, 36) # scaled
233
+ assert_equal expected, @canvas.to_s
234
+ end
235
+
236
+ # def test_class_read_chunk
237
+ # type, data = PNG.read_chunk @IHDR_chunk
238
+
239
+ # assert_equal 'IHDR', type
240
+ # assert_equal @IHDR_data, data
241
+ # end
242
+
243
+ # def test_class_read_IDAT
244
+ # canvas = PNG::Canvas.new 10, 10, PNG::Color::White
245
+
246
+ # data = "x\332c\370O$`\030UH_\205\000#\373\216\200"
247
+
248
+ # PNG.read_IDAT data, canvas
249
+
250
+ # assert_equal @blob, PNG.new(canvas).to_blob
251
+ # end
252
+
253
+ # def test_class_read_IHDR
254
+ # canvas = PNG.read_IHDR @IHDR_data
255
+ # assert_equal 10, canvas.width
256
+ # assert_equal 10, canvas.height
257
+ # end
258
+ end
259
+
260
+ class TestPng::TestColor < Test::Unit::TestCase
261
+ def setup
262
+ @color = PNG::Color.new 0x01, 0x02, 0x03, 0x04
263
+ end
264
+
265
+ def test_class_from_str
266
+ @color = PNG::Color.from "0x01020304"
267
+ test_r
268
+ test_g
269
+ test_b
270
+ test_a
271
+ end
272
+
273
+ def test_class_from_int
274
+ @color = PNG::Color.from 0x01020304
275
+ test_r
276
+ test_g
277
+ test_b
278
+ test_a
279
+ end
280
+
281
+ def test_r
282
+ assert_equal 0x01, @color.r
283
+ end
284
+
285
+ def test_g
286
+ assert_equal 0x02, @color.g
287
+ end
288
+
289
+ def test_b
290
+ assert_equal 0x03, @color.b
291
+ end
292
+
293
+ def test_a
294
+ assert_equal 0x04, @color.a
295
+ end
296
+
297
+ def test_blend
298
+ c1 = @color
299
+ c2 = PNG::Color.new 0xFF, 0xFE, 0xFD, 0xFC
300
+
301
+ assert_equal PNG::Color.new(0xfb, 0xfa, 0xf9, 0xf8), c1.blend(c2)
302
+ end
303
+
304
+ def test_intensity
305
+ assert_equal PNG::Color.new(0x01, 0x02, 0x03, 0x3c), @color.intensity(0xf00)
306
+ end
307
+
308
+ def test_inspect
309
+ assert_equal "#<PNG::Color 01 02 03 04>", @color.inspect
310
+ end
311
+
312
+ def test_inspect_name
313
+ assert_equal "#<PNG::Color Red>", PNG::Color::Red.inspect
314
+ end
315
+
316
+ def test_to_ascii
317
+ assert_equal '00', PNG::Color::White.to_ascii, "white"
318
+ assert_equal '++', PNG::Color::Yellow.to_ascii, "yellow"
319
+ assert_equal ',,', PNG::Color::Red.to_ascii, "red"
320
+ assert_equal '..', PNG::Color::Black.to_ascii, "black"
321
+ assert_equal ' ', PNG::Color::Background.to_ascii, "background"
322
+ end
323
+
324
+ def test_to_ascii_alpha
325
+ assert_equal '00', PNG::Color.new(255,255,255,255).to_ascii
326
+ assert_equal '00', PNG::Color.new(255,255,255,192).to_ascii
327
+ assert_equal '++', PNG::Color.new(255,255,255,191).to_ascii
328
+ assert_equal '++', PNG::Color.new(255,255,255,127).to_ascii
329
+ assert_equal ',,', PNG::Color.new(255,255,255,126).to_ascii
330
+ assert_equal ',,', PNG::Color.new(255,255,255, 64).to_ascii
331
+ assert_equal '..', PNG::Color.new(255,255,255, 63).to_ascii
332
+ assert_equal '..', PNG::Color.new(255,255,255, 1).to_ascii
333
+ assert_equal ' ', PNG::Color.new(255,255,255, 0).to_ascii
334
+ end
335
+
336
+ def test_to_s_name
337
+ assert_equal 'Red', PNG::Color::Red.to_s
338
+ end
339
+
340
+ def test_to_s
341
+ obj = PNG::Color.new(255,255,255, 0)
342
+ assert_equal '#<PNG::Color:0xXXXXXX>', obj.to_s.sub(/0x[0-9a-f]+/, '0xXXXXXX')
343
+ end
344
+
345
+ # def test_values
346
+ # raise NotImplementedError, 'Need to write test_values'
347
+ # end
348
+ end
349
+
350
+ end
351
+
352
+ class TestPng::TestPie < Test::Unit::TestCase
353
+ def setup
354
+
355
+ end
356
+
357
+ def test_pie_chart_odd
358
+ expected =
359
+ [" .. ",
360
+ " ,,,,,,........ ",
361
+ " ,,,,,,,,.......... ",
362
+ " ,,,,,,,,.......... ",
363
+ " ,,,,,,,,.......... ",
364
+ ",,,,,,,,,,............",
365
+ " ,,,,,,,,,,,,,,,,,, ",
366
+ " ,,,,,,,,,,,,,,,,,, ",
367
+ " ,,,,,,,,,,,,,,,,,, ",
368
+ " ,,,,,,,,,,,,,, ",
369
+ " ,, ",
370
+ nil].join("\n")
371
+
372
+ actual = PNG::pie_chart(11, 0.25, PNG::Color::Black, PNG::Color::Green)
373
+ assert_equal expected, actual.to_s
374
+ end
375
+
376
+ def test_pie_chart_even
377
+ expected =
378
+ [" .. ",
379
+ " ,,,,,,........ ",
380
+ " ,,,,,,,,.......... ",
381
+ " ,,,,,,,,.......... ",
382
+ " ,,,,,,,,.......... ",
383
+ ",,,,,,,,,,............",
384
+ " ,,,,,,,,,,,,,,,,,, ",
385
+ " ,,,,,,,,,,,,,,,,,, ",
386
+ " ,,,,,,,,,,,,,,,,,, ",
387
+ " ,,,,,,,,,,,,,, ",
388
+ " ,, ",
389
+ nil].join("\n")
390
+
391
+ actual = PNG::pie_chart(10, 0.25, PNG::Color::Black, PNG::Color::Green)
392
+ assert_equal expected, actual.to_s
393
+ end
394
+
395
+ def util_angle(expect, x, y)
396
+ actual = PNG.angle(x, y)
397
+ case expect
398
+ when Integer then
399
+ assert_equal(expect, actual,
400
+ "[#{x}, #{y}] should be == #{expect}, was #{actual}")
401
+ else
402
+ assert_in_delta(expect, actual, 0.5)
403
+ end
404
+ end
405
+
406
+ def test_math_is_hard_lets_go_shopping
407
+ util_angle 0, 0, 0
408
+ (25..500).step(25) do |n|
409
+ util_angle 0, 0, n
410
+ util_angle 90, n, 0
411
+ util_angle 180, 0, -n
412
+ util_angle 270, -n, 0
413
+ end
414
+
415
+ util_angle 359.5, -1, 250
416
+ util_angle 0.0, 0, 250
417
+ util_angle 0.5, 1, 250
418
+
419
+ util_angle 89.5, 250, 1
420
+ util_angle 90.0, 250, 0
421
+ util_angle 90.5, 250, -1
422
+ end
423
+ end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.8.99
2
+ rubygems_version: 0.9.0.9
3
3
  specification_version: 1
4
4
  name: png
5
5
  version: !ruby/object:Gem::Version
6
- version: 1.0.0
7
- date: 2006-08-31 00:00:00 -07:00
8
- summary: A pure ruby PNG library
6
+ version: 1.1.0
7
+ date: 2007-03-26 00:00:00 -07:00
8
+ summary: An almost-pure-ruby PNG library
9
9
  require_paths:
10
10
  - lib
11
- email: ryand-ruby@zenspider.com
12
- homepage:
13
- rubyforge_project:
14
- description: png allows you to write a PNG file without any C libraries.
11
+ email: support@zenspider.com
12
+ homepage: http://www.zenspider.com/ZSS/Products/png/
13
+ rubyforge_project: seattlerb
14
+ description: "PNG is an almost-pure-ruby PNG library. It lets you write a PNG without any C libraries. == FEATURES * Very simple interface. * Outputs simple PNG files with ease. * Basic PNG reader as well (someday it might do compositing and the like!). * Almost pure ruby, does require a compiler. == SYNOPSYS require 'png' canvas = PNG::Canvas.new 200, 200 # Set a point to a color canvas[100, 100] = PNG::Color::Black # draw an anti-aliased line canvas.line 50, 50, 100, 50, PNG::Color::Blue png = PNG.new canvas png.save 'blah.png'"
15
15
  autorequire:
16
16
  default_executable:
17
17
  bindir: bin
@@ -28,14 +28,20 @@ cert_chain:
28
28
  post_install_message:
29
29
  authors:
30
30
  - Ryan Davis
31
+ - Eric Hodel
31
32
  files:
33
+ - History.txt
32
34
  - Manifest.txt
33
- - README
35
+ - README.txt
34
36
  - Rakefile
35
37
  - example/lines.rb
38
+ - example/profile.rb
39
+ - example/profile_lines.rb
36
40
  - lib/png.rb
37
- test_files: []
38
-
41
+ - lib/png/pie.rb
42
+ - test/test_png.rb
43
+ test_files:
44
+ - test/test_png.rb
39
45
  rdoc_options: []
40
46
 
41
47
  extra_rdoc_files: []
@@ -46,5 +52,22 @@ extensions: []
46
52
 
47
53
  requirements: []
48
54
 
49
- dependencies: []
50
-
55
+ dependencies:
56
+ - !ruby/object:Gem::Dependency
57
+ name: RubyInline
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Version::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.5.0
64
+ version:
65
+ - !ruby/object:Gem::Dependency
66
+ name: hoe
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: 1.2.0
73
+ version:
data/README DELETED
@@ -1,28 +0,0 @@
1
- = png
2
-
3
- == About
4
-
5
- png is a pure-ruby PNG library. It lets you write a PNG without any C
6
- libraries. Of course it is "slow".
7
-
8
- == Installing png
9
-
10
- Just install the gem:
11
-
12
- $ sudo gem install png
13
-
14
- == Using png
15
-
16
- require 'png'
17
-
18
- canvas = PNG::Canvas.new 200, 200
19
-
20
- # Set a point to a color
21
- canvas[100, 100] = PNG::Color::Black
22
-
23
- # draw an anti-aliased line
24
- canvas.line 50, 50, 100, 50, PNG::Color::Blue
25
-
26
- png = PNG.new canvas
27
- png.save 'blah.png'
28
-