Last active
August 29, 2015 14:05
-
-
Save robmiller/ace382aefd4316bfb4ce to your computer and use it in GitHub Desktop.
A parser for PNG files, for my own education.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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