Skip to main content

Overview

PNG uses CRC-32 (Cyclic Redundancy Check) to detect data corruption in chunks. Every chunk includes a 4-byte CRC computed over the chunk’s type and data fields. You must implement the CRC-32 algorithm to validate chunks as you read them.

What is CRC?

CRC (Cyclic Redundancy Check) is an error-detecting code that:

Detects corruption

Identifies if data has been modified or corrupted

Fast computation

Uses lookup tables for efficient calculation

Standardized

PNG uses CRC-32 with polynomial 0xEDB88320

Not cryptographic

Only detects accidental corruption, not malicious changes

PNG CRC specification

CRC parameters

Polynomial
hex
0xEDB88320 (bit-reversed version of 0x04C11DB7)
Initial value
hex
0xFFFFFFFF (all bits set)
Final XOR
hex
0xFFFFFFFF (invert all bits)
Input
bytes
Chunk type (4 bytes) + chunk data (variable length)
The CRC is computed over the type AND data fields, but NOT the length or CRC fields themselves.

CRC-32 algorithm

High-level overview

1

Initialize CRC table

Build a 256-entry lookup table (done once, can be static)
2

Initialize CRC value

Start with 0xFFFFFFFF
3

Process each byte

For each byte in the input:
  • XOR CRC with byte
  • Use low 8 bits as table index
  • XOR table value with CRC shifted right 8 bits
4

Finalize

XOR final CRC with 0xFFFFFFFF

Building the lookup table

The lookup table is computed once and can be reused:
// Global or static variable
static uint32_t crc_table[256];
static int table_computed = 0;

void make_crc_table(void) {
    uint32_t c;
    int n, k;
    
    for (n = 0; n < 256; n++) {
        c = (uint32_t) n;
        for (k = 0; k < 8; k++) {
            if (c & 1) {
                c = 0xEDB88320 ^ (c >> 1);
            } else {
                c = c >> 1;
            }
        }
        crc_table[n] = c;
    }
    table_computed = 1;
}

CRC computation

Once the table is built, compute CRC over a buffer:
uint32_t png_crc(const uint8_t *buf, size_t len) {
    uint32_t c;
    size_t n;
    
    // Build table on first call
    if (!table_computed) {
        make_crc_table();
    }
    
    // Initialize
    c = 0xFFFFFFFF;
    
    // Process each byte
    for (n = 0; n < len; n++) {
        c = crc_table[(c ^ buf[n]) & 0xFF] ^ (c >> 8);
    }
    
    // Finalize (invert all bits)
    return c ^ 0xFFFFFFFF;
}

Using CRC in PNG chunks

Computing CRC for a chunk

The CRC is computed over the type (4 bytes) + data (variable length):
// Given a chunk structure:
// typedef struct {
//     uint32_t length;
//     char     type[5];
//     uint8_t *data;
//     uint32_t crc;
// } png_chunk_t;

uint32_t compute_chunk_crc(const png_chunk_t *chunk) {
    // Need to combine type + data into one buffer
    size_t total_len = 4 + chunk->length;
    uint8_t *buffer = malloc(total_len);
    
    if (!buffer) {
        return 0;
    }
    
    // Copy type (4 bytes, not including null terminator)
    memcpy(buffer, chunk->type, 4);
    
    // Copy data (if any)
    if (chunk->length > 0 && chunk->data) {
        memcpy(buffer + 4, chunk->data, chunk->length);
    }
    
    // Compute CRC
    uint32_t crc = png_crc(buffer, total_len);
    
    free(buffer);
    return crc;
}

Validating a chunk

After reading a chunk, validate its CRC:
int png_read_chunk(FILE *fp, png_chunk_t *out) {
    // ... read length, type, data, and stored CRC ...
    
    // Compute CRC over type + data
    uint32_t computed = compute_chunk_crc(out);
    
    // Compare with stored CRC
    if (computed != out->crc) {
        // CRC mismatch - chunk is corrupted!
        png_free_chunk(out);
        return -1;
    }
    
    return 0; // CRC is valid
}

Example: Step-by-step CRC

Let’s compute the CRC for an IEND chunk (which has no data):
1

Input bytes

Type: “IEND” = [0x49, 0x45, 0x4E, 0x44]Data: (none)
2

Initialize

c = 0xFFFFFFFF
3

Process byte 0x49

index = (c ^ 0x49) & 0xFF = 0xB6
c = crc_table[0xB6] ^ (c >> 8)
4

Process bytes 0x45, 0x4E, 0x44

Repeat for each byte using the lookup table
5

Finalize

result = c ^ 0xFFFFFFFF = 0xAE426082
The CRC for “IEND” with no data is 0xAE426082.

