1use anyhow::{Context, Result};
4use oxigeo_core::{
5 buffer::RasterBuffer,
6 io::FileDataSource,
7 types::{GeoTransform, NoDataValue, RasterDataType},
8};
9use oxigeo_geotiff::{
10 CogWriter, CogWriterOptions, Compression, GeoTiffReader, GeoTiffWriter, GeoTiffWriterOptions,
11 WriterConfig,
12};
13use std::path::Path;
14
15#[derive(Debug, Clone)]
17pub struct RasterInfo {
18 pub width: u64,
20 pub height: u64,
22 pub bands: u32,
24 pub data_type: RasterDataType,
26 pub geo_transform: Option<GeoTransform>,
28 pub epsg_code: Option<u32>,
30 pub no_data_value: Option<f64>,
32}
33
34pub fn read_raster_info(path: &Path) -> Result<RasterInfo> {
36 let source = FileDataSource::open(path)
37 .with_context(|| format!("Failed to open file: {}", path.display()))?;
38
39 let reader = GeoTiffReader::open(source)
40 .with_context(|| format!("Failed to read GeoTIFF: {}", path.display()))?;
41
42 let width = reader.width();
43 let height = reader.height();
44 let bands = reader.band_count();
45 let data_type = reader
46 .data_type()
47 .ok_or_else(|| anyhow::anyhow!("Could not determine data type"))?;
48 let geo_transform = reader.geo_transform().copied();
49 let epsg_code = reader.epsg_code();
50 let nodata = reader.nodata();
51 let no_data_value = nodata.as_f64();
52
53 Ok(RasterInfo {
54 width,
55 height,
56 bands,
57 data_type,
58 geo_transform,
59 epsg_code,
60 no_data_value,
61 })
62}
63
64pub fn read_band(path: &Path, band_index: u32) -> Result<RasterBuffer> {
76 let source = FileDataSource::open(path)
77 .with_context(|| format!("Failed to open file: {}", path.display()))?;
78
79 let reader = GeoTiffReader::open(source)
80 .with_context(|| format!("Failed to read GeoTIFF: {}", path.display()))?;
81
82 let width = reader.width();
83 let height = reader.height();
84 let data_type = reader
85 .data_type()
86 .ok_or_else(|| anyhow::anyhow!("Could not determine data type"))?;
87 let nodata = reader.nodata();
88 let samples_per_pixel = reader.band_count();
89
90 if band_index >= samples_per_pixel {
91 anyhow::bail!(
92 "Band index {} out of range (file has {} band(s))",
93 band_index,
94 samples_per_pixel
95 );
96 }
97
98 let data = reader
99 .read_band(0, band_index as usize)
100 .with_context(|| "Failed to read band data")?;
101
102 check_plane_len(
103 data.len(),
104 width,
105 height,
106 band_index,
107 data_type.size_bytes(),
108 )?;
109
110 RasterBuffer::new(data, width, height, data_type, nodata)
111 .with_context(|| "Failed to create RasterBuffer from band data")
112}
113
114fn check_plane_len(
119 got: usize,
120 width: u64,
121 height: u64,
122 band_index: u32,
123 bytes_per_sample: usize,
124) -> Result<()> {
125 let expected = (width as usize)
126 .checked_mul(height as usize)
127 .and_then(|px| px.checked_mul(bytes_per_sample))
128 .ok_or_else(|| {
129 anyhow::anyhow!(
130 "Raster dimensions {}x{} ({} bytes/sample) overflow usize",
131 width,
132 height,
133 bytes_per_sample
134 )
135 })?;
136
137 if got != expected {
138 anyhow::bail!(
139 "Unexpected band {} data size: got {} bytes, expected {} ({}x{} x {} byte(s))",
140 band_index,
141 got,
142 expected,
143 width,
144 height,
145 bytes_per_sample
146 );
147 }
148 Ok(())
149}
150
151pub fn read_band_region(
159 path: &Path,
160 band_index: u32,
161 x_offset: u64,
162 y_offset: u64,
163 width: u64,
164 height: u64,
165) -> Result<RasterBuffer> {
166 let source = FileDataSource::open(path)
167 .with_context(|| format!("Failed to open file: {}", path.display()))?;
168
169 let reader = GeoTiffReader::open(source)
170 .with_context(|| format!("Failed to read GeoTIFF: {}", path.display()))?;
171
172 let img_width = reader.width();
174 let img_height = reader.height();
175
176 if x_offset >= img_width || y_offset >= img_height {
177 anyhow::bail!(
178 "Region offset ({}, {}) is outside image bounds ({}x{})",
179 x_offset,
180 y_offset,
181 img_width,
182 img_height
183 );
184 }
185
186 let actual_width = width.min(img_width.saturating_sub(x_offset));
188 let actual_height = height.min(img_height.saturating_sub(y_offset));
189
190 if actual_width == 0 || actual_height == 0 {
191 anyhow::bail!("Invalid region dimensions");
192 }
193
194 let data_type = reader
195 .data_type()
196 .ok_or_else(|| anyhow::anyhow!("Could not determine data type"))?;
197 let nodata = reader.nodata();
198 let samples_per_pixel = reader.band_count();
199
200 if band_index >= samples_per_pixel {
201 anyhow::bail!(
202 "Band index {} out of range (file has {} band(s))",
203 band_index,
204 samples_per_pixel
205 );
206 }
207
208 let output = reader
209 .read_window(
210 0,
211 band_index as usize,
212 x_offset,
213 y_offset,
214 actual_width,
215 actual_height,
216 )
217 .with_context(|| {
218 format!(
219 "Failed to read region ({}, {}) {}x{} of band {}",
220 x_offset, y_offset, actual_width, actual_height, band_index
221 )
222 })?;
223
224 check_plane_len(
225 output.len(),
226 actual_width,
227 actual_height,
228 band_index,
229 data_type.size_bytes(),
230 )?;
231
232 RasterBuffer::new(output, actual_width, actual_height, data_type, nodata)
233 .with_context(|| "Failed to create RasterBuffer from region data")
234}
235
236pub fn write_single_band(
238 path: &Path,
239 buffer: &RasterBuffer,
240 geo_transform: Option<GeoTransform>,
241 epsg_code: Option<u32>,
242 no_data_value: Option<f64>,
243) -> Result<()> {
244 let mut config = WriterConfig::new(buffer.width(), buffer.height(), 1, buffer.data_type());
246
247 if let Some(gt) = geo_transform {
249 config = config.with_geo_transform(gt);
250 }
251
252 if let Some(epsg) = epsg_code {
254 config = config.with_epsg_code(epsg);
255 }
256
257 if let Some(no_data) = no_data_value {
259 let nodata_val = match buffer.data_type() {
260 RasterDataType::Int8
261 | RasterDataType::Int16
262 | RasterDataType::Int32
263 | RasterDataType::Int64
264 | RasterDataType::UInt8
265 | RasterDataType::UInt16
266 | RasterDataType::UInt32
267 | RasterDataType::UInt64 => NoDataValue::Integer(no_data as i64),
268 _ => NoDataValue::Float(no_data),
269 };
270 config = config.with_nodata(nodata_val);
271 }
272
273 let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
275 .with_context(|| format!("Failed to create GeoTIFF: {}", path.display()))?;
276
277 writer
279 .write(buffer.as_bytes())
280 .with_context(|| format!("Failed to write band to {}", path.display()))?;
281
282 Ok(())
283}
284
285pub fn write_multi_band(
287 path: &Path,
288 buffers: &[RasterBuffer],
289 geo_transform: Option<GeoTransform>,
290 epsg_code: Option<u32>,
291 no_data_value: Option<f64>,
292) -> Result<()> {
293 if buffers.is_empty() {
294 anyhow::bail!("No bands provided");
295 }
296
297 let first_width = buffers[0].width();
299 let first_height = buffers[0].height();
300 let first_data_type = buffers[0].data_type();
301 for (i, buffer) in buffers.iter().enumerate().skip(1) {
302 if buffer.width() != first_width || buffer.height() != first_height {
303 anyhow::bail!(
304 "Band {} has different dimensions ({} x {}) than first band ({} x {})",
305 i,
306 buffer.width(),
307 buffer.height(),
308 first_width,
309 first_height
310 );
311 }
312 if buffer.data_type() != first_data_type {
313 anyhow::bail!(
314 "Band {} has different data type ({:?}) than first band ({:?})",
315 i,
316 buffer.data_type(),
317 first_data_type
318 );
319 }
320 }
321
322 let bytes_per_pixel = first_data_type.size_bytes() as u64;
324 let pixel_count = first_width * first_height;
325 let total_bytes = (pixel_count * bytes_per_pixel * buffers.len() as u64) as usize;
326 let mut interleaved_data = vec![0u8; total_bytes];
327
328 for pixel_idx in 0..pixel_count {
329 for (band_idx, buffer) in buffers.iter().enumerate() {
330 let src_offset = (pixel_idx * bytes_per_pixel) as usize;
331 let dst_offset = ((pixel_idx * bytes_per_pixel) * buffers.len() as u64
332 + band_idx as u64 * bytes_per_pixel) as usize;
333 let src_end = src_offset + (bytes_per_pixel as usize);
334 let dst_end = dst_offset + (bytes_per_pixel as usize);
335 interleaved_data[dst_offset..dst_end]
336 .copy_from_slice(&buffer.as_bytes()[src_offset..src_end]);
337 }
338 }
339
340 let mut config = WriterConfig::new(
342 first_width,
343 first_height,
344 buffers.len() as u16,
345 first_data_type,
346 );
347
348 if let Some(gt) = geo_transform {
350 config = config.with_geo_transform(gt);
351 }
352
353 if let Some(epsg) = epsg_code {
355 config = config.with_epsg_code(epsg);
356 }
357
358 if let Some(no_data) = no_data_value {
360 let nodata_val = match first_data_type {
361 RasterDataType::Int8
362 | RasterDataType::Int16
363 | RasterDataType::Int32
364 | RasterDataType::Int64
365 | RasterDataType::UInt8
366 | RasterDataType::UInt16
367 | RasterDataType::UInt32
368 | RasterDataType::UInt64 => NoDataValue::Integer(no_data as i64),
369 _ => NoDataValue::Float(no_data),
370 };
371 config = config.with_nodata(nodata_val);
372 }
373
374 let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
376 .with_context(|| format!("Failed to create GeoTIFF: {}", path.display()))?;
377
378 writer
380 .write(&interleaved_data)
381 .with_context(|| format!("Failed to write bands to {}", path.display()))?;
382
383 Ok(())
384}
385
386#[derive(Debug, Clone)]
388pub struct CogWriteOptions {
389 pub geo_transform: Option<GeoTransform>,
391 pub epsg_code: Option<u32>,
393 pub no_data_value: Option<f64>,
395 pub overview_levels: Vec<u32>,
398 pub tile_size: u32,
400 pub compression: Compression,
402}
403
404impl Default for CogWriteOptions {
405 fn default() -> Self {
406 Self {
407 geo_transform: None,
408 epsg_code: None,
409 no_data_value: None,
410 overview_levels: vec![2, 4, 8, 16],
411 tile_size: 256,
412 compression: Compression::Lzw,
413 }
414 }
415}
416
417pub fn write_raster_cog(
422 path: &Path,
423 buffers: &[RasterBuffer],
424 options: CogWriteOptions,
425) -> Result<()> {
426 let CogWriteOptions {
427 geo_transform,
428 epsg_code,
429 no_data_value,
430 overview_levels,
431 tile_size,
432 compression,
433 } = options;
434 if buffers.is_empty() {
435 anyhow::bail!("No bands provided for COG write");
436 }
437
438 let first_width = buffers[0].width();
439 let first_height = buffers[0].height();
440 let first_data_type = buffers[0].data_type();
441
442 for (i, buffer) in buffers.iter().enumerate().skip(1) {
443 if buffer.width() != first_width || buffer.height() != first_height {
444 anyhow::bail!(
445 "Band {} has different dimensions than the first band ({} x {} vs {} x {})",
446 i,
447 buffer.width(),
448 buffer.height(),
449 first_width,
450 first_height
451 );
452 }
453 if buffer.data_type() != first_data_type {
454 anyhow::bail!(
455 "Band {} has different data type ({:?}) than first band ({:?})",
456 i,
457 buffer.data_type(),
458 first_data_type
459 );
460 }
461 }
462
463 let bytes_per_pixel = first_data_type.size_bytes() as u64;
465 let pixel_count = first_width * first_height;
466 let total_bytes = (pixel_count * bytes_per_pixel * buffers.len() as u64) as usize;
467 let mut interleaved_data = vec![0u8; total_bytes];
468
469 for pixel_idx in 0..pixel_count {
470 for (band_idx, buffer) in buffers.iter().enumerate() {
471 let src_offset = (pixel_idx * bytes_per_pixel) as usize;
472 let dst_offset = ((pixel_idx * bytes_per_pixel) * buffers.len() as u64
473 + band_idx as u64 * bytes_per_pixel) as usize;
474 let src_end = src_offset + bytes_per_pixel as usize;
475 let dst_end = dst_offset + bytes_per_pixel as usize;
476 interleaved_data[dst_offset..dst_end]
477 .copy_from_slice(&buffer.as_bytes()[src_offset..src_end]);
478 }
479 }
480
481 let generate_overviews = !overview_levels.is_empty();
482
483 let mut config = WriterConfig::new(
484 first_width,
485 first_height,
486 buffers.len() as u16,
487 first_data_type,
488 )
489 .with_compression(compression)
490 .with_tile_size(tile_size, tile_size);
491
492 if let Some(gt) = geo_transform {
493 config = config.with_geo_transform(gt);
494 }
495 if let Some(epsg) = epsg_code {
496 config = config.with_epsg_code(epsg);
497 }
498 if let Some(no_data) = no_data_value {
499 let nodata_val = match first_data_type {
500 RasterDataType::Int8
501 | RasterDataType::Int16
502 | RasterDataType::Int32
503 | RasterDataType::Int64
504 | RasterDataType::UInt8
505 | RasterDataType::UInt16
506 | RasterDataType::UInt32
507 | RasterDataType::UInt64 => NoDataValue::Integer(no_data as i64),
508 _ => NoDataValue::Float(no_data),
509 };
510 config = config.with_nodata(nodata_val);
511 }
512
513 use oxigeo_geotiff::OverviewResampling;
514 config = config.with_overviews(generate_overviews, OverviewResampling::Average);
515 if generate_overviews {
516 config = config.with_overview_levels(overview_levels);
517 }
518
519 let mut writer = CogWriter::create(path, config, CogWriterOptions::default())
520 .with_context(|| format!("Failed to create COG: {}", path.display()))?;
521
522 writer
523 .write(&interleaved_data)
524 .with_context(|| format!("Failed to write COG data to {}", path.display()))?;
525
526 Ok(())
527}
528
529pub fn read_raster_info_uri(uri: &str) -> Result<RasterInfo> {
535 if crate::util::cloud::is_cloud_uri(uri) || uri.starts_with("file://") {
536 anyhow::bail!(
539 "cloud URI reading for raster requires GeoTiffReader<DataSource>; \
540 use a local file path for now (got: {})",
541 uri
542 );
543 }
544 read_raster_info(Path::new(uri))
545}
546
547pub fn calculate_subset_geotransform(
549 original: &GeoTransform,
550 x_offset: u64,
551 y_offset: u64,
552) -> GeoTransform {
553 let new_origin_x = original.origin_x + (x_offset as f64 * original.pixel_width);
554 let new_origin_y = original.origin_y + (y_offset as f64 * original.pixel_height);
555
556 GeoTransform {
557 origin_x: new_origin_x,
558 origin_y: new_origin_y,
559 pixel_width: original.pixel_width,
560 pixel_height: original.pixel_height,
561 row_rotation: original.row_rotation,
562 col_rotation: original.col_rotation,
563 }
564}
565
566pub fn geo_to_pixel_window(
568 geo_transform: &GeoTransform,
569 min_x: f64,
570 min_y: f64,
571 max_x: f64,
572 max_y: f64,
573 raster_width: u64,
574 raster_height: u64,
575) -> Result<(u64, u64, u64, u64)> {
576 let det = geo_transform.pixel_width * geo_transform.pixel_height
578 - geo_transform.row_rotation * geo_transform.col_rotation;
579
580 if det.abs() < 1e-10 {
581 anyhow::bail!("Invalid geotransform: determinant is zero");
582 }
583
584 let calc_pixel_x = |geo_x: f64, geo_y: f64| -> f64 {
588 (geo_transform.pixel_height * (geo_x - geo_transform.origin_x)
589 - geo_transform.col_rotation * (geo_y - geo_transform.origin_y))
590 / det
591 };
592
593 let calc_pixel_y = |geo_x: f64, geo_y: f64| -> f64 {
594 (-geo_transform.row_rotation * (geo_x - geo_transform.origin_x)
595 + geo_transform.pixel_width * (geo_y - geo_transform.origin_y))
596 / det
597 };
598
599 let px_min_x = calc_pixel_x(min_x, max_y);
600 let px_max_x = calc_pixel_x(max_x, min_y);
601 let px_min_y = calc_pixel_y(min_x, max_y);
602 let px_max_y = calc_pixel_y(max_x, min_y);
603
604 let x_off = px_min_x.max(0.0).floor() as u64;
606 let y_off = px_min_y.max(0.0).floor() as u64;
607 let x_max = px_max_x.min(raster_width as f64).ceil() as u64;
608 let y_max = px_max_y.min(raster_height as f64).ceil() as u64;
609
610 let width = x_max.saturating_sub(x_off);
611 let height = y_max.saturating_sub(y_off);
612
613 if width == 0 || height == 0 {
614 anyhow::bail!("Bounding box does not intersect raster");
615 }
616
617 Ok((x_off, y_off, width, height))
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_calculate_subset_geotransform() {
626 let original = GeoTransform {
627 origin_x: 0.0,
628 origin_y: 100.0,
629 pixel_width: 1.0,
630 pixel_height: -1.0,
631 row_rotation: 0.0,
632 col_rotation: 0.0,
633 };
634
635 let subset = calculate_subset_geotransform(&original, 10, 5);
636 assert_eq!(subset.origin_x, 10.0);
637 assert_eq!(subset.origin_y, 95.0);
638 assert_eq!(subset.pixel_width, 1.0);
639 assert_eq!(subset.pixel_height, -1.0);
640 }
641
642 #[test]
643 fn test_geo_to_pixel_window() {
644 let geo_transform = GeoTransform {
645 origin_x: 0.0,
646 origin_y: 100.0,
647 pixel_width: 1.0,
648 pixel_height: -1.0,
649 row_rotation: 0.0,
650 col_rotation: 0.0,
651 };
652
653 let result = geo_to_pixel_window(&geo_transform, 10.0, 80.0, 20.0, 90.0, 100, 100);
654 assert!(result.is_ok());
655
656 let (x_off, y_off, width, height) = result.expect("should succeed");
657 assert_eq!(x_off, 10);
658 assert_eq!(y_off, 10);
659 assert_eq!(width, 10);
660 assert_eq!(height, 10);
661 }
662}