Anatomy of a PNG File, Byte by Byte

PNG is one of the more pleasant binary formats to read by hand. It is built from self-describing chunks, every one is checksummed, and the format encodes information in a place most designs would not think to use β€” the capitalisation of its own field names. This page walks through the structure completely, with a real file in hex.

The structure

An 8-byte signature, then a sequence of chunks. Each chunk is: 4-byte length, 4-byte type, the data, 4-byte CRC. The first chunk is always IHDR and the last is always IEND. Everything else is between them, in almost any order.

The signature

89 50 4E 47 0D 0A 1A 0A β”‚ β””β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”¬β”€β”˜ β”‚ └── LF β”‚ β”‚ β”‚ └───── DOS end-of-file β”‚ β”‚ └───────── CRLF β”‚ └───────────────── the letters "PNG" └──────────────────────── high bit set

Every byte is doing a job, and the design is worth appreciating:

  • 89 β€” has the high bit set. Software that strips the eighth bit, as some 7-bit transfer paths did, changes this byte and the corruption is immediately detectable.
  • 50 4E 47 β€” PNG, readable in any hex dump or text editor.
  • 0D 0A β€” a CRLF pair. A transfer that converts line endings to Unix style deletes the 0D.
  • 1A β€” DOS end-of-file. Running TYPE image.png stops here rather than dumping binary across the terminal.
  • 0A β€” a bare LF. A transfer converting to Windows line endings turns this into 0D 0A, again detectably.

The two line-ending bytes catch conversion in both directions. In 1995, transferring a binary file over FTP in ASCII mode was a common and silently destructive mistake, and these eight bytes turn it into an immediate, clear failure.

Chunk structure

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Length β”‚ Type β”‚ Data β”‚ CRC β”‚ β”‚ 4 bytes β”‚ 4 bytes β”‚ Length bytesβ”‚ 4 bytes β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ // Length counts the DATA only β€” not the type, not the CRC. // CRC-32 is computed over the TYPE and DATA together. // All multi-byte integers are big-endian.

Two design consequences follow from the length coming first. A decoder can always skip a chunk it does not recognise β€” read the length, jump that far, continue. And it can validate every chunk independently, so corruption is localised and identifiable rather than cascading.

πŸ’‘ Every chunk is checksummed

A CRC-32 on each chunk means a decoder can tell you exactly which part of a file is damaged. JPEG has nothing comparable β€” a corrupt JPEG simply renders wrongly from the point of damage onwards, with no way to identify where or what.

The capitalisation rule

This is the cleverest thing in the format. A chunk type is four ASCII letters, and the case of each letter is a bit of machine-readable information.

LetterUppercase meansLowercase means
1stCritical β€” a decoder that cannot handle it must failAncillary β€” safe to skip
2ndPublic β€” registered in the specificationPrivate β€” application-specific
3rdReserved β€” always uppercase(reserved for future use)
4thUnsafe to copy β€” depends on pixel dataSafe to copy into an edited file

The fourth bit is the subtle one and solves a real problem. Suppose an editor opens a PNG containing a chunk it has never heard of, modifies the image, and saves. Should it keep the unknown chunk?

It depends entirely on what that chunk describes. A copyright notice should survive. A chunk describing histogram data for the original pixels is now wrong and must be dropped. The editor cannot know which β€” but the chunk name tells it, without understanding the chunk at all.

IHDR β†’ critical, public, unsafe to copy IDAT β†’ critical, public, unsafe to copy tEXt β†’ ancillary, public, safe to copy (text survives editing) gAMA β†’ ancillary, public, unsafe to copy (gamma describes these pixels) hIST β†’ ancillary, public, unsafe to copy (histogram of these pixels) prVt β†’ ancillary, private, safe to copy

Four bits of metadata carried in a field that had to exist anyway, at zero cost. It is the kind of design detail that separates formats which age well from formats which do not.

The four critical chunks

IHDR β€” the header

Always first, always exactly 13 bytes:

