dct-io
Read and write the hidden numbers inside JPEG files — without touching the pixels.
What even is a JPEG?
When you save a photo as a JPEG, your computer doesn't store every pixel's colour directly. Instead it chops the image into tiny 8×8 pixel tiles and runs a maths trick called the Discrete Cosine Transform (DCT) on each one. Think of it like describing a song with a list of bass, mid, and treble levels rather than writing out every sound wave. The result is a list of 64 numbers per tile — the DCT coefficients — that capture the image's frequencies from coarse (the big shapes) to fine (tiny details).
Those numbers are then rounded (this is the "lossy" part) and packed tightly using Huffman coding — a compression trick that assigns shorter codes to more common values.
This crate peels back those layers. It reads the compressed data, unpacks the Huffman codes, and hands you the raw coefficient numbers. You can change them and write a new JPEG that looks identical to any image viewer but has your modifications baked in at the compression level.
What can you do with this?
- Steganography — hide data inside a JPEG by tweaking the least significant bit of coefficients (the JSteg technique). The image looks the same; the bits are yours.
- Watermarking — embed an invisible signature that survives re-saving.
- Forensic analysis — inspect the raw coefficient structure to detect tampering or double compression.
- Research / signal processing — work directly in the frequency domain without decoding to pixels first.
What this crate does NOT do
- Decode pixel values (no inverse-DCT, no dequantisation — pixels stay out of it)
- Support progressive JPEG, lossless JPEG, or arithmetic coding (returns an error)
- Support JPEG 2000
Supported JPEG variants
- Baseline DCT (SOF0) — the most common JPEG you'll encounter
- Extended sequential DCT (SOF1)
- Grayscale (1 channel) and colour (3 channels, typically Y/Cb/Cr)
- All standard chroma subsampling ratios (4:4:4, 4:2:2, 4:2:0, etc.)
- EXIF and JFIF headers
- Restart markers (DRI / RST0–RST7)
Installation
[]
= "0.1"
Examples
Read and modify coefficients
use ;
let jpeg = read?;
let mut coeffs = read_coefficients?;
// Flip the LSB of every AC coefficient with |v| >= 2 in the Y (luminance) channel.
// These are "eligible" positions — changing them doesn't shift zero runs,
// so the output is a valid JPEG that looks identical to the original.
for block in &mut coeffs.components.blocks
let modified = write_coefficients?;
write?;
Inspect image metadata cheaply
use inspect;
let jpeg = read?;
let info = inspect?; // does NOT decode the entropy stream
println!;
for comp in &info.components
Count how many bits you can hide
use ;
let jpeg = read?;
// Quick path — counts without allocating the full coefficient array:
let n = eligible_ac_count?;
println!;
// Or after you already have coefficients:
let coeffs = read_coefficients?;
println!;
Query block counts
use block_count;
let jpeg = read?;
let counts = block_count?;
for in counts.iter.enumerate
Coefficient layout
Each block: [i16; 64] is in JPEG zigzag scan order:
- Index 0 — the DC coefficient (represents the average brightness/colour of the tile)
- Indices 1–63 — AC coefficients in zigzag order (higher index = higher frequency detail)
The values are the quantized coefficients exactly as stored in the file. They have not been dequantized; multiply by the quantization table if you want the pre-quantized DCT values.
The "eligible" positions for safe LSB embedding are AC coefficients with |v| >= 2.
Modifying those never changes whether a coefficient is zero or non-zero, so the
Huffman code lengths (and therefore the re-encoded stream structure) stay the same.
Safety and security
#![forbid(unsafe_code)]— zero unsafe Rust in this crate, guaranteed at compile time- No panics on crafted input — every error path returns a
DctError; the parser validates dimensions, sampling factors, Huffman table structure, component indices, and MCU counts before touching any data - Allocation cap — MCU count is capped at 1 million (~67 megapixels) to prevent memory exhaustion from a malicious input
- Huffman overflow guard — canonical code overflow in malformed DHT segments is caught before it can write out-of-bounds into the LUT
- Fuzz targets included (see
fuzz/) — run withcargo fuzz run fuzz_read
Error handling
All public functions are marked #[must_use] — the compiler will warn if you forget
to handle the returned Result.
Limitations
Roundtrip identity
write_coefficients(jpeg, read_coefficients(jpeg)?) produces byte-identical output
for JPEGs encoded by libjpeg, libjpeg-turbo, and most standard encoders. Exotic
encoders that use non-standard Huffman table construction or non-standard EOB placement
may decode identically but not round-trip byte-for-byte.
Multiple scans
Only the first SOS (start-of-scan) segment is processed. Baseline JPEG always has exactly one scan; progressive JPEG is not supported.
Fuzzing
Licence
Licensed under either of:
at your option.