PNG: Portable Network Graphics — Complete Technical Deep Dive
PNG (Portable Network Graphics) was created in 1995 specifically to replace GIF without its limitations — no patent-encumbered LZW compression, full truecolor support, and proper alpha transparency. Today PNG is the dominant format for web graphics requiring transparency, lossless screenshot storage, and any image where exact pixel values must be preserved. Understanding PNG's chunk architecture, filtering system, and compression model unlocks effective optimization strategies.
PNG Design Philosophy
PNG was designed by the PNG Development Group (1994–1996) in direct response to the LZW patent controversy surrounding GIF. The key design goals:
- Lossless: Pixel values are preserved exactly after compression-decompression
- Full color depth: Up to 16 bits per channel, not just 8-bit indexed
- True alpha: 8-bit or 16-bit alpha channel (256 transparency levels, not GIF's 1-bit on/off)
- Streaming-friendly: Can be read progressively without seeking backward
- Extensible: Chunk-based architecture allows adding new metadata without breaking existing readers
PNG File Structure: Chunks
A PNG file is a sequence of chunks (length-type-data-CRC):
PNG File Structure
├── PNG Signature (8 bytes): 0x89 'P' 'N' 'G' '\r' '\n' 0x1A '\n'
│ (Non-ASCII byte 0x89 ensures binary mode transfers; CRLF detects line-ending conversions)
│
├── IHDR chunk (must be first) — Image Header
│ ├── Length: 13 (4 bytes, big-endian)
│ ├── Type: "IHDR"
│ ├── Width (4 bytes)
│ ├── Height (4 bytes)
│ ├── Bit depth (1 byte): 1, 2, 4, 8, or 16
│ ├── Color type (1 byte):
│ │ 0 = Grayscale
│ │ 2 = Truecolor (RGB)
│ │ 3 = Indexed-color (palette)
│ │ 4 = Grayscale + alpha
│ │ 6 = Truecolor + alpha (RGBA) ← Most common for web graphics
│ ├── Compression method (0 = deflate — only defined value)
│ ├── Filter method (0 — only defined value)
│ └── Interlace method (0=None, 1=Adam7)
│ └── CRC (4 bytes)
│
├── [Ancillary chunks — optional, order matters for some]
│ ├── cHRM — Chromaticity: white point and primary chromaticities
│ ├── gAMA — Image gamma
│ ├── iCCP — Embedded ICC profile (replaces gAMA/cHRM if present)
│ ├── sRGB — sRGB color space declaration (rendering intent)
│ ├── bKGD — Background color suggestion
│ ├── pHYs — Physical pixel dimensions (DPI / pixels per unit)
│ ├── tIME — Last modification time
│ ├── tEXt — Textual metadata (keyword=text, uncompressed)
│ ├── zTXt — Compressed text metadata
│ ├── iTXt — International text metadata (UTF-8)
│ ├── sBIT — Significant bits (for images stored at higher depth than captured)
│ └── tRNS — Transparency (for indexed or truecolor PNG without alpha channel)
│
├── PLTE chunk (required for indexed-color, optional for truecolor)
│ └── RGB palette entries (1-256 entries, 3 bytes each)
│
├── IDAT chunk(s) — Image Data (one or more, concatenated)
│ └── DEFLATE-compressed, filtered pixel data
│
└── IEND chunk — Image End (always last, empty data, CRC validates)
Each chunk has a 4-byte CRC32 of the type + data for integrity verification. Unknown chunk types with lowercase first letter are "safe to copy" (ancillary safe); uppercase = critical (IHDR, PLTE, IDAT, IEND) or "unsafe to copy" if unknown.
PNG Color Types and Bit Depths
| Color type | Value | Allowed bit depths | Channels | Max colors | Use |
|---|---|---|---|---|---|
| Grayscale | 0 | 1, 2, 4, 8, 16 | 1 | 65536 | Scans, masks |
| Truecolor | 2 | 8, 16 | 3 (RGB) | 16.7M | Photos without alpha |
| Indexed | 3 | 1, 2, 4, 8 | 1+PLTE | 256 | Icons, small graphics |
| Gray+Alpha | 4 | 8, 16 | 2 | 65536 | Grayscale with transparency |
| RGBA | 6 | 8, 16 | 4 | 16.7M + alpha | Web graphics, overlays |
Most web graphics use color type 6 (RGBA), bit depth 8 — providing 16.7 million colors plus 256 levels of transparency. The iCCP chunk with an embedded sRGB profile ensures consistent color rendering across displays.
PNG Filtering: Pre-compression Transform
PNG applies a reversible filter to each scanline before DEFLATE compression. The filter predicts each pixel from its neighbors and stores the prediction error, which has lower entropy and compresses better:
| Filter | ID | Formula | Best for |
|---|---|---|---|
| None | 0 | Filt(x) = Orig(x) |
Indexed-color, palette images |
| Sub | 1 | Filt(x) = Orig(x) - Orig(x-1) |
Horizontal gradients |
| Up | 2 | Filt(x) = Orig(x) - Prior(x) |
Vertical gradients |
| Average | 3 | Filt(x) = Orig(x) - floor((Orig(x-1)+Prior(x))/2) |
Photos |
| Paeth | 4 | Paeth predictor (uses left, above, upper-left) | General purpose (usually best) |
The encoder can choose a different filter for each scanline. Optimal filter selection is the most impactful factor in PNG compression efficiency (besides bit depth and color type reduction).
PNG DEFLATE Compression
PNG uses DEFLATE (zlib format) — the same algorithm used in ZIP files. DEFLATE combines:
- LZ77 back-references: Finds repeated byte sequences and replaces them with (distance, length) references
- Huffman coding: Variable-length entropy coding of the symbols
The DEFLATE compression level (1–9) trades speed for compression ratio. PNG encoders default to level 6; maximum level 9 can reduce file size by 5–15% at much higher CPU cost.
Working with PNG in Python
from PIL import Image, ImageFilter
import struct
import zlib
# ── Basic PNG operations ─────────────────────────
with Image.open('input.png') as img:
print(f"Size: {img.size}, Mode: {img.mode}")
print(f"Info: {img.info}") # Metadata: dpi, icc_profile, gamma, etc.
# Access pixel data
pixels = img.load()
r, g, b, a = pixels[100, 100] # RGBA values at (x=100, y=100)
# Convert to different color type
img.convert('RGB').save('no_alpha.png') # Remove alpha
img.convert('L').save('grayscale.png') # Grayscale
img.convert('P').save('indexed_256.png') # 256-color indexed
# Save with maximum compression
img.save('compressed.png', optimize=True, compress_level=9)
# ── Create PNG with metadata ──────────────────────
from PIL import PngImagePlugin
img = Image.new('RGBA', (400, 300), (74, 144, 226, 255))
meta = PngImagePlugin.PngInfo()
meta.add_text("Title", "KaijuConverter Test Image")
meta.add_text("Software", "Python Pillow")
meta.add_itxt("Description", "Test image with RGBA color and metadata", lang="en",
tkey="Description")
img.save('with_metadata.png', pnginfo=meta, dpi=(96, 96))
# ── Read PNG chunks directly ──────────────────────
def parse_png_chunks(filepath):
"""Read PNG chunk structure without decoding image data."""
chunks = []
with open(filepath, 'rb') as f:
signature = f.read(8)
assert signature == b'\x89PNG\r\n\x1a\n', "Not a PNG file"
while True:
header = f.read(8)
if len(header) < 8:
break
length, chunk_type = struct.unpack('>I4s', header)
chunk_type = chunk_type.decode('ascii')
data = f.read(length)
crc = struct.unpack('>I', f.read(4))[0]
computed_crc = zlib.crc32(chunk_type.encode() + data) & 0xFFFFFFFF
chunks.append({
'type': chunk_type,
'length': length,
'crc_ok': crc == computed_crc
})
if chunk_type == 'IEND':
break
return chunks
for chunk in parse_png_chunks('image.png'):
status = '✓' if chunk['crc_ok'] else '✗'
print(f" {status} {chunk['type']:4s} ({chunk['length']:,} bytes)")
# ── PNG optimization pipeline ────────────────────
import subprocess
import os
def optimize_png(input_path, output_path):
"""Multi-tool PNG optimization pipeline."""
# Step 1: Strip metadata and optimize with oxipng
subprocess.run(['oxipng', '--opt', 'max', '--strip', 'all',
'-o', output_path, input_path], check=True)
# Step 2: Check result
orig = os.path.getsize(input_path)
opt = os.path.getsize(output_path)
print(f"Optimized: {orig//1024}KB → {opt//1024}KB ({(1-opt/orig)*100:.1f}% saved)")
PNG Optimization Tools
# oxipng — Rust-based, very fast, excellent results
# Install: cargo install oxipng
oxipng --opt max --strip all input.png -o output.png
# pngcrush — Classic tool, tries many strategies
pngcrush -brute input.png output.png # Very slow but thorough
pngcrush -q -reduce input.png output.png # Quick reduction
# zopflipng — Google's Zopfli compression
zopflipng -m input.png output.png # -m = more optimization passes
# optipng — Standard optimization
optipng -o7 input.png
# Lossless PNG → Lossy PNG (palette quantization)
# pngquant — reduce to 256 colors with lossy quantization
pngquant --quality 80-95 --speed 1 input.png -o output-quantized.png
# This can reduce RGBA PNG by 60-80% with minimal visible quality loss
APNG: Animated PNG Extension
APNG (Animated PNG) adds animation to PNG using three new chunk types:
- acTL: Animation Control (frame count, play count)
- fcTL: Frame Control (delay, dimensions, dispose/blend operations per frame)
- fdAT: Frame Data (compressed pixel data, like IDAT but for non-default frames)
The first IDAT in an APNG is also the static fallback (displayed by PNG readers that don't understand APNG). APNG has ~95% browser support as of 2024.
import apng # pip install apng
# Create APNG from PNG files
a = apng.APNG()
for i in range(10):
a.append(apng.PNG.open(f'frame_{i:04d}.png'), delay=100) # 100ms per frame
a.save('animation.apng')
PNG vs JPEG vs WebP vs AVIF
| Scenario | Best Format | Reason |
|---|---|---|
| Photo for web | WebP (or AVIF) | 30-50% smaller than PNG lossless |
| Logo with transparency | PNG or SVG | Lossless + alpha |
| Screenshot | PNG | Lossless, exact text reproduction |
| Animated graphic | WebP anim or APNG | Better than GIF |
| Print/archival | TIFF or PNG-16 | Lossless, 16-bit depth |
| Icons/sprites | PNG or SVG | Transparency support |
| Gradient background | WebP lossy | PNG deflate struggles with gradients |
Summary
PNG's chunk architecture, filter system, and DEFLATE compression make it the definitive format for lossless web graphics — particularly anything requiring accurate transparency. For optimization, the highest impact actions are: (1) reduce to the minimum necessary bit depth and color type, (2) apply palette quantization with pngquant for photographs, (3) use oxipng for lossless size reduction on all PNG files before production deployment. PNG remains essential wherever pixel-perfect lossless rendering with transparency is required.