1use std::path::Path;
6
7use geonative_core::raster::{
8 Band, BandDescriptor, PixelType, RasterLayer, RasterProfile, RasterTile,
9};
10use geonative_core::{Crs, Result as CoreResult};
11
12use crate::error::{ImageError, Result};
13use crate::worldfile;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ImageKind {
17 Jpeg,
18 Png,
19}
20
21impl ImageKind {
22 pub fn from_extension(ext: &str) -> Result<Self> {
23 match ext.to_ascii_lowercase().as_str() {
24 "jpg" | "jpeg" => Ok(Self::Jpeg),
25 "png" => Ok(Self::Png),
26 other => Err(ImageError::unsupported(format!(
27 "extension .{other} (v0.1 supports .jpg / .jpeg / .png)"
28 ))),
29 }
30 }
31}
32
33#[derive(Debug)]
38pub struct ImageRaster {
39 pixels: Vec<u8>,
41 profile: RasterProfile,
43}
44
45impl ImageRaster {
46 pub fn open(image_path: impl AsRef<Path>) -> Result<Self> {
53 Self::open_with_crs(image_path, Crs::Unknown)
54 }
55
56 pub fn open_with_crs(image_path: impl AsRef<Path>, crs: Crs) -> Result<Self> {
59 let path = image_path.as_ref();
60 let ext = path.extension().and_then(|s| s.to_str()).ok_or_else(|| {
61 ImageError::malformed(format!("image path has no extension: {}", path.display()))
62 })?;
63 let kind = ImageKind::from_extension(ext)?;
64
65 let image_bytes = std::fs::read(path)?;
67 let decoded = match kind {
68 ImageKind::Jpeg => decode_jpeg(&image_bytes)?,
69 ImageKind::Png => decode_png(&image_bytes)?,
70 };
71
72 let wf_path = worldfile::find_sidecar(path)?;
74 let wf_text = std::fs::read_to_string(&wf_path)?;
75 let geo_transform = worldfile::parse(&wf_text)?;
76
77 let bands: Vec<BandDescriptor> = decoded
79 .band_names()
80 .into_iter()
81 .map(|name| BandDescriptor::new(Some(name.into()), PixelType::U8))
82 .collect();
83 let profile = RasterProfile {
84 width: decoded.width,
85 height: decoded.height,
86 bands,
87 geo_transform,
88 crs,
89 tile_size: [decoded.width, decoded.height], pyramid_levels: 1,
91 };
92
93 Ok(Self {
94 pixels: decoded.pixels,
95 profile,
96 })
97 }
98
99 pub fn pixels(&self) -> &[u8] {
103 &self.pixels
104 }
105}
106
107impl RasterLayer for ImageRaster {
108 fn profile(&self) -> &RasterProfile {
109 &self.profile
110 }
111
112 fn read_tile(&self, level: u8, x: u32, y: u32) -> CoreResult<RasterTile> {
113 if level != 0 {
114 return Err(ImageError::Unsupported(format!(
115 "single-image raster has only level 0; got {level}"
116 ))
117 .into());
118 }
119 if x != 0 || y != 0 {
120 return Err(ImageError::Unsupported(format!(
121 "single-image raster has only tile (0, 0); got ({x}, {y})"
122 ))
123 .into());
124 }
125 let nbands = self.profile.bands.len();
127 let pixels = (self.profile.width as usize) * (self.profile.height as usize);
128 let stride = nbands; let mut bands = Vec::with_capacity(nbands);
130 for bi in 0..nbands {
131 let mut data = Vec::with_capacity(pixels);
132 for p in 0..pixels {
133 data.push(self.pixels[p * stride + bi]);
134 }
135 bands.push(Band::new(self.profile.bands[bi].clone(), data));
136 }
137 Ok(RasterTile {
138 width: self.profile.width,
139 height: self.profile.height,
140 bands,
141 geo_transform: self.profile.geo_transform,
142 crs: self.profile.crs.clone(),
143 })
144 }
145}
146
147struct Decoded {
149 width: u32,
150 height: u32,
151 pixels: Vec<u8>,
153 nbands: u8,
155}
156
157impl Decoded {
158 fn band_names(&self) -> Vec<&'static str> {
159 match self.nbands {
160 1 => vec!["grey"],
161 3 => vec!["red", "green", "blue"],
162 4 => vec!["red", "green", "blue", "alpha"],
163 _ => Vec::new(),
164 }
165 }
166}
167
168fn decode_jpeg(bytes: &[u8]) -> Result<Decoded> {
169 let mut dec = jpeg_decoder::Decoder::new(bytes);
170 let pixels = dec
171 .decode()
172 .map_err(|e| ImageError::Jpeg(format!("{e:?}")))?;
173 let info = dec
174 .info()
175 .ok_or_else(|| ImageError::Jpeg("missing JPEG info after decode".into()))?;
176 let nbands = match info.pixel_format {
177 jpeg_decoder::PixelFormat::L8 => 1,
178 jpeg_decoder::PixelFormat::RGB24 => 3,
179 jpeg_decoder::PixelFormat::CMYK32 => {
180 return Err(ImageError::unsupported(
181 "CMYK JPEGs (use RGB output instead)",
182 ))
183 }
184 _ => {
186 return Err(ImageError::unsupported(format!(
187 "JPEG pixel format {:?} (v0.1 supports L8 + RGB24)",
188 info.pixel_format
189 )))
190 }
191 };
192 Ok(Decoded {
193 width: info.width as u32,
194 height: info.height as u32,
195 pixels,
196 nbands,
197 })
198}
199
200fn decode_png(bytes: &[u8]) -> Result<Decoded> {
201 let decoder = png::Decoder::new(std::io::Cursor::new(bytes));
202 let mut reader = decoder
203 .read_info()
204 .map_err(|e| ImageError::Png(e.to_string()))?;
205 let info = reader.info().clone();
206
207 if info.bit_depth != png::BitDepth::Eight {
209 return Err(ImageError::unsupported(format!(
210 "PNG bit depth {:?} (v0.1 supports 8-bit)",
211 info.bit_depth
212 )));
213 }
214
215 let mut buf = vec![0u8; reader.output_buffer_size()];
216 let frame = reader
217 .next_frame(&mut buf)
218 .map_err(|e| ImageError::Png(e.to_string()))?;
219 buf.truncate(frame.buffer_size());
220
221 let nbands = match info.color_type {
222 png::ColorType::Grayscale => 1,
223 png::ColorType::Rgb => 3,
224 png::ColorType::Rgba => 4,
225 png::ColorType::GrayscaleAlpha => 2,
226 png::ColorType::Indexed => {
227 return Err(ImageError::unsupported(
228 "indexed (palette) PNGs — decode to RGB before upload",
229 ))
230 }
231 };
232
233 Ok(Decoded {
234 width: info.width,
235 height: info.height,
236 pixels: buf,
237 nbands,
238 })
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn workdir(name: &str) -> std::path::PathBuf {
246 let dir =
247 std::env::temp_dir().join(format!("imgraster_test_{}_{}", std::process::id(), name));
248 let _ = std::fs::remove_dir_all(&dir);
249 std::fs::create_dir_all(&dir).unwrap();
250 dir
251 }
252
253 fn write_test_png(path: &Path, width: u32, height: u32, fill: [u8; 3]) {
258 let file = std::fs::File::create(path).unwrap();
259 let buf = std::io::BufWriter::new(file);
260 let mut enc = png::Encoder::new(buf, width, height);
261 enc.set_color(png::ColorType::Rgb);
262 enc.set_depth(png::BitDepth::Eight);
263 let mut writer = enc.write_header().unwrap();
264 let pixels: Vec<u8> = (0..(width * height))
265 .flat_map(|_| fill.iter().copied())
266 .collect();
267 writer.write_image_data(&pixels).unwrap();
268 }
269
270 #[test]
271 fn opens_png_with_world_file() {
272 let dir = workdir("png_with_wf");
273 let img = dir.join("ortho.png");
274 write_test_png(&img, 4, 4, [10, 20, 30]);
275 let wld = dir.join("ortho.pgw");
276 std::fs::write(&wld, "0.5\n0\n0\n-0.5\n144\n-37\n").unwrap();
277
278 let r = ImageRaster::open_with_crs(&img, Crs::Epsg(4326)).unwrap();
279 let p = r.profile();
280 assert_eq!(p.width, 4);
281 assert_eq!(p.height, 4);
282 assert_eq!(p.bands.len(), 3);
283 assert_eq!(p.crs, Crs::Epsg(4326));
284 assert!((p.geo_transform.origin[0] - 143.75).abs() < 1e-9);
286 assert!((p.geo_transform.origin[1] - (-36.75)).abs() < 1e-9);
287 }
288
289 #[test]
290 fn read_tile_returns_full_image_as_one_tile() {
291 let dir = workdir("read_tile");
292 let img = dir.join("ortho.png");
293 write_test_png(&img, 4, 4, [10, 20, 30]);
294 let wld = dir.join("ortho.pgw");
295 std::fs::write(&wld, "0.5\n0\n0\n-0.5\n144\n-37\n").unwrap();
296
297 let r = ImageRaster::open_with_crs(&img, Crs::Epsg(4326)).unwrap();
298 let t = r.read_tile(0, 0, 0).unwrap();
299 assert_eq!(t.width, 4);
300 assert_eq!(t.height, 4);
301 assert_eq!(t.bands.len(), 3);
302 assert!(t.bands[0].data.iter().all(|&v| v == 10));
304 assert!(t.bands[1].data.iter().all(|&v| v == 20));
305 assert!(t.bands[2].data.iter().all(|&v| v == 30));
306 }
307
308 #[test]
309 fn missing_world_file_errors_clearly() {
310 let dir = workdir("missing_wf");
311 let img = dir.join("noworld.png");
312 write_test_png(&img, 2, 2, [0, 0, 0]);
313
314 let err = ImageRaster::open(&img).unwrap_err();
315 assert!(matches!(err, ImageError::MissingWorldFile { .. }));
316 }
317
318 #[test]
319 fn unsupported_extension_errors() {
320 let dir = workdir("bad_ext");
321 let img = dir.join("file.gif");
322 std::fs::write(&img, b"").unwrap();
323 let err = ImageRaster::open(&img).unwrap_err();
324 assert!(matches!(err, ImageError::Unsupported(_)));
325 }
326
327 #[test]
328 fn read_tile_rejects_non_zero_tile() {
329 let dir = workdir("bad_tile");
330 let img = dir.join("ortho.png");
331 write_test_png(&img, 2, 2, [0, 0, 0]);
332 std::fs::write(dir.join("ortho.pgw"), "0.5\n0\n0\n-0.5\n0\n0\n").unwrap();
333 let r = ImageRaster::open(&img).unwrap();
334 assert!(r.read_tile(0, 1, 0).is_err());
335 assert!(r.read_tile(1, 0, 0).is_err());
336 }
337}