Class: PNG

Inherits:
Object
  • Object
show all
Defined in:
lib/png.rb,
lib/png/pie.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'

Defined Under Namespace

Classes: Canvas, Color

Constant Summary collapse

VERSION =
'1.1.0'
SIGNATURE =
[137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
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



237
238
239
240
241
242
# File 'lib/png.rb', line 237

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)


142
143
144
145
# File 'lib/png.rb', line 142

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.



120
121
122
# File 'lib/png.rb', line 120

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

.load(png) ⇒ Object

Raises:

  • (ArgumentError)


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/png.rb', line 124

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

  type, data = read_chunk png

  raise ArgumentError, 'Invalid PNG, no IHDR chunk' unless type == 'IHDR'

  canvas = read_IHDR data
  type, data = read_chunk png
  read_IDAT data, canvas
  type, data = read_chunk png
  raise 'oh no! IEND not next? crashing and burning!' unless type == 'IEND'

  new canvas
end

.paeth(a, b, c) ⇒ Object

left, above, upper left



147
148
149
150
151
152
153
154
155
156
# File 'lib/png.rb', line 147

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(png) ⇒ Object



158
159
160
161
162
163
164
165
# File 'lib/png.rb', line 158

def self.read_chunk(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

  return type, data
end

.read_IDAT(data, canvas) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/png.rb', line 167

def self.read_IDAT(data, canvas)
  data = Zlib::Inflate.inflate(data).unpack 'C*'
  scanline_length = 4 * canvas.width + 1 # for filter
  row = 0
  until data.empty? do
    row_data = data.slice! 0, scanline_length
    filter = row_data.shift
    case filter
    when 0 then # None
    when 1 then # Sub
      row_data.each_with_index do |byte, index|
        left = index < 4 ? 0 : row_data[index - 4]
        row_data[index] = (byte + left) % 256
        #p [byte, left, row_data[index]]
      end
    when 2 then # Up
      row_data.each_with_index do |byte, index|
        col = index / 4
        upper = row == 0 ? 0 : canvas[col, row - 1].values[index % 4]
        row_data[index] = (upper + byte) % 256
      end
    when 3 then # Average
      row_data.each_with_index do |byte, index|
        col = index / 4
        upper = row == 0 ? 0 : canvas[col, row - 1].values[index % 4]
        left = index < 4 ? 0 : row_data[index - 4]

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

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

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

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

    row += 1
  end
end

.read_IHDR(data) ⇒ Object

Raises:

  • (ArgumentError)


228
229
230
231
232
# File 'lib/png.rb', line 228

def self.read_IHDR(data)
  width, height, *rest = data.unpack 'N2C5'
  raise ArgumentError, 'unsupported PNG file' unless rest == [8, 6, 0, 0, 0]
  return PNG::Canvas.new(height, width)
end

Instance Method Details

#save(path) ⇒ Object

Writes the PNG to path.



247
248
249
# File 'lib/png.rb', line 247

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

#to_blobObject

Raw PNG data



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/png.rb', line 254

def to_blob
  blob = []

  blob << SIGNATURE
  blob << PNG.chunk('IHDR', [@width, @height, @bits, 6, 0, 0, 0 ].pack("N2C5"))
  # 0 == filter type code "none"
  data = self.png_join
  blob << PNG.chunk('IDAT', Zlib::Deflate.deflate(data))
  blob << PNG.chunk('IEND', '')
  blob.join
end