Writing a PNG Decoder in F#, Part I

Over the next few months, I’m going to be posting a series on how to write PNG decoder in F#. I’ve spent a fair amount of time looking at libpng, and I think we can do better.

First off, PNG files start with a header, so here is a quick and dirty check for a header matching the PNG spec:

let headerIsValid (stm:Stream) =
    let validHeader = [|  0x89uy ; 0x50uy ; 0x4euy ; 0x47uy ; 0x0duy ; 0x0auy ; 0x1auy ; 0x0auy |]
    let header = Array.zeroCreate 8
    stm.Read(header, 0, header.Length) |> ignore
    (Array.compareWith Operators.compare validHeader header) = 0

In this case, I’m returning true if the header is valid, false otherwise.
PNG files are made up of a series of chunks. Each chunk is the format:

|length|tag|data|crc|

where length is a 4 byte length of the data section, tag is a 4 byte code, data is length bytes of data and crc is the 4 byte network order crc32 value of the tag and the data. In libpng, there is a lot of work done to ensure that the chunks are in the right order. This is all well and good, I suppose, but it’s not very forgiving and it makes their code much more complicated. Futhermore, since each chunk comes with a size, we can skip over the file and build and index of the chunks before we begin reading any actual data.
The tag will be represented as an F# record. Tags in PNG files are 4 ASCII characters, so we’ll model it that way:

let headerIsValid (stm:Stream) =
[<System.Diagnostics.DebuggerDisplay("{ToString()}")>]
[<StructuredFormatDisplay("{c0}{c1}{c2}{c3}")>]
type FourCC = 
    {
        c0: char
        c1: char
        c2: char
        c3: char
    }
with
    override this.ToString() = String.Format("{0}{1}{2}{3}", this.c0, this.c1, this.c2, this.c3)
 
let fourCCStr (str:string) =
    if str.Length <> 4 then raise (ArgumentOutOfRangeException("str", "str must have excetly 4 characters."))
    { c0 = str.[0]; c1 = str.[1]; c2 = str.[2]; c3 = str.[3]; }
 
let readFourCC (stm:Stream) =
    ensureRead stm 4 |> ignore
    { c0 = stm.ReadByte() |> char; c1 = stm.ReadByte() |> char; c2 = stm.ReadByte() |> char; c3 = stm.ReadByte() |> char; }

I also gave you a couple convenience routines for converting a string to a FourCC and for reading a FourCC from a stream, but there’s an IO routine that I slipped in there: ensureRead.

let ensureRead (stm:Stream) n =
    if stm.Length - stm.Position >= (int64)n then n
    else raise (Exception(String.Format("Unexpected end of file while trying to read {0} bytes.", n)))

which makes sure that there are at least n bytes to read in the file, returning n if so. This is handy because anytime we want to read n bytes from a file can wrap the desired number of bytes to read with a call to ensureRead and if they’re there, everything will work as expected. And while I’m going on about I/O, here are some handy routines for reading numeric values from a stream:

let rec readCombined (stm:Stream) acc count =
    if count = 0 then acc else readCombined stm ((acc <<< 8) ||| stm.ReadByte()) (count - 1)
 
let readInt (stm:Stream) =
    readCombined stm 0 (ensureRead stm 4)

Finally for the representation of a chunk, I’m also using an F# record:

type Chunk =
    {
        name:FourCC
        length: uint32
        crc: uint32
        position: int64
    }
 
let makeChunk name length crc position =
    { name = name; length = length; crc = crc; position = position }

Because I’m lazy, I also put in a factory function because unlike a lot of F#, the record initialization syntax is tedious.
In the record, I have all the segments of a chunk except for the raw data itself. In its place, I’ve put ‘position’ which tells me where the data starts in the file. Ultimately, our collection of Chunks will be a Dictionary<FourCC, List<Chunk>>.
In the next installment, I’ll show you how to read in all the chunk information from a PNG file.
 

2 thoughts on “Writing a PNG Decoder in F#, Part I”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.