use crate::codec::jpeg::JpegImage;
use crate::codec::jpeg::dct::DctGrid;
use crate::codec::jpeg::pixels;
use crate::stego::armor::ecc;
use crate::stego::armor::embedding::{self, stdm_embed, stdm_extract_soft};
use crate::stego::armor::fft2d;
use crate::stego::armor::fortress;
use crate::stego::armor::repetition;
use crate::stego::armor::resample;
use crate::stego::armor::selection::compute_stability_map;
use crate::stego::armor::spreading::{generate_spreading_vectors, SPREAD_LEN};
use crate::stego::armor::template;
use crate::stego::crypto;
use crate::stego::error::StegoError;
use crate::stego::frame;
use crate::stego::payload::{self, PayloadData};
use crate::stego::permute;
use crate::stego::progress;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use crate::stego::quality::{self, EncodeQuality, ArmorMetrics};
const HEADER_UNITS: usize = embedding::HEADER_UNITS; const HEADER_COPIES: usize = embedding::HEADER_COPIES;
pub const ARMOR_ENCODE_STEPS: u32 = 6;
const ARMOR_ENCODE_FORTRESS_STEPS: u32 = 3;
pub fn armor_encode(
image_bytes: &[u8],
message: &str,
passphrase: &str,
) -> Result<Vec<u8>, StegoError> {
armor_encode_impl(image_bytes, message, passphrase)
.map(|(bytes, _)| bytes)
}
pub fn armor_encode_with_quality(
image_bytes: &[u8],
message: &str,
passphrase: &str,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
armor_encode_impl(image_bytes, message, passphrase)
}
fn armor_encode_impl(
image_bytes: &[u8],
message: &str,
passphrase: &str,
) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
progress::init(ARMOR_ENCODE_STEPS);
let payload_bytes = payload::encode_payload(message, &[])?;
let mut img = JpegImage::from_bytes(image_bytes)?;
let fi = img.frame_info();
crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let use_compact = passphrase.is_empty();
if let Ok(max_fort) = fortress::fortress_max_frame_bytes_ext(&img, use_compact) {
let fortress_frame = if use_compact {
let ct = crypto::encrypt_with(
&payload_bytes,
passphrase,
&crypto::FORTRESS_EMPTY_SALT,
&crypto::FORTRESS_EMPTY_NONCE,
)?;
frame::build_fortress_compact_frame(payload_bytes.len(), &ct)
} else {
let (ct, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
frame::build_frame(payload_bytes.len(), &salt, &nonce, &ct)
};
if fortress_frame.len() <= max_fort {
progress::set_total(ARMOR_ENCODE_FORTRESS_STEPS);
pre_settle_for_fortress(&mut img)?;
progress::advance();
let fort_result = fortress::fortress_encode(&mut img, &fortress_frame, passphrase)?;
progress::advance();
let stego_bytes = if let Ok(bytes) = img.to_bytes() { bytes } else {
img.rebuild_huffman_tables();
img.to_bytes().map_err(StegoError::InvalidJpeg)?
};
progress::advance();
let fill_ratio = fortress_frame.len() as f64 / max_fort as f64;
let fort_qt_id = img.frame_info().components[0].quant_table_id as usize;
let fort_mean_qt = img.quant_table(fort_qt_id)
.map_or(10.0, |qt| embedding::compute_mean_qt(&qt.values));
let encode_quality = quality::armor_robustness_score(&ArmorMetrics {
repetition_factor: fort_result.repetition_factor,
parity_symbols: fort_result.parity_symbols,
fortress: true,
mean_qt: fort_mean_qt,
fill_ratio,
delta: 12.0,
});
return Ok((stego_bytes, encode_quality));
}
}
let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
embed_dft_template(&mut img, passphrase, message)?;
progress::advance();
pre_clamp_y_channel(&mut img)?;
progress::advance();
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt = img
.quant_table(qt_id)
.ok_or(StegoError::NoLuminanceChannel)?;
let cost_map = compute_stability_map(img.dct_grid(0), qt);
progress::advance();
let structural_key = crypto::derive_armor_structural_key(passphrase)?;
let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
let spread_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
let positions = permute::select_and_permute(&cost_map, &perm_seed);
let num_units = positions.len() / SPREAD_LEN;
if num_units == 0 {
return Err(StegoError::ImageTooSmall);
}
let n_used = num_units * SPREAD_LEN;
let positions = &positions[..n_used];
let mean_qt = embedding::compute_mean_qt(&qt.values);
let header_byte = embedding::encode_mean_qt(mean_qt);
let bootstrap_delta = embedding::BOOTSTRAP_DELTA;
let reference_delta = embedding::compute_delta_from_mean_qt(mean_qt, 1);
let mut all_bits = Vec::with_capacity(num_units);
for _ in 0..HEADER_COPIES {
for bp in (0..8).rev() {
all_bits.push((header_byte >> bp) & 1);
}
}
let payload_units = if num_units > HEADER_UNITS {
num_units - HEADER_UNITS
} else {
return Err(StegoError::ImageTooSmall);
};
let phase2_result: Option<(usize, Vec<u8>)> = {
let mut found = None;
for &parity in &ecc::PARITY_TIERS {
let rs_encoded = ecc::rs_encode_blocks_with_parity(&frame_bytes, parity);
let rs_bits_len = rs_encoded.len() * 8;
if rs_bits_len <= payload_units {
let r = repetition::compute_r(rs_bits_len, payload_units);
if r >= 3 {
found = Some((parity, rs_encoded));
break;
}
}
}
found
};
let (armor_r, armor_parity, armor_delta);
let embed_delta_fn: Box<dyn Fn(usize) -> f64> = if let Some((chosen_parity, rs_encoded)) = phase2_result {
let rs_bits = frame::bytes_to_bits(&rs_encoded);
let r = repetition::compute_r(rs_bits.len(), payload_units);
let rs_bit_count_aligned = payload_units / r;
let mut rs_bits_padded = rs_bits;
rs_bits_padded.resize(rs_bit_count_aligned, 0);
let (rep_bits, _) = repetition::repetition_encode(&rs_bits_padded, payload_units);
let adaptive_delta = embedding::compute_delta_from_mean_qt(mean_qt, r);
armor_r = r;
armor_parity = chosen_parity;
armor_delta = adaptive_delta;
all_bits.extend_from_slice(&rep_bits[..payload_units.min(rep_bits.len())]);
Box::new(move |bit_idx| {
if bit_idx < HEADER_UNITS { bootstrap_delta } else { adaptive_delta }
})
} else {
let rs_encoded = ecc::rs_encode_blocks(&frame_bytes);
let rs_bits = frame::bytes_to_bits(&rs_encoded);
if rs_bits.len() > payload_units {
return Err(StegoError::MessageTooLarge);
}
armor_r = 1;
armor_parity = 64;
armor_delta = reference_delta;
let mut payload_bits = rs_bits;
payload_bits.resize(payload_units, 0);
all_bits.extend_from_slice(&payload_bits);
Box::new(move |bit_idx| {
if bit_idx < HEADER_UNITS { bootstrap_delta } else { reference_delta }
})
};
let embed_count = all_bits.len().min(num_units);
let vectors = generate_spreading_vectors(&spread_seed, embed_count);
progress::advance();
let grid_mut = img.dct_grid_mut(0);
for bit_idx in 0..embed_count {
let group_start = bit_idx * SPREAD_LEN;
let group = &positions[group_start..group_start + SPREAD_LEN];
let mut coeffs = [0.0f64; SPREAD_LEN];
for (k, pos) in group.iter().enumerate() {
coeffs[k] = flat_get(grid_mut, pos.flat_idx as usize) as f64;
}
let delta = embed_delta_fn(bit_idx);
stdm_embed(&mut coeffs, &vectors[bit_idx], all_bits[bit_idx], delta);
for (k, pos) in group.iter().enumerate() {
let new_val = coeffs[k].round() as i16;
flat_set(grid_mut, pos.flat_idx as usize, new_val);
}
}
progress::advance();
let stego_bytes = if let Ok(bytes) = img.to_bytes() { bytes } else {
img.rebuild_huffman_tables();
img.to_bytes().map_err(StegoError::InvalidJpeg)?
};
progress::advance();
let fill_ratio = frame_bytes.len() as f64 / (payload_units / 8).max(1) as f64;
let encode_quality = quality::armor_robustness_score(&ArmorMetrics {
repetition_factor: armor_r,
parity_symbols: armor_parity,
fortress: false,
mean_qt,
fill_ratio,
delta: armor_delta,
});
Ok((stego_bytes, encode_quality))
}
#[derive(Debug, Clone)]
pub struct DecodeQuality {
pub mode: u8,
pub rs_errors_corrected: u32,
pub rs_error_capacity: u32,
pub integrity_percent: u8,
pub repetition_factor: u8,
pub parity_len: u16,
pub geometry_corrected: bool,
pub template_peaks_detected: u8,
pub estimated_rotation_deg: f32,
pub estimated_scale: f32,
pub dft_ring_used: bool,
pub dft_ring_capacity: u16,
pub fortress_used: bool,
pub signal_strength: f64,
}
impl DecodeQuality {
pub fn ghost() -> Self {
Self {
mode: crate::stego::frame::MODE_GHOST,
rs_errors_corrected: 0,
rs_error_capacity: 0,
integrity_percent: 100,
repetition_factor: 0,
parity_len: 0,
geometry_corrected: false,
template_peaks_detected: 0,
estimated_rotation_deg: 0.0,
estimated_scale: 1.0,
dft_ring_used: false,
dft_ring_capacity: 0,
fortress_used: false,
signal_strength: 0.0,
}
}
pub fn from_rs_stats_with_signal(
stats: &ecc::RsDecodeStats,
repetition_factor: u8,
parity_len: u16,
signal_strength: f64,
reference_llr: f64,
) -> Self {
let integrity = compute_integrity(signal_strength, stats, reference_llr);
Self {
mode: crate::stego::frame::MODE_ARMOR,
rs_errors_corrected: stats.total_errors as u32,
rs_error_capacity: stats.error_capacity as u32,
integrity_percent: integrity,
repetition_factor,
parity_len,
geometry_corrected: false,
template_peaks_detected: 0,
estimated_rotation_deg: 0.0,
estimated_scale: 1.0,
dft_ring_used: false,
dft_ring_capacity: 0,
fortress_used: false,
signal_strength,
}
}
}
fn compute_integrity(signal_strength: f64, rs_stats: &ecc::RsDecodeStats, reference_llr: f64) -> u8 {
let llr_score = if reference_llr > 0.0 {
(signal_strength / reference_llr).clamp(0.0, 1.0)
} else {
1.0 };
let rs_score = if rs_stats.error_capacity == 0 {
1.0
} else {
let ratio = rs_stats.total_errors as f64 / rs_stats.error_capacity as f64;
(1.0 - ratio).max(0.0)
};
let combined = 0.7 * llr_score + 0.3 * rs_score;
(combined * 100.0).round().clamp(0.0, 100.0) as u8
}
fn compute_avg_abs_llr(llrs: &[f64]) -> f64 {
if llrs.is_empty() {
return 0.0;
}
let sum: f64 = llrs.iter().map(|llr| llr.abs()).sum();
sum / llrs.len() as f64
}
pub fn armor_decode(stego_bytes: &[u8], passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
let img = JpegImage::from_bytes(stego_bytes)?;
if img.num_components() > 0
&& let Ok(result) = fortress::fortress_decode(&img, passphrase) {
return Ok(result);
}
match try_armor_decode(&img, passphrase) {
Ok(result) => Ok(result),
Err(new_err) => {
progress::advance(); match try_geometric_recovery(stego_bytes, passphrase) {
Ok(result) => Ok(result),
Err(_) => Err(new_err),
}
}
}
}
pub(crate) fn try_armor_decode(img: &JpegImage, passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt = img
.quant_table(qt_id)
.ok_or(StegoError::NoLuminanceChannel)?;
let cost_map = compute_stability_map(img.dct_grid(0), qt);
let structural_key = crypto::derive_armor_structural_key(passphrase)?;
let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
let spread_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
let positions = permute::select_and_permute(&cost_map, &perm_seed);
let num_units = positions.len() / SPREAD_LEN;
if num_units == 0 {
return Err(StegoError::ImageTooSmall);
}
let n_used = num_units * SPREAD_LEN;
let positions = &positions[..n_used];
let vectors = generate_spreading_vectors(&spread_seed, num_units);
let grid = img.dct_grid(0);
if num_units <= HEADER_UNITS {
return Err(StegoError::ImageTooSmall);
}
let header_byte = extract_header_byte(grid, positions, &vectors, embedding::BOOTSTRAP_DELTA, 0);
let header_mean_qt = embedding::decode_mean_qt(header_byte);
let current_mean_qt = embedding::compute_mean_qt(&qt.values);
let payload_units = num_units - HEADER_UNITS;
let mut raw_candidates = Vec::with_capacity(24);
raw_candidates.push(header_mean_qt);
raw_candidates.push(current_mean_qt);
for step in 1..=10 {
let factor = step as f64 * 0.03;
raw_candidates.push(header_mean_qt * (1.0 - factor));
raw_candidates.push(header_mean_qt * (1.0 + factor));
}
let mut candidates: Vec<f64> = Vec::with_capacity(raw_candidates.len());
for &c in &raw_candidates {
if c > 0.1 && !candidates.iter().any(|&existing| (existing - c).abs() < 0.1) {
candidates.push(c);
}
}
let nc = candidates.len() as u32;
if progress::get().1 == 0 {
let total = (2 * (1 + nc + nc) + 1).max(50);
progress::set_total(total);
}
progress::advance();
#[cfg(feature = "parallel")]
{
let result = candidates.par_iter().find_map_first(|&mean_qt| {
if progress::is_cancelled() { return Some(Err(StegoError::Cancelled)); }
let reference_delta = embedding::compute_delta_from_mean_qt(mean_qt, 1);
match decode_phase1_with_offset(
grid, positions, &vectors, reference_delta, num_units, HEADER_UNITS,
passphrase,
) {
Ok(result) => Some(Ok(result)),
Err(StegoError::DecryptionFailed) => Some(Err(StegoError::DecryptionFailed)),
Err(_) => { progress::advance(); None }
}
});
match result {
Some(Ok(payload)) => return Ok(payload),
Some(Err(e)) => return Err(e),
None => {} }
}
#[cfg(not(feature = "parallel"))]
for &mean_qt in &candidates {
progress::check_cancelled()?;
let reference_delta = embedding::compute_delta_from_mean_qt(mean_qt, 1);
if let Ok(result) = decode_phase1_with_offset(
grid, positions, &vectors, reference_delta, num_units, HEADER_UNITS,
passphrase,
) {
return Ok(result);
}
progress::advance();
}
let mut all_p2_candidates: Vec<(usize, usize, f64)> = Vec::new();
for &mean_qt in &candidates {
for &parity in &ecc::PARITY_TIERS {
let candidate_rs = compute_candidate_rs(payload_units, parity);
for r in candidate_rs {
let delta = embedding::compute_delta_from_mean_qt(mean_qt, r);
all_p2_candidates.push((parity, r, delta));
}
}
}
let mut cached_llrs: Vec<(f64, Vec<f64>)> = Vec::new();
for &(_, _, delta) in &all_p2_candidates {
get_or_extract_llrs(
&mut cached_llrs, delta,
grid, positions, &vectors, num_units, HEADER_UNITS,
);
}
progress::check_cancelled()?;
let llr_cache: &[(f64, Vec<f64>)] = &cached_llrs;
let find_llrs = |delta: f64| -> &[f64] {
for (cached_delta, llrs) in llr_cache.iter() {
if (cached_delta - delta).abs() < 0.001 {
return llrs;
}
}
&[]
};
let try_p2_candidate = |&(parity, r, adaptive_delta): &(usize, usize, f64)| -> Option<Result<(PayloadData, DecodeQuality), StegoError>> {
if progress::is_cancelled() { return Some(Err(StegoError::Cancelled)); }
let raw_llrs = find_llrs(adaptive_delta);
let rs_bit_count = payload_units / r;
if rs_bit_count == 0 { return None; }
let used_llrs = rs_bit_count * r;
if used_llrs > raw_llrs.len() { return None; }
let (voted_bits, rep_quality) = repetition::repetition_decode_soft_with_quality(
&raw_llrs[..used_llrs], rs_bit_count,
);
let voted_bytes = frame::bits_to_bytes(&voted_bits);
let (decoded_frame, rs_stats) = try_rs_decode_frame_with_parity(&voted_bytes, parity)?;
let parsed = frame::parse_frame(&decoded_frame).ok()?;
match crypto::decrypt(&parsed.ciphertext, passphrase, &parsed.salt, &parsed.nonce) {
Ok(plaintext) => {
let len = parsed.plaintext_len as usize;
if len > plaintext.len() { return None; }
let payload_data = payload::decode_payload(&plaintext[..len]).ok()?;
let reference_llr = adaptive_delta / 2.0;
let quality = DecodeQuality::from_rs_stats_with_signal(
&rs_stats, r as u8, parity as u16,
rep_quality.avg_abs_llr_per_copy, reference_llr,
);
Some(Ok((payload_data, quality)))
}
Err(StegoError::DecryptionFailed) => Some(Err(StegoError::DecryptionFailed)),
Err(_) => None,
}
};
#[cfg(feature = "parallel")]
let p2_result = all_p2_candidates.par_iter().find_map_first(try_p2_candidate);
#[cfg(not(feature = "parallel"))]
let p2_result = all_p2_candidates.iter().find_map(try_p2_candidate);
for _ in 0..candidates.len() { progress::advance(); }
match p2_result {
Some(Ok(payload)) => return Ok(payload),
Some(Err(e)) => return Err(e),
None => {}
}
Err(StegoError::FrameCorrupted)
}
pub(crate) fn armor_decode_no_fortress(img: &JpegImage, stego_bytes: &[u8], passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
match try_armor_decode(img, passphrase) {
Ok(result) => Ok(result),
Err(_stdm_err) => {
progress::check_cancelled()?;
match try_geometric_recovery(stego_bytes, passphrase) {
Ok(result) => Ok(result),
Err(_) => Err(_stdm_err),
}
}
}
}
pub(crate) fn try_geometric_recovery(stego_bytes: &[u8], passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
use crate::stego::armor::dft_payload;
let img = JpegImage::from_bytes(stego_bytes)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let (luma_pixels, w, h) = pixels::jpeg_to_luma_f64(&img)
.ok_or(StegoError::NoLuminanceChannel)?;
let spectrum = fft2d::fft2d(&luma_pixels, w, h);
let peaks = template::generate_template_peaks(passphrase, w, h)?;
let detected = template::detect_template(&spectrum, &peaks);
let transform = template::estimate_transform(&detected)
.ok_or(StegoError::FrameCorrupted)?;
if transform.rotation_rad.abs() < 0.001 && (transform.scale - 1.0).abs() < 0.001 {
if let Some(ring_bytes) = dft_payload::extract_ring_payload(&spectrum, passphrase)
&& let Ok(text) = std::str::from_utf8(&ring_bytes) {
let ring_cap = dft_payload::ring_capacity(w, h);
return Ok((PayloadData { text: text.to_string(), files: vec![] }, DecodeQuality {
mode: crate::stego::frame::MODE_ARMOR,
rs_errors_corrected: 0,
rs_error_capacity: 0,
integrity_percent: 50, repetition_factor: 0,
parity_len: 0,
geometry_corrected: false,
template_peaks_detected: detected.len() as u8,
estimated_rotation_deg: 0.0,
estimated_scale: 1.0,
dft_ring_used: true,
dft_ring_capacity: ring_cap as u16,
fortress_used: false,
signal_strength: 0.0,
}));
}
return Err(StegoError::FrameCorrupted);
}
drop(spectrum);
let corrected_pixels = resample::resample_bilinear(
&luma_pixels, w, h, &transform, w, h,
);
drop(luma_pixels);
let mut corrected_img = img;
pixels::luma_f64_to_jpeg(&corrected_pixels, w, h, &mut corrected_img)
.ok_or(StegoError::NoLuminanceChannel)?;
drop(corrected_pixels);
match try_armor_decode(&corrected_img, passphrase) {
Ok((text, mut quality)) => {
quality.geometry_corrected = true;
quality.template_peaks_detected = detected.len() as u8;
quality.estimated_rotation_deg = transform.rotation_rad.to_degrees() as f32;
quality.estimated_scale = transform.scale as f32;
return Ok((text, quality));
}
Err(_) => {
{
let (cp, cw, ch) = pixels::jpeg_to_luma_f64(&corrected_img)
.ok_or(StegoError::NoLuminanceChannel)?;
let corrected_spectrum = fft2d::fft2d(&cp, cw, ch);
if let Some(ring_bytes) = dft_payload::extract_ring_payload(&corrected_spectrum, passphrase)
&& let Ok(text) = std::str::from_utf8(&ring_bytes) {
let ring_cap = dft_payload::ring_capacity(cw, ch);
return Ok((PayloadData { text: text.to_string(), files: vec![] }, DecodeQuality {
mode: crate::stego::frame::MODE_ARMOR,
rs_errors_corrected: 0,
rs_error_capacity: 0,
integrity_percent: 50,
repetition_factor: 0,
parity_len: 0,
geometry_corrected: true,
template_peaks_detected: detected.len() as u8,
estimated_rotation_deg: transform.rotation_rad.to_degrees() as f32,
estimated_scale: transform.scale as f32,
dft_ring_used: true,
dft_ring_capacity: ring_cap as u16,
fortress_used: false,
signal_strength: 0.0,
}));
}
}
}
}
Err(StegoError::FrameCorrupted)
}
fn extract_header_byte(
grid: &DctGrid,
positions: &[crate::stego::permute::CoeffPos],
vectors: &[[f64; SPREAD_LEN]],
delta: f64,
offset: usize,
) -> u8 {
let mut header_llrs = [0.0f64; 56]; for i in 0..56 {
let unit_idx = offset + i;
let group_start = unit_idx * SPREAD_LEN;
let group = &positions[group_start..group_start + SPREAD_LEN];
let mut coeffs = [0.0f64; SPREAD_LEN];
for (k, pos) in group.iter().enumerate() {
coeffs[k] = flat_get(grid, pos.flat_idx as usize) as f64;
}
header_llrs[i] = stdm_extract_soft(&coeffs, &vectors[unit_idx], delta);
}
let mut byte = 0u8;
for bit_pos in 0..8 {
let mut total = 0.0;
for copy in 0..7 {
total += header_llrs[copy * 8 + bit_pos];
}
if total < 0.0 {
byte |= 1 << (7 - bit_pos);
}
}
byte
}
fn decode_phase1_with_offset(
grid: &DctGrid,
positions: &[crate::stego::permute::CoeffPos],
vectors: &[[f64; SPREAD_LEN]],
delta: f64,
num_units: usize,
payload_offset: usize,
passphrase: &str,
) -> Result<(PayloadData, DecodeQuality), StegoError> {
let payload_units = num_units - payload_offset;
let mut all_llrs = Vec::with_capacity(payload_units);
for unit_idx in payload_offset..num_units {
let group_start = unit_idx * SPREAD_LEN;
let group = &positions[group_start..group_start + SPREAD_LEN];
let mut coeffs = [0.0f64; SPREAD_LEN];
for (k, pos) in group.iter().enumerate() {
coeffs[k] = flat_get(grid, pos.flat_idx as usize) as f64;
}
all_llrs.push(stdm_extract_soft(&coeffs, &vectors[unit_idx], delta));
}
let signal_strength = compute_avg_abs_llr(&all_llrs);
let reference_llr = delta / 2.0;
let extracted_bits: Vec<u8> = all_llrs.iter()
.map(|&llr| if llr >= 0.0 { 0 } else { 1 })
.collect();
let extracted_bytes = frame::bits_to_bytes(&extracted_bits);
let (decoded_frame, rs_stats) = try_rs_decode_frame(&extracted_bytes)?;
let parsed = frame::parse_frame(&decoded_frame)?;
let plaintext = crypto::decrypt(
&parsed.ciphertext,
passphrase,
&parsed.salt,
&parsed.nonce,
)?;
let len = parsed.plaintext_len as usize;
if len > plaintext.len() {
return Err(StegoError::FrameCorrupted);
}
let payload_data = payload::decode_payload(&plaintext[..len])?;
let quality = DecodeQuality::from_rs_stats_with_signal(
&rs_stats, 1, ecc::parity_len() as u16, signal_strength, reference_llr,
);
Ok((payload_data, quality))
}
pub(super) fn compute_candidate_rs(payload_units: usize, parity: usize) -> Vec<usize> {
let mut rs_set = std::collections::BTreeSet::new();
let min_frame = frame::FRAME_OVERHEAD;
let max_frame = frame::MAX_FRAME_BYTES;
for frame_len in min_frame..=max_frame {
let rs_encoded_len = ecc::rs_encoded_len_with_parity(frame_len, parity);
let rs_bits = rs_encoded_len * 8;
if rs_bits > payload_units {
break;
}
let r = repetition::compute_r(rs_bits, payload_units);
if r >= 3 {
rs_set.insert(r);
}
}
rs_set.into_iter().collect()
}
pub(super) fn compute_candidate_rs_compact(payload_units: usize, parity: usize) -> Vec<usize> {
let mut rs_set = std::collections::BTreeSet::new();
let min_frame = frame::FORTRESS_COMPACT_FRAME_OVERHEAD;
let max_frame = frame::MAX_FRAME_BYTES;
for frame_len in min_frame..=max_frame {
let rs_encoded_len = ecc::rs_encoded_len_with_parity(frame_len, parity);
let rs_bits = rs_encoded_len * 8;
if rs_bits > payload_units {
break;
}
let r = repetition::compute_r(rs_bits, payload_units);
if r >= 3 {
rs_set.insert(r);
}
}
rs_set.into_iter().collect()
}
const LLR_CACHE_MAX: usize = 5;
fn get_or_extract_llrs(
cache: &mut Vec<(f64, Vec<f64>)>,
delta: f64,
grid: &DctGrid,
positions: &[crate::stego::permute::CoeffPos],
vectors: &[[f64; SPREAD_LEN]],
num_units: usize,
payload_offset: usize,
) {
for i in 0..cache.len() {
if (cache[i].0 - delta).abs() < 0.001 {
if i < cache.len() - 1 {
let entry = cache.remove(i);
cache.push(entry);
}
return;
}
}
let unit_indices: Vec<usize> = (payload_offset..num_units).collect();
let extract_one = |&unit_idx: &usize| -> f64 {
let group_start = unit_idx * SPREAD_LEN;
let group = &positions[group_start..group_start + SPREAD_LEN];
let mut coeffs = [0.0f64; SPREAD_LEN];
for (k, pos) in group.iter().enumerate() {
coeffs[k] = flat_get(grid, pos.flat_idx as usize) as f64;
}
stdm_extract_soft(&coeffs, &vectors[unit_idx], delta)
};
#[cfg(feature = "parallel")]
let llrs: Vec<f64> = unit_indices.par_iter().map(extract_one).collect();
#[cfg(not(feature = "parallel"))]
let llrs: Vec<f64> = unit_indices.iter().map(extract_one).collect();
if cache.len() >= LLR_CACHE_MAX {
cache.remove(0); }
cache.push((delta, llrs));
}
fn pre_clamp_y_channel(img: &mut JpegImage) -> Result<(), StegoError> {
let qt_id = img.frame_info().components[0].quant_table_id as usize;
let qt_values = img.quant_table(qt_id)
.ok_or(StegoError::NoLuminanceChannel)?.values;
let grid = img.dct_grid_mut(0);
let process_block = |chunk: &mut [i16]| {
let quantized: [i16; 64] = chunk.try_into().unwrap();
let mut px = pixels::idct_block(&quantized, &qt_values);
for p in px.iter_mut() {
*p = p.clamp(0.0, 255.0);
}
let settled = pixels::dct_block(&px, &qt_values);
chunk.copy_from_slice(&settled);
};
#[cfg(feature = "parallel")]
grid.coeffs_mut().par_chunks_mut(64).for_each(process_block);
#[cfg(not(feature = "parallel"))]
grid.coeffs_mut().chunks_mut(64).for_each(process_block);
Ok(())
}
pub(super) fn try_rs_decode_frame_with_parity(
extracted_bytes: &[u8],
parity: usize,
) -> Option<(Vec<u8>, ecc::RsDecodeStats)> {
let k_max = 255 - parity;
let min_data = 2usize.min(k_max);
for data_len in min_data..=k_max.min(extracted_bytes.len().saturating_sub(parity)) {
let block_len = data_len + parity;
if block_len > extracted_bytes.len() {
break;
}
if let Ok((first_block_data, first_errors)) =
ecc::rs_decode_with_parity(&extracted_bytes[..block_len], data_len, parity)
&& first_block_data.len() >= 2 {
let pt_len =
u16::from_be_bytes([first_block_data[0], first_block_data[1]]) as usize;
if pt_len == 0 {
continue;
}
let ct_len = pt_len + 16;
let total_frame_len = 2 + 16 + 12 + ct_len + 4;
if total_frame_len > frame::MAX_FRAME_BYTES {
continue;
}
let rs_encoded_len =
ecc::rs_encoded_len_with_parity(total_frame_len, parity);
if rs_encoded_len > extracted_bytes.len() {
continue;
}
if total_frame_len == data_len {
let t_max = parity / 2;
let stats = ecc::RsDecodeStats {
total_errors: first_errors,
error_capacity: t_max,
max_block_errors: first_errors,
num_blocks: 1,
};
return Some((first_block_data, stats));
}
if total_frame_len < data_len {
let t_max = parity / 2;
let stats = ecc::RsDecodeStats {
total_errors: first_errors,
error_capacity: t_max,
max_block_errors: first_errors,
num_blocks: 1,
};
return Some((first_block_data[..total_frame_len].to_vec(), stats));
}
if total_frame_len > data_len
&& let Ok((decoded, stats)) = ecc::rs_decode_blocks_with_parity(
&extracted_bytes[..rs_encoded_len],
total_frame_len,
parity,
) {
return Some((decoded, stats));
}
}
}
None
}
pub(super) fn try_rs_decode_compact_frame_with_parity(
extracted_bytes: &[u8],
parity: usize,
) -> Option<(Vec<u8>, ecc::RsDecodeStats)> {
let k_max = 255 - parity;
let min_data = 2usize.min(k_max);
for data_len in min_data..=k_max.min(extracted_bytes.len().saturating_sub(parity)) {
let block_len = data_len + parity;
if block_len > extracted_bytes.len() {
break;
}
if let Ok((first_block_data, first_errors)) =
ecc::rs_decode_with_parity(&extracted_bytes[..block_len], data_len, parity)
&& first_block_data.len() >= 2 {
let pt_len =
u16::from_be_bytes([first_block_data[0], first_block_data[1]]) as usize;
if pt_len == 0 {
continue;
}
let ct_len = pt_len + 16;
let total_frame_len = 2 + ct_len + 4;
if total_frame_len > frame::MAX_FRAME_BYTES {
continue;
}
let rs_encoded_len =
ecc::rs_encoded_len_with_parity(total_frame_len, parity);
if rs_encoded_len > extracted_bytes.len() {
continue;
}
if total_frame_len == data_len {
let t_max = parity / 2;
let stats = ecc::RsDecodeStats {
total_errors: first_errors,
error_capacity: t_max,
max_block_errors: first_errors,
num_blocks: 1,
};
return Some((first_block_data, stats));
}
if total_frame_len < data_len {
let t_max = parity / 2;
let stats = ecc::RsDecodeStats {
total_errors: first_errors,
error_capacity: t_max,
max_block_errors: first_errors,
num_blocks: 1,
};
return Some((first_block_data[..total_frame_len].to_vec(), stats));
}
if total_frame_len > data_len
&& let Ok((decoded, stats)) = ecc::rs_decode_blocks_with_parity(
&extracted_bytes[..rs_encoded_len],
total_frame_len,
parity,
) {
return Some((decoded, stats));
}
}
}
None
}
fn try_rs_decode_frame(extracted_bytes: &[u8]) -> Result<(Vec<u8>, ecc::RsDecodeStats), StegoError> {
let parity = ecc::parity_len();
let min_data = crate::stego::frame::FRAME_OVERHEAD;
let max_first_block_data = 191usize;
for data_len in min_data..=max_first_block_data.min(extracted_bytes.len().saturating_sub(parity))
{
let block_len = data_len + parity;
if block_len > extracted_bytes.len() {
break;
}
if let Ok((first_block_data, first_errors)) = ecc::rs_decode(&extracted_bytes[..block_len], data_len)
&& first_block_data.len() >= 2 {
let pt_len =
u16::from_be_bytes([first_block_data[0], first_block_data[1]]) as usize;
if pt_len == 0 {
continue;
}
let ct_len = pt_len + 16;
let total_frame_len = 2 + 16 + 12 + ct_len + 4;
if total_frame_len > frame::MAX_FRAME_BYTES {
continue;
}
let rs_encoded_len = ecc::rs_encoded_len(total_frame_len);
if rs_encoded_len > extracted_bytes.len() {
continue;
}
if total_frame_len <= data_len {
let stats = ecc::RsDecodeStats {
total_errors: first_errors,
error_capacity: ecc::T_MAX,
max_block_errors: first_errors,
num_blocks: 1,
};
let frame_data = if total_frame_len == data_len {
first_block_data
} else {
first_block_data[..total_frame_len].to_vec()
};
return Ok((frame_data, stats));
}
if let Ok((decoded, stats)) = ecc::rs_decode_blocks(
&extracted_bytes[..rs_encoded_len],
total_frame_len,
) {
return Ok((decoded, stats));
}
}
}
Err(StegoError::FrameCorrupted)
}
fn flat_get(grid: &DctGrid, flat_idx: usize) -> i16 {
let bw = grid.blocks_wide();
let block_idx = flat_idx / 64;
let pos = flat_idx % 64;
let br = block_idx / bw;
let bc = block_idx % bw;
let i = pos / 8;
let j = pos % 8;
grid.get(br, bc, i, j)
}
fn flat_set(grid: &mut DctGrid, flat_idx: usize, val: i16) {
let bw = grid.blocks_wide();
let block_idx = flat_idx / 64;
let pos = flat_idx % 64;
let br = block_idx / bw;
let bc = block_idx % bw;
let i = pos / 8;
let j = pos % 8;
grid.set(br, bc, i, j, val);
}
#[derive(Debug, Clone)]
pub struct ArmorCapacityInfo {
pub fortress_capacity: usize,
pub stdm_capacity: usize,
}
pub fn armor_capacity_info(jpeg_bytes: &[u8]) -> Result<ArmorCapacityInfo, StegoError> {
let img = JpegImage::from_bytes(jpeg_bytes)?;
if img.num_components() == 0 {
return Err(StegoError::NoLuminanceChannel);
}
let fortress_cap = fortress::fortress_capacity(&img).unwrap_or(0);
let stdm_cap = super::capacity::estimate_armor_capacity(&img).unwrap_or(0);
Ok(ArmorCapacityInfo {
fortress_capacity: fortress_cap,
stdm_capacity: stdm_cap,
})
}
fn embed_dft_template(img: &mut JpegImage, passphrase: &str, message: &str) -> Result<(), StegoError> {
let (luma_pixels, w, h) = pixels::jpeg_to_luma_f64(img)
.ok_or(StegoError::NoLuminanceChannel)?;
let mut spectrum = fft2d::fft2d(&luma_pixels, w, h);
drop(luma_pixels);
let peaks = template::generate_template_peaks(passphrase, w, h)?;
template::embed_template(&mut spectrum, &peaks);
use crate::stego::armor::dft_payload;
let ring_cap = dft_payload::ring_capacity(w, h);
if ring_cap > 0 && !message.is_empty() {
let max_byte = message.len().min(ring_cap);
let truncated_len = message[..max_byte]
.char_indices()
.last()
.map_or(0, |(i, c)| i + c.len_utf8());
let truncated = &message.as_bytes()[..truncated_len];
dft_payload::embed_ring_payload(&mut spectrum, truncated, passphrase)?;
}
let modified = fft2d::ifft2d(&spectrum);
drop(spectrum);
pixels::luma_f64_to_jpeg(&modified, w, h, img)
.ok_or(StegoError::NoLuminanceChannel)?;
Ok(())
}
const JPEG_BASE_LUMINANCE_QT: [u16; 64] = [
16, 11, 10, 16, 24, 40, 51, 61,
12, 12, 14, 19, 26, 58, 60, 55,
14, 13, 16, 24, 40, 57, 69, 56,
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68, 109, 103, 77,
24, 35, 55, 64, 81, 104, 113, 92,
49, 64, 78, 87, 103, 121, 120, 101,
72, 92, 95, 98, 112, 100, 103, 99,
];
const JPEG_BASE_CHROMINANCE_QT: [u16; 64] = [
17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
];
fn compute_jpeg_qt(base: &[u16; 64], qf: u32) -> [u16; 64] {
let scale = if qf >= 50 { 200 - 2 * qf } else { 5000 / qf };
let mut qt = [0u16; 64];
for i in 0..64 {
let val = (base[i] as u32 * scale + 50) / 100;
qt[i] = val.clamp(1, 255) as u16;
}
qt
}
fn pre_settle_for_fortress(img: &mut JpegImage) -> Result<(), StegoError> {
use crate::codec::jpeg::dct::QuantTable;
let num_components = img.num_components();
let target_qf = 75u32;
let mut new_qts: Vec<(usize, [u16; 64], [u16; 64])> = Vec::new();
for comp_idx in 0..num_components {
let qt_id = img.frame_info().components[comp_idx].quant_table_id as usize;
let old_qt = img.quant_table(qt_id)
.ok_or(StegoError::NoLuminanceChannel)?.values;
let base = if comp_idx == 0 {
&JPEG_BASE_LUMINANCE_QT
} else {
&JPEG_BASE_CHROMINANCE_QT
};
let new_qt = compute_jpeg_qt(base, target_qf);
let grid = img.dct_grid_mut(comp_idx);
let process_block = |chunk: &mut [i16]| {
let quantized: [i16; 64] = chunk.try_into().unwrap();
let mut px = pixels::idct_block(&quantized, &old_qt);
for p in px.iter_mut() {
*p = p.clamp(0.0, 255.0);
}
let settled = pixels::dct_block(&px, &new_qt);
chunk.copy_from_slice(&settled);
};
#[cfg(feature = "parallel")]
grid.coeffs_mut().par_chunks_mut(64).for_each(process_block);
#[cfg(not(feature = "parallel"))]
grid.coeffs_mut().chunks_mut(64).for_each(process_block);
new_qts.push((qt_id, old_qt, new_qt));
}
let mut replaced = [false; 4];
for (qt_id, _old, new_qt) in &new_qts {
if !replaced[*qt_id] {
img.set_quant_table(*qt_id, QuantTable::new(*new_qt));
replaced[*qt_id] = true;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_integrity_pristine() {
let stats = ecc::RsDecodeStats {
total_errors: 0,
error_capacity: 32,
max_block_errors: 0,
num_blocks: 1,
};
let integrity = compute_integrity(15.0, &stats, 15.0);
assert_eq!(integrity, 100);
}
#[test]
fn compute_integrity_half_signal() {
let stats = ecc::RsDecodeStats {
total_errors: 0,
error_capacity: 32,
max_block_errors: 0,
num_blocks: 1,
};
let integrity = compute_integrity(7.5, &stats, 15.0);
assert_eq!(integrity, 65);
}
#[test]
fn compute_integrity_zero_signal() {
let stats = ecc::RsDecodeStats {
total_errors: 0,
error_capacity: 32,
max_block_errors: 0,
num_blocks: 1,
};
let integrity = compute_integrity(0.0, &stats, 15.0);
assert_eq!(integrity, 30);
}
#[test]
fn compute_integrity_with_rs_errors() {
let stats = ecc::RsDecodeStats {
total_errors: 16,
error_capacity: 32,
max_block_errors: 16,
num_blocks: 1,
};
let integrity = compute_integrity(15.0, &stats, 15.0);
assert_eq!(integrity, 85);
}
#[test]
fn compute_integrity_both_degraded() {
let stats = ecc::RsDecodeStats {
total_errors: 16,
error_capacity: 32,
max_block_errors: 16,
num_blocks: 1,
};
let integrity = compute_integrity(7.5, &stats, 15.0);
assert_eq!(integrity, 50);
}
#[test]
fn compute_integrity_signal_exceeds_reference() {
let stats = ecc::RsDecodeStats {
total_errors: 0,
error_capacity: 32,
max_block_errors: 0,
num_blocks: 1,
};
let integrity = compute_integrity(20.0, &stats, 15.0);
assert_eq!(integrity, 100);
}
#[test]
fn compute_integrity_zero_reference() {
let stats = ecc::RsDecodeStats {
total_errors: 0,
error_capacity: 0,
max_block_errors: 0,
num_blocks: 0,
};
let integrity = compute_integrity(5.0, &stats, 0.0);
assert_eq!(integrity, 100);
}
#[test]
fn compute_avg_abs_llr_basic() {
let llrs = vec![5.0, -3.0, 4.0, -2.0];
let avg = compute_avg_abs_llr(&llrs);
assert!((avg - 3.5).abs() < 1e-10);
}
#[test]
fn compute_avg_abs_llr_empty() {
assert_eq!(compute_avg_abs_llr(&[]), 0.0);
}
#[test]
fn decode_quality_ghost_unchanged() {
let q = DecodeQuality::ghost();
assert_eq!(q.integrity_percent, 100, "Ghost always 100%");
assert_eq!(q.signal_strength, 0.0);
}
#[test]
fn decode_quality_from_rs_stats_with_signal_pristine() {
let stats = ecc::RsDecodeStats {
total_errors: 0,
error_capacity: 32,
max_block_errors: 0,
num_blocks: 1,
};
let q = DecodeQuality::from_rs_stats_with_signal(&stats, 5, 64, 15.0, 15.0);
assert_eq!(q.integrity_percent, 100);
assert!((q.signal_strength - 15.0).abs() < 1e-10);
assert_eq!(q.repetition_factor, 5);
assert_eq!(q.parity_len, 64);
}
}