Skip to main content

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
const char*
required
Path to the PNG file to open
Returns: FILE pointer positioned after the 8-byte signature, or NULL on error

Implementation

1

Open file

Open the file in binary read mode
2

Read signature

Read the first 8 bytes
3

Validate signature

Compare against the expected PNG signature
4

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] = {
        0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
    };
    
    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);
fp
FILE*
required
File pointer positioned at the start of a chunk
out
png_chunk_t*
required
Pointer to chunk structure to fill
Returns: 0 on success, -1 on error

Implementation strategy

1

Read length

Read 4 bytes, convert from big-endian
2

Read type

Read 4 bytes, copy to type string with null terminator
3

Allocate data

Allocate length bytes for chunk data
4

Read data

Read length bytes into allocated buffer
5

Read CRC

Read 4 bytes, convert from big-endian
6

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

png_extract_ihdr() function

Convenience function to read and parse the IHDR chunk:
int png_extract_ihdr(FILE *fp, png_ihdr_t *out);
1

Read first chunk

Call png_read_chunk() to get the first chunk
2

Verify type

Ensure chunk type is “IHDR”
3

Parse IHDR

Call png_parse_ihdr() to extract fields
4

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;
}

png_extract_plte() function

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

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;
}
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 done
png_chunk_t chunk;
png_read_chunk(fp, &chunk);

// ... use chunk ...

png_free_chunk(&chunk); // CRITICAL
Problem: chunk.type has 4 characters, need 5 for null terminatorSolution: The type field is char type[5] - add null at index 4
fread(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

Build docs developers (and LLMs) love