use oxigeo_cli::util::raster::{self, CogWriteOptions};
use oxigeo_core::{
buffer::RasterBuffer,
io::FileDataSource,
types::{GeoTransform, NoDataValue, RasterDataType},
};
use oxigeo_geotiff::{
Compression, GeoTiffReader, GeoTiffWriter, GeoTiffWriterOptions, WriterConfig,
};
use std::path::PathBuf;
type TestResult = anyhow::Result<()>;
fn make_uint8_band(width: u64, height: u64) -> anyhow::Result<RasterBuffer> {
let pixels: Vec<u8> = (0..(width * height) as usize)
.map(|i| (i % 256) as u8)
.collect();
RasterBuffer::new(
pixels,
width,
height,
RasterDataType::UInt8,
NoDataValue::None,
)
.map_err(|e| anyhow::anyhow!("{}", e))
}
fn write_interleaved_multiband(
path: &std::path::Path,
width: u64,
height: u64,
band_values: &[u8],
tiled: bool,
) -> anyhow::Result<()> {
let band_count = band_values.len();
let pixel_count = (width * height) as usize;
let mut data = vec![0u8; pixel_count * band_count];
for pixel in 0..pixel_count {
for (band_idx, &value) in band_values.iter().enumerate() {
data[pixel * band_count + band_idx] = value;
}
}
let mut config = WriterConfig::new(width, height, band_count as u16, RasterDataType::UInt8);
config.generate_overviews = false;
if tiled {
config.tile_width = Some(64);
config.tile_height = Some(64);
} else {
config.tile_width = None;
config.tile_height = None;
}
config.geo_transform = Some(standard_geotransform());
let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
.map_err(|e| anyhow::anyhow!("{}", e))?;
writer.write(&data).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(())
}
fn assert_band_is_constant(buffer: &RasterBuffer, expected: u8, context: &str) {
for (i, byte) in buffer.as_bytes().iter().enumerate() {
assert_eq!(
*byte, expected,
"{context}: byte {i} was {byte}, expected constant {expected}"
);
}
}
fn standard_geotransform() -> GeoTransform {
GeoTransform {
origin_x: 0.0,
origin_y: 100.0,
pixel_width: 1.0,
pixel_height: -1.0,
row_rotation: 0.0,
col_rotation: 0.0,
}
}
fn tmp_path(filename: &str) -> PathBuf {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut p = std::env::temp_dir();
p.push(format!(
"oxigeo_cli_conv_{}_{}_{}",
std::process::id(),
unique,
filename
));
p
}
#[test]
fn test_convert_geotiff_identity() -> TestResult {
let band = make_uint8_band(64, 64)?;
let out_path = tmp_path("identity.tif");
let _ = std::fs::remove_file(&out_path);
raster::write_multi_band(
&out_path,
&[band],
Some(standard_geotransform()),
Some(4326),
None,
)?;
assert!(out_path.exists(), "output file should exist after write");
let info = raster::read_raster_info(&out_path)?;
assert_eq!(info.width, 64, "width mismatch");
assert_eq!(info.height, 64, "height mismatch");
assert_eq!(info.bands, 1, "band count mismatch");
assert_eq!(info.epsg_code, Some(4326), "EPSG mismatch");
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_convert_cog_roundtrip() -> TestResult {
let band = make_uint8_band(256, 256)?;
let out_path = tmp_path("cog_roundtrip.tif");
let _ = std::fs::remove_file(&out_path);
let options = CogWriteOptions {
geo_transform: Some(standard_geotransform()),
epsg_code: Some(4326),
no_data_value: None,
overview_levels: vec![2, 4],
tile_size: 256,
compression: Compression::Lzw,
};
raster::write_raster_cog(&out_path, &[band], options)?;
assert!(out_path.exists(), "COG output should exist");
let info = raster::read_raster_info(&out_path)?;
assert_eq!(info.width, 256);
assert_eq!(info.height, 256);
assert_eq!(info.bands, 1);
assert_eq!(info.epsg_code, Some(4326));
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_convert_compression_deflate() -> TestResult {
let band = make_uint8_band(256, 256)?;
let out_path = tmp_path("cog_deflate.tif");
let _ = std::fs::remove_file(&out_path);
let options = CogWriteOptions {
geo_transform: None,
epsg_code: None,
no_data_value: None,
overview_levels: Vec::new(),
tile_size: 256,
compression: Compression::AdobeDeflate,
};
raster::write_raster_cog(&out_path, &[band], options)?;
let info = raster::read_raster_info(&out_path)?;
assert_eq!(info.width, 256);
assert_eq!(info.height, 256);
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_convert_cog_multiband() -> TestResult {
let r = make_uint8_band(128, 128)?;
let g = make_uint8_band(128, 128)?;
let b = make_uint8_band(128, 128)?;
let out_path = tmp_path("cog_multiband.tif");
let _ = std::fs::remove_file(&out_path);
let options = CogWriteOptions {
geo_transform: Some(standard_geotransform()),
epsg_code: None,
no_data_value: None,
overview_levels: Vec::new(),
tile_size: 128,
compression: Compression::Lzw,
};
raster::write_raster_cog(&out_path, &[r, g, b], options)?;
let info = raster::read_raster_info(&out_path)?;
assert_eq!(info.width, 128);
assert_eq!(info.height, 128);
assert_eq!(info.bands, 3);
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_convert_tile_size_respected() -> TestResult {
let band = make_uint8_band(128, 128)?;
let out_path = tmp_path("cog_tile128.tif");
let _ = std::fs::remove_file(&out_path);
let options = CogWriteOptions {
geo_transform: None,
epsg_code: None,
no_data_value: None,
overview_levels: Vec::new(),
tile_size: 128,
compression: Compression::Lzw,
};
raster::write_raster_cog(&out_path, &[band], options)?;
let source =
FileDataSource::open(&out_path).map_err(|e| anyhow::anyhow!("open datasource: {}", e))?;
let reader =
GeoTiffReader::open(source).map_err(|e| anyhow::anyhow!("open geotiff reader: {}", e))?;
let tile_size = reader.tile_size();
assert!(tile_size.is_some(), "COG must be tiled");
let (tw, th) = tile_size.ok_or_else(|| anyhow::anyhow!("tile size missing"))?;
assert_eq!(tw, 128, "tile width should be 128");
assert_eq!(th, 128, "tile height should be 128");
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_convert_cog_rejects_empty_bands() {
let out_path = tmp_path("cog_empty.tif");
let result = raster::write_raster_cog(&out_path, &[], CogWriteOptions::default());
assert!(
result.is_err(),
"write_raster_cog should error on empty band list"
);
}
const MULTIBAND_VALUES: [u8; 3] = [10, 120, 250];
#[test]
fn test_read_band_multiband_tiled() -> TestResult {
let out_path = tmp_path("read_band_tiled.tif");
let _ = std::fs::remove_file(&out_path);
write_interleaved_multiband(&out_path, 32, 32, &MULTIBAND_VALUES, true)?;
for (band_idx, &expected) in MULTIBAND_VALUES.iter().enumerate() {
let band = raster::read_band(&out_path, band_idx as u32)?;
assert_eq!(band.width(), 32);
assert_eq!(band.height(), 32);
assert_eq!(
band.as_bytes().len(),
32 * 32,
"band {band_idx}: buffer should hold exactly one band's worth of data"
);
assert_band_is_constant(&band, expected, &format!("tiled band {band_idx}"));
}
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_read_band_multiband_striped() -> TestResult {
let out_path = tmp_path("read_band_striped.tif");
let _ = std::fs::remove_file(&out_path);
write_interleaved_multiband(&out_path, 20, 17, &MULTIBAND_VALUES, false)?;
for (band_idx, &expected) in MULTIBAND_VALUES.iter().enumerate() {
let band = raster::read_band(&out_path, band_idx as u32)?;
assert_eq!(band.width(), 20);
assert_eq!(band.height(), 17);
assert_eq!(
band.as_bytes().len(),
20 * 17,
"band {band_idx}: buffer should hold exactly one band's worth of data"
);
assert_band_is_constant(&band, expected, &format!("striped band {band_idx}"));
}
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_read_band_region_multiband_tiled() -> TestResult {
let out_path = tmp_path("read_band_region_tiled.tif");
let _ = std::fs::remove_file(&out_path);
write_interleaved_multiband(&out_path, 96, 80, &MULTIBAND_VALUES, true)?;
for (band_idx, &expected) in MULTIBAND_VALUES.iter().enumerate() {
let region = raster::read_band_region(&out_path, band_idx as u32, 10, 5, 50, 40)?;
assert_eq!(region.width(), 50);
assert_eq!(region.height(), 40);
assert_eq!(region.as_bytes().len(), 50 * 40);
assert_band_is_constant(®ion, expected, &format!("tiled region band {band_idx}"));
}
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_read_band_region_multiband_striped() -> TestResult {
let out_path = tmp_path("read_band_region_striped.tif");
let _ = std::fs::remove_file(&out_path);
write_interleaved_multiband(&out_path, 40, 30, &MULTIBAND_VALUES, false)?;
for (band_idx, &expected) in MULTIBAND_VALUES.iter().enumerate() {
let region = raster::read_band_region(&out_path, band_idx as u32, 5, 3, 20, 15)?;
assert_eq!(region.width(), 20);
assert_eq!(region.height(), 15);
assert_eq!(region.as_bytes().len(), 20 * 15);
assert_band_is_constant(
®ion,
expected,
&format!("striped region band {band_idx}"),
);
}
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_read_band_out_of_range_rejected() -> TestResult {
let out_path = tmp_path("read_band_oob.tif");
let _ = std::fs::remove_file(&out_path);
write_interleaved_multiband(&out_path, 16, 16, &MULTIBAND_VALUES, true)?;
let err = raster::read_band(&out_path, 3).expect_err("band index 3 is out of range");
assert!(
err.to_string().contains("out of range"),
"unexpected error message: {err}"
);
let err = raster::read_band_region(&out_path, 99, 0, 0, 4, 4)
.expect_err("band index 99 is out of range");
assert!(
err.to_string().contains("out of range"),
"unexpected error message: {err}"
);
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_read_band_single_band_still_works() -> TestResult {
let out_path = tmp_path("read_band_single.tif");
let _ = std::fs::remove_file(&out_path);
write_interleaved_multiband(&out_path, 24, 24, &[77], true)?;
let band = raster::read_band(&out_path, 0)?;
assert_eq!(band.as_bytes().len(), 24 * 24);
assert_band_is_constant(&band, 77, "single band");
let _ = std::fs::remove_file(&out_path);
Ok(())
}
#[test]
fn test_cloud_uri_classification() {
use oxigeo_cli::util::cloud::is_cloud_uri;
assert!(is_cloud_uri("s3://bucket/key"));
assert!(is_cloud_uri("gs://bucket/obj"));
assert!(is_cloud_uri("az://container/blob"));
assert!(!is_cloud_uri("/local/path.tif"));
assert!(!is_cloud_uri("file:///local.tif"));
assert!(!is_cloud_uri("relative/path.tif"));
assert!(!is_cloud_uri(""));
}
fn write_positional_multiband(
path: &std::path::Path,
width: u64,
height: u64,
band_count: usize,
tiled: bool,
) -> anyhow::Result<()> {
let mut data = vec![0u8; (width * height) as usize * band_count];
for row in 0..height as usize {
for col in 0..width as usize {
for band in 0..band_count {
let pixel = row * width as usize + col;
data[pixel * band_count + band] = positional_sample(band, row, col);
}
}
}
let mut config = WriterConfig::new(width, height, band_count as u16, RasterDataType::UInt8);
config.generate_overviews = false;
if tiled {
config.tile_width = Some(64);
config.tile_height = Some(64);
} else {
config.tile_width = None;
config.tile_height = None;
}
config.geo_transform = Some(standard_geotransform());
let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())
.map_err(|e| anyhow::anyhow!("{}", e))?;
writer.write(&data).map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(())
}
fn positional_sample(band: usize, row: usize, col: usize) -> u8 {
((band * 37 + row * 7 + col * 3) % 251) as u8
}
fn assert_window_matches(
buffer: &RasterBuffer,
band: usize,
x_offset: usize,
y_offset: usize,
width: usize,
height: usize,
context: &str,
) {
let bytes = buffer.as_bytes();
assert_eq!(
bytes.len(),
width * height,
"{context}: window must hold exactly one band's worth of samples"
);
for row in 0..height {
for col in 0..width {
let got = bytes[row * width + col];
let want = positional_sample(band, y_offset + row, x_offset + col);
assert_eq!(
got,
want,
"{context}: window pixel ({row}, {col}) = source \
({}, {}) of band {band}: expected {want}, got {got}",
y_offset + row,
x_offset + col
);
}
}
}
#[test]
fn test_issue_14_read_band_region_multiband_window_is_positionally_correct() -> TestResult {
const BANDS: usize = 3;
for (label, tiled, w, h) in [("tiled", true, 96u64, 80u64), ("striped", false, 40, 30)] {
let out_path = tmp_path(&format!("issue_14_region_{label}.tif"));
let _ = std::fs::remove_file(&out_path);
write_positional_multiband(&out_path, w, h, BANDS, tiled)?;
let (x_off, y_off, win_w, win_h) = (7usize, 11usize, 23usize, 13usize);
for band in 0..BANDS {
let region = raster::read_band_region(
&out_path,
band as u32,
x_off as u64,
y_off as u64,
win_w as u64,
win_h as u64,
)?;
assert_eq!(region.width(), win_w as u64);
assert_eq!(region.height(), win_h as u64);
assert_window_matches(
®ion,
band,
x_off,
y_off,
win_w,
win_h,
&format!("{label} region band {band}"),
);
}
let _ = std::fs::remove_file(&out_path);
}
Ok(())
}
#[test]
fn test_issue_14_read_band_full_plane_is_positionally_correct() -> TestResult {
const BANDS: usize = 3;
for (label, tiled, w, h) in [("tiled", true, 96u64, 80u64), ("striped", false, 40, 30)] {
let out_path = tmp_path(&format!("issue_14_full_{label}.tif"));
let _ = std::fs::remove_file(&out_path);
write_positional_multiband(&out_path, w, h, BANDS, tiled)?;
for band in 0..BANDS {
let plane = raster::read_band(&out_path, band as u32)?;
assert_eq!(plane.width(), w);
assert_eq!(plane.height(), h);
assert_window_matches(
&plane,
band,
0,
0,
w as usize,
h as usize,
&format!("{label} full band {band}"),
);
}
let _ = std::fs::remove_file(&out_path);
}
Ok(())
}
#[test]
fn test_issue_14_read_band_region_clamps_overhanging_window() -> TestResult {
let out_path = tmp_path("issue_14_region_overhang.tif");
let _ = std::fs::remove_file(&out_path);
write_positional_multiband(&out_path, 40, 30, 2, false)?;
let region = raster::read_band_region(&out_path, 1, 30, 20, 20, 20)?;
assert_eq!(region.width(), 10, "width must clamp to the raster extent");
assert_eq!(
region.height(),
10,
"height must clamp to the raster extent"
);
assert_window_matches(®ion, 1, 30, 20, 10, 10, "clamped region band 1");
let _ = std::fs::remove_file(&out_path);
Ok(())
}