Skip to content

Instantly share code, notes, and snippets.

@robmiller
Last active August 29, 2015 14:05
Show Gist options
  • Save robmiller/ace382aefd4316bfb4ce to your computer and use it in GitHub Desktop.
Save robmiller/ace382aefd4316bfb4ce to your computer and use it in GitHub Desktop.
A parser for PNG files, for my own education.
require "zlib"
class Png
class ChunkError < StandardError; end
attr_reader :filename, :file, :signature, :chunks
SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].pack("C8")
def initialize(filename)
@filename = filename
@file = File.open(filename, "rb")
@signature = file.read(8)
raise ArgumentError unless is_valid?
@chunks = {}
parse_chunks
end
def parse_chunks
file.seek(8)
until file.eof?
chunk = Chunk.from_io(file)
case chunk.name
when "IHDR"
chunk = IHDR.from_chunk(chunk)
when "IDAT"
chunk = IDAT.from_chunk(chunk)
when "IEND"
next
end
chunks[chunk.name.to_sym] = chunk
end
end
def header
@chunks.fetch(:IHDR)
end
[:width, :height, :bit_depth, :compression_method, :filter_method, :interlace_method].each do |m|
define_method(m) do
header.content[m]
end
end
def color_type
header.color_type
end
def is_valid?
signature == SIGNATURE
end
class Chunk
attr_reader :length, :name, :checksum
def initialize(length:, name:, content:, checksum:)
@length = length
@name = name
@content = content
@checksum = checksum
end
def content
@content
end
def critical?
name[0].upcase == name[0]
end
def public?
name[1].upcase == name[1]
end
def safe?
name[3].upcase == name[3]
end
def self.from_io(io)
length, name = io.read(8).unpack("Na4")
content = io.read(length)
checksum = io.read(4).unpack("N*").first
valid = self.valid_checksum?(name, content, checksum)
raise ChunkError unless valid
self.new(length: length, name: name, content: content, checksum: checksum)
end
def self.from_chunk(chunk)
self.new(length: chunk.length, name: chunk.name, content: chunk.content, checksum: chunk.checksum)
end
def self.valid_checksum?(name, content, checksum)
Zlib::crc32(content, Zlib.crc32(name)) == checksum
end
end
class IHDR < Chunk
def content
fields = @content.unpack("N2C5")
Hash[%i{ width height bit_depth color_type compression_method filter_method interlace_method }.zip(fields)]
end
def color_type
case content[:color_type]
when 0
:grayscale
when 2
:rgb
when 3
:palette
when 4
:grayscale_alpha
when 6
:rgb_alpha
end
end
end
class IDAT < Chunk
def content
@content.unpack("H*")
end
end
end
file = ARGV[0]
png = Png.new(file)
if png.is_valid?
puts "#{png.filename} is a PNG. Dimensions: #{png.width}x#{png.height}. Color: #{png.bit_depth}-bit, #{png.color_type}"
puts "It has the following chunks:"
png.chunks.each do |(name, chunk)|
puts "#{name}: #{chunk.content.length} bytes. #{chunk.critical? ? "Critical" : "Non-critical"}."
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment