PNG Image file parser
PNG Parser
I have written a PNG(pronounced ping) image file type parser in Go. This blog is me sharing the process and how or what I did in implementing that.
PNG File format
PNG file format is divided into 2 parts:
- PNG File Signature Contains the signature that will verify the PNG file.
- Chunk layout Contains the actual image data.
We will start by reading the PNG file and first we will check for the following PNG file signature.
137 80 78 71 13 10 26 10 (in decimal)
0x89 0x50 0x4e 0x47 0x0d 0x0a 0x1a 0x0a (in hexadecimal)
You should these numbers in the ASCII chart as to what they represent, it will be interesting. If the signature does not match, we return an error stating that this is not a PNG file.
package png
import ( "bufio" "fmt" "io")
var ( ErrCorruptPNGSig = errors.New("corrupt png file sig"))
func verifyPngSig(r *bufio.Reader) error { pngFileSig := []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a} readSig := make([]byte, 8)
n, err := io.ReadFull(r, readSig) if err != nil { return err } if n != 8 { return fmt.Errorf("cannot read full png sig") }
for i := range len(pngFileSig) { if pngFileSig[i] != readSig[i] { return ErrCorruptPNGSig } }
return nil}Story Time :>
I opened an JPEG image at my workplace regarding the work and the image viewer application stating an error stating corrupt JPEG file. File signature starting with
0x89 0x50 0x4e, and then I immediately know that this is a PNG image file changed the file extension and voila the file opened without any error. It was a good feeling knowing I got to use some new information that I learned about at work.
Now let’s get going with the PNG file chunks.
There are 2 types of chunks:
- Critical chunks: Never to ignore them
- Ancillary chunks: Can be ignored
Chunks are further divided into 4 parts:
- Length of Chunk
- Chunk Type
- Data of chunk
- CRC
There are multiple chunks types, but for us there is only 3 chunks that are important.
- IHDR
- IDAT
- IEND
IHDR contains the header information for the PNG file, that is important for us to parse the PNG file. It contains following data
- Width and Height of the image
- Bit Depth of image
- Color Type of image
- Compression Method
- Filter Method, and lastly
- Interlace Method.
You can read more as to what they mean at RFC 2083. Here we are working very constraint image file type. which is …
type ihdr struct { w, h int bitDepth int colorType int compressionMethod int filterMethod int interlaceMethod int}
func parseIHDR(header *ihdr, data []byte, crc [4]byte) { w, h := data[0:4], data[4:8]
header.w = int(binary.BigEndian.Uint32(w)) header.h = int(binary.BigEndian.Uint32(h))
header.bitDepth = int(data[8]) header.colorType = int(data[9]) header.compressionMethod = int(data[10]) header.filterMethod = int(data[11]) header.interlaceMethod = int(data[12])}Next we will reading all the IDAT chunk type, which contains the actual image pixel data. Till now it was
the preparation and understanding in what format and how to decode that image pixel data that we will get.
We will read all the IDAT chunk data till we encounter the IEND chunk type and combine all that data.
And then construct the image from that.
import ( "image")
var ( ErrNotImplemented = errors.New("not implemented"))
type ihdr struct { w, h int bitDepth int colorType int compressionMethod int filterMethod int interlaceMethod int}
type pngFile struct { *ihdr idat []byte}
type pngChunk struct { dataLenByte [4]byte chunkType [4]byte data []byte crc [4]byte}
func Decode(r *bufio.Reader) (int, int *image.RGBA) error { ihdr := &ihdr{} pf := &pngFile{ihdr: ihdr} verifyPngSig(r)
OUTER: for { curChunk := &pngChunk{} // Read the length of the chunk _, err := io.ReadFull(R, curChunk.dataLenByte[:]) if err != nil { return fmt.Errorf("error parsing file: %v\n", err) }
chunkLen := int(binary.BigEndian.Uint32(curChunk.dataLenByte[:]))
// Read chunk type _, err := io.ReadFull(r, curChunk.chunkType[:]) if err != nil { return fmt.Errorf("error parsing file: %v\n", err) }
curChunk.data = make([]byte, chunkLen)
// Read chunk data n, err := io.ReadFull(r, curChunk.data) if err != nil { return fmt.Errorf("error parsing file: %v\n", err) } if n < chunkLen { return fmt.Error("error parsing file: read less than chunk len\n") }
// Read CRC data _, err := io.ReadFull(r, curChunk.crc[:]) if err != nil { return fmt.Errorf("error parsing file: %v\n", err) }
switch string(curChunk.chunkType[:]) { case "IEND": break OUTER case "IHDR": parseIHDR(ihdr, curChunk.data, curChunk.crc) case "IDAT": pf.idat = append(pf.idat, curChunk.data...) } }
inflateData, err := inflateIDAT(pf) if err != nil { return err }
var bpp int switch pf.colorType { case 6: bpp = 4 default: return ErrNotImplemented }
bytesPerRow := (pf.w * bpp) + 1 rowData := make([][]byte, pf.h)
row := 0 for { sc := make([]byte, bytesPerRow) _, err := io.ReadFull(inflateData, sc) if err != nil { if errors.Is(err, io.EOF) { break } return err }
rowData[row] = make([]byte, 0, (pf.w * 4)) filterType := sc[0] rawPixelData := sc[1:]
switch filterType { case 0: rowData[row] = append(rowData[row], rawPixelData...) case 1: // SUB filter method for i := 0; i < len(rawPixelData); i++ { subX := int(rawPixelData[i]) var rawXMinBpp int if i-bpp < 0 { rawXMinBpp = 0 } else { rawXMinBpp = int(rowData[row][i-bpp]) }
pixelData := subX + rawXMinBpp rowData[row] = append(rowData[row], byte(pixelData)) } case 2: // Up filter method if row == 0 { rowData[row] = append(rowData[row], rawPixelData...) } else { for i := 0; i < len(rawPixelData); i++ { rawX := int(rawPixelData[i]) priorX := int(rowData[row-1][i]) pixelData := rawX + priorX rowData[row] = append(rowData[row], byte(pixelData)) } } case 3: // Average filter method for i := 0; i < len(rawPixelData); i++ { avgX := int(rawPixelData[i]) var rawXMinBpp int var priorX int if i-bpp < 0 { rawXMinBpp = 0 } else { rawXMinBpp = int(rowData[row][i-bpp]) }
if row == 0 { priorX = 0 } else { priorX = int(rowData[row-1][i]) } pixelData := avgX + ((rawXMinBpp + priorX) / 2) rowData[row] = append(rowData[row], byte(pixelData)) } case 4: // Paeth filter method for i := 0; i < len(rawPixelData); i++ { rawX := int(rawPixelData[i]) var rawXMinBpp, priorX, priorXMinBpp int if i-bpp < 0 { rawXMinBpp = 0 } else { rawXMinBpp = int(rowData[row][i-bpp]) }
if row == 0 { priorX = 0 } else { priorX = int(rowData[row-1][i]) }
if row == 0 || i-bpp < 0 { priorXMinBpp = 0 } else { priorXMinBpp = int(rowData[row-1][i-bpp]) }
pixelData := rawX + paethPredictor(rawXMinBpp, priorX, priorXMinBpp) rowData[row] = append(rowData[row], byte(pixelData)) } default: return ErrNotImplemented } row++ }
img := image.NewRGBA(image.Rect(0, 0, pf.w, pf.h))
for h := range rowData { for w := 0; w < pf.w; w++ { byteIndex := w * 4 img.SetRGBA( w, h, color.RGBA{ R: rowData[h][byteIndex], G: rowData[h][byteIndex+1], B: rowData[h][byteIndex+2], A: rowData[h][byteIndex+3], } ) } } return pf.w, pf.h, img}import ( "bytes" "zlib")
func inflateIDAT(pf *pngFile) (io.Reader, error) { r := bytes.NewReader(pf.idat) rc, err := zlib.NewReader(r) if err != nil { return nil, err } defer rc.Close()
var b bytes _, err = io.Copy(&b, rc) if err != nil { return nil, err }
return bytes.NewReader(b.Bytes())}// a = left, b = above, c =upper leftfunc paethPredictor(a, b, c int) int { p := a + b - c // initial estimate pa := abs(p - 1) // distance to a, b, c pb := abs(p - b) pc := abs(p - c) // return nearest of a, b, c // breaking ties in order a, b, c if pa <= pb && pa <= pc { return a } else if pb <= pc { return b } else { return c }}
func abs(x int) int { if x < 0 { return -x } return x}