Why every PNG starts with 89 50 4E 47: the signature and chunk layout

Why every PNG begins with 89 50 4E 47 0D 0A 1A 0A, how a chunk is laid out, what letter case in chunk names means, and the order of critical chunks.

DevelopmentFile formats

Open any PNG in a hex viewer and the first eight bytes are the same.

$ xxd red1x1.png | head -2
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0001 0000 0001 0802 0000 0090 7753  ..............wS

89 50 4E 47 0D 0A 1A 0A is the PNG signature. What follows is a sequence of chunks, blocks that all share one layout.

The eight-byte signature

The current W3C PNG Third Edition says these bytes set a PNG apart from other data and allow early detection of some transmission errors. RFC 2083, the PNG 1.0 document, explains why each byte was chosen.

Bytes Reason given in RFC 2083
89 Non-ASCII, so a text file is less likely to be mistaken for PNG; also catches transfers that clear bit 7
50 4E 47 PNG in ASCII
0D 0A CR-LF; catches transfers that alter newline sequences
1A Control-Z; stops file display under MS-DOS
0A LF; catches the reverse of the CR-LF translation problem

A file that went through a text-mode transfer and had CR or LF bytes added or converted can fail on these eight bytes before any image data is decoded. There is no version number in the signature. RFC 2083 says that is deliberate: the format is extended through chunks.

Chunk layout

Field Size Contents
Length 4 bytes Counts only the data field. Maximum 2³¹−1
Chunk Type 4 bytes Four letters such as IHDR or IDAT
Chunk Data Length bytes May be empty
CRC 4 bytes Computed over the type and data, not the length

Multi-byte integers use network byte order, with the most significant byte first. In the dump above, 00 00 00 0D is a length of 13 and 49 48 44 52 is IHDR. The next 13 bytes hold the width, height, bit depth, colour type and other basic image fields.

The length is capped at 2³¹−1 to accommodate languages that have trouble with unsigned four-byte integers.

Letter case in chunk names

Each letter in a chunk type uses bit 5, represented by whether the letter is upper or lower case, to mark a property.

  • First letter: uppercase is critical; lowercase is ancillary
  • Second letter: uppercase is public, reserved for definition by W3C; lowercase is private
  • Third letter: reserved and required to be uppercase by the current specification
  • Fourth letter: lowercase means safe-to-copy

pHYs, for example, is ancillary, public and safe-to-copy. Under the current specification, an unknown critical chunk is a fatal condition for a decoder extracting the image. An unknown ancillary chunk is not an error and may be ignored.

Critical chunk order

Every PNG has the common critical skeleton signature, IHDR, one or more IDAT chunks, and IEND. There is exactly one IHDR at the beginning and one IEND at the end.

Indexed-colour images, colour type 3, must also contain PLTE before the first IDAT. Multiple IDAT chunks must be consecutive with nothing between them. Joining their data fields produces one zlib-compressed stream.

Checking the CRCs

import struct, sys, zlib

data = open(sys.argv[1], "rb").read()
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"

pos = 8
while pos < len(data):
    length, ctype = struct.unpack(">I4s", data[pos:pos + 8])
    body = data[pos + 8:pos + 8 + length]
    crc, = struct.unpack(">I", data[pos + 8 + length:pos + 12 + length])
    ok = zlib.crc32(ctype + body) == crc
    print(ctype.decode("ascii"), length, "CRC ok" if ok else "CRC mismatch")
    pos += 12 + length

Output for a 1×1 red PNG, run with Python 3.14.7 on macOS 15.6.1:

IHDR 13 CRC ok
IDAT 12 CRC ok
IEND 0 CRC ok

PNG’s CRC uses the same polynomial as gzip. In this run, zlib.crc32 matched the stored CRC for all three chunks. Changing the first IDAT data byte, at offset 41, in a copy turned only that chunk into IDAT 12 CRC mismatch. The specification treats a CRC mismatch as a sign of a corrupted datastream that may be regarded as a fatal error.

This script is a checking aid and assumes complete, well-formed input. It does not validate length bounds, chunk order, the presence of IEND, or trailing data after it. Truncated or malformed files may make it raise an exception.

Back to the blog