use oxigeo_core::io::DataSource;
use oxigeo_core::types::RasterDataType;
use oxigeo_geotiff::GeoTiffReader;
use oxigeo_geotiff::tiff::SampleFormat;
use crate::error::{QcError, QcResult};
const SCAN_CHUNK_MAX_BYTES: u64 = 8 << 20;
#[derive(Debug, Clone, Copy)]
pub(crate) struct RasterScan {
pub(crate) width: u64,
pub(crate) height: u64,
pub(crate) band_count: usize,
pub(crate) data_type: RasterDataType,
pub(crate) sample_format: SampleFormat,
pub(crate) bytes_per_sample: usize,
chunk_rows: u64,
}
impl RasterScan {
pub(crate) fn probe<S: DataSource>(reader: &GeoTiffReader<S>) -> QcResult<Self> {
let data_type = reader
.data_type()
.ok_or_else(|| QcError::RasterError("data type unknown".to_string()))?;
let bytes_per_sample = data_type.size_bytes();
if bytes_per_sample == 0 {
return Err(QcError::RasterError(format!(
"unsupported sample type {data_type:?} (zero bytes per sample)"
)));
}
let width = reader.width();
let row_bytes = width.max(1).saturating_mul(bytes_per_sample as u64);
let budget_rows = (SCAN_CHUNK_MAX_BYTES / row_bytes.max(1)).max(1);
let chunk_rows = match reader.tile_size() {
Some((_, tile_height)) if tile_height > 0 => {
let tile_height = u64::from(tile_height);
if tile_height <= budget_rows {
tile_height * (budget_rows / tile_height)
} else {
budget_rows
}
}
_ => budget_rows,
};
Ok(Self {
width,
height: reader.height(),
band_count: reader.band_count() as usize,
data_type,
sample_format: sample_format_of(data_type),
bytes_per_sample,
chunk_rows: chunk_rows.max(1),
})
}
pub(crate) const fn total_pixels(&self) -> u64 {
self.width * self.height
}
}
pub(crate) mod native {
macro_rules! native_reader {
($name:ident, $ty:ty, $width:literal) => {
#[doc = concat!("Reads one host-native `", stringify!($ty), "` from the front of `bytes`.")]
pub(crate) fn $name(bytes: &[u8]) -> Option<$ty> {
let head: [u8; $width] = bytes.get(..$width)?.try_into().ok()?;
Some(<$ty>::from_ne_bytes(head))
}
};
}
native_reader!(read_u16, u16, 2);
native_reader!(read_i16, i16, 2);
native_reader!(read_u32, u32, 4);
native_reader!(read_i32, i32, 4);
native_reader!(read_u64, u64, 8);
native_reader!(read_i64, i64, 8);
native_reader!(read_f32, f32, 4);
native_reader!(read_f64, f64, 8);
}
const fn sample_format_of(data_type: RasterDataType) -> SampleFormat {
match data_type {
RasterDataType::UInt8
| RasterDataType::UInt16
| RasterDataType::UInt32
| RasterDataType::UInt64 => SampleFormat::UnsignedInteger,
RasterDataType::Int8
| RasterDataType::Int16
| RasterDataType::Int32
| RasterDataType::Int64 => SampleFormat::SignedInteger,
RasterDataType::Float32 | RasterDataType::Float64 => SampleFormat::IeeeFloatingPoint,
RasterDataType::CFloat32 | RasterDataType::CFloat64 => SampleFormat::ComplexFloatingPoint,
}
}
pub(crate) fn scan_band<S, F>(
reader: &GeoTiffReader<S>,
scan: &RasterScan,
band: usize,
mut visit: F,
) -> QcResult<()>
where
S: DataSource,
F: FnMut(u64, &[u8]) -> QcResult<()>,
{
if scan.width == 0 || scan.height == 0 {
return Ok(());
}
if band >= scan.band_count {
return Err(QcError::RasterError(format!(
"band {band} is out of range for a {}-band raster",
scan.band_count
)));
}
let row_bytes = usize::try_from(scan.width)
.ok()
.and_then(|w| w.checked_mul(scan.bytes_per_sample))
.ok_or_else(|| {
QcError::RasterError(format!(
"raster row of {} samples does not fit in memory on this target",
scan.width
))
})?;
let mut buffer: Vec<u8> = Vec::new();
let mut first_row = 0u64;
while first_row < scan.height {
let rows = scan.chunk_rows.min(scan.height - first_row);
let len = usize::try_from(rows)
.ok()
.and_then(|r| r.checked_mul(row_bytes))
.ok_or_else(|| {
QcError::RasterError("scan stripe does not fit in memory".to_string())
})?;
buffer.resize(len, 0);
reader
.read_window_into(0, band, 0, first_row, scan.width, rows, &mut buffer[..len])
.map_err(|e| QcError::RasterError(format!("read_window_into failed: {e}")))?;
visit(first_row, &buffer[..len])?;
first_row += rows;
}
Ok(())
}