use crate::{Dataset, OxiGeoError, Result};
use oxigeo_core::buffer::{RasterBuffer, RasterElement};
#[cfg(any(feature = "geotiff", feature = "vrt"))]
use crate::DatasetFormat;
#[cfg(any(feature = "geotiff", feature = "vrt"))]
use oxigeo_core::types::RasterDataType;
#[cfg(feature = "geotiff")]
use oxigeo_core::io::FileDataSource;
#[cfg(feature = "geotiff")]
use oxigeo_core::types::NoDataValue;
#[cfg(feature = "geotiff")]
use oxigeo_geotiff::GeoTiffReader;
#[cfg(feature = "geotiff")]
type LocalTiffReader = GeoTiffReader<FileDataSource>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PixelWindow {
pub col: u32,
pub row: u32,
pub width: u32,
pub height: u32,
}
#[cfg(feature = "geotiff")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SourceWindow {
x: u64,
y: u64,
width: u64,
height: u64,
full_band: bool,
}
#[cfg(feature = "geotiff")]
impl SourceWindow {
fn pixel_count(&self) -> Result<usize> {
usize::try_from(self.width)
.ok()
.and_then(|w| {
usize::try_from(self.height)
.ok()
.and_then(|h| w.checked_mul(h))
})
.ok_or_else(|| OxiGeoError::Internal {
message: format!(
"window {}×{} overflows the address space",
self.width, self.height
),
})
}
}
#[cfg(feature = "geotiff")]
pub(crate) fn crop_interleaved(
data: &[u8],
full_width: u32,
full_height: u32,
window: PixelWindow,
) -> Option<Vec<u8>> {
let full_w = full_width as usize;
let full_h = full_height as usize;
if full_w == 0 || full_h == 0 {
return None;
}
let total_px = full_w.checked_mul(full_h)?;
if total_px == 0 || !data.len().is_multiple_of(total_px) {
return None;
}
let stride = data.len() / total_px;
let col = window.col as usize;
let row = window.row as usize;
let w = window.width as usize;
let h = window.height as usize;
if col.checked_add(w)? > full_w || row.checked_add(h)? > full_h {
return None;
}
let mut out = Vec::with_capacity(w.saturating_mul(h).saturating_mul(stride));
for r in 0..h {
let src_row_start = (row + r) * full_w;
for c in 0..w {
let px = (src_row_start + col + c) * stride;
out.extend_from_slice(&data[px..px + stride]);
}
}
Some(out)
}
impl Dataset {
pub fn read_band(&self, band: u32) -> Result<RasterBuffer> {
self.validate_band(band)?;
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, None)?;
return self.read_buffer_with(&reader, band, window);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, None)?;
return self.read_vrt_buffer(&reader, band, window);
}
Err(self.unsupported("read_band()"))
}
pub fn read_band_into<T: RasterElement>(&self, band: u32, dst: &mut [T]) -> Result<()> {
self.validate_band(band)?;
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, None)?;
return self.read_typed_with(&reader, band, window, dst);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, None)?;
return self.read_vrt_interleaved(&reader, &[band], window, dst);
}
let _ = dst;
Err(self.unsupported("read_band_into()"))
}
pub fn bands(&self) -> BandIter<'_> {
BandIter {
dataset: self,
next_band: 0,
band_count: self.info.band_count,
#[cfg(feature = "geotiff")]
reader: None,
#[cfg(feature = "geotiff")]
reader_unavailable: false,
}
}
pub fn read_window(
&self,
band: u32,
col: u32,
row: u32,
width: u32,
height: u32,
) -> Result<RasterBuffer> {
Self::validate_window_size(width, height)?;
self.validate_band(band)?;
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, Some((col, row, width, height)))?;
return self.read_buffer_with(&reader, band, window);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, Some((col, row, width, height)))?;
return self.read_vrt_buffer(&reader, band, window);
}
let _ = (col, row);
Err(self.unsupported("read_window()"))
}
pub fn read_window_into<T: RasterElement>(
&self,
band: u32,
col: u32,
row: u32,
width: u32,
height: u32,
dst: &mut [T],
) -> Result<()> {
Self::validate_window_size(width, height)?;
self.validate_band(band)?;
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, Some((col, row, width, height)))?;
return self.read_typed_with(&reader, band, window, dst);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, Some((col, row, width, height)))?;
return self.read_vrt_interleaved(&reader, &[band], window, dst);
}
let _ = (col, row, dst);
Err(self.unsupported("read_window_into()"))
}
pub fn read_interleaved<T: RasterElement>(&self, bands: Option<&[u32]>) -> Result<Vec<T>> {
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, None)?;
return self.alloc_interleaved(&reader, bands, window);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, None)?;
let selection = self.resolve_vrt_bands(bands)?;
let pixels =
usize::try_from(window.x_size.saturating_mul(window.y_size)).map_err(|_| {
OxiGeoError::InvalidParameter {
parameter: "window",
message: "window is too large for this platform".to_string(),
}
})?;
let mut out = vec![T::default(); pixels.saturating_mul(selection.len())];
self.read_vrt_interleaved(&reader, &selection, window, &mut out)?;
return Ok(out);
}
let _ = bands;
Err(self.unsupported("read_interleaved()"))
}
pub fn read_interleaved_into<T: RasterElement>(
&self,
bands: Option<&[u32]>,
dst: &mut [T],
) -> Result<()> {
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, None)?;
let selection = self.resolve_bands(&reader, bands)?;
return self.read_interleaved_with(&reader, &selection, window, dst);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, None)?;
let selection = self.resolve_vrt_bands(bands)?;
return self.read_vrt_interleaved(&reader, &selection, window, dst);
}
let _ = (bands, dst);
Err(self.unsupported("read_interleaved_into()"))
}
pub fn read_window_interleaved<T: RasterElement>(
&self,
bands: Option<&[u32]>,
col: u32,
row: u32,
width: u32,
height: u32,
) -> Result<Vec<T>> {
Self::validate_window_size(width, height)?;
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, Some((col, row, width, height)))?;
return self.alloc_interleaved(&reader, bands, window);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, Some((col, row, width, height)))?;
let selection = self.resolve_vrt_bands(bands)?;
let pixels =
usize::try_from(window.x_size.saturating_mul(window.y_size)).map_err(|_| {
OxiGeoError::InvalidParameter {
parameter: "window",
message: "window is too large for this platform".to_string(),
}
})?;
let mut out = vec![T::default(); pixels.saturating_mul(selection.len())];
self.read_vrt_interleaved(&reader, &selection, window, &mut out)?;
return Ok(out);
}
let _ = (bands, col, row);
Err(self.unsupported("read_window_interleaved()"))
}
pub fn read_window_interleaved_into<T: RasterElement>(
&self,
bands: Option<&[u32]>,
col: u32,
row: u32,
width: u32,
height: u32,
dst: &mut [T],
) -> Result<()> {
Self::validate_window_size(width, height)?;
#[cfg(feature = "geotiff")]
if matches!(self.info.format, DatasetFormat::GeoTiff) {
let reader = self.open_geotiff_reader()?;
let window = self.resolve_source_window(&reader, Some((col, row, width, height)))?;
let selection = self.resolve_bands(&reader, bands)?;
return self.read_interleaved_with(&reader, &selection, window, dst);
}
#[cfg(feature = "vrt")]
if matches!(self.info.format, DatasetFormat::Vrt) {
let reader = self.open_vrt_reader()?;
let window = self.resolve_vrt_window(&reader, Some((col, row, width, height)))?;
let selection = self.resolve_vrt_bands(bands)?;
return self.read_vrt_interleaved(&reader, &selection, window, dst);
}
let _ = (bands, col, row, dst);
Err(self.unsupported("read_window_interleaved_into()"))
}
fn validate_band(&self, band: u32) -> Result<()> {
if self.info.band_count > 0 && band >= self.info.band_count {
return Err(OxiGeoError::InvalidParameter {
parameter: "band",
message: format!(
"band index {} is out of range (dataset has {} bands)",
band, self.info.band_count
),
});
}
Ok(())
}
fn validate_window_size(width: u32, height: u32) -> Result<()> {
if width == 0 || height == 0 {
return Err(OxiGeoError::InvalidParameter {
parameter: "window",
message: format!("window size must be non-zero, got {width}×{height}"),
});
}
Ok(())
}
#[cfg(feature = "vrt")]
fn open_vrt_reader(&self) -> Result<oxigeo_vrt::VrtReader> {
oxigeo_vrt::VrtReader::open(&self.path).map_err(|e| {
OxiGeoError::Format(oxigeo_core::error::FormatError::InvalidHeader {
message: format!("failed to open VRT '{}': {e}", self.path),
})
})
}
#[cfg(feature = "vrt")]
fn resolve_vrt_window(
&self,
reader: &oxigeo_vrt::VrtReader,
request: Option<(u32, u32, u32, u32)>,
) -> Result<oxigeo_vrt::PixelRect> {
let (file_w, file_h) = (reader.width(), reader.height());
let (view_x, view_y, view_w, view_h) = match self.clip_window {
Some(window) => (
u64::from(window.col),
u64::from(window.row),
u64::from(window.width),
u64::from(window.height),
),
None => (0, 0, file_w, file_h),
};
if view_x.saturating_add(view_w) > file_w || view_y.saturating_add(view_h) > file_h {
return Err(OxiGeoError::Internal {
message: format!(
"clip window [{view_x},{view_y} {view_w}×{view_h}] does not fit VRT raster {file_w}×{file_h}"
),
});
}
let (x, y, width, height) = match request {
Some((col, row, width, height)) => {
let (col, row) = (u64::from(col), u64::from(row));
let (width, height) = (u64::from(width), u64::from(height));
if col.saturating_add(width) > view_w || row.saturating_add(height) > view_h {
return Err(OxiGeoError::InvalidParameter {
parameter: "window",
message: format!(
"window [{col},{row} {width}×{height}] extends past the dataset extent {view_w}×{view_h}"
),
});
}
(view_x + col, view_y + row, width, height)
}
None => (view_x, view_y, view_w, view_h),
};
Ok(oxigeo_vrt::PixelRect::new(x, y, width, height))
}
#[cfg(feature = "vrt")]
fn read_vrt_buffer(
&self,
reader: &oxigeo_vrt::VrtReader,
band: u32,
window: oxigeo_vrt::PixelRect,
) -> Result<RasterBuffer> {
let band_1based = usize::try_from(band)
.map_err(|_| OxiGeoError::InvalidParameter {
parameter: "band",
message: format!("band index {band} does not fit this platform's usize"),
})?
.saturating_add(1);
reader.read_window(band_1based, window).map_err(|e| {
OxiGeoError::Format(oxigeo_core::error::FormatError::InvalidHeader {
message: format!("failed to read VRT '{}' band {band}: {e}", self.path),
})
})
}
#[cfg(feature = "vrt")]
fn read_vrt_interleaved<T: RasterElement>(
&self,
reader: &oxigeo_vrt::VrtReader,
bands: &[u32],
window: oxigeo_vrt::PixelRect,
dst: &mut [T],
) -> Result<()> {
let pixels =
usize::try_from(window.x_size.saturating_mul(window.y_size)).map_err(|_| {
OxiGeoError::InvalidParameter {
parameter: "window",
message: "window is too large for this platform".to_string(),
}
})?;
let expected =
pixels
.checked_mul(bands.len())
.ok_or_else(|| OxiGeoError::InvalidParameter {
parameter: "window",
message: "window × band count overflows".to_string(),
})?;
if dst.len() != expected {
return Err(OxiGeoError::InvalidParameter {
parameter: "dst",
message: format!(
"destination length {} does not match the {expected} samples this read produces",
dst.len()
),
});
}
let stride = bands.len();
for (slot, band) in bands.iter().enumerate() {
let buffer = self.read_vrt_buffer(reader, *band, window)?;
let samples = buffer.as_bytes();
let source_type = buffer.data_type();
let sample_size = source_type.size_bytes();
for index in 0..pixels {
let offset = index * sample_size;
let value = samples
.get(offset..offset + sample_size)
.and_then(|bytes| read_native_sample(bytes, source_type))
.unwrap_or(0.0);
dst[index * stride + slot] = T::from_raster_f64(value);
}
}
Ok(())
}
#[cfg(feature = "vrt")]
fn resolve_vrt_bands(&self, bands: Option<&[u32]>) -> Result<Vec<u32>> {
match bands {
Some(list) => {
if list.is_empty() {
return Err(OxiGeoError::InvalidParameter {
parameter: "bands",
message: "band selection must name at least one band".to_string(),
});
}
for band in list {
self.validate_band(*band)?;
}
Ok(list.to_vec())
}
None => Ok((0..self.info.band_count).collect()),
}
}
fn unsupported(&self, operation: &str) -> OxiGeoError {
OxiGeoError::NotSupported {
operation: format!(
"{operation} is not supported for format '{}' (enable the 'geotiff' feature for GeoTIFF support)",
self.info.format.driver_name()
),
}
}
#[cfg(feature = "geotiff")]
fn open_geotiff_reader(&self) -> Result<LocalTiffReader> {
let source = FileDataSource::open(&self.path).map_err(|e| {
OxiGeoError::Io(oxigeo_core::error::IoError::Read {
message: format!("failed to open '{}': {e}", self.path),
})
})?;
GeoTiffReader::open(source)
}
#[cfg(feature = "geotiff")]
fn resolve_source_window(
&self,
reader: &LocalTiffReader,
request: Option<(u32, u32, u32, u32)>,
) -> Result<SourceWindow> {
let file_w = reader.width();
let file_h = reader.height();
let (view_x, view_y, view_w, view_h) = match self.clip_window {
Some(window) => (
u64::from(window.col),
u64::from(window.row),
u64::from(window.width),
u64::from(window.height),
),
None => (0, 0, file_w, file_h),
};
if view_x.saturating_add(view_w) > file_w || view_y.saturating_add(view_h) > file_h {
return Err(OxiGeoError::Internal {
message: format!(
"clip window [{view_x},{view_y} {view_w}×{view_h}] does not fit source raster {file_w}×{file_h}"
),
});
}
let (x, y, width, height) = match request {
Some((col, row, width, height)) => {
let (col, row) = (u64::from(col), u64::from(row));
let (width, height) = (u64::from(width), u64::from(height));
if col.saturating_add(width) > view_w || row.saturating_add(height) > view_h {
return Err(OxiGeoError::InvalidParameter {
parameter: "window",
message: format!(
"window [{col},{row} {width}×{height}] extends past dataset extent {view_w}×{view_h}"
),
});
}
(view_x + col, view_y + row, width, height)
}
None => (view_x, view_y, view_w, view_h),
};
Ok(SourceWindow {
x,
y,
width,
height,
full_band: x == 0 && y == 0 && width == file_w && height == file_h,
})
}
#[cfg(feature = "geotiff")]
fn sample_size(reader: &LocalTiffReader, data_type: RasterDataType) -> Result<usize> {
let pixels = reader.band_pixel_count(0)?;
if pixels == 0 {
return Ok(data_type.size_bytes());
}
Ok(reader.band_byte_len(0)? / pixels)
}
#[cfg(feature = "geotiff")]
fn read_buffer_with(
&self,
reader: &LocalTiffReader,
band: u32,
window: SourceWindow,
) -> Result<RasterBuffer> {
let data_type = reader.data_type().unwrap_or(RasterDataType::UInt8);
let sample_size = Self::sample_size(reader, data_type)?;
let len = window
.pixel_count()?
.checked_mul(sample_size)
.ok_or_else(|| OxiGeoError::Internal {
message: format!(
"window {}×{} of {sample_size}-byte samples overflows the address space",
window.width, window.height
),
})?;
let mut bytes = vec![0u8; len];
if window.full_band {
reader.read_band_into(0, band as usize, &mut bytes)?;
} else {
reader.read_window_into(
0,
band as usize,
window.x,
window.y,
window.width,
window.height,
&mut bytes,
)?;
}
RasterBuffer::new(
bytes,
window.width,
window.height,
data_type,
NoDataValue::None,
)
.map_err(|e| OxiGeoError::Internal {
message: format!("failed to create RasterBuffer: {e}"),
})
}
#[cfg(feature = "geotiff")]
fn read_typed_with<T: RasterElement>(
&self,
reader: &LocalTiffReader,
band: u32,
window: SourceWindow,
dst: &mut [T],
) -> Result<()> {
let expected = window.pixel_count()?;
if dst.len() != expected {
return Err(OxiGeoError::InvalidParameter {
parameter: "dst",
message: format!(
"destination buffer must hold exactly {expected} elements ({}×{} pixels), got {}",
window.width,
window.height,
dst.len()
),
});
}
if window.full_band {
reader.read_band_into_typed(0, band as usize, dst)
} else {
reader.read_window_into_typed(
0,
band as usize,
window.x,
window.y,
window.width,
window.height,
dst,
)
}
}
#[cfg(feature = "geotiff")]
fn resolve_bands<'a>(
&self,
reader: &LocalTiffReader,
bands: Option<&'a [u32]>,
) -> Result<std::borrow::Cow<'a, [u32]>> {
match bands {
Some(list) => {
if list.is_empty() {
return Err(OxiGeoError::InvalidParameter {
parameter: "bands",
message: "band selection must name at least one band".to_string(),
});
}
for &band in list {
self.validate_band(band)?;
}
Ok(std::borrow::Cow::Borrowed(list))
}
None => {
let count = if self.info.band_count > 0 {
self.info.band_count
} else {
reader.band_count()
};
if count == 0 {
return Err(OxiGeoError::InvalidParameter {
parameter: "bands",
message: "dataset reports no raster bands to interleave".to_string(),
});
}
Ok(std::borrow::Cow::Owned((0..count).collect()))
}
}
}
#[cfg(feature = "geotiff")]
fn alloc_interleaved<T: RasterElement>(
&self,
reader: &LocalTiffReader,
bands: Option<&[u32]>,
window: SourceWindow,
) -> Result<Vec<T>> {
let selection = self.resolve_bands(reader, bands)?;
let len = window
.pixel_count()?
.checked_mul(selection.len())
.ok_or_else(|| OxiGeoError::Internal {
message: format!(
"{}×{} pixels × {} bands overflows the address space",
window.width,
window.height,
selection.len()
),
})?;
let mut out = vec![T::default(); len];
self.read_interleaved_with(reader, &selection, window, &mut out)?;
Ok(out)
}
#[cfg(feature = "geotiff")]
fn read_interleaved_with<T: RasterElement>(
&self,
reader: &LocalTiffReader,
bands: &[u32],
window: SourceWindow,
dst: &mut [T],
) -> Result<()> {
if bands.is_empty() {
return Err(OxiGeoError::InvalidParameter {
parameter: "bands",
message: "band selection must name at least one band".to_string(),
});
}
let pixels = window.pixel_count()?;
let slots = bands.len();
let expected = pixels
.checked_mul(slots)
.ok_or_else(|| OxiGeoError::Internal {
message: format!(
"{}×{} pixels × {slots} bands overflows the address space",
window.width, window.height
),
})?;
if dst.len() != expected {
return Err(OxiGeoError::InvalidParameter {
parameter: "dst",
message: format!(
"destination buffer must hold exactly {expected} elements ({}×{} pixels × {slots} bands), got {}",
window.width,
window.height,
dst.len()
),
});
}
if let [band] = bands {
return self.read_typed_with(reader, *band, window, dst);
}
let selection = bands
.iter()
.map(|&band| {
usize::try_from(band).map_err(|_| OxiGeoError::Internal {
message: format!("band index {band} overflows the address space"),
})
})
.collect::<Result<Vec<usize>>>()?;
if window.full_band {
reader.read_bands_into_typed(0, &selection, dst)
} else {
reader.read_window_bands_into_typed(
0,
&selection,
window.x,
window.y,
window.width,
window.height,
dst,
)
}
}
}
pub struct BandIter<'a> {
dataset: &'a Dataset,
next_band: u32,
band_count: u32,
#[cfg(feature = "geotiff")]
reader: Option<LocalTiffReader>,
#[cfg(feature = "geotiff")]
reader_unavailable: bool,
}
impl BandIter<'_> {
#[cfg(feature = "geotiff")]
fn read_shared(&mut self, band: u32) -> Option<Result<RasterBuffer>> {
if !matches!(self.dataset.info.format, DatasetFormat::GeoTiff) {
return None;
}
if self.reader.is_none() && !self.reader_unavailable {
match self.dataset.open_geotiff_reader() {
Ok(reader) => self.reader = Some(reader),
Err(_) => self.reader_unavailable = true,
}
}
let reader = self.reader.as_ref()?;
Some(
self.dataset
.resolve_source_window(reader, None)
.and_then(|window| self.dataset.read_buffer_with(reader, band, window)),
)
}
}
impl Iterator for BandIter<'_> {
type Item = Result<RasterBuffer>;
fn next(&mut self) -> Option<Self::Item> {
if self.next_band >= self.band_count {
return None;
}
let band = self.next_band;
self.next_band += 1;
#[cfg(feature = "geotiff")]
if let Some(result) = self.read_shared(band) {
return Some(result);
}
Some(self.dataset.read_band(band))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = (self.band_count.saturating_sub(self.next_band)) as usize;
(remaining, Some(remaining))
}
}
impl core::iter::ExactSizeIterator for BandIter<'_> {}
#[cfg(feature = "vrt")]
fn read_native_sample(bytes: &[u8], data_type: RasterDataType) -> Option<f64> {
let value = match data_type {
RasterDataType::UInt8 => f64::from(*bytes.first()?),
RasterDataType::Int8 => f64::from(*bytes.first()? as i8),
RasterDataType::UInt16 => f64::from(u16::from_ne_bytes(bytes.try_into().ok()?)),
RasterDataType::Int16 => f64::from(i16::from_ne_bytes(bytes.try_into().ok()?)),
RasterDataType::UInt32 => f64::from(u32::from_ne_bytes(bytes.try_into().ok()?)),
RasterDataType::Int32 => f64::from(i32::from_ne_bytes(bytes.try_into().ok()?)),
RasterDataType::Float32 => f64::from(f32::from_ne_bytes(bytes.try_into().ok()?)),
RasterDataType::Float64 => f64::from_ne_bytes(bytes.try_into().ok()?),
_ => return None,
};
Some(value)
}
#[cfg(all(test, feature = "geotiff"))]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
struct TempPath(PathBuf);
impl TempPath {
fn new(name: &str) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
Self(std::env::temp_dir().join(format!(
"oxigeo_raster_read_{}_{seq}_{name}",
std::process::id()
)))
}
}
impl std::ops::Deref for TempPath {
type Target = Path;
fn deref(&self) -> &Path {
&self.0
}
}
impl AsRef<Path> for TempPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl Drop for TempPath {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
use crate::{BoundingBox, DatasetInfo, GeoTransform};
use oxigeo_core::types::RasterDataType;
#[test]
fn test_crop_interleaved_single_band() {
let data: Vec<u8> = (0u8..16).collect();
let window = PixelWindow {
col: 1,
row: 1,
width: 2,
height: 2,
};
let out = crop_interleaved(&data, 4, 4, window).expect("crop");
assert_eq!(out, vec![5, 6, 9, 10]);
}
#[test]
fn test_crop_interleaved_multiband_stride() {
let data: Vec<u8> = vec![
0, 0, 0, 1, 0, 0,
0, 1, 0, 1, 1, 0,
];
let window = PixelWindow {
col: 1,
row: 0,
width: 1,
height: 2,
};
let out = crop_interleaved(&data, 2, 2, window).expect("crop");
assert_eq!(out, vec![1, 0, 0, 1, 1, 0]);
}
#[test]
fn test_crop_interleaved_out_of_bounds_none() {
let data: Vec<u8> = (0u8..16).collect();
let window = PixelWindow {
col: 3,
row: 3,
width: 2,
height: 2,
};
assert!(crop_interleaved(&data, 4, 4, window).is_none());
}
pub(crate) fn write_test_geotiff_4x4(path: &std::path::Path) {
use crate::builder::{DatasetCreateBuilder, OutputFormat};
let mut writer = DatasetCreateBuilder::new(path, OutputFormat::GeoTiff)
.create()
.expect("create writer");
writer.set_dimensions(4, 4, 1).expect("dims");
writer.set_data_type(RasterDataType::UInt8);
writer.set_geo_transform(GeoTransform::north_up(0.0, 4.0, 1.0, 1.0));
let data: Vec<u8> = (0u8..16).collect();
writer.write_all_bands(&data).expect("write bands");
writer.finalize().expect("finalize");
}
pub(crate) fn dataset_over_4x4(path: &std::path::Path) -> Dataset {
let gt = GeoTransform::north_up(0.0, 4.0, 1.0, 1.0);
let info = DatasetInfo {
format: crate::DatasetFormat::GeoTiff,
path: Some(path.to_string_lossy().into_owned()),
width: Some(4),
height: Some(4),
band_count: 1,
geotransform: Some(gt),
data_type: Some(RasterDataType::UInt8),
..DatasetInfo::default()
};
Dataset::from_info(path.to_string_lossy().into_owned(), info)
}
#[test]
fn test_read_window_reads_real_pixels() {
let path = TempPath::new("read_window_test.tif");
write_test_geotiff_4x4(&path);
let ds = Dataset::open(path.to_str().expect("path")).expect("open");
let buf = ds.read_window(0, 1, 1, 2, 2).expect("read window");
assert_eq!(buf.width(), 2);
assert_eq!(buf.height(), 2);
assert_eq!(buf.as_bytes(), &[5u8, 6, 9, 10]);
assert!(ds.read_window(0, 3, 3, 2, 2).is_err());
}
#[test]
fn test_read_band_into_matches_read_band() {
let path = TempPath::new("read_band_into_unit.tif");
write_test_geotiff_4x4(&path);
let ds = Dataset::open(path.to_str().expect("path")).expect("open");
let buf = ds.read_band(0).expect("read band");
let mut dst = vec![0u8; 16];
ds.read_band_into(0, &mut dst).expect("read band into");
assert_eq!(dst.as_slice(), buf.as_bytes());
let mut wide = vec![0.0f64; 16];
ds.read_band_into(0, &mut wide).expect("read band into f64");
let expected: Vec<f64> = (0..16).map(|v| v as f64).collect();
assert_eq!(wide, expected);
let mut short = vec![0.0f64; 15];
let err = ds
.read_band_into(0, &mut short)
.expect_err("wrong length must error");
assert!(
err.to_string().contains("16"),
"error should name the expected length: {err}"
);
}
#[test]
fn test_read_into_honours_clip_window() {
let path = TempPath::new("read_into_clip_unit.tif");
write_test_geotiff_4x4(&path);
let ds = dataset_over_4x4(&path);
let gt = ds.geotransform().copied().expect("gt");
let (x0, y0) = gt.pixel_to_world(1.0, 1.0);
let (x1, y1) = gt.pixel_to_world(3.0, 3.0);
let bbox = BoundingBox::new(x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)).expect("bbox");
let clipped = ds.clip(bbox).expect("clip");
let mut dst = vec![0u8; clipped.width() as usize * clipped.height() as usize];
clipped.read_band_into(0, &mut dst).expect("clipped read");
assert_eq!(dst, vec![5u8, 6, 9, 10]);
let mut full = vec![0u8; 16];
assert!(clipped.read_band_into(0, &mut full).is_err());
let mut one = vec![0u8; 1];
clipped
.read_window_into(0, 1, 1, 1, 1, &mut one)
.expect("clipped window");
assert_eq!(one, vec![10u8]);
}
#[test]
fn test_clip_is_honored_by_statistics() {
let path = TempPath::new("clip_stats_test.tif");
write_test_geotiff_4x4(&path);
let ds = dataset_over_4x4(&path);
let full = ds.statistics(0).expect("full stats");
assert_eq!(full.valid_count, 16);
let gt = ds.geotransform().copied().expect("gt");
let (x0, y0) = gt.pixel_to_world(1.0, 1.0);
let (x1, y1) = gt.pixel_to_world(3.0, 3.0);
let bbox = BoundingBox::new(x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)).expect("bbox");
let clipped = ds.clip(bbox).expect("clip");
assert_eq!(clipped.width(), 2, "clipped width");
assert_eq!(clipped.height(), 2, "clipped height");
let clip_stats = clipped.statistics(0).expect("clip stats");
assert_eq!(clip_stats.valid_count, 4, "clip honored by statistics");
assert_eq!(clip_stats.min, 5.0);
assert_eq!(clip_stats.max, 10.0);
}
#[test]
fn test_clip_is_honored_by_convert() {
let path = TempPath::new("clip_convert_src.tif");
write_test_geotiff_4x4(&path);
let out = TempPath::new("clip_convert_out.tif");
let ds = dataset_over_4x4(&path);
let gt = ds.geotransform().copied().expect("gt");
let (x0, y0) = gt.pixel_to_world(1.0, 1.0);
let (x1, y1) = gt.pixel_to_world(3.0, 3.0);
let bbox = BoundingBox::new(x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)).expect("bbox");
let clipped = ds.clip(bbox).expect("clip");
let converted = clipped
.convert(
&out,
crate::DatasetFormat::GeoTiff,
crate::ConversionOptions::default(),
)
.expect("convert");
assert_eq!(converted.width(), 2);
assert_eq!(converted.height(), 2);
let stats = converted.statistics(0).expect("stats");
assert_eq!(stats.valid_count, 4);
}
}