use geonative_core::raster::GeoTransform;
use crate::error::{ImageError, Result};
pub fn parse(text: &str) -> Result<GeoTransform> {
let mut nums = [0.0f64; 6];
let mut count = 0usize;
for (i, line) in text.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if count >= 6 {
return Err(ImageError::world_file(format!(
"world file has more than 6 numeric lines (extra on line {})",
i + 1
)));
}
nums[count] = trimmed
.parse::<f64>()
.map_err(|e| ImageError::world_file(format!("line {} ('{}'): {e}", i + 1, trimmed)))?;
count += 1;
}
if count != 6 {
return Err(ImageError::world_file(format!(
"world file needs 6 numeric lines, got {count}"
)));
}
let pixel_w = nums[0];
let rot_y = nums[1];
let rot_x = nums[2];
let pixel_h = nums[3];
let centre_x = nums[4];
let centre_y = nums[5];
let origin_x = centre_x - pixel_w * 0.5;
let origin_y = centre_y - pixel_h * 0.5;
Ok(GeoTransform {
origin: [origin_x, origin_y],
pixel_size: [pixel_w, pixel_h],
rotation: [rot_x, rot_y],
})
}
pub fn extensions_for(image_ext: &str) -> &'static [&'static str] {
match image_ext.to_ascii_lowercase().as_str() {
"jpg" | "jpeg" => &["jgw", "jpgw", "wld"],
"png" => &["pgw", "pngw", "wld"],
"tif" | "tiff" => &["tfw", "tifw", "wld"],
"bmp" => &["bpw", "wld"],
"gif" => &["gfw", "wld"],
_ => &["wld"],
}
}
pub fn find_sidecar(image_path: &std::path::Path) -> Result<std::path::PathBuf> {
let stem = image_path.file_stem().ok_or_else(|| {
ImageError::malformed(format!("image path has no stem: {}", image_path.display()))
})?;
let parent = image_path.parent().unwrap_or(std::path::Path::new("."));
let image_ext = image_path
.extension()
.and_then(|s| s.to_str())
.unwrap_or("");
for ext in extensions_for(image_ext) {
for candidate in [
parent.join(format!("{}.{ext}", stem.to_string_lossy())),
parent.join(format!("{}.{}", stem.to_string_lossy(), ext.to_uppercase())),
] {
if candidate.exists() {
return Ok(candidate);
}
}
}
Err(ImageError::MissingWorldFile {
image: image_path.display().to_string(),
expected: format!(
"{}/{}.{{{}}}",
parent.display(),
stem.to_string_lossy(),
extensions_for(image_ext).join(",")
),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_canonical_north_up() {
let text = "0.5\n0\n0\n-0.5\n144.96\n-37.81\n";
let gt = parse(text).unwrap();
assert_eq!(gt.pixel_size, [0.5, -0.5]);
assert_eq!(gt.rotation, [0.0, 0.0]);
assert!((gt.origin[0] - (144.96 - 0.25)).abs() < 1e-9);
assert!((gt.origin[1] - (-37.81 - (-0.25))).abs() < 1e-9);
assert!(gt.is_north_up());
}
#[test]
fn tolerates_blank_lines() {
let text = "0.5\n\n0\n0\n-0.5\n144\n-37\n\n";
let gt = parse(text).unwrap();
assert_eq!(gt.pixel_size[0], 0.5);
}
#[test]
fn rejects_too_few_numbers() {
let text = "0.5\n0\n0\n";
assert!(parse(text).is_err());
}
#[test]
fn rejects_garbage() {
let text = "not_a_number\n0\n0\n0\n0\n0\n";
assert!(parse(text).is_err());
}
#[test]
fn extensions_for_jpg() {
let exts = extensions_for("jpg");
assert!(exts.contains(&"jgw"));
}
#[test]
fn extensions_for_png() {
let exts = extensions_for("png");
assert!(exts.contains(&"pgw"));
}
#[test]
fn find_sidecar_works() {
let dir = std::env::temp_dir().join(format!("wf_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let img = dir.join("ortho.jpg");
std::fs::write(&img, b"").unwrap();
let wld = dir.join("ortho.jgw");
std::fs::write(&wld, "0.5\n0\n0\n-0.5\n144\n-37\n").unwrap();
let found = find_sidecar(&img).unwrap();
assert_eq!(found, wld);
}
#[test]
fn find_sidecar_missing_errors() {
let dir = std::env::temp_dir().join(format!("wf_test_miss_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let img = dir.join("noworld.jpg");
std::fs::write(&img, b"").unwrap();
let err = find_sidecar(&img).unwrap_err();
assert!(matches!(err, ImageError::MissingWorldFile { .. }));
}
}