png 1.1.0 → 1.2.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.
Binary file
@@ -0,0 +1,73 @@
1
+ # encoding: BINARY
2
+
3
+ require 'png/reader'
4
+
5
+ ##
6
+ # Implements a simple bitmap font by extracting letters from a PNG.
7
+
8
+ class PNG::Font
9
+ LETTERS = (('A'..'Z').to_a +
10
+ ('a'..'z').to_a +
11
+ ('0'..'9').to_a + [" "] * 16 +
12
+ '({[<!@#$%^&*?_+-=;,"/~>]})'.split(//))
13
+
14
+ attr_reader :height, :width, :canvas
15
+
16
+ def self.default
17
+ @@default ||= new(File.join(File.dirname(__FILE__), "default_font.png"))
18
+ end
19
+
20
+ def initialize(png_file)
21
+ @canvas = PNG.load_file png_file
22
+ @height, @width = canvas.height / 4, canvas.width / 26
23
+ @cache = {}
24
+ end
25
+
26
+ def coordinates c
27
+ i = LETTERS.index c
28
+
29
+ raise ArgumentError, "Can't find #{c.inspect}" unless i
30
+
31
+ x = (i % 26) * width
32
+ y = (3 - (i / 26)) * height # start from the top (3rd row)
33
+
34
+ return x, y, x+width-1, y+height-1
35
+ end
36
+
37
+ def [] c
38
+ c = c.chr unless String === c
39
+ x0, y0, x1, y1 = coordinates c
40
+
41
+ @cache[c] ||= @canvas.extract(x0, y0, x1, y1)
42
+ end
43
+ end
44
+
45
+ class PNG::Canvas
46
+ ##
47
+ # Write a string at [x, y] with font, optionally specifying a font,
48
+ # an alignment of :left, :center, or :right and the style to draw
49
+ # the annotation (see #composite).
50
+ #
51
+ # require 'png/font'
52
+
53
+ def annotate(string, x, y,
54
+ font = PNG::Font.default, align = :left, style = :overwrite)
55
+ case align
56
+ when :left then
57
+ # do nothing
58
+ when :center then
59
+ x -= string.length * font.width / 2
60
+ when :right then
61
+ x -= string.length * font.width
62
+ else
63
+ raise ArgumentError, "Unknown align: #{align.inspect}"
64
+ end
65
+
66
+ x_offset, width = 0, font.width
67
+
68
+ string.split(//).each do |char|
69
+ self.composite font[char], x + x_offset, y
70
+ x_offset += width
71
+ end
72
+ end
73
+ end
@@ -1,4 +1,4 @@
1
- #!/usr/local/bin/ruby -w
1
+ # encoding: BINARY
2
2
 
3
3
  require 'png'
4
4
 
@@ -0,0 +1,142 @@
1
+ # encoding: BINARY
2
+
3
+ require 'png'
4
+ require 'enumerator'
5
+
6
+ class PNG
7
+ def self.load_file path, metadata_only = false
8
+ file = File.open(path, 'rb') { |f| f.read }
9
+ self.load file, metadata_only
10
+ end
11
+
12
+ def self.load png, metadata_only = false
13
+ png = png.dup
14
+ signature = png.slice! 0, 8
15
+ raise ArgumentError, 'Invalid PNG signature' unless signature == SIGNATURE
16
+
17
+ ihdr = read_chunk 'IHDR', png
18
+
19
+ bit_depth, color_type, width, height = read_IHDR ihdr, metadata_only
20
+
21
+ return [width, height, bit_depth] if metadata_only
22
+
23
+ canvas = PNG::Canvas.new width, height
24
+
25
+ type = png.slice(4, 4).unpack('a4').first
26
+ read_chunk type, png if type == 'iCCP' # Ignore color profile
27
+
28
+ read_IDAT read_chunk('IDAT', png), bit_depth, color_type, canvas
29
+ read_chunk 'IEND', png
30
+
31
+ canvas
32
+ end
33
+
34
+ def self.read_chunk expected_type, png
35
+ size, type = png.slice!(0, 8).unpack 'Na4'
36
+ data, crc = png.slice!(0, size + 4).unpack "a#{size}N"
37
+
38
+ check_crc type, data, crc
39
+
40
+ raise ArgumentError, "Expected #{expected_type} chunk, not #{type}" unless
41
+ type == expected_type
42
+
43
+ return data
44
+ end
45
+
46
+ def self.check_crc type, data, crc
47
+ return true if (type + data).png_crc == crc
48
+ raise ArgumentError, "Invalid CRC encountered in #{type} chunk"
49
+ end
50
+
51
+ def self.read_IHDR data, metadata_only = false
52
+ width, height, bit_depth, color_type, *rest = data.unpack 'N2C5'
53
+
54
+ unless metadata_only then
55
+ raise ArgumentError, "Wrong bit depth: #{bit_depth}" unless
56
+ bit_depth == 8
57
+ raise ArgumentError, "Wrong color type: #{color_type}" unless
58
+ color_type == RGBA or color_type = RGB
59
+ raise ArgumentError, "Unsupported options: #{rest.inspect}" unless
60
+ rest == [0, 0, 0]
61
+ end
62
+
63
+ return bit_depth, color_type, width, height
64
+ end
65
+
66
+ def self.read_IDAT data, bit_depth, color_type, canvas
67
+ data = Zlib::Inflate.inflate(data).unpack 'C*'
68
+
69
+ pixel_size = color_type == RGBA ? 4 : 3
70
+
71
+ height = canvas.height
72
+ scanline_length = pixel_size * canvas.width + 1 # for filter
73
+
74
+ row = canvas.height - 1
75
+ until data.empty? do
76
+ row_data = data.slice! 0, scanline_length
77
+
78
+ filter = row_data.shift
79
+ case filter
80
+ when NONE then
81
+ when SUB then
82
+ row_data.each_with_index do |byte, index|
83
+ left = index < pixel_size ? 0 : row_data[index - pixel_size]
84
+ row_data[index] = (byte + left) % 256
85
+ end
86
+ when UP then
87
+ row_data.each_with_index do |byte, index|
88
+ col = index / pixel_size
89
+ upper = row == 0 ? 0 : canvas[col, row + 1].values[index % pixel_size]
90
+ row_data[index] = (upper + byte) % 256
91
+ end
92
+ when AVG then
93
+ row_data.each_with_index do |byte, index|
94
+ col = index / pixel_size
95
+ upper = row == 0 ? 0 : canvas[col, row + 1].values[index % pixel_size]
96
+ left = index < pixel_size ? 0 : row_data[index - pixel_size]
97
+
98
+ row_data[index] = (byte + ((left + upper)/2).floor) % 256
99
+ end
100
+ when PAETH then
101
+ left = upper = upper_left = nil
102
+ row_data.each_with_index do |byte, index|
103
+ col = index / pixel_size
104
+
105
+ left = index < pixel_size ? 0 : row_data[index - pixel_size]
106
+ if row == height then
107
+ upper = upper_left = 0
108
+ else
109
+ upper = canvas[col, row + 1].values[index % pixel_size]
110
+ upper_left = col == 0 ? 0 :
111
+ canvas[col - 1, row + 1].values[index % pixel_size]
112
+ end
113
+
114
+ paeth = paeth left, upper, upper_left
115
+ row_data[index] = (byte + paeth) % 256
116
+ end
117
+ else
118
+ raise ArgumentError, "invalid filter algorithm #{filter}"
119
+ end
120
+
121
+ col = 0
122
+ row_data.each_slice pixel_size do |slice|
123
+ slice << 0xFF if pixel_size == 3
124
+ canvas[col, row] = PNG::Color.new(*slice)
125
+ col += 1
126
+ end
127
+
128
+ row -= 1
129
+ end
130
+ end
131
+
132
+ def self.paeth a, b, c # left, above, upper left
133
+ p = a + b - c
134
+ pa = (p - a).abs
135
+ pb = (p - b).abs
136
+ pc = (p - c).abs
137
+
138
+ return a if pa <= pb && pa <= pc
139
+ return b if pb <= pc
140
+ c
141
+ end
142
+ end
@@ -1,43 +1,37 @@
1
- require 'test/unit'
1
+ dir = File.expand_path "~/.ruby_inline"
2
+ if test ?d, dir then
3
+ require 'fileutils'
4
+ puts "nuking #{dir}"
5
+ # force removal, Windoze is bitching at me, something to hunt later...
6
+ FileUtils.rm_r dir, :force => true
7
+ end
8
+
9
+ require 'minitest/autorun'
2
10
  require 'rubygems'
3
11
  require 'png'
12
+ require 'png/reader'
4
13
  require 'png/pie'
5
14
 
6
- class TestPng < Test::Unit::TestCase
7
-
15
+ class TestPng < MiniTest::Unit::TestCase
8
16
  def setup
9
17
  @canvas = PNG::Canvas.new 5, 10, PNG::Color::White
10
18
  @png = PNG.new @canvas
11
19
 
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
20
  @blob = <<-EOF.unpack('m*').first
19
21
  iVBORw0KGgoAAAANSUhEUgAAAAUAAAAKCAYAAAB8OZQwAAAAD0lEQVR4nGP4
20
22
  jwUwDGVBALuJxzlQugpEAAAAAElFTkSuQmCC
21
23
  EOF
22
24
  end
23
25
 
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
26
  def test_class_chunk
39
27
  chunk = PNG.chunk 'IHDR', [10, 10, 8, 6, 0, 0, 0 ].pack('N2C5')
40
- assert_equal @IHDR_chunk, chunk
28
+
29
+ header_crc = "\2152\317\275"
30
+ header_data = "\000\000\000\n\000\000\000\n\b\006\000\000\000"
31
+ header_length = "\000\000\000\r"
32
+ header_chunk = "#{header_length}IHDR#{header_data}#{header_crc}"
33
+
34
+ assert_equal header_chunk, chunk
41
35
  end
42
36
 
43
37
  def test_class_chunk_empty
@@ -53,17 +47,104 @@ jwUwDGVBALuJxzlQugpEAAAAAElFTkSuQmCC
53
47
  def test_save
54
48
  path = "blah.png"
55
49
  @png.save(path)
56
- assert_equal @blob, File.read(path)
50
+ file = File.open(path, 'rb') { |f| f.read }
51
+ assert_equal @blob, file
57
52
  ensure
58
53
  assert_equal 1, File.unlink(path)
59
54
  end
60
55
 
61
- class TestCanvas < Test::Unit::TestCase
56
+ end
57
+
58
+ class TestCanvas < MiniTest::Unit::TestCase
62
59
 
63
60
  def setup
64
61
  @canvas = PNG::Canvas.new 5, 10, PNG::Color::White
65
62
  end
66
63
 
64
+ def test_composite_default
65
+ canvas1, canvas2 = util_composite_canvases
66
+
67
+ canvas1.composite canvas2, 1, 1
68
+
69
+ expected = " xxxxxxxx
70
+ xxxxxxxx
71
+ xx..xxxx
72
+ ..xxxxxx
73
+ ".gsub(/ /, '')
74
+
75
+ assert_equal expected, canvas1.to_s.gsub(/ /, 'x')
76
+ end
77
+
78
+ def test_composite_underlay
79
+ canvas1, canvas2 = util_composite_canvases
80
+
81
+ canvas1.composite canvas2, 1, 1, :add
82
+
83
+ expected = " xxxxxxxx
84
+ xxxx..xx
85
+ xx00xxxx
86
+ ..xxxxxx
87
+ ".gsub(/ /, '')
88
+
89
+ assert_equal expected, canvas1.to_s.gsub(/ /, 'x')
90
+ end
91
+
92
+ def test_composite_overlay
93
+ canvas1, canvas2 = util_composite_canvases
94
+
95
+ canvas1.composite canvas2, 1, 1, :overlay
96
+
97
+ expected = " xxxxxxxx
98
+ xxxx..xx
99
+ xx..xxxx
100
+ ..xxxxxx
101
+ ".gsub(/ /, '')
102
+
103
+ assert_equal expected, canvas1.to_s.gsub(/ /, 'x')
104
+ end
105
+
106
+ def test_composite_blend
107
+ canvas1, canvas2 = util_composite_canvases
108
+
109
+ canvas1.composite canvas2, 1, 1, :blend
110
+
111
+ expected = " xxxxxxxx
112
+ xxxx..xx
113
+ xx,,xxxx
114
+ ..xxxxxx
115
+ ".gsub(/ /, '')
116
+
117
+ assert_equal expected, canvas1.to_s.gsub(/ /, 'x')
118
+ end
119
+
120
+ def test_composite_bad_style
121
+ canvas1, canvas2 = util_composite_canvases
122
+
123
+ assert_raises RuntimeError do
124
+ canvas1.composite canvas2, 1, 1, :bad
125
+ end
126
+ end
127
+
128
+ def test_extract
129
+ canvas1, _ = util_composite_canvases
130
+
131
+ expected = " xxxxxxxx
132
+ xxxx..xx
133
+ xx00xxxx
134
+ ..xxxxxx
135
+ ".gsub(/ /, '')
136
+
137
+ assert_equal expected, canvas1.to_s.gsub(/ /, 'x')
138
+
139
+ canvas2 = canvas1.extract(1, 1, 2, 2)
140
+
141
+ expected = " xx..
142
+ 00xx
143
+ ".gsub(/ /, '')
144
+
145
+ assert_equal expected, canvas2.to_s.gsub(/ /, 'x')
146
+ end
147
+
67
148
  def test_index
68
149
  assert_equal PNG::Color::White, @canvas[1, 2]
69
150
  assert_same @canvas[1, 2], @canvas.data[1][2]
@@ -162,7 +243,7 @@ class TestCanvas < Test::Unit::TestCase
162
243
  end
163
244
 
164
245
  def test_point
165
- assert_equal PNG::Color.new(0xfe, 0x00, 0xfe, 0xfe),
246
+ assert_equal PNG::Color.new(0xff, 0x7f, 0xff, 0xff),
166
247
  @canvas.point(0, 0, PNG::Color::Magenta)
167
248
  # flunk "this doesn't test ANYTHING"
168
249
  end
@@ -171,15 +252,15 @@ class TestCanvas < Test::Unit::TestCase
171
252
  @canvas.line 0, 9, 4, 0, PNG::Color::Black
172
253
 
173
254
  expected = <<-EOF
174
- ..00000000
175
255
  ,,00000000
176
- 00,,000000
256
+ ,,00000000
257
+ ,,,,000000
177
258
  00..000000
178
- 00++++0000
259
+ 00,,,,0000
179
260
  0000..0000
180
- 0000++++00
261
+ 0000,,,,00
181
262
  000000..00
182
- 000000,,00
263
+ 000000,,,,
183
264
  00000000..
184
265
  EOF
185
266
 
@@ -190,25 +271,21 @@ class TestCanvas < Test::Unit::TestCase
190
271
  @canvas.line 0, 0, 4, 9, PNG::Color::Black
191
272
 
192
273
  expected = <<-EOF
193
- 00000000..
194
274
  00000000,,
195
- 000000,,00
275
+ 00000000,,
276
+ 000000,,,,
196
277
  000000..00
197
- 0000++++00
278
+ 0000,,,,00
198
279
  0000..0000
199
- 00++++0000
280
+ 00,,,,0000
200
281
  00..000000
201
- 00,,000000
282
+ ,,,,000000
202
283
  ..00000000
203
284
  EOF
204
285
 
205
286
  assert_equal expected, @canvas.to_s
206
287
  end
207
288
 
208
- def util_ascii_art(width, height)
209
- (("0" * width * 2) + "\n") * height
210
- end
211
-
212
289
  def test_to_s_normal
213
290
  @canvas = PNG::Canvas.new 5, 10, PNG::Color::White
214
291
  expected = util_ascii_art(5, 10)
@@ -233,31 +310,39 @@ class TestCanvas < Test::Unit::TestCase
233
310
  assert_equal expected, @canvas.to_s
234
311
  end
235
312
 
236
- # def test_class_read_chunk
237
- # type, data = PNG.read_chunk @IHDR_chunk
313
+ def util_composite_canvases
314
+ canvas1 = PNG::Canvas.new 4, 4
315
+ canvas1[0, 0] = PNG::Color::Black
316
+ canvas1[1, 1] = PNG::Color::White
317
+ canvas1[2, 2] = PNG::Color::Black
238
318
 
239
- # assert_equal 'IHDR', type
240
- # assert_equal @IHDR_data, data
241
- # end
319
+ expected = " xxxxxxxx
320
+ xxxx..xx
321
+ xx00xxxx
322
+ ..xxxxxx
323
+ ".gsub(/ /, '')
242
324
 
243
- # def test_class_read_IDAT
244
- # canvas = PNG::Canvas.new 10, 10, PNG::Color::White
325
+ assert_equal expected, canvas1.to_s.gsub(/ /, 'x')
245
326
 
246
- # data = "x\332c\370O$`\030UH_\205\000#\373\216\200"
247
327
 
248
- # PNG.read_IDAT data, canvas
328
+ canvas2 = PNG::Canvas.new 2, 2
329
+ canvas2[0, 0] = PNG::Color::Black
249
330
 
250
- # assert_equal @blob, PNG.new(canvas).to_blob
251
- # end
331
+ expected = " xxxx
332
+ ..xx
333
+ ".gsub(/ /, '')
252
334
 
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
335
+ assert_equal expected, canvas2.to_s.gsub(/ /, 'x')
336
+
337
+ return canvas1, canvas2
338
+ end
339
+
340
+ def util_ascii_art(width, height)
341
+ (("0" * width * 2) + "\n") * height
342
+ end
258
343
  end
259
344
 
260
- class TestPng::TestColor < Test::Unit::TestCase
345
+ class TestPng::TestColor < MiniTest::Unit::TestCase
261
346
  def setup
262
347
  @color = PNG::Color.new 0x01, 0x02, 0x03, 0x04
263
348
  end
@@ -295,10 +380,16 @@ class TestPng::TestColor < Test::Unit::TestCase
295
380
  end
296
381
 
297
382
  def test_blend
298
- c1 = @color
299
- c2 = PNG::Color.new 0xFF, 0xFE, 0xFD, 0xFC
383
+ # c1 = @color
384
+ # c2 = PNG::Color.new 0xFF, 0xFE, 0xFD, 0xFC
385
+
386
+ # assert_equal PNG::Color.new(0xFB, 0xFA, 0xF9, 0xF8), c1.blend(c2)
387
+
388
+ c1 = PNG::Color::White
389
+ c2 = PNG::Color::Black
300
390
 
301
- assert_equal PNG::Color.new(0xfb, 0xfa, 0xf9, 0xf8), c1.blend(c2)
391
+ assert_equal PNG::Color::Gray, c2.blend(c1)
392
+ assert_equal PNG::Color::Gray, c1.blend(c2)
302
393
  end
303
394
 
304
395
  def test_intensity
@@ -313,6 +404,21 @@ class TestPng::TestColor < Test::Unit::TestCase
313
404
  assert_equal "#<PNG::Color Red>", PNG::Color::Red.inspect
314
405
  end
315
406
 
407
+ def test_pipe
408
+ b = PNG::Color::Black
409
+ w = PNG::Color::White
410
+ t = PNG::Color::Background
411
+
412
+ # first non-transparent
413
+ assert_equal b, b | t
414
+ assert_equal b, t | b
415
+
416
+ assert_equal b, b | w
417
+ assert_equal w, w | b
418
+
419
+ assert_equal t, t | t
420
+ end
421
+
316
422
  def test_to_ascii
317
423
  assert_equal '00', PNG::Color::White.to_ascii, "white"
318
424
  assert_equal '++', PNG::Color::Yellow.to_ascii, "yellow"
@@ -325,7 +431,7 @@ class TestPng::TestColor < Test::Unit::TestCase
325
431
  assert_equal '00', PNG::Color.new(255,255,255,255).to_ascii
326
432
  assert_equal '00', PNG::Color.new(255,255,255,192).to_ascii
327
433
  assert_equal '++', PNG::Color.new(255,255,255,191).to_ascii
328
- assert_equal '++', PNG::Color.new(255,255,255,127).to_ascii
434
+ assert_equal ',,', PNG::Color.new(255,255,255,127).to_ascii
329
435
  assert_equal ',,', PNG::Color.new(255,255,255,126).to_ascii
330
436
  assert_equal ',,', PNG::Color.new(255,255,255, 64).to_ascii
331
437
  assert_equal '..', PNG::Color.new(255,255,255, 63).to_ascii
@@ -342,18 +448,22 @@ class TestPng::TestColor < Test::Unit::TestCase
342
448
  assert_equal '#<PNG::Color:0xXXXXXX>', obj.to_s.sub(/0x[0-9a-f]+/, '0xXXXXXX')
343
449
  end
344
450
 
451
+ def test_equals2
452
+ assert_equal PNG::Color.new(255,255,255, 0), PNG::Color.new(255,255,255, 0)
453
+ end
454
+
455
+ def test_hash
456
+ a = PNG::Color.new(255,255,255, 0)
457
+ b = PNG::Color.new(255,255,255, 0)
458
+ assert_equal a.hash, b.hash
459
+ end
460
+
345
461
  # def test_values
346
462
  # raise NotImplementedError, 'Need to write test_values'
347
463
  # end
348
464
  end
349
465
 
350
- end
351
-
352
- class TestPng::TestPie < Test::Unit::TestCase
353
- def setup
354
-
355
- end
356
-
466
+ class TestPng::TestPie < MiniTest::Unit::TestCase
357
467
  def test_pie_chart_odd
358
468
  expected =
359
469
  [" .. ",