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> {
71 let source = FileDataSource::open(path)
72 .with_context(|| format!("Failed to open file: {}", path.display()))?;
73
74 let reader = GeoTiffReader::open(source)
75 .with_context(|| format!("Failed to read GeoTIFF: {}", path.display()))?;
76
77 let width = reader.width();
78 let height = reader.height();
79 let data_type = reader
80 .data_type()
81 .ok_or_else(|| anyhow::anyhow!("Could not determine data type"))?;
82 let nodata = reader.nodata();
83 let samples_per_pixel = reader.band_count();
84
85 if band_index >= samples_per_pixel {
86 anyhow::bail!(
87 "Band index {} out of range (file has {} band(s))",
88 band_index,
89 samples_per_pixel
90 );
91 }
92
93 let raw = reader
94 .read_band(0, band_index as usize)
95 .with_context(|| "Failed to read band data")?;
96
97 let data = extract_single_band(
98 &raw,
99 width,
100 height,
101 band_index,
102 data_type.size_bytes(),
103 samples_per_pixel as usize,
104 )?;
105
106 RasterBuffer::new(data, width, height, data_type, nodata)
107 .with_context(|| "Failed to create RasterBuffer from band data")
108}
109
110fn extract_single_band(
122 raw: &[u8],
123 width: u64,
124 height: u64,
125 band_index: u32,
126 bytes_per_sample: usize,
127 samples_per_pixel: usize,
128) -> Result<Vec<u8>> {
129 let pixel_count = (width * height) as usize;
130 let single_band_len = pixel_count * bytes_per_sample;
131
132 if raw.len() == single_band_len {
133 return Ok(raw.to_vec());
134 }
135
136 if band_index as usize >= samples_per_pixel {
137 anyhow::bail!(
138 "Band index {} out of range (file has {} band(s))",
139 band_index,
140 samples_per_pixel
141 );
142 }
143
144 let interleaved_len = single_band_len * samples_per_pixel;
145 if raw.len() != interleaved_len {
146 anyhow::bail!(
147 "Unexpected band data size: got {} bytes, expected {} (single-band) or {} \
148 (interleaved, {} band(s))",
149 raw.len(),
150 single_band_len,
151 interleaved_len,
152 samples_per_pixel
153 );
154 }
155
156 let mut out = vec![0u8; single_band_len];
157 let band_offset = band_index as usize * bytes_per_sample;
158 let pixel_stride = bytes_per_sample * samples_per_pixel;
159
160 for pixel in 0..pixel_count {
161 let src_start = pixel * pixel_stride + band_offset;
162 let dst_start = pixel * bytes_per_sample;
163 out[dst_start..dst_start + bytes_per_sample]
164 .copy_from_slice(&raw[src_start..src_start + bytes_per_sample]);
165 }
166
167 Ok(out)
168}
169
170pub fn read_band_region(
172 path: &Path,
173 band_index: u32,
174 x_offset: u64,
175 y_offset: u64,
176 width: u64,
177 height: u64,
178) -> Result<RasterBuffer> {
179 let source = FileDataSource::open(path)
180 .with_context(|| format!("Failed to open file: {}", path.display()))?;
181
182 let reader = GeoTiffReader::open(source)
183 .with_context(|| format!("Failed to read GeoTIFF: {}", path.display()))?;
184
185 let img_width = reader.width();
187 let img_height = reader.height();
188
189 if x_offset >= img_width || y_offset >= img_height {
190 anyhow::bail!(
191 "Region offset ({}, {}) is outside image bounds ({}x{})",
192 x_offset,
193 y_offset,
194 img_width,
195 img_height
196 );
197 }
198
199 let actual_width = width.min(img_width.saturating_sub(x_offset));
201 let actual_height = height.min(img_height.saturating_sub(y_offset));
202
203 if actual_width == 0 || actual_height == 0 {
204 anyhow::bail!("Invalid region dimensions");
205 }
206
207 let data_type = reader
208 .data_type()
209 .ok_or_else(|| anyhow::anyhow!("Could not determine data type"))?;
210 let nodata = reader.nodata();
211
212 let bytes_per_sample = data_type.size_bytes();
214 let samples_per_pixel = reader.band_count() as usize;
215
216 if band_index as usize >= samples_per_pixel {
217 anyhow::bail!(
218 "Band index {} out of range (file has {} band(s))",
219 band_index,
220 samples_per_pixel
221 );
222 }
223
224 let tile_size = reader.tile_size();
226
227 if tile_size.is_none() {
228 let region = ImageRegion::new(
230 img_width,
231 img_height,
232 x_offset,
233 y_offset,
234 actual_width,
235 actual_height,
236 );
237 let config = RasterConfig::new(bytes_per_sample, samples_per_pixel, data_type, nodata);
238 return read_and_subset_strip(&reader, band_index, region, config);
239 }
240
241 let (tile_width, tile_height) =
243 tile_size.ok_or_else(|| anyhow::anyhow!("Tile size not available"))?;
244 let tile_width = tile_width as u64;
245 let tile_height = tile_height as u64;
246
247 let (tiles_x, tiles_y) = reader.tile_count();
248
249 let tile_x_start = (x_offset / tile_width) as u32;
251 let tile_y_start = (y_offset / tile_height) as u32;
252 let tile_x_end = (x_offset + actual_width)
253 .div_ceil(tile_width)
254 .min(tiles_x as u64) as u32;
255 let tile_y_end = (y_offset + actual_height)
256 .div_ceil(tile_height)
257 .min(tiles_y as u64) as u32;
258
259 let output_size = (actual_width * actual_height) as usize * bytes_per_sample;
261 let mut output = vec![0u8; output_size];
262
263 let src_pixel_stride = bytes_per_sample * samples_per_pixel;
267 let band_offset = band_index as usize * bytes_per_sample;
268
269 for tile_y in tile_y_start..tile_y_end {
271 for tile_x in tile_x_start..tile_x_end {
272 let tile_data = reader
273 .read_tile(0, tile_x, tile_y)
274 .with_context(|| format!("Failed to read tile ({}, {})", tile_x, tile_y))?;
275
276 let tile_img_x = tile_x as u64 * tile_width;
278 let tile_img_y = tile_y as u64 * tile_height;
279
280 let copy_x_start = x_offset.max(tile_img_x);
282 let copy_y_start = y_offset.max(tile_img_y);
283 let copy_x_end = (x_offset + actual_width).min(tile_img_x + tile_width);
284 let copy_y_end = (y_offset + actual_height).min(tile_img_y + tile_height);
285
286 if samples_per_pixel == 1 {
287 for row in copy_y_start..copy_y_end {
289 let tile_row = (row - tile_img_y) as usize;
290 let tile_col_start = (copy_x_start - tile_img_x) as usize;
291 let tile_col_end = (copy_x_end - tile_img_x) as usize;
292
293 let out_row = (row - y_offset) as usize;
294 let out_col_start = (copy_x_start - x_offset) as usize;
295
296 let src_offset =
297 (tile_row * tile_width as usize + tile_col_start) * bytes_per_sample;
298 let dst_offset =
299 (out_row * actual_width as usize + out_col_start) * bytes_per_sample;
300 let copy_bytes = (tile_col_end - tile_col_start) * bytes_per_sample;
301
302 if src_offset + copy_bytes <= tile_data.len()
303 && dst_offset + copy_bytes <= output.len()
304 {
305 output[dst_offset..dst_offset + copy_bytes]
306 .copy_from_slice(&tile_data[src_offset..src_offset + copy_bytes]);
307 }
308 }
309 continue;
310 }
311
312 for row in copy_y_start..copy_y_end {
315 let tile_row = (row - tile_img_y) as usize;
316 let out_row = (row - y_offset) as usize;
317
318 for col in copy_x_start..copy_x_end {
319 let tile_col = (col - tile_img_x) as usize;
320 let out_col = (col - x_offset) as usize;
321
322 let src_offset = (tile_row * tile_width as usize + tile_col) * src_pixel_stride
323 + band_offset;
324 let dst_offset = (out_row * actual_width as usize + out_col) * bytes_per_sample;
325
326 if src_offset + bytes_per_sample <= tile_data.len()
327 && dst_offset + bytes_per_sample <= output.len()
328 {
329 output[dst_offset..dst_offset + bytes_per_sample]
330 .copy_from_slice(&tile_data[src_offset..src_offset + bytes_per_sample]);
331 }
332 }
333 }
334 }
335 }
336
337 RasterBuffer::new(output, actual_width, actual_height, data_type, nodata)
338 .with_context(|| "Failed to create RasterBuffer from region data")
339}
340
341#[derive(Debug, Clone, Copy)]
343struct ImageRegion {
344 img_width: u64,
345 img_height: u64,
346 x_offset: u64,
347 y_offset: u64,
348 width: u64,
349 height: u64,
350}
351
352impl ImageRegion {
353 fn new(
354 img_width: u64,
355 img_height: u64,
356 x_offset: u64,
357 y_offset: u64,
358 width: u64,
359 height: u64,
360 ) -> Self {
361 Self {
362 img_width,
363 img_height,
364 x_offset,
365 y_offset,
366 width,
367 height,
368 }
369 }
370}
371
372#[derive(Debug, Clone, Copy)]
374struct RasterConfig {
375 bytes_per_sample: usize,
376 samples_per_pixel: usize,
377 data_type: RasterDataType,
378 nodata: NoDataValue,
379}
380
381impl RasterConfig {
382 fn new(
383 bytes_per_sample: usize,
384 samples_per_pixel: usize,
385 data_type: RasterDataType,
386 nodata: NoDataValue,
387 ) -> Self {
388 Self {
389 bytes_per_sample,
390 samples_per_pixel,
391 data_type,
392 nodata,
393 }
394 }
395}
396
397fn read_and_subset_strip(
399 reader: &GeoTiffReader<FileDataSource>,
400 band_index: u32,
401 region: ImageRegion,
402 config: RasterConfig,
403) -> Result<RasterBuffer> {
404 let raw = reader
408 .read_band(0, band_index as usize)
409 .with_context(|| "Failed to read band data")?;
410
411 let data = extract_single_band(
412 &raw,
413 region.img_width,
414 region.img_height,
415 band_index,
416 config.bytes_per_sample,
417 config.samples_per_pixel,
418 )?;
419
420 let output_size = (region.width * region.height) as usize * config.bytes_per_sample;
422 let mut output = vec![0u8; output_size];
423
424 for row in 0..region.height {
425 let src_row = region.y_offset + row;
426 if src_row >= region.img_height {
427 break;
428 }
429
430 let src_offset =
431 (src_row * region.img_width + region.x_offset) as usize * config.bytes_per_sample;
432 let dst_offset = (row * region.width) as usize * config.bytes_per_sample;
433 let copy_bytes = region.width as usize * config.bytes_per_sample;
434
435 if src_offset + copy_bytes <= data.len() && dst_offset + copy_bytes <= output.len() {
436 output[dst_offset..dst_offset + copy_bytes]
437 .copy_from_slice(&data[src_offset..src_offset + copy_bytes]);
438 }
439 }
440
441 RasterBuffer::new(
442 output,
443 region.width,
444 region.height,
445 config.data_type,
446 config.nodata,
447 )
448 .with_context(|| "Failed to create RasterBuffer from subsetted data")
449}
450
451pub fn write_single_band(
453 path: &Path,
454 buffer: &RasterBuffer,
455 geo_transform: Option<GeoTransform>,
456 epsg_code: Option<u32>,
457 no_data_value: Option<f64>,
458) -> Result<()> {
459 let mut config = WriterConfig::new(buffer.width(), buffer.height(), 1, buffer.data_type());
461
462 if let Some(gt) = geo_transform {
464 config = config.with_geo_transform(gt);
465 }
466
467 if let Some(epsg) = epsg_code {
469 config = config.with_epsg_code(epsg);
470 }
471
472 if let Some(no_data) = no_data_value {
474 let nodata_val = match buffer.data_type() {
475 RasterDataType::Int8
476 | RasterDataType::Int16
477 | RasterDataType::Int32
478 | RasterDataType::Int64
479 | RasterDataType::UInt8
480 | RasterDataType::UInt16
481 | RasterDataType::UInt32
482 | RasterDataType::UInt64 => NoDataValue::Integer(no_data as i64),
483 _ => NoDataValue::Float(no_data),
484 };
485 config = config.with_nodata(nodata_val);
486 }
487
488 let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
490 .with_context(|| format!("Failed to create GeoTIFF: {}", path.display()))?;
491
492 writer
494 .write(buffer.as_bytes())
495 .with_context(|| format!("Failed to write band to {}", path.display()))?;
496
497 Ok(())
498}
499
500pub fn write_multi_band(
502 path: &Path,
503 buffers: &[RasterBuffer],
504 geo_transform: Option<GeoTransform>,
505 epsg_code: Option<u32>,
506 no_data_value: Option<f64>,
507) -> Result<()> {
508 if buffers.is_empty() {
509 anyhow::bail!("No bands provided");
510 }
511
512 let first_width = buffers[0].width();
514 let first_height = buffers[0].height();
515 let first_data_type = buffers[0].data_type();
516 for (i, buffer) in buffers.iter().enumerate().skip(1) {
517 if buffer.width() != first_width || buffer.height() != first_height {
518 anyhow::bail!(
519 "Band {} has different dimensions ({} x {}) than first band ({} x {})",
520 i,
521 buffer.width(),
522 buffer.height(),
523 first_width,
524 first_height
525 );
526 }
527 if buffer.data_type() != first_data_type {
528 anyhow::bail!(
529 "Band {} has different data type ({:?}) than first band ({:?})",
530 i,
531 buffer.data_type(),
532 first_data_type
533 );
534 }
535 }
536
537 let bytes_per_pixel = first_data_type.size_bytes() as u64;
539 let pixel_count = first_width * first_height;
540 let total_bytes = (pixel_count * bytes_per_pixel * buffers.len() as u64) as usize;
541 let mut interleaved_data = vec![0u8; total_bytes];
542
543 for pixel_idx in 0..pixel_count {
544 for (band_idx, buffer) in buffers.iter().enumerate() {
545 let src_offset = (pixel_idx * bytes_per_pixel) as usize;
546 let dst_offset = ((pixel_idx * bytes_per_pixel) * buffers.len() as u64
547 + band_idx as u64 * bytes_per_pixel) as usize;
548 let src_end = src_offset + (bytes_per_pixel as usize);
549 let dst_end = dst_offset + (bytes_per_pixel as usize);
550 interleaved_data[dst_offset..dst_end]
551 .copy_from_slice(&buffer.as_bytes()[src_offset..src_end]);
552 }
553 }
554
555 let mut config = WriterConfig::new(
557 first_width,
558 first_height,
559 buffers.len() as u16,
560 first_data_type,
561 );
562
563 if let Some(gt) = geo_transform {
565 config = config.with_geo_transform(gt);
566 }
567
568 if let Some(epsg) = epsg_code {
570 config = config.with_epsg_code(epsg);
571 }
572
573 if let Some(no_data) = no_data_value {
575 let nodata_val = match first_data_type {
576 RasterDataType::Int8
577 | RasterDataType::Int16
578 | RasterDataType::Int32
579 | RasterDataType::Int64
580 | RasterDataType::UInt8
581 | RasterDataType::UInt16
582 | RasterDataType::UInt32
583 | RasterDataType::UInt64 => NoDataValue::Integer(no_data as i64),
584 _ => NoDataValue::Float(no_data),
585 };
586 config = config.with_nodata(nodata_val);
587 }
588
589 let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
591 .with_context(|| format!("Failed to create GeoTIFF: {}", path.display()))?;
592
593 writer
595 .write(&interleaved_data)
596 .with_context(|| format!("Failed to write bands to {}", path.display()))?;
597
598 Ok(())
599}
600
601#[derive(Debug, Clone)]
603pub struct CogWriteOptions {
604 pub geo_transform: Option<GeoTransform>,
606 pub epsg_code: Option<u32>,
608 pub no_data_value: Option<f64>,
610 pub overview_levels: Vec<u32>,
613 pub tile_size: u32,
615 pub compression: Compression,
617}
618
619impl Default for CogWriteOptions {
620 fn default() -> Self {
621 Self {
622 geo_transform: None,
623 epsg_code: None,
624 no_data_value: None,
625 overview_levels: vec![2, 4, 8, 16],
626 tile_size: 256,
627 compression: Compression::Lzw,
628 }
629 }
630}
631
632pub fn write_raster_cog(
637 path: &Path,
638 buffers: &[RasterBuffer],
639 options: CogWriteOptions,
640) -> Result<()> {
641 let CogWriteOptions {
642 geo_transform,
643 epsg_code,
644 no_data_value,
645 overview_levels,
646 tile_size,
647 compression,
648 } = options;
649 if buffers.is_empty() {
650 anyhow::bail!("No bands provided for COG write");
651 }
652
653 let first_width = buffers[0].width();
654 let first_height = buffers[0].height();
655 let first_data_type = buffers[0].data_type();
656
657 for (i, buffer) in buffers.iter().enumerate().skip(1) {
658 if buffer.width() != first_width || buffer.height() != first_height {
659 anyhow::bail!(
660 "Band {} has different dimensions than the first band ({} x {} vs {} x {})",
661 i,
662 buffer.width(),
663 buffer.height(),
664 first_width,
665 first_height
666 );
667 }
668 if buffer.data_type() != first_data_type {
669 anyhow::bail!(
670 "Band {} has different data type ({:?}) than first band ({:?})",
671 i,
672 buffer.data_type(),
673 first_data_type
674 );
675 }
676 }
677
678 let bytes_per_pixel = first_data_type.size_bytes() as u64;
680 let pixel_count = first_width * first_height;
681 let total_bytes = (pixel_count * bytes_per_pixel * buffers.len() as u64) as usize;
682 let mut interleaved_data = vec![0u8; total_bytes];
683
684 for pixel_idx in 0..pixel_count {
685 for (band_idx, buffer) in buffers.iter().enumerate() {
686 let src_offset = (pixel_idx * bytes_per_pixel) as usize;
687 let dst_offset = ((pixel_idx * bytes_per_pixel) * buffers.len() as u64
688 + band_idx as u64 * bytes_per_pixel) as usize;
689 let src_end = src_offset + bytes_per_pixel as usize;
690 let dst_end = dst_offset + bytes_per_pixel as usize;
691 interleaved_data[dst_offset..dst_end]
692 .copy_from_slice(&buffer.as_bytes()[src_offset..src_end]);
693 }
694 }
695
696 let generate_overviews = !overview_levels.is_empty();
697
698 let mut config = WriterConfig::new(
699 first_width,
700 first_height,
701 buffers.len() as u16,
702 first_data_type,
703 )
704 .with_compression(compression)
705 .with_tile_size(tile_size, tile_size);
706
707 if let Some(gt) = geo_transform {
708 config = config.with_geo_transform(gt);
709 }
710 if let Some(epsg) = epsg_code {
711 config = config.with_epsg_code(epsg);
712 }
713 if let Some(no_data) = no_data_value {
714 let nodata_val = match first_data_type {
715 RasterDataType::Int8
716 | RasterDataType::Int16
717 | RasterDataType::Int32
718 | RasterDataType::Int64
719 | RasterDataType::UInt8
720 | RasterDataType::UInt16
721 | RasterDataType::UInt32
722 | RasterDataType::UInt64 => NoDataValue::Integer(no_data as i64),
723 _ => NoDataValue::Float(no_data),
724 };
725 config = config.with_nodata(nodata_val);
726 }
727
728 use oxigeo_geotiff::OverviewResampling;
729 config = config.with_overviews(generate_overviews, OverviewResampling::Average);
730 if generate_overviews {
731 config = config.with_overview_levels(overview_levels);
732 }
733
734 let mut writer = CogWriter::create(path, config, CogWriterOptions::default())
735 .with_context(|| format!("Failed to create COG: {}", path.display()))?;
736
737 writer
738 .write(&interleaved_data)
739 .with_context(|| format!("Failed to write COG data to {}", path.display()))?;
740
741 Ok(())
742}
743
744pub fn read_raster_info_uri(uri: &str) -> Result<RasterInfo> {
750 if crate::util::cloud::is_cloud_uri(uri) || uri.starts_with("file://") {
751 anyhow::bail!(
754 "cloud URI reading for raster requires GeoTiffReader<DataSource>; \
755 use a local file path for now (got: {})",
756 uri
757 );
758 }
759 read_raster_info(Path::new(uri))
760}
761
762pub fn calculate_subset_geotransform(
764 original: &GeoTransform,
765 x_offset: u64,
766 y_offset: u64,
767) -> GeoTransform {
768 let new_origin_x = original.origin_x + (x_offset as f64 * original.pixel_width);
769 let new_origin_y = original.origin_y + (y_offset as f64 * original.pixel_height);
770
771 GeoTransform {
772 origin_x: new_origin_x,
773 origin_y: new_origin_y,
774 pixel_width: original.pixel_width,
775 pixel_height: original.pixel_height,
776 row_rotation: original.row_rotation,
777 col_rotation: original.col_rotation,
778 }
779}
780
781pub fn geo_to_pixel_window(
783 geo_transform: &GeoTransform,
784 min_x: f64,
785 min_y: f64,
786 max_x: f64,
787 max_y: f64,
788 raster_width: u64,
789 raster_height: u64,
790) -> Result<(u64, u64, u64, u64)> {
791 let det = geo_transform.pixel_width * geo_transform.pixel_height
793 - geo_transform.row_rotation * geo_transform.col_rotation;
794
795 if det.abs() < 1e-10 {
796 anyhow::bail!("Invalid geotransform: determinant is zero");
797 }
798
799 let calc_pixel_x = |geo_x: f64, geo_y: f64| -> f64 {
803 (geo_transform.pixel_height * (geo_x - geo_transform.origin_x)
804 - geo_transform.col_rotation * (geo_y - geo_transform.origin_y))
805 / det
806 };
807
808 let calc_pixel_y = |geo_x: f64, geo_y: f64| -> f64 {
809 (-geo_transform.row_rotation * (geo_x - geo_transform.origin_x)
810 + geo_transform.pixel_width * (geo_y - geo_transform.origin_y))
811 / det
812 };
813
814 let px_min_x = calc_pixel_x(min_x, max_y);
815 let px_max_x = calc_pixel_x(max_x, min_y);
816 let px_min_y = calc_pixel_y(min_x, max_y);
817 let px_max_y = calc_pixel_y(max_x, min_y);
818
819 let x_off = px_min_x.max(0.0).floor() as u64;
821 let y_off = px_min_y.max(0.0).floor() as u64;
822 let x_max = px_max_x.min(raster_width as f64).ceil() as u64;
823 let y_max = px_max_y.min(raster_height as f64).ceil() as u64;
824
825 let width = x_max.saturating_sub(x_off);
826 let height = y_max.saturating_sub(y_off);
827
828 if width == 0 || height == 0 {
829 anyhow::bail!("Bounding box does not intersect raster");
830 }
831
832 Ok((x_off, y_off, width, height))
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838
839 #[test]
840 fn test_calculate_subset_geotransform() {
841 let original = GeoTransform {
842 origin_x: 0.0,
843 origin_y: 100.0,
844 pixel_width: 1.0,
845 pixel_height: -1.0,
846 row_rotation: 0.0,
847 col_rotation: 0.0,
848 };
849
850 let subset = calculate_subset_geotransform(&original, 10, 5);
851 assert_eq!(subset.origin_x, 10.0);
852 assert_eq!(subset.origin_y, 95.0);
853 assert_eq!(subset.pixel_width, 1.0);
854 assert_eq!(subset.pixel_height, -1.0);
855 }
856
857 #[test]
858 fn test_geo_to_pixel_window() {
859 let geo_transform = GeoTransform {
860 origin_x: 0.0,
861 origin_y: 100.0,
862 pixel_width: 1.0,
863 pixel_height: -1.0,
864 row_rotation: 0.0,
865 col_rotation: 0.0,
866 };
867
868 let result = geo_to_pixel_window(&geo_transform, 10.0, 80.0, 20.0, 90.0, 100, 100);
869 assert!(result.is_ok());
870
871 let (x_off, y_off, width, height) = result.expect("should succeed");
872 assert_eq!(x_off, 10);
873 assert_eq!(y_off, 10);
874 assert_eq!(width, 10);
875 assert_eq!(height, 10);
876 }
877}