use log::{info, debug, warn};
use std::cmp::min;
use std::path::Path;
use image::{DynamicImage, ImageBuffer, Rgb};
use crate::tiff::errors::{TiffResult, TiffError};
use crate::utils::logger::Logger;
use crate::extractor::Region;
use crate::coordinate::BoundingBox;
use crate::tiff::TiffReader;
use crate::tiff::is_geotiff_tag;
use crate::tiff::geo_key_parser::GeoKeyParser;
use crate::tiff::types::TIFF;
use crate::tiff::ifd::IFD;
use crate::io::byte_order::ByteOrderHandler;
use crate::utils::coordinate_transformer;
pub fn parse_bbox(bbox_str: &str) -> TiffResult<BoundingBox> {
BoundingBox::from_string(bbox_str)
.map_err(|e| TiffError::GenericError(e))
}
pub fn calculate_geotransform(
ifd: &IFD,
byte_order_handler: &Box<dyn ByteOrderHandler>,
file_path: &str
) -> TiffResult<[f64; 6]> {
let pixel_scale = GeoKeyParser::read_model_pixel_scale_values(ifd, byte_order_handler, file_path)?;
let tiepoint = GeoKeyParser::read_model_tiepoint_values(ifd, byte_order_handler, file_path)?;
if pixel_scale.len() < 2 || tiepoint.len() < 6 {
return Err(TiffError::GenericError("Incomplete GeoTIFF information".to_string()));
}
let pixel_width = pixel_scale[0];
let pixel_height = -pixel_scale[1]; let origin_x = tiepoint[3] - tiepoint[0] * pixel_width;
let origin_y = tiepoint[4] + tiepoint[1] * -pixel_height;
let geotransform = [
origin_x,
pixel_width,
0.0,
origin_y,
0.0,
pixel_height
];
debug!("Calculated geotransform: [{:.1}, {:.1}, {:.1}, {:.1}, {:.1}, {:.1}]",
geotransform[0], geotransform[1], geotransform[2],
geotransform[3], geotransform[4], geotransform[5]);
Ok(geotransform)
}
pub fn generic_crs_to_pixel_region(
bbox: &BoundingBox,
geotransform: &[f64],
img_width: u32,
img_height: u32,
source_epsg: u32,
target_epsg: u32,
radius_meters: Option<f64>
) -> Region {
info!("Converting coordinates from EPSG:{} to EPSG:{}", source_epsg, target_epsg);
if source_epsg == 4326 && target_epsg == 3857 {
return convert_wgs84_to_web_mercator(bbox, geotransform, img_width, img_height);
}
if source_epsg == target_epsg {
return convert_same_crs_to_pixels(bbox, geotransform, img_width, img_height);
}
let transformed_bbox = try_transform_bbox(bbox, source_epsg, target_epsg);
let region = convert_same_crs_to_pixels(&transformed_bbox, geotransform, img_width, img_height);
let adjusted_region = adjust_region_to_image_bounds(
region,
img_width,
img_height,
radius_meters,
geotransform
);
info!("Generic CRS conversion result: ({}, {}) with size {}x{}",
adjusted_region.x, adjusted_region.y, adjusted_region.width, adjusted_region.height);
adjusted_region
}
fn try_transform_bbox(bbox: &BoundingBox, source_epsg: u32, target_epsg: u32) -> BoundingBox {
let mut transformed = bbox.clone();
if source_epsg == 4326 {
let center_lat = (bbox.min_y + bbox.max_y) / 2.0;
let meters_per_degree_lat = 111_320.0; let meters_per_degree_lon = 111_320.0 * f64::cos(center_lat * std::f64::consts::PI / 180.0);
transformed.min_x = bbox.min_x * meters_per_degree_lon;
transformed.max_x = bbox.max_x * meters_per_degree_lon;
transformed.min_y = bbox.min_y * meters_per_degree_lat;
transformed.max_y = bbox.max_y * meters_per_degree_lat;
}
transformed
}
fn convert_same_crs_to_pixels(
bbox: &BoundingBox,
geotransform: &[f64],
img_width: u32,
img_height: u32
) -> Region {
debug!("Converting coordinates to pixels using direct geotransform");
let origin_x = geotransform[0];
let pixel_width = geotransform[1];
let origin_y = geotransform[3];
let pixel_height = geotransform[5];
let min_x_pixel = ((bbox.min_x - origin_x) / pixel_width).floor() as i64;
let max_y_pixel = ((bbox.min_y - origin_y) / pixel_height).floor() as i64;
let max_x_pixel = ((bbox.max_x - origin_x) / pixel_width).ceil() as i64;
let min_y_pixel = ((bbox.max_y - origin_y) / pixel_height).floor() as i64;
debug!("Pixel region: ({}, {}) to ({}, {})",
min_x_pixel, min_y_pixel, max_x_pixel, max_y_pixel);
let x = min_x_pixel.max(0).min(img_width as i64 - 1) as u32;
let y = min_y_pixel.max(0).min(img_height as i64 - 1) as u32;
let width = ((max_x_pixel - min_x_pixel).max(1) as u32).min(img_width - x);
let height = ((max_y_pixel - min_y_pixel).max(1) as u32).min(img_height - y);
Region::new(x, y, width, height)
}
fn convert_wgs84_to_web_mercator(
bbox: &BoundingBox,
geotransform: &[f64],
img_width: u32,
img_height: u32
) -> Region {
info!("Converting WGS84 coordinates to Web Mercator for extraction");
use std::f64::consts::PI;
let lon_min = bbox.min_x;
let lat_min = bbox.min_y;
let lon_max = bbox.max_x;
let lat_max = bbox.max_y;
let lat_min_clamped = lat_min.max(-85.06).min(85.06);
let lat_max_clamped = lat_max.max(-85.06).min(85.06);
debug!("WGS84 bbox: lon_min={}, lat_min={}, lon_max={}, lat_max={}",
lon_min, lat_min, lon_max, lat_max);
let x_min = lon_min * 20037508.34 / 180.0;
let x_max = lon_max * 20037508.34 / 180.0;
let y_min = f64::ln(f64::tan((lat_min_clamped + 90.0) * PI / 360.0)) * 20037508.34 / PI;
let y_max = f64::ln(f64::tan((lat_max_clamped + 90.0) * PI / 360.0)) * 20037508.34 / PI;
debug!("Web Mercator bbox: x_min={}, y_min={}, x_max={}, y_max={}",
x_min, y_min, x_max, y_max);
let origin_x = geotransform[0];
let pixel_width = geotransform[1];
let origin_y = geotransform[3];
let pixel_height = geotransform[5];
let min_x_pixel = ((x_min - origin_x) / pixel_width).floor() as i64;
let max_y_pixel = ((y_min - origin_y) / pixel_height).floor() as i64;
let max_x_pixel = ((x_max - origin_x) / pixel_width).ceil() as i64;
let min_y_pixel = ((y_max - origin_y) / pixel_height).floor() as i64;
debug!("Raw pixel coordinates: ({}, {}) to ({}, {})",
min_x_pixel, min_y_pixel, max_x_pixel, max_y_pixel);
let x_in_bounds = min_x_pixel < img_width as i64 && max_x_pixel >= 0;
let y_in_bounds = min_y_pixel < img_height as i64 && max_y_pixel >= 0;
if !x_in_bounds || !y_in_bounds {
let size = if let Some(radius) = bbox.radius_meters {
((radius * 2.0) / pixel_width.abs() as f64) as u32
} else {
1000 };
let center_x = img_width / 2;
let center_y = img_height / 2;
debug!("Region outside image bounds, using centered region of size {}", size);
return Region::new(
center_x.saturating_sub(size / 2),
center_y.saturating_sub(size / 2),
size.min(img_width),
size.min(img_height)
);
}
let x = min_x_pixel.max(0).min(img_width as i64 - 1) as u32;
let y = min_y_pixel.max(0).min(img_height as i64 - 1) as u32;
let width = ((max_x_pixel - min_x_pixel).max(1) as u32).min(img_width - x);
let height = ((max_y_pixel - min_y_pixel).max(1) as u32).min(img_height - y);
debug!("Adjusted pixel region: x={}, y={}, width={}, height={}",
x, y, width, height);
Region::new(x, y, width, height)
}
fn adjust_region_to_image_bounds(
region: Region,
img_width: u32,
img_height: u32,
radius_meters: Option<f64>,
geotransform: &[f64]
) -> Region {
if region.x >= img_width || region.y >= img_height || region.width == 0 || region.height == 0 {
warn!("Region is completely outside image bounds or has zero size");
debug!("Region: x={}, y={}, w={}, h={}, Image: {}x{}",
region.x, region.y, region.width, region.height, img_width, img_height);
let center_x = img_width / 2;
let center_y = img_height / 2;
let size = if let Some(radius) = radius_meters {
let pixel_width = geotransform[1].abs();
let size_in_pixels = ((radius * 2.0) / pixel_width as f64).ceil() as u32;
debug!("Calculated fallback size of {} pixels from radius {} meters (pixel width {} meters)",
size_in_pixels, radius, pixel_width);
size_in_pixels.min(5000).max(100) } else {
100
};
let half_size = size / 2;
return Region::new(
center_x.saturating_sub(half_size),
center_y.saturating_sub(half_size),
size.min(img_width - center_x.saturating_sub(half_size)),
size.min(img_height - center_y.saturating_sub(half_size))
);
}
let mut x = region.x;
let mut y = region.y;
let mut width = region.width;
let mut height = region.height;
if x >= img_width {
x = img_width - 1;
}
if y >= img_height {
y = img_height - 1;
}
if x + width > img_width {
width = img_width - x;
}
if y + height > img_height {
height = img_height - y;
}
if width == 0 { width = 1; }
if height == 0 { height = 1; }
Region::new(x, y, width, height)
}
pub fn determine_extraction_region(
bbox: BoundingBox,
tiff: &TIFF,
reader: &TiffReader,
input_file: &str,
logger: &Logger
) -> TiffResult<Region> {
info!("Determining extraction region");
let direct_region = Region::new(
bbox.min_x as u32,
bbox.min_y as u32,
(bbox.max_x - bbox.min_x) as u32,
(bbox.max_y - bbox.min_y) as u32
);
let radius_meters = bbox.radius_meters;
let source_epsg = if let Some(epsg_code) = bbox.epsg {
info!("Using source EPSG:{} coordinates", epsg_code);
epsg_code
} else {
info!("No source EPSG code specified, assuming direct pixel coordinates");
return Ok(direct_region);
};
let has_geotiff_tags = tiff.ifds.iter().any(|ifd|
ifd.entries.iter().any(|entry| is_geotiff_tag(entry.tag)));
if !has_geotiff_tags || tiff.ifds.is_empty() {
info!("No GeoTIFF tags found, using bounding box as pixel coordinates");
return Ok(direct_region);
}
let ifd = &tiff.ifds[0];
let byte_order_handler = match reader.get_byte_order_handler() {
Some(handler) => handler,
None => {
info!("No byte order handler available, using direct coordinate conversion");
return Ok(direct_region);
}
};
let file_path = reader.get_file_path().unwrap_or(input_file);
let (img_width, img_height) = match ifd.get_dimensions() {
Some((w, h)) => (w as u32, h as u32),
None => {
warn!("Could not determine image dimensions");
return Ok(direct_region);
}
};
debug!("Image dimensions from IFD #0: {}x{}", img_width, img_height);
match calculate_geotransform(ifd, byte_order_handler, file_path) {
Ok(geotransform) => {
info!("Converting geographic coordinates to pixel coordinates");
let geo_info = match GeoKeyParser::extract_geo_info(ifd, byte_order_handler, file_path) {
Ok(info) => {
info!("Found projection information: EPSG:{}", info.epsg_code);
info
},
Err(e) => {
warn!("Failed to extract GeoTIFF info: {}, using fallback", e);
return Ok(direct_region);
}
};
let target_epsg = geo_info.epsg_code;
info!("Image CRS is EPSG:{}", target_epsg);
let region = generic_crs_to_pixel_region(
&bbox,
&geotransform,
img_width,
img_height,
source_epsg,
target_epsg,
radius_meters
);
info!("Final extraction region: x={}, y={}, width={}, height={}",
region.x, region.y, region.width, region.height);
Ok(region)
},
Err(e) => {
info!("GeoTIFF conversion failed: {}, using direct coordinate conversion", e);
Ok(direct_region)
}
}
}
pub fn apply_horizontal_predictor(data: &mut [u8], width: usize, height: usize) {
for row in 0..height {
let start = row * width;
let end = min(start + width, data.len());
for i in (start + 1)..end {
data[i] = data[i].wrapping_add(data[i - 1]);
}
}
}
pub fn copy_pixel(
data: &[u8],
image: &mut ImageBuffer<Rgb<u8>, Vec<u8>>,
global_x: u32,
global_y: u32,
data_idx: usize,
region: Region
) -> bool {
if global_x < region.x || global_x >= region.end_x() ||
global_y < region.y || global_y >= region.end_y() {
return false;
}
if data_idx >= data.len() {
return false;
}
let buf_x = global_x - region.x;
let buf_y = global_y - region.y;
let value = data[data_idx];
image.put_pixel(buf_x, buf_y, Rgb([value, value, value]));
true
}
pub fn is_in_region(x: u32, y: u32, region: &Region) -> bool {
x >= region.x && x < region.end_x() && y >= region.y && y < region.end_y()
}
pub fn calc_buffer_coords(global_x: u32, global_y: u32, region: &Region) -> (u32, u32) {
(global_x - region.x, global_y - region.y)
}
pub fn apply_shape_mask(image: &DynamicImage, shape: &str) -> DynamicImage {
if shape.to_lowercase() != "circle" {
return image.clone();
}
let width = image.width();
let height = image.height();
let mut rgba = image::RgbaImage::new(width, height);
let center_x = width as f32 / 2.0;
let center_y = height as f32 / 2.0;
let radius = (width.min(height) / 2) as f32;
let rgb = image.to_rgb8();
for y in 0..height {
for x in 0..width {
let dx = x as f32 - center_x;
let dy = y as f32 - center_y;
let distance_squared = dx*dx + dy*dy;
if distance_squared <= radius*radius {
let pixel = rgb.get_pixel(x, y);
rgba.put_pixel(x, y, image::Rgba([pixel[0], pixel[1], pixel[2], 255]));
} else {
rgba.put_pixel(x, y, image::Rgba([0, 0, 0, 0]));
}
}
}
DynamicImage::ImageRgba8(rgba)
}
pub fn ensure_png_extension(path: &str) -> String {
let path = Path::new(path);
if let Some(ext) = path.extension() {
if ext.to_string_lossy().to_lowercase() == "png" {
return path.to_string_lossy().to_string();
}
}
let stem = path.file_stem().unwrap_or_default();
let parent = path.parent().unwrap_or_else(|| Path::new(""));
parent.join(format!("{}.png", stem.to_string_lossy())).to_string_lossy().to_string()
}