use crate::codec::jpeg::JpegImage;
use crate::codec::jpeg::dct::DctGrid;
use crate::stego::frame::{FRAME_OVERHEAD, FRAME_OVERHEAD_EXT};
use crate::stego::error::StegoError;
use crate::stego::shadow;
const MIN_CAPACITY_RATIO: f64 = 5.0;
const MIN_CAPACITY_RATIO_SI: f64 = 3.5;
fn count_nonzero_ac(grid: &DctGrid) -> usize {
let bw = grid.blocks_wide();
let bt = grid.blocks_tall();
let mut count = 0usize;
for br in 0..bt {
for bc in 0..bw {
let blk = grid.block(br, bc);
for k in 1..64 { if blk[k] != 0 {
count += 1;
}
}
}
}
count
}
fn capacity_from_usable(usable: usize, ratio: f64) -> usize {
let max_frame_bits = (usable as f64 / ratio) as usize;
let max_frame_bytes = max_frame_bits / 8;
if max_frame_bytes <= FRAME_OVERHEAD {
return 0;
}
let capacity = max_frame_bytes - FRAME_OVERHEAD;
if capacity > u16::MAX as usize {
max_frame_bytes.saturating_sub(FRAME_OVERHEAD_EXT)
} else {
capacity
}
}
pub fn estimate_capacity(img: &JpegImage) -> Result<usize, StegoError> {
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let usable = count_nonzero_ac(img.dct_grid(0));
Ok(capacity_from_usable(usable, MIN_CAPACITY_RATIO))
}
pub fn estimate_capacity_si(img: &JpegImage) -> Result<usize, StegoError> {
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let usable = count_nonzero_ac(img.dct_grid(0));
Ok(capacity_from_usable(usable, MIN_CAPACITY_RATIO_SI))
}
pub fn estimate_shadow_capacity(img: &JpegImage) -> Result<usize, StegoError> {
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let y_nzac = count_nonzero_ac(img.dct_grid(0));
Ok(shadow::shadow_capacity(y_nzac))
}
pub fn estimate_capacity_with_shadows(
img: &JpegImage,
shadow_count: usize,
shadow_total_bytes: usize,
is_si: bool,
) -> Result<usize, StegoError> {
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let y_nzac = count_nonzero_ac(img.dct_grid(0));
let ratio = if is_si { MIN_CAPACITY_RATIO_SI } else { MIN_CAPACITY_RATIO };
let parity = 16usize;
let shadow_frame_overhead = 46usize; let total_shadow_frame_bytes = shadow_count * shadow_frame_overhead + shadow_total_bytes;
let k = 255 - parity; let full_blocks = total_shadow_frame_bytes.div_ceil(k);
let shadow_rs_bytes = full_blocks * 255;
let shadow_bits = shadow_rs_bytes * 8;
let effective_nzac = y_nzac.saturating_sub(shadow_bits);
Ok(capacity_from_usable(effective_nzac, ratio))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capacity_reasonable_for_photo() {
let data = std::fs::read("test-vectors/image/photo_320x240_q75_420.jpg").unwrap();
let img = JpegImage::from_bytes(&data).unwrap();
let cap = estimate_capacity(&img).unwrap();
assert!(cap > 100, "capacity {cap} is too low for 320x240");
assert!(cap < 5000, "capacity {cap} is suspiciously high");
}
#[test]
fn si_capacity_higher_than_standard() {
let data = std::fs::read("test-vectors/image/photo_320x240_q75_420.jpg").unwrap();
let img = JpegImage::from_bytes(&data).unwrap();
let cap_j = estimate_capacity(&img).unwrap();
let cap_si = estimate_capacity_si(&img).unwrap();
assert!(
cap_si > cap_j,
"SI capacity ({cap_si}) should exceed J-UNIWARD capacity ({cap_j})"
);
let ratio = cap_si as f64 / cap_j as f64;
assert!(
ratio > 1.3 && ratio < 1.6,
"SI/J ratio {ratio:.2} should be ~1.43"
);
}
}