use image::DynamicImage;
use log::{debug, info, warn};
use crate::tiff::errors::{TiffError, TiffResult};
use crate::tiff::ifd::IFD;
use crate::tiff::TiffReader;
use crate::tiff::constants::{tags, field_types, photometric};
use crate::tiff::IFDEntry;
use crate::tiff::TiffBuilder;
use crate::extractor::Region;
use crate::tiff::geo_key_parser::GeoKeyParser;
pub struct ImageValueStats {
pub min_value: u64,
pub max_value: u64,
}
pub fn calculate_grayscale_stats(image: &DynamicImage) -> ImageValueStats {
let gray_image = image.to_luma8();
let mut min_value: u8 = 255;
let mut max_value: u8 = 0;
for pixel in gray_image.pixels() {
let value = pixel.0[0];
min_value = min_value.min(value);
max_value = max_value.max(value);
}
info!("Calculated pixel value range: {} to {}", min_value, max_value);
ImageValueStats {
min_value: min_value as u64,
max_value: max_value as u64,
}
}
pub fn calculate_rgb_stats(image: &DynamicImage) -> ImageValueStats {
let rgb_image = image.to_rgb8();
let mut min_values = [255u8, 255u8, 255u8];
let mut max_values = [0u8, 0u8, 0u8];
for pixel in rgb_image.pixels() {
for i in 0..3 {
min_values[i] = min_values[i].min(pixel.0[i]);
max_values[i] = max_values[i].max(pixel.0[i]);
}
}
info!("Calculated pixel value ranges: R({} to {}), G({} to {}), B({} to {})",
min_values[0], max_values[0], min_values[1], max_values[1], min_values[2], max_values[2]);
let overall_min = *min_values.iter().min().unwrap() as u64;
let overall_max = *max_values.iter().max().unwrap() as u64;
ImageValueStats {
min_value: overall_min,
max_value: overall_max,
}
}
pub fn process_grayscale_image(
image: &DynamicImage,
builder: &mut TiffBuilder,
ifd_index: usize,
bits_per_sample: u16
) -> TiffResult<()> {
info!("Processing grayscale image data");
let gray_image = image.to_luma8();
let stats = calculate_grayscale_stats(image);
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::MIN_SAMPLE_VALUE, field_types::SHORT, 1, stats.min_value));
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::MAX_SAMPLE_VALUE, field_types::SHORT, 1, stats.max_value));
let gray_data = gray_image.into_raw();
builder.add_basic_gray_tags(ifd_index, image.width(), image.height(), bits_per_sample);
builder.setup_single_strip(ifd_index, gray_data);
Ok(())
}
pub fn process_rgb_image(
image: &DynamicImage,
builder: &mut TiffBuilder,
ifd_index: usize
) -> TiffResult<()> {
info!("Processing RGB image data");
let rgb_image = image.to_rgb8();
let stats = calculate_rgb_stats(image);
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::MIN_SAMPLE_VALUE, field_types::SHORT, 1, stats.min_value));
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::MAX_SAMPLE_VALUE, field_types::SHORT, 1, stats.max_value));
let rgb_data = rgb_image.into_raw();
builder.add_basic_rgb_tags(ifd_index, image.width(), image.height());
builder.setup_single_strip(ifd_index, rgb_data);
Ok(())
}
pub fn extract_nodata_value(ifd: &IFD, reader: &TiffReader) -> String {
let nodata_entry = match ifd.get_entry(tags::GDAL_NODATA) {
Some(entry) => entry,
None => {
info!("No NoData tag found in original file, using 255");
return "255".to_string();
}
};
if nodata_entry.field_type != field_types::ASCII {
warn!("NoData tag has unexpected field type {}, using default 255", nodata_entry.field_type);
return "255".to_string();
}
let nodata_str = match reader.read_ascii_string_at_offset(nodata_entry.value_offset, nodata_entry.count) {
Ok(str) => str,
Err(e) => {
warn!("Failed to read NoData value: {:?}, using default 255", e);
return "255".to_string();
}
};
let trimmed = nodata_str.trim_end_matches('\0');
info!("Found NoData value in original file: '{}'", trimmed);
if trimmed == ":w" || trimmed.is_empty() {
"255".to_string()
} else {
trimmed.to_string()
}
}
pub fn extract_gdal_metadata(ifd: &IFD, reader: &TiffReader) -> Option<String> {
let meta_entry = match ifd.get_entry(tags::GDAL_METADATA) {
Some(entry) => entry,
None => return None,
};
if meta_entry.field_type != field_types::ASCII {
return None;
}
reader.read_ascii_string_at_offset(meta_entry.value_offset, meta_entry.count).ok()
}
pub fn determine_extraction_region(region: Option<Region>, ifd: &IFD) -> TiffResult<Region> {
let dimensions = ifd.get_dimensions()
.ok_or_else(|| TiffError::GenericError(
"Missing image dimensions".to_string()))?;
let (img_width, img_height) = dimensions;
info!("Image dimensions: {}x{}", img_width, img_height);
let region = match region {
Some(region) => region,
None => return Ok(Region::new(0, 0, img_width as u32, img_height as u32)),
};
if region.end_x() > img_width as u32 || region.end_y() > img_height as u32 {
return Err(TiffError::GenericError(
format!("Region ({},{} - {}x{}) exceeds image dimensions ({}x{})",
region.x, region.y, region.width, region.height,
img_width, img_height)
));
}
Ok(region)
}
pub fn get_tiff_image_properties(ifd: &IFD) -> (u16, u16, u16) {
let samples_per_pixel = ifd.get_samples_per_pixel() as u16; info!("Image has {} samples per pixel", samples_per_pixel);
let bits_per_sample = ifd.get_entry(tags::BITS_PER_SAMPLE)
.map(|e| e.value_offset as u16)
.unwrap_or(8);
info!("Image has {} bits per sample", bits_per_sample);
let photometric = ifd.get_entry(tags::PHOTOMETRIC_INTERPRETATION)
.map(|e| e.value_offset as u16)
.unwrap_or(photometric::BLACK_IS_ZERO);
info!("Image has photometric interpretation: {}", photometric);
(bits_per_sample, photometric, samples_per_pixel)
}
pub fn setup_tiff_tags(
builder: &mut TiffBuilder,
ifd_index: usize,
original_ifd: &IFD,
image: &DynamicImage
) -> TiffResult<()> {
let exclude_tags = [
tags::IMAGE_WIDTH, tags::IMAGE_LENGTH,
tags::BITS_PER_SAMPLE, tags::COMPRESSION,
tags::STRIP_OFFSETS, tags::ROWS_PER_STRIP,
tags::STRIP_BYTE_COUNTS,
tags::MIN_SAMPLE_VALUE, tags::MAX_SAMPLE_VALUE,
tags::TILE_WIDTH, tags::TILE_LENGTH, tags::TILE_OFFSETS, tags::TILE_BYTE_COUNTS,
tags::MODEL_PIXEL_SCALE_TAG, tags::MODEL_TIEPOINT_TAG,
tags::GEO_KEY_DIRECTORY_TAG, tags::GEO_DOUBLE_PARAMS_TAG, tags::GEO_ASCII_PARAMS_TAG
];
builder.copy_tags_from(ifd_index, original_ifd, &exclude_tags);
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::IMAGE_WIDTH, field_types::LONG, 1, image.width() as u64));
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::IMAGE_LENGTH, field_types::LONG, 1, image.height() as u64));
Ok(())
}
pub fn read_geotiff_info(
ifd: &IFD,
reader: &TiffReader,
file_path: &str
) -> (Vec<f64>, Vec<f64>) {
let byte_order_handler = match reader.get_byte_order_handler() {
Some(handler) => handler,
None => {
warn!("Byte order handler not available, using default geotransform");
return (vec![1.0, 1.0, 0.0], vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
}
};
let pixel_scale = GeoKeyParser::read_model_pixel_scale_values(
ifd,
byte_order_handler,
file_path
).unwrap_or_else(|_| {
warn!("Failed to read pixel scale, using default values");
vec![1.0, 1.0, 0.0]
});
let tiepoint = GeoKeyParser::read_model_tiepoint_values(
ifd,
byte_order_handler,
file_path
).unwrap_or_else(|_| {
warn!("Failed to read tiepoint, using default values");
vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
});
info!("Pixel scale: {:?}", pixel_scale);
info!("Tiepoint: {:?}", tiepoint);
(pixel_scale, tiepoint)
}
pub fn set_photometric_interpretation(
builder: &mut TiffBuilder,
ifd_index: usize,
photometric_value: u16
) {
let existing_idx = builder.ifds[ifd_index].entries.iter().position(|e|
e.tag == tags::PHOTOMETRIC_INTERPRETATION);
match existing_idx {
Some(idx) => {
debug!("Updating existing PhotometricInterpretation to {}", photometric_value);
builder.ifds[ifd_index].entries[idx] = IFDEntry::new(
tags::PHOTOMETRIC_INTERPRETATION, field_types::SHORT, 1, photometric_value as u64);
},
None => {
debug!("Adding PhotometricInterpretation tag with value {}", photometric_value);
builder.ifds[ifd_index].add_entry(IFDEntry::new(
tags::PHOTOMETRIC_INTERPRETATION, field_types::SHORT, 1, photometric_value as u64));
}
}
}