geonative_image/
worldfile.rs1use geonative_core::raster::GeoTransform;
24
25use crate::error::{ImageError, Result};
26
27pub fn parse(text: &str) -> Result<GeoTransform> {
29 let mut nums = [0.0f64; 6];
30 let mut count = 0usize;
31 for (i, line) in text.lines().enumerate() {
32 let trimmed = line.trim();
33 if trimmed.is_empty() {
34 continue;
35 }
36 if count >= 6 {
37 return Err(ImageError::world_file(format!(
38 "world file has more than 6 numeric lines (extra on line {})",
39 i + 1
40 )));
41 }
42 nums[count] = trimmed
43 .parse::<f64>()
44 .map_err(|e| ImageError::world_file(format!("line {} ('{}'): {e}", i + 1, trimmed)))?;
45 count += 1;
46 }
47 if count != 6 {
48 return Err(ImageError::world_file(format!(
49 "world file needs 6 numeric lines, got {count}"
50 )));
51 }
52
53 let pixel_w = nums[0];
54 let rot_y = nums[1];
55 let rot_x = nums[2];
56 let pixel_h = nums[3];
57 let centre_x = nums[4];
58 let centre_y = nums[5];
59
60 let origin_x = centre_x - pixel_w * 0.5;
62 let origin_y = centre_y - pixel_h * 0.5;
63
64 Ok(GeoTransform {
65 origin: [origin_x, origin_y],
66 pixel_size: [pixel_w, pixel_h],
67 rotation: [rot_x, rot_y],
68 })
69}
70
71pub fn extensions_for(image_ext: &str) -> &'static [&'static str] {
74 match image_ext.to_ascii_lowercase().as_str() {
75 "jpg" | "jpeg" => &["jgw", "jpgw", "wld"],
76 "png" => &["pgw", "pngw", "wld"],
77 "tif" | "tiff" => &["tfw", "tifw", "wld"],
78 "bmp" => &["bpw", "wld"],
79 "gif" => &["gfw", "wld"],
80 _ => &["wld"],
81 }
82}
83
84pub fn find_sidecar(image_path: &std::path::Path) -> Result<std::path::PathBuf> {
86 let stem = image_path.file_stem().ok_or_else(|| {
87 ImageError::malformed(format!("image path has no stem: {}", image_path.display()))
88 })?;
89 let parent = image_path.parent().unwrap_or(std::path::Path::new("."));
90 let image_ext = image_path
91 .extension()
92 .and_then(|s| s.to_str())
93 .unwrap_or("");
94 for ext in extensions_for(image_ext) {
95 for candidate in [
96 parent.join(format!("{}.{ext}", stem.to_string_lossy())),
97 parent.join(format!("{}.{}", stem.to_string_lossy(), ext.to_uppercase())),
98 ] {
99 if candidate.exists() {
100 return Ok(candidate);
101 }
102 }
103 }
104 Err(ImageError::MissingWorldFile {
105 image: image_path.display().to_string(),
106 expected: format!(
107 "{}/{}.{{{}}}",
108 parent.display(),
109 stem.to_string_lossy(),
110 extensions_for(image_ext).join(",")
111 ),
112 })
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn parses_canonical_north_up() {
121 let text = "0.5\n0\n0\n-0.5\n144.96\n-37.81\n";
122 let gt = parse(text).unwrap();
123 assert_eq!(gt.pixel_size, [0.5, -0.5]);
124 assert_eq!(gt.rotation, [0.0, 0.0]);
125 assert!((gt.origin[0] - (144.96 - 0.25)).abs() < 1e-9);
128 assert!((gt.origin[1] - (-37.81 - (-0.25))).abs() < 1e-9);
129 assert!(gt.is_north_up());
130 }
131
132 #[test]
133 fn tolerates_blank_lines() {
134 let text = "0.5\n\n0\n0\n-0.5\n144\n-37\n\n";
135 let gt = parse(text).unwrap();
136 assert_eq!(gt.pixel_size[0], 0.5);
137 }
138
139 #[test]
140 fn rejects_too_few_numbers() {
141 let text = "0.5\n0\n0\n";
142 assert!(parse(text).is_err());
143 }
144
145 #[test]
146 fn rejects_garbage() {
147 let text = "not_a_number\n0\n0\n0\n0\n0\n";
148 assert!(parse(text).is_err());
149 }
150
151 #[test]
152 fn extensions_for_jpg() {
153 let exts = extensions_for("jpg");
154 assert!(exts.contains(&"jgw"));
155 }
156
157 #[test]
158 fn extensions_for_png() {
159 let exts = extensions_for("png");
160 assert!(exts.contains(&"pgw"));
161 }
162
163 #[test]
164 fn find_sidecar_works() {
165 let dir = std::env::temp_dir().join(format!("wf_test_{}", std::process::id()));
166 let _ = std::fs::remove_dir_all(&dir);
167 std::fs::create_dir_all(&dir).unwrap();
168
169 let img = dir.join("ortho.jpg");
170 std::fs::write(&img, b"").unwrap();
171 let wld = dir.join("ortho.jgw");
172 std::fs::write(&wld, "0.5\n0\n0\n-0.5\n144\n-37\n").unwrap();
173
174 let found = find_sidecar(&img).unwrap();
175 assert_eq!(found, wld);
176 }
177
178 #[test]
179 fn find_sidecar_missing_errors() {
180 let dir = std::env::temp_dir().join(format!("wf_test_miss_{}", std::process::id()));
181 let _ = std::fs::remove_dir_all(&dir);
182 std::fs::create_dir_all(&dir).unwrap();
183 let img = dir.join("noworld.jpg");
184 std::fs::write(&img, b"").unwrap();
185 let err = find_sidecar(&img).unwrap_err();
186 assert!(matches!(err, ImageError::MissingWorldFile { .. }));
187 }
188}