Reporting CRC status

When printing chunk summaries, indicate if CRC is valid:
PRINT_CHUNK_INFO(i, chunk);
// Output: Chunk 0: Type=IHDR, Length=13, CRC=valid
The macro uses the chunk’s validity (you can add a flag to the struct):
typedef struct {
    uint32_t length;
    char     type[5];
    uint8_t *data;
    uint32_t crc;
    int      crc_valid; // Add this field
} png_chunk_t;
Or compute it on-the-fly:
for (int i = 0; i < count; i++) {
    uint32_t computed = compute_chunk_crc(&chunks[i]);
    int valid = (computed == chunks[i].crc);
    
    printf("  Chunk %d: Type=%s, Length=%u, CRC=%s\n",
           i, chunks[i].type, chunks[i].length,
           valid ? "valid" : "invalid");
}

Using the PNG spec sample code

The PNG specification appendix includes sample C code for CRC computation. You are explicitly allowed to use this code: PNG Specification - Sample CRC Code
The spec’s sample code is well-tested and handles all edge cases correctly. Using it can save you time and prevent bugs.

Common errors

Problem: Computing CRC over data onlySolution: CRC covers type (4 bytes) + data
// WRONG
uint32_t crc = png_crc(chunk->data, chunk->length);

// CORRECT
uint8_t *buffer = malloc(4 + chunk->length);
memcpy(buffer, chunk->type, 4);
memcpy(buffer + 4, chunk->data, chunk->length);
uint32_t crc = png_crc(buffer, 4 + chunk->length);
free(buffer);
Problem: Copying 5 bytes from type[5] instead of 4Solution: Only copy 4 bytes (the actual type, not null terminator)
// WRONG
memcpy(buffer, chunk->type, 5); // Includes '\0'

// CORRECT
memcpy(buffer, chunk->type, 4); // Only "IHDR", not '\0'
Problem: CRC in file is big-endian, comparing with little-endian valueSolution: Convert stored CRC from big-endian when reading
uint8_t crc_bytes[4];
fread(crc_bytes, 1, 4, fp);

// Convert from big-endian
out->crc = read_u32_be(crc_bytes);
Problem: Not inverting bits at the endSolution: XOR with 0xFFFFFFFF as the last step
// Inside png_crc()
for (n = 0; n < len; n++) {
    c = crc_table[(c ^ buf[n]) & 0xFF] ^ (c >> 8);
}

// CRITICAL: Finalize
return c ^ 0xFFFFFFFF;

Testing CRC implementation

Known CRC values

Test your implementation with these known values:
// "IEND" with no data
uint8_t iend[] = {0x49, 0x45, 0x4E, 0x44};
assert(png_crc(iend, 4) == 0xAE426082);

// "IHDR" (just the type, not data)
uint8_t ihdr[] = {0x49, 0x48, 0x44, 0x52};
uint32_t result = png_crc(ihdr, 4);
// Will differ based on actual IHDR data

Validating real chunks

# Examine a PNG file
$ hd tests/data/test.png | less

# Find IEND chunk (usually at end)
# Should see: 00 00 00 00 49 45 4E 44 AE 42 60 82
#            |length=0| |IEND| |CRC=0xAE426082|

Using the summary command

$ bin/png -f tests/data/test.png -s
Chunk Summary for tests/data/test.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=IEND, Length=0, CRC=valid
All CRCs should show “valid” for uncorrupted PNG files.

Performance considerations

Table computation

  • Build the lookup table once (use a static flag)
  • Don’t rebuild it on every CRC call
  • Consider making it const after initialization

Memory allocation

When combining type + data:
// Option 1: Allocate temporary buffer (shown above)
// Pro: Simple, clear
// Con: Requires malloc/free

// Option 2: Compute in two passes
uint32_t compute_chunk_crc_no_alloc(const png_chunk_t *chunk) {
    if (!table_computed) make_crc_table();
    
    uint32_t c = 0xFFFFFFFF;
    
    // Process type (4 bytes)
    for (int i = 0; i < 4; i++) {
        c = crc_table[(c ^ chunk->type[i]) & 0xFF] ^ (c >> 8);
    }
    
    // Process data
    for (uint32_t i = 0; i < chunk->length; i++) {
        c = crc_table[(c ^ chunk->data[i]) & 0xFF] ^ (c >> 8);
    }
    
    return c ^ 0xFFFFFFFF;
}
// Pro: No allocation
// Con: Slightly less clear

Reference

For the complete CRC algorithm specification, see:

Next steps

Steganography

Learn LSB message encoding

CRC API

Review the CRC API reference

Build docs developers (and LLMs) love