use crate::decoders::CcittParams;
use crate::error::{Error, Result};
pub fn decompress_ccitt(data: &[u8], params: &CcittParams) -> Result<Vec<u8>> {
if params.columns == 0 {
return Err(Error::Decode("CCITT decompression requires /Columns parameter".to_string()));
}
let width = params.columns;
let height_opt = params.rows;
log::debug!(
"CCITT decompression: {} bytes, {}x{} pixels, K={}, BlackIs1={}",
data.len(),
params.columns,
params.rows.unwrap_or(0),
params.k,
params.black_is_1
);
if params.is_group_3() {
log::debug!("CCITT Group 3 decompression requested (K={})", params.k);
} else {
log::debug!("CCITT Group 4 decompression requested");
}
let in_house = crate::decoders::ccitt::decode(data, params);
let fax_result = match in_house {
Ok(decoded) => {
if decoded.recovered_partial {
log::warn!(
"CCITT: recovered {} rows then padded white (truncated/damaged stream, {}x{}, {} bytes)",
decoded.rows_decoded,
params.columns,
params.rows.unwrap_or(0),
data.len()
);
}
Ok(decoded.data)
},
Err(in_house_err) => {
log::debug!("CCITT in-house decode declined ({in_house_err}); trying fax crate");
decompress_with_fax(data, width, height_opt, params)
},
};
match fax_result {
Ok(mut output) => {
if params.black_is_1 {
invert_bilevel_pixels(&mut output);
}
Ok(output)
},
Err(e) => {
log::warn!(
"CCITT decompression failed ({}x{}, {} bytes, K={}, EncodedByteAlign={}): {} — substituting blank image (DECODE FAILED, not a blank scan)",
params.columns,
params.rows.unwrap_or(0),
data.len(),
params.k,
params.encoded_byte_align,
e
);
let bytes_per_row = (width as usize).div_ceil(8);
let rows = usize::try_from(params.rows.unwrap_or(1)).map_err(|_| {
Error::Decode("CCITT row count exceeds platform limits".to_string())
})?;
let expected_bytes = rows
.checked_mul(bytes_per_row)
.ok_or_else(|| Error::Decode("CCITT fallback size overflow".to_string()))?;
let fallback_len = expected_bytes.max(bytes_per_row);
let mut fallback = Vec::new();
fallback.try_reserve_exact(fallback_len).map_err(|_| {
Error::Decode(format!("Unable to allocate {fallback_len} bytes for CCITT fallback"))
})?;
fallback.resize(fallback_len, 0);
Ok(fallback)
},
}
}
fn decompress_with_fax(
data: &[u8],
width: u32,
height: Option<u32>,
params: &CcittParams,
) -> Result<Vec<u8>> {
let width_usize = width as usize;
log::debug!(
"Attempting CCITT decompression with fax crate: width={}, height={:?}, data_len={}, K={}",
width,
height,
data.len(),
params.k
);
match try_decode_with_fax(data, width_usize, height, params) {
Ok(output) if !output.is_empty() => {
return Ok(output);
},
Ok(_empty) => {
log::debug!("First attempt returned no data, trying with leading zeros stripped");
},
Err(e) => {
log::debug!("First attempt failed: {}, trying with leading zeros stripped", e);
},
}
let first_nonzero = data
.iter()
.position(|byte| *byte != 0)
.unwrap_or(data.len());
let trimmed_len = data.len() - first_nonzero;
let mut trimmed_data = Vec::new();
trimmed_data.try_reserve_exact(trimmed_len).map_err(|_| {
Error::Decode(format!("Unable to allocate {trimmed_len} bytes for trimmed CCITT input"))
})?;
trimmed_data.extend_from_slice(&data[first_nonzero..]);
if trimmed_data.len() < data.len() && !trimmed_data.is_empty() {
log::debug!(
"Stripped {} leading zero bytes ({} -> {}), attempting decompression",
data.len() - trimmed_data.len(),
data.len(),
trimmed_data.len()
);
log::debug!(
"Data after stripping zeros, first 32 bytes: {}",
trimmed_data
.iter()
.take(32)
.map(|b| format!("{:02x}", b))
.collect::<Vec<_>>()
.join(" ")
);
match try_decode_with_fax(&trimmed_data, width_usize, height, params) {
Ok(output) if !output.is_empty() => {
log::trace!("Successfully decompressed after stripping leading zeros!");
return Ok(output);
},
Ok(_) => {
log::debug!("Strip attempt also returned no data");
},
Err(e) => {
log::debug!("Strip attempt also failed: {}", e);
},
}
}
Err(Error::Decode(
"CCITT decompression failed: fax decoder returned no output".to_string(),
))
}
fn try_decode_with_fax(
data: &[u8],
width: usize,
height: Option<u32>,
params: &CcittParams,
) -> Result<Vec<u8>> {
use fax::decoder;
let bytes_per_row = width.div_ceil(8);
let max_rows = height.map(|rows| rows as usize);
let mut output = Vec::new();
if let Some(rows) = max_rows {
let capacity = bytes_per_row
.checked_mul(rows)
.ok_or_else(|| Error::Decode("CCITT fax output size overflow".to_string()))?;
output.try_reserve_exact(capacity).map_err(|_| {
Error::Decode(format!("Unable to allocate {capacity} bytes for CCITT fax output"))
})?;
}
let mut rows_decoded = 0usize;
let bytes_iter = data.iter().copied();
let success = if params.is_group_4() {
log::debug!("Using Group 4 (T.6) decoder");
let mut callback_error = None;
let success =
decoder::decode_g4(bytes_iter, width as u32, height, |transitions: &[u32]| {
if callback_error.is_none() && max_rows.is_none_or(|rows| rows_decoded < rows) {
if let Err(error) = append_transition_row(&mut output, transitions, width) {
callback_error = Some(error);
return;
}
rows_decoded += 1;
}
});
if let Some(error) = callback_error {
return Err(error);
}
success
} else {
log::debug!("Using Group 3 (T.4) decoder");
let mut callback_error = None;
let success = decoder::decode_g3(bytes_iter, |transitions: &[u32]| {
if callback_error.is_none() && max_rows.is_none_or(|rows| rows_decoded < rows) {
if let Err(error) = append_transition_row(&mut output, transitions, width) {
callback_error = Some(error);
return;
}
rows_decoded += 1;
}
});
if let Some(error) = callback_error {
return Err(error);
}
success
};
if success.is_some() && !output.is_empty() {
log::debug!(
"CCITT decompression successful: {} bytes input -> {} bytes output ({} rows)",
data.len(),
output.len(),
rows_decoded
);
Ok(output)
} else if success.is_some() {
log::debug!("CCITT decoder returned success but no rows produced");
Ok(Vec::new())
} else {
log::warn!("CCITT fax decoder returned None");
Err(Error::Decode("CCITT fax decoder failed".to_string()))
}
}
pub(crate) fn transitions_to_bytes<T: Copy + Into<u32>>(
transitions: &[T],
width: usize,
) -> Result<Vec<u8>> {
let bytes_per_row = width.div_ceil(8);
let mut row_bytes = Vec::new();
row_bytes.try_reserve_exact(bytes_per_row).map_err(|_| {
Error::Decode(format!("Unable to allocate {bytes_per_row} bytes for a CCITT row"))
})?;
row_bytes.resize(bytes_per_row, 0);
let mut is_black = false; let mut start_pos: usize = 0;
for &transition_pos in transitions {
let transition_pos = Into::<u32>::into(transition_pos) as usize;
if is_black {
for pixel_idx in start_pos..transition_pos.min(width) {
let byte_idx = pixel_idx / 8;
let bit_idx = 7 - (pixel_idx % 8);
row_bytes[byte_idx] |= 1 << bit_idx;
}
}
is_black = !is_black;
start_pos = transition_pos;
}
if is_black && start_pos < width {
for pixel_idx in start_pos..width {
let byte_idx = pixel_idx / 8;
let bit_idx = 7 - (pixel_idx % 8);
row_bytes[byte_idx] |= 1 << bit_idx;
}
}
Ok(row_bytes)
}
pub(crate) fn append_transition_row<T: Copy + Into<u32>>(
output: &mut Vec<u8>,
transitions: &[T],
width: usize,
) -> Result<()> {
let row = transitions_to_bytes(transitions, width)?;
output.try_reserve(row.len()).map_err(|_| {
Error::Decode(format!("Unable to grow CCITT output by {} bytes", row.len()))
})?;
output.extend_from_slice(&row);
Ok(())
}
#[deprecated(
since = "0.1.5",
note = "Use decompress_ccitt with CcittParams instead"
)]
pub fn decompress_ccitt_group4(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
let params = CcittParams {
columns: width,
rows: Some(height),
..Default::default()
};
decompress_ccitt(data, ¶ms)
}
fn invert_bilevel_pixels(data: &mut [u8]) {
for byte in data.iter_mut() {
*byte = !*byte;
}
}
pub fn bilevel_to_grayscale(bilevel_data: &[u8], width: u32, height: u32) -> Vec<u8> {
let width = width as usize;
let height = height as usize;
let mut grayscale = Vec::with_capacity(width * height);
for row_idx in 0..height {
let row_start = row_idx * width.div_ceil(8);
for col_idx in 0..width {
let byte_idx = row_start + (col_idx / 8);
if byte_idx < bilevel_data.len() {
let bit_pos = 7 - (col_idx % 8);
let bit = (bilevel_data[byte_idx] >> bit_pos) & 1;
grayscale.push(if bit == 0 { 0xFF } else { 0x00 });
} else {
grayscale.push(0xFF);
}
}
}
grayscale
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bilevel_to_grayscale() {
let bilevel = vec![0b10000001];
let grayscale = bilevel_to_grayscale(&bilevel, 8, 1);
assert_eq!(grayscale.len(), 8);
assert_eq!(grayscale[0], 0x00, "Pixel 0 should be black");
assert_eq!(grayscale[1], 0xFF, "Pixel 1 should be white");
assert_eq!(grayscale[7], 0x00, "Pixel 7 should be black");
}
#[test]
fn test_bilevel_to_grayscale_padding() {
let bilevel = vec![0b10000001];
let grayscale = bilevel_to_grayscale(&bilevel, 5, 1);
assert_eq!(grayscale.len(), 5);
assert_eq!(grayscale[0], 0x00); assert_eq!(grayscale[1], 0xFF); assert_eq!(grayscale[4], 0xFF); }
#[test]
fn test_transitions_to_bytes() {
let row_u16 = transitions_to_bytes(&[2u16, 5, 7], 8).expect("pack u16 row");
assert_eq!(row_u16.len(), 1);
assert_eq!(row_u16[0], 0b00111001);
let row_u32 = transitions_to_bytes(&[2u32, 5, 7], 8).expect("pack u32 row");
assert_eq!(row_u32, row_u16, "u32 and u16 transitions must pack identically");
}
#[test]
fn test_transitions_to_bytes_beyond_u16() {
let width = 70_000usize;
let row = transitions_to_bytes(&[65_536u32, 65_544], width).expect("pack wide row");
assert_eq!(row.len(), width.div_ceil(8));
assert_eq!(row[65_536 / 8], 0xFF, "the 8 pixels at 65536.. must be black");
assert!(row[..65_536 / 8].iter().all(|&b| b == 0), "everything before must stay white");
}
}