Overview
This page covers the implementation of PNG chunk reading functions from the png_reader.h API. You’ll learn how to open PNG files, read chunks sequentially, validate CRCs, and extract specific chunk types.
Opening PNG files
png_open() function
The first step is opening a PNG file and validating its signature:
FILE * png_open ( const char * path );
Path to the PNG file to open
Returns: FILE pointer positioned after the 8-byte signature, or NULL on error
Implementation
Open file
Open the file in binary read mode
Read signature
Read the first 8 bytes
Validate signature
Compare against the expected PNG signature
Return file pointer
If valid, return the FILE pointer positioned at the first chunk
FILE * png_open ( const char * path ) {
const uint8_t PNG_SIGNATURE [ 8 ] = {
0x 89 , 0x 50 , 0x 4E , 0x 47 , 0x 0D , 0x 0A , 0x 1A , 0x 0A
};
FILE * fp = fopen (path, "rb" );
if ( ! fp) {
return NULL ;
}
uint8_t signature [ 8 ];
if ( fread (signature, 1 , 8 , fp) != 8 ) {
fclose (fp);
return NULL ;
}
if ( memcmp (signature, PNG_SIGNATURE, 8 ) != 0 ) {
fclose (fp);
return NULL ;
}
// File pointer now positioned at first chunk
return fp;
}
Reading chunks
png_read_chunk() function
Reads the next chunk from an open PNG file:
int png_read_chunk (FILE * fp , png_chunk_t * out );
File pointer positioned at the start of a chunk
Pointer to chunk structure to fill
Returns: 0 on success, -1 on error
Implementation strategy
Read length
Read 4 bytes, convert from big-endian
Read type
Read 4 bytes, copy to type string with null terminator
Allocate data
Allocate length bytes for chunk data
Read data
Read length bytes into allocated buffer
Read CRC
Read 4 bytes, convert from big-endian
Validate CRC
Compute CRC over type + data, compare with stored CRC
Example implementation
int png_read_chunk (FILE * fp , png_chunk_t * out ) {
if ( ! fp || ! out) {
return - 1 ;
}
// Read length (big-endian)
uint8_t len_bytes [ 4 ];
if ( fread (len_bytes, 1 , 4 , fp) != 4 ) {
return - 1 ;
}
out -> length = read_u32_be (len_bytes);
// Read type (4 ASCII characters)
if ( fread ( out -> type , 1 , 4 , fp) != 4 ) {
return - 1 ;
}
out -> type [ 4 ] = ' \0 ' ; // Null terminate
// Allocate and read data
if ( out -> length > 0 ) {
out -> data = malloc ( out -> length );
if ( ! out -> data ) {
return - 1 ;
}
if ( fread ( out -> data , 1 , out -> length , fp) != out -> length ) {
free ( out -> data );
out -> data = NULL ;
return - 1 ;
}
} else {
out -> data = NULL ;
}
// Read CRC (big-endian)
uint8_t crc_bytes [ 4 ];
if ( fread (crc_bytes, 1 , 4 , fp) != 4 ) {
free ( out -> data );
out -> data = NULL ;
return - 1 ;
}
out -> crc = read_u32_be (crc_bytes);
// Validate CRC
uint32_t computed_crc = png_crc_chunk (out);
if (computed_crc != out -> crc ) {
// CRC mismatch - still return success but mark it
// (some functions may want to handle this differently)
free ( out -> data );
out -> data = NULL ;
return - 1 ;
}
return 0 ;
}
Computing CRC for validation
uint32_t png_crc_chunk ( const png_chunk_t * chunk ) {
// CRC is computed over type + data
// We need to combine them into one buffer
size_t total_len = 4 + chunk -> length ; // type (4) + data
uint8_t * buffer = malloc (total_len);
if ( ! buffer) {
return 0 ;
}
// Copy type
memcpy (buffer, chunk -> type , 4 );
// Copy data (if any)
if ( chunk -> length > 0 && chunk -> data ) {
memcpy (buffer + 4 , chunk -> data , chunk -> length );
}
uint32_t crc = png_crc (buffer, total_len);
free (buffer);
return crc;
}
Freeing chunks
png_free_chunk() function
Safely frees memory allocated for chunk data:
void png_free_chunk ( png_chunk_t * chunk ) {
if (chunk && chunk -> data ) {
free ( chunk -> data );
chunk -> data = NULL ;
}
}
Always call png_free_chunk() when you’re done with a chunk. The data field is dynamically allocated.
Reading specific chunks
Convenience function to read and parse the IHDR chunk:
int png_extract_ihdr (FILE * fp , png_ihdr_t * out );
Read first chunk
Call png_read_chunk() to get the first chunk
Verify type
Ensure chunk type is “IHDR”
Parse IHDR
Call png_parse_ihdr() to extract fields
Free chunk
Clean up the chunk data
int png_extract_ihdr (FILE * fp , png_ihdr_t * out ) {
if ( ! fp || ! out) {
return - 1 ;
}
png_chunk_t chunk;
if ( png_read_chunk (fp, & chunk) < 0 ) {
return - 1 ;
}
if ( strcmp ( chunk . type , "IHDR" ) != 0 ) {
png_free_chunk ( & chunk);
return - 1 ;
}
int result = png_parse_ihdr ( & chunk, out);
png_free_chunk ( & chunk);
return result;
}
Searches for and extracts the PLTE chunk:
int png_extract_plte (FILE * fp , png_color_t ** out_colors , size_t * out_count );
PLTE may not be the second chunk. You must read chunks sequentially until you find PLTE or reach IDAT/IEND.
int png_extract_plte (FILE * fp , png_color_t ** out_colors , size_t * out_count ) {
if ( ! fp || ! out_colors || ! out_count) {
return - 1 ;
}
png_chunk_t chunk;
// Skip IHDR (we assume fp is positioned after signature)
if ( png_read_chunk (fp, & chunk) < 0 ) {
return - 1 ;
}
png_free_chunk ( & chunk);
// Read chunks until we find PLTE
while ( 1 ) {
if ( png_read_chunk (fp, & chunk) < 0 ) {
return - 1 ;
}
if ( strcmp ( chunk . type , "PLTE" ) == 0 ) {
// Found it!
int result = png_parse_plte ( & chunk, out_colors, out_count);
png_free_chunk ( & chunk);
return result;
}
// Stop if we hit IDAT or IEND (PLTE must come before these)
if ( strcmp ( chunk . type , "IDAT" ) == 0 ||
strcmp ( chunk . type , "IEND" ) == 0 ) {
png_free_chunk ( & chunk);
return - 1 ; // PLTE not found
}
png_free_chunk ( & chunk);
}
}
Creating chunk summaries
png_summary() function
Reads all chunks and returns a summary array:
int png_summary ( const char * filename , png_chunk_t ** out_summary );
Returns: Number of chunks, or -1 on error
The summary only stores type, length, and CRC validity. It does NOT store the actual chunk data to save memory.
int png_summary ( const char * filename , png_chunk_t ** out_summary ) {
FILE * fp = png_open (filename);
if ( ! fp) {
return - 1 ;
}
png_chunk_t * chunks = NULL ;
int count = 0 ;
int capacity = 16 ;
chunks = malloc (capacity * sizeof ( png_chunk_t ));
if ( ! chunks) {
fclose (fp);
return - 1 ;
}
png_chunk_t chunk;
while ( png_read_chunk (fp, & chunk) == 0 ) {
// Expand array if needed
if (count >= capacity) {
capacity *= 2 ;
png_chunk_t * new_chunks = realloc (chunks, capacity * sizeof ( png_chunk_t ));
if ( ! new_chunks) {
free (chunks);
fclose (fp);
return - 1 ;
}
chunks = new_chunks;
}
// Store summary info (not actual data)
chunks [count]. length = chunk . length ;
memcpy ( chunks [count]. type , chunk . type , 5 );
chunks [count]. crc = chunk . crc ;
chunks [count]. data = NULL ; // Don't store data in summary
count ++ ;
// Free the chunk data
png_free_chunk ( & chunk);
// Stop at IEND
if ( strcmp ( chunk . type , "IEND" ) == 0 ) {
break ;
}
}
fclose (fp);
* out_summary = chunks;
return count;
}
Using utility functions
The util.h header provides helpful functions:
read_exact()
Reads exactly N bytes or fails:
int read_exact (FILE * fp , void * buf , size_t n );
Better than fread() because it fails if fewer than N bytes are available.
read_u32_be()
Converts big-endian bytes to native uint32_t:
uint32_t read_u32_be ( const uint8_t * bytes ) {
return ( bytes [ 0 ] << 24 ) | ( bytes [ 1 ] << 16 ) |
( bytes [ 2 ] << 8 ) | bytes [ 3 ];
}
Common errors
Not checking return values
Problem: Assuming file operations always succeedSolution: Check every fread(), malloc(), and function call// WRONG
fread (buffer, 1 , 4 , fp);
// CORRECT
if ( fread (buffer, 1 , 4 , fp) != 4 ) {
return - 1 ;
}
Forgetting to convert endianness
Problem: Reading multi-byte values as little-endianSolution: Always use big-endian conversion// WRONG
uint32_t length = * ( uint32_t * )bytes; // Little-endian on x64
// CORRECT
uint32_t length = read_u32_be (bytes);
Problem: Not freeing chunk dataSolution: Always call png_free_chunk() when donepng_chunk_t chunk;
png_read_chunk (fp, & chunk );
// ... use chunk ...
png_free_chunk ( & chunk ); // CRITICAL
Not null-terminating type string
Problem: chunk.type has 4 characters, need 5 for null terminatorSolution: The type field is char type[5] - add null at index 4fread (chunk.type, 1 , 4 , fp);
chunk. type [ 4 ] = ' \0 ' ; // REQUIRED
Testing your implementation
Basic test
FILE * fp = png_open ( "tests/data/test.png" );
assert (fp != NULL );
png_chunk_t chunk;
assert ( png_read_chunk (fp, & chunk ) == 0 );
assert ( strcmp (chunk.type, "IHDR" ) == 0 );
assert (chunk.length == 13 );
png_free_chunk ( & chunk );
fclose (fp);
Summary test
$ bin/png -f tests/data/Large_batman_3.png -s
Chunk Summary for tests/data/Large_batman_3.png:
Chunk 0: Type=IHDR, Length=13, CRC=valid
Chunk 1: Type=PLTE, Length=504, CRC=valid
Chunk 2: Type=IDAT, Length=8192, CRC=valid
Chunk 3: Type=IDAT, Length=8192, CRC=valid
...
Chunk N: Type=IEND, Length=0, CRC=valid
Next steps
CRC validation Implement CRC-32 checksums
Chunks API Review chunk parsing functions