Class: PNG

Inherits:
Object
  • Object
show all
Defined in:
lib/png.rb,
lib/png/pie.rb,
lib/png/reader.rb

Overview

An almost-pure-ruby Portable Network Graphics (PNG) writer.

www.libpng.org/pub/png/spec/1.2/

PNG supports: + 8 bit truecolor PNGs

PNG does not support: + any other color depth + extra data chunks + filters

Example

require 'png'

canvas = PNG::Canvas.new 200, 200
canvas[100, 100] = PNG::Color::Black
canvas.line 50, 50, 100, 50, PNG::Color::Blue
png = PNG.new canvas
png.save 'blah.png'

TODO:

+ Get everything orinted entirely on [x,y,h,w] with x,y origin being

bottom left.

Defined Under Namespace

Classes: Canvas, Color, Font

Constant Summary collapse

VERSION =
'1.2.0'
SIGNATURE =
[137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
GRAY =

Color Types:

0
RGB =

DEPTH = 1,2,4,8,16

2
INDEXED =

DEPTH = 8,16

3
GRAYA =

DEPTH = 1,2,4,8

4
RGBA =

DEPTH = 8,16

6
NONE =

Filter Types:

0
SUB =
1
UP =
2
AVG =
3
PAETH =
4
FULL =
360.0
HALF =
FULL / 2

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(canvas) ⇒ PNG

Creates a new PNG object using canvas



170
171
172
173
174
175
# File 'lib/png.rb', line 170

def initialize(canvas)
  @height = canvas.height
  @width = canvas.width
  @bits = 8
  @data = canvas.data
end

Class Method Details

.angle(x, y) ⇒ Object



9
10
11
12
13
# File 'lib/png/pie.rb', line 9

def self.angle(x, y)
  return 0 if x == 0 and y == 0
  rad_to_deg = 180.0 / Math::PI
  (Math.atan2(-y, x) * rad_to_deg + 90) % 360
end

.check_crc(type, data, crc) ⇒ Object

Raises:

  • (ArgumentError)


46
47
48
49
# File 'lib/png/reader.rb', line 46

def self.check_crc type, data, crc
  return true if (type + data).png_crc == crc
  raise ArgumentError, "Invalid CRC encountered in #{type} chunk"
end

.chunk(type, data = "") ⇒ Object

Creates a PNG chunk of type type that contains data.



163
164
165
# File 'lib/png.rb', line 163

def self.chunk(type, data="")
  [data.size, type, data, (type + data).png_crc].pack("Na*a*N")
end

.load(png, metadata_only = false) ⇒ Object

Raises:

  • (ArgumentError)


12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/png/reader.rb', line 12

def self.load png,  = false
  png = png.dup
  signature = png.slice! 0, 8
  raise ArgumentError, 'Invalid PNG signature' unless signature == SIGNATURE

  ihdr = read_chunk 'IHDR', png

  bit_depth, color_type, width, height = read_IHDR ihdr, 

  return [width, height, bit_depth] if 

  canvas = PNG::Canvas.new width, height

  type = png.slice(4, 4).unpack('a4').first
  read_chunk type, png if type == 'iCCP' # Ignore color profile

  read_IDAT read_chunk('IDAT', png), bit_depth, color_type, canvas
  read_chunk 'IEND', png

  canvas
end

.load_file(path, metadata_only = false) ⇒ Object



7
8
9
10
# File 'lib/png/reader.rb', line 7

def self.load_file path,  = false
  file = File.open(path, 'rb') { |f| f.read }
  self.load file, 
end

.paeth(a, b, c) ⇒ Object

left, above, upper left



132
133
134
135
136
137
138
139
140
141
# File 'lib/png/reader.rb', line 132

def self.paeth a, b, c # left, above, upper left
  p = a + b - c
  pa = (p - a).abs
  pb = (p - b).abs
  pc = (p - c).abs

  return a if pa <= pb && pa <= pc
  return b if pb <= pc
  c
end

.pie_chart(diameter, pct_green, good_color = PNG::Color::Green, bad_color = PNG::Color::Red) ⇒ Object

Makes a pie chart you can pass to PNG.new:

png = PNG.new pie_chart(250, 0.30)
png.save "pie.png"
system 'open pie.png'


22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/png/pie.rb', line 22

def self.pie_chart(diameter, pct_green,
              good_color=PNG::Color::Green, bad_color=PNG::Color::Red)
  diameter += 1 if diameter % 2 == 0
  radius = (diameter / 2.0).to_i
  pct_in_deg = FULL * pct_green
  rad_to_deg = HALF / Math::PI

  canvas = PNG::Canvas.new(diameter, diameter)

  (-radius..radius).each do |x|
    (-radius..radius).each do |y|
      magnitude = Math.sqrt(x*x + y*y)
      if magnitude <= radius then
        angle = PNG.angle(x, y)
        color = ((angle <= pct_in_deg) ? good_color : bad_color)

        rx, ry = x+radius, y+radius

        canvas[ rx, ry ] = color
      end
    end
  end

  canvas
end

.read_chunk(expected_type, png) ⇒ Object

Raises:

  • (ArgumentError)


34
35
36
37
38
39
40
41
42
43
44
# File 'lib/png/reader.rb', line 34

def self.read_chunk expected_type, png
  size, type = png.slice!(0, 8).unpack 'Na4'
  data, crc = png.slice!(0, size + 4).unpack "a#{size}N"

  check_crc type, data, crc

  raise ArgumentError, "Expected #{expected_type} chunk, not #{type}" unless
    type == expected_type

  return data
end

.read_IDAT(data, bit_depth, color_type, canvas) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/png/reader.rb', line 66

def self.read_IDAT data, bit_depth, color_type, canvas
  data = Zlib::Inflate.inflate(data).unpack 'C*'

  pixel_size = color_type == RGBA ? 4 : 3

  height = canvas.height
  scanline_length = pixel_size * canvas.width + 1 # for filter

  row = canvas.height - 1
  until data.empty? do
    row_data = data.slice! 0, scanline_length

    filter = row_data.shift
    case filter
    when NONE then
    when SUB then
      row_data.each_with_index do |byte, index|
        left = index < pixel_size ? 0 : row_data[index - pixel_size]
        row_data[index] = (byte + left) % 256
      end
    when UP then
      row_data.each_with_index do |byte, index|
        col = index / pixel_size
        upper = row == 0 ? 0 : canvas[col, row + 1].values[index % pixel_size]
        row_data[index] = (upper + byte) % 256
      end
    when AVG then
      row_data.each_with_index do |byte, index|
        col = index / pixel_size
        upper = row == 0 ? 0 : canvas[col, row + 1].values[index % pixel_size]
        left = index < pixel_size ? 0 : row_data[index - pixel_size]

        row_data[index] = (byte + ((left + upper)/2).floor) % 256
      end
    when PAETH then
      left = upper = upper_left = nil
      row_data.each_with_index do |byte, index|
        col = index / pixel_size

        left = index < pixel_size ? 0 : row_data[index - pixel_size]
        if row == height then
          upper = upper_left = 0
        else
          upper = canvas[col, row + 1].values[index % pixel_size]
          upper_left = col == 0 ? 0 :
            canvas[col - 1, row + 1].values[index % pixel_size]
        end

        paeth = paeth left, upper, upper_left
        row_data[index] = (byte + paeth) % 256
      end
    else
      raise ArgumentError, "invalid filter algorithm #{filter}"
    end

    col = 0
    row_data.each_slice pixel_size do |slice|
      slice << 0xFF if pixel_size == 3
      canvas[col, row] = PNG::Color.new(*slice)
      col += 1
    end

    row -= 1
  end
end

.read_IHDR(data, metadata_only = false) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/png/reader.rb', line 51

def self.read_IHDR data,  = false
  width, height, bit_depth, color_type, *rest = data.unpack 'N2C5'

  unless  then
    raise ArgumentError, "Wrong bit depth: #{bit_depth}" unless
      bit_depth == 8
    raise ArgumentError, "Wrong color type: #{color_type}" unless
      color_type == RGBA or color_type = RGB
    raise ArgumentError, "Unsupported options: #{rest.inspect}" unless
      rest == [0, 0, 0]
  end

  return bit_depth, color_type, width, height
end

Instance Method Details

#save(path) ⇒ Object

Writes the PNG to path.



180
181
182
183
184
# File 'lib/png.rb', line 180

def save(path)
  File.open path, 'wb' do |f|
    f.write to_blob
  end
end

#to_blobObject

Raw PNG data



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/png.rb', line 189

def to_blob
  blob = []

  header = [@width, @height, @bits, RGBA, NONE, NONE, NONE]

  blob << SIGNATURE
  blob << PNG.chunk('IHDR', header.pack("N2C5"))
  blob << PNG.chunk('IDAT', Zlib::Deflate.deflate(self.png_join))
  blob << PNG.chunk('IEND', '')
  blob.join
end