OffsetSizeField
04Width in pixels
44Height in pixels
81Bit depth (1, 2, 4, 8 or 16)
91Colour type
101Compression method (always 0)
111Filter method (always 0)
121Interlace (0 = none, 1 = Adam7)
Colour typeMeaningChannelsValid bit depths
0Greyscale11, 2, 4, 8, 16
2Truecolour (RGB)38, 16
3Indexed (palette)11, 2, 4, 8
4Greyscale + alpha28, 16
6Truecolour + alpha48, 16

Types 1 and 5 do not exist β€” the numbering is a bit field where bit 0 means palette, bit 1 means colour, bit 2 means alpha, and the missing values are combinations that make no sense.

βœ… Choosing colour type 3 is the biggest easy saving

An image with 256 or fewer distinct colours β€” a logo, a chart, an icon, a flat-design illustration β€” stored as type 3 uses one byte per pixel instead of four. That is a 75% reduction before compression even begins.

Most export dialogues default to type 6 regardless of content. Running pngquant or checking the "8-bit" option in your export settings is frequently the single largest PNG size win available, and for flat-colour graphics it is visually lossless.

IDAT β€” the pixel data

All IDAT chunks concatenated form a single zlib stream. Decompress it and you get the raw scanlines β€” each one prefixed by a single byte naming the filter applied to that row.

// After decompression, per row: [filter type][pixel data for row 0] [filter type][pixel data for row 1] [filter type][pixel data for row 2] ...
FilterNameEach byte is stored as
0NoneThe raw value
1SubDifference from the byte to the left
2UpDifference from the byte above
3AverageDifference from the mean of left and above
4PaethDifference from a predictor using left, above and above-left

The filters compress nothing themselves. They transform the data into a form that compresses well β€” a smooth gradient becomes a long run of the same small difference, and DEFLATE handles runs extremely efficiently.

The filter is chosen per row, so a good encoder tries all five on every scanline and keeps whichever produces the most compressible output. This is exactly what oxipng and zopflipng do more thoroughly than typical export code, which is why they shrink files by 10–30% without altering a single pixel.

PLTE and IEND

PLTE holds up to 256 RGB triples, three bytes each. Required for colour type 3, optional as a suggested quantisation for types 2 and 6, forbidden for greyscale types.

IEND is always the same twelve bytes and carries no data at all:

00 00 00 00 length = 0 49 45 4E 44 "IEND" AE 42 60 82 CRC β€” always this value

AE 42 60 82 at the end of a file is a reliable PNG marker. It is also useful for a specific recovery case: data appended after IEND is ignored by decoders, so files sometimes carry hidden payloads there.

Ancillary chunks worth knowing

ChunkHolds
tRNSTransparency for palette or a single colour, without a full alpha channel
gAMAGamma value of the source
cHRMChromaticity and white point
sRGBDeclares sRGB and a rendering intent
iCCPAn embedded ICC colour profile
tEXtLatin-1 key/value text
zTXtCompressed text
iTXtUTF-8 text, optionally compressed β€” where XMP metadata lives
pHYsPhysical pixel dimensions β€” the DPI field
bKGDSuggested background colour
tIMELast modification time
acTL fcTL fdATAPNG animation control and frames

⚠️ PNG carries metadata too

PNG has no EXIF by default, which leads people to assume PNGs are metadata-free. They are not β€” tEXt and iTXt chunks routinely carry software names, author fields, comments, and full XMP blocks including copyright and sometimes location data.

Strip them explicitly before publishing: oxipng --strip safe keeps the chunks that affect rendering and removes the rest.

How APNG stays compatible

APNG is a good illustration of the naming rules working. Its three chunks are all lowercase-initial, so they are ancillary β€” a decoder without APNG support skips them entirely and renders the IDAT as a normal still image.

So an animated PNG shown in software that has never heard of APNG displays the first frame correctly rather than failing. That graceful degradation was designed in, and it is why APNG could be deployed by browsers without breaking anything.

Reading a real file

# The first 60 bytes of a small PNG xxd -l 60 image.png 00000000: 89504e47 0d0a1a0a 0000000d 49484452 .PNG........IHDR 00000010: 00000100 000000c0 08060000 00xxxxxx ................ 00000020: xxxx0000 00197048 59730000 0b130000# Reading it: # 89504e47 0d0a1a0a β†’ the signature # 0000000d β†’ next chunk is 13 bytes # 49484452 β†’ "IHDR" # 00000100 β†’ width = 256 # 000000c0 β†’ height = 192 # 08 β†’ bit depth 8 # 06 β†’ colour type 6 (RGBA) # 00 00 00 β†’ deflate, standard filters, no interlace # ...then a 4-byte CRC, then the next chunk: pHYs
# List every chunk without writing a parser pngcheck -v image.png # Chunk-level detail plus any text metadata exiftool -v3 image.png # Optimise and strip non-rendering chunks oxipng -o 4 --strip safe image.png

Adam7 interlacing

Interlaced PNG stores the image in seven passes, each a sparse sampling of the whole picture, so a partially downloaded file shows a low-resolution version of the entire image rather than the top portion.

// The pattern that repeats every 8Γ—8 pixels 1 6 4 6 2 6 4 6 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7 3 6 4 6 3 6 4 6 7 7 7 7 7 7 7 7 5 6 5 6 5 6 5 6 7 7 7 7 7 7 7 7

Pass 1 is one pixel in 64. Each subsequent pass fills in more. The technique is named after Adam Costello, who designed it.

It made real sense over a modem. Today it is usually a mistake: interlacing typically makes the file 20–35% larger, because splitting the image into seven sparse passes destroys the local coherence the prediction filters depend on. On a modern connection the progressive display buys nothing that matters and costs bandwidth on every load.

Working with PNG files?

Convert, compress and strip metadata from images entirely in your browser β€” nothing is uploaded to a server.

Open the Image Compressor β†’

Summary

  • The 8-byte signature is a corruption test for the transfer failures of its era.
  • Chunks are length, type, data, CRC β€” self-describing, so unknown chunks are skippable.
  • Capitalisation in chunk names carries four bits of meaning, including whether an editor may preserve a chunk it does not understand.
  • IHDR, PLTE, IDAT, IEND are the critical chunks.
  • Colour type 3 uses a quarter of the bytes for images with 256 or fewer colours.
  • Filters run per scanline, which is why optimisers can shrink files without changing pixels.
  • PNG does carry metadata in tEXt and iTXt chunks. Strip it before publishing.
  • Interlacing costs 20–35% and buys little today.

Frequently Asked Questions

What are the first bytes of a PNG file?

89 50 4E 47 0D 0A 1A 0A. The 50 4E 47 is the letters PNG in ASCII. The surrounding bytes are a deliberate corruption test β€” the high-bit byte, a CRLF pair, a DOS end-of-file marker and a bare LF all detect specific ways a binary file can be damaged in transfer.

What is a PNG chunk?

The unit PNG is built from. Each chunk has a 4-byte length, a 4-byte type name, the data itself, and a 4-byte CRC-32 checksum. Because the length is stated up front, a decoder can skip any chunk it does not recognise and continue β€” which is what makes the format extensible.

Why does the capitalisation of PNG chunk names matter?

Each of the four letters carries a bit of information in its case. An uppercase first letter means the chunk is critical and a decoder that cannot handle it must stop. Lowercase means ancillary and safely skippable. The second letter marks public versus private, and the fourth marks whether an editor may copy the chunk into a modified file it does not understand.

What are the four critical PNG chunks?

IHDR holds the dimensions and colour format and must come first. PLTE holds the palette, required only for indexed-colour images. IDAT holds the compressed pixel data and may be split across many chunks. IEND marks the end and contains no data at all.

Why is PNG image data split across multiple IDAT chunks?

Because a chunk's length field limits it, and splitting lets an encoder write data in a streaming fashion without buffering the whole image. Decoders concatenate every IDAT chunk into one compressed stream before decompressing, so the split points are arbitrary and carry no meaning.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.