Permalink
Browse files

Initial commit - work in progress.

  • Loading branch information...
wvanbergen committed Jan 4, 2010
0 parents commit aa8a9378eedfc02aa1d0d1e05c313badc76594a7
@@ -0,0 +1 @@
+.DS_Store
@@ -0,0 +1,9 @@
+require 'zlib'
+
+require 'chunky_png/datastream'
+require 'chunky_png/chunk'
+require 'chunky_png/reader'
+require 'chunky_png/pixel_matrix'
+require 'chunky_png/color'
+require 'chunky_png/image'
+
@@ -0,0 +1,83 @@
+module ChunkyPNG
+ class Chunk
+
+ def self.read(io)
+
+ length, type = io.read(8).unpack('Na4')
+ content = io.read(length)
+ crc = io.read(4).unpack('N').first
+
+ # verify_crc!(type, content, crc)
+
+ CHUNK_TYPES.fetch(type, Generic).read(type, content)
+ end
+
+ class Base
+ attr_accessor :type
+
+ def initialize(type, attributes = {})
+ self.type = type
+ attributes.each { |k, v| send("#{k}=", v) }
+ end
+
+ def write_with_crc(io, content)
+ message = type + content
+ io << [content.length].pack('N') << message << [Zlib.crc32(message)].pack('N')
+ end
+
+ def write(io)
+ write_with_crc(io, content || '')
+ end
+ end
+
+ class Generic < Base
+
+ attr_accessor :content
+
+ def initialize(type, content = '')
+ super(type, :content => content)
+ end
+
+ def self.read(type, content)
+ self.new(type, content)
+ end
+ end
+
+ class Header < Base
+
+ COLOR_GRAYSCALE = 0
+ COLOR_TRUECOLOR = 2
+ COLOR_INDEXED = 3
+ COLOR_GRAYSCALE_ALPHA = 4
+ COLOR_TRUECOLOR_ALPHA = 6
+
+ attr_accessor :width, :height, :depth, :color, :compression, :filtering, :interlace
+
+ def initialize(attrs = {})
+ super('IHDR', attrs)
+ @depth ||= 8
+ @color ||= COLOR_TRUECOLOR
+ @compression ||= 0
+ @filtering ||= 0
+ @interlace ||= 0
+ end
+
+ def self.read(type, content)
+ fields = content.unpack('NNC*5')
+ self.new(:width => fields[0], :height => fields[1], :depth => fields[2], :color => fields[3],
+ :compression => fields[4], :filtering => fields[5], :interlace => fields[6])
+ end
+
+ def content
+ [width, height, depth, color, compression, filtering, interlace].pack('NNC5')
+ end
+ end
+
+ # Maps chunk types to classes.
+ # If a chunk type is not given in this hash, a generic chunk type will be used.
+ CHUNK_TYPES = {
+ 'IHDR' => Header,
+ }
+
+ end
+end
@@ -0,0 +1,23 @@
+module ChunkyPNG
+ class Color < Struct.new(:r, :g, :b, :a)
+
+ BLACK = self.new( 0, 0, 0, 255)
+ WHITE = self.new(255, 255, 255, 255)
+
+ def to_true_color
+ [r, g, b].pack('CCC')
+ end
+
+ def inspect
+ '#%02x%02x%02x' % [r, g, b]
+ end
+
+ def self.rgb(r, g, b)
+ self.new(r, g, b, 255)
+ end
+
+ def self.rgba(r, g, b, a)
+ self.new(r, g, b, a)
+ end
+ end
+end
@@ -0,0 +1,31 @@
+module ChunkyPNG
+
+ class Datastream
+
+ SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack('C*8')
+
+ attr_accessor :chunks
+
+ def initialize
+ @chunks = []
+ end
+
+ def header
+ chunks.first
+ end
+
+ def write(io)
+ io << SIGNATURE
+ chunks.each { |c| c.write(io) }
+ end
+
+ def pixel_matrix
+ @pixel_matrix ||= begin
+ data = ""
+ chunks.each { |c| data << c.content if c.type == 'IDAT' }
+ streamdata = Zlib::Inflate.inflate(data)
+ matrix = PNG::PixelMatrix.load(header, streamdata)
+ end
+ end
+ end
+end
@@ -0,0 +1,23 @@
+module ChunkyPNG
+ class Image
+
+ attr_reader :width, :height, :pixelvector
+
+ def initialize(width, height)
+ @width, @height = width, height
+ black_pixel = PNG::Color.new(255, 0, 0)
+ @pixelvector = Array.new(width * height, black_pixel)
+ end
+
+
+ def write(io)
+ datastream = PNG::Datastream.new
+ datastream.chunks << PNG::Chunk::Header.new(:width => width, :height => height)
+
+ pixels = @pixelvector.map(&:to_true_color).join('')
+ datastream.chunks << PNG::Chunk::PixelData.new(pixels)
+ datastream.chunks << PNG::Chunk::Generic.new('IEND')
+ datastream.write(io)
+ end
+ end
+end
@@ -0,0 +1,75 @@
+module ChunkyPNG
+
+ class PixelMatrix
+
+ FILTER_NONE = 0
+ FILTER_SUB = 1
+ FILTER_UP = 2
+ FILTER_AVERAGE = 3
+ FILTER_PAETH = 4
+
+ attr_accessor :pixels, :width, :height
+
+ def self.load(header, content)
+ matrix = self.new(header.width, header.height)
+ matrix.decode_pixelstream(content, header)
+ return matrix
+ end
+
+ def initialize(width, height, background_color = PNG::Color::Black)
+ @width, @height = width, height
+ @pixels = Array.new(width * height)
+ end
+
+ def decode_pixelstream(stream, header = nil)
+ verify_length!(stream.length)
+
+ decoded_bytes = Array.new(header.width * 3, 0)
+ @pixels = []
+ height.times do |line_no|
+ position = line_no * (width * 3 + 1)
+ line_length = header.width * 3
+ bytes = stream.unpack("@#{position}CC#{line_length}")
+ filter = bytes.shift
+ decoded_bytes = decode_scanline(filter, bytes, decoded_bytes, header)
+ @pixels += decode_pixels(decoded_bytes, header)
+ end
+ end
+
+ def decode_pixels(bytes, header)
+ (0...width).map do |i|
+ PNG::Color.rgb(bytes[i*3+0], bytes[i*3+1], bytes[i*3+2])
+ end
+ end
+
+ def decode_scanline(filter, bytes, previous_bytes, header = nil)
+ case filter
+ when FILTER_NONE then decode_scanline_none( bytes, previous_bytes, header)
+ when FILTER_SUB then decode_scanline_sub( bytes, previous_bytes, header)
+ when FILTER_UP then decode_scanline_up( bytes, previous_bytes, header)
+ when FILTER_AVERAGE then raise "Average filter are not yet supported!"
+ when FILTER_PAETH then raise "Paeth filter are not yet supported!"
+ else raise "Unknown filter type"
+ end
+ end
+
+ def decode_scanline_none(bytes, previous_bytes, header = nil)
+ bytes
+ end
+
+ def decode_scanline_sub(bytes, previous_bytes, header = nil)
+ bytes.each_with_index { |b, i| bytes[i] = (b + bytes[i-3]) % 256 }
+ bytes
+ end
+
+ def decode_scanline_up(bytes, previous_bytes, header = nil)
+ bytes.each_with_index { |b, i| bytes[i] = (b + previous_bytes[i]) % 256 }
+ bytes
+ end
+
+ def verify_length!(bytes)
+ raise "Invalid stream length!" unless bytes == width * height * 3 + height
+ end
+
+ end
+end
@@ -0,0 +1,38 @@
+module ChunkyPNG
+
+ class Reader
+
+ attr_reader :io
+ attr_reader :datastream
+
+ def self.io
+ self.new(io).datastream
+ end
+
+ def self.file(file)
+ File.open(file, 'r') do |f|
+ self.new(f).datastream
+ end
+ end
+
+ def self.string(string)
+ self.new(StringIO.new(string)).datastream
+ end
+
+ protected
+
+ def initialize(io)
+ @io = io
+ verify_signature!
+
+ @datastream = PNG::Datastream.new
+ @datastream.chunks << PNG::Chunk.read(@io) until @io.eof?
+ return @datastream
+ end
+
+ def verify_signature!
+ signature = @io.read(PNG::Datastream::SIGNATURE.length)
+ raise "PNG signature not found!" unless signature == PNG::Datastream::SIGNATURE
+ end
+ end
+end

0 comments on commit aa8a937

Please sign in to comment.