class PNG

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.

Constants

AVG
FULL
GRAY

Color Types:

GRAYA
HALF
INDEXED
NONE

Filter Types:

PAETH
RGB
RGBA
SIGNATURE
SUB
UP
VERSION

Public Class Methods

angle(x, y) click to toggle source
# File lib/png/pie.rb, line 8
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) click to toggle source
# File lib/png/reader.rb, line 45
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 = "") click to toggle source

Creates a PNG chunk of type type that contains data.

# File lib/png.rb, line 162
def self.chunk type, data = ""
  [data.size, type, data, (type + data).png_crc].pack("Na*a*N")
end
load(png, metadata_only = false) click to toggle source
# File lib/png/reader.rb, line 11
def self.load png, metadata_only = 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, metadata_only

  return [width, height, bit_depth] if metadata_only

  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) click to toggle source
# File lib/png/reader.rb, line 6
def self.load_file path, metadata_only = false
  file = File.open(path, "rb") { |f| f.read }
  self.load file, metadata_only
end
new(canvas) click to toggle source

Creates a new PNG object using canvas

# File lib/png.rb, line 169
def initialize canvas
  @height = canvas.height
  @width = canvas.width
  @bits = 8
  @data = canvas.data
end
paeth(a, b, c) click to toggle source
# File lib/png/reader.rb, line 131
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) click to toggle source

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

png = PNG.new pie_chart(250, 0.30)
png.save "pie.png"
system 'open pie.png'
# File lib/png/pie.rb, line 21
def self.pie_chart(diameter, pct_green,
                   good_color = PNG::Color::Green,
                   bad_color = PNG::Color::Red)
  diameter += 1 if diameter.even?
  radius = (diameter / 2.0).to_i
  pct_in_deg = FULL * pct_green

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

  (-radius..radius).each do |x|
    (-radius..radius).each do |y|
      magnitude = Math.sqrt(x*x + y*y)

      next if magnitude > radius

      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

  canvas
end
read_IDAT(data, bit_depth, color_type, canvas) click to toggle source
# File lib/png/reader.rb, line 65
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) click to toggle source
# File lib/png/reader.rb, line 50
def self.read_IHDR data, metadata_only = false
  width, height, bit_depth, color_type, *rest = data.unpack "N2C5"

  unless metadata_only 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
read_chunk(expected_type, png) click to toggle source
# File lib/png/reader.rb, line 33
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

  data
end

Public Instance Methods

png_join() click to toggle source
# File lib/png.rb, line 154
def png_join
  @data.map { |row| "\0" + row.map(&:values).join }.join
end
save(path) click to toggle source

Writes the PNG to path.

# File lib/png.rb, line 179
def save path
  File.open path, "wb" do |f|
    f.write to_blob
  end
end
to_blob() click to toggle source

Raw PNG data

# File lib/png.rb, line 188
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