use crate::formats::RasterDriver;
use crate::{Error, GeoTiffWriter, RasterDataset, RasterWriteOptions, Result};
use wbraster::raster::RasterData;
const STRIP_BYTES: usize = 64 << 20;
pub(crate) fn strip_rows(width: usize, bands: usize, value_size: usize, block: Option<usize>) -> usize {
let row = (width * bands.max(1) * value_size).max(1);
let block = block.unwrap_or(1).max(1);
(STRIP_BYTES / row / block).max(1) * block
}
pub(crate) fn overwrites(output: &str, inputs: &[String]) -> bool {
let Ok(out) = std::fs::canonicalize(output) else { return false };
inputs.iter().any(|p| std::fs::canonicalize(p).is_ok_and(|p| p == out))
}
pub(crate) struct StripWriter {
inner: Option<Inner>,
path: String,
rows: usize,
bands: usize,
width: usize,
}
enum Inner {
#[cfg(not(target_arch = "wasm32"))]
Thread { tx: std::sync::mpsc::SyncSender<(RasterData, usize)>, handle: Option<std::thread::JoinHandle<Result<()>>> },
#[cfg(target_arch = "wasm32")]
Tiff(GeoTiffWriter),
Whole { ds: Box<RasterDataset>, options: Box<RasterWriteOptions>, done: usize },
}
impl StripWriter {
pub fn create(header: &RasterDataset, path: &str, options: &RasterWriteOptions) -> Result<Self> {
let r = &header.raster;
let (rows, bands, width) = (r.rows, r.bands, r.cols);
let tiff = options.driver.or_else(|| RasterDriver::for_extension(path)) == Some(RasterDriver::GTiff)
&& options.mask.is_none()
&& options.overviews.is_empty();
if tiff && crate::drivers::gtiff_write::TiffOptions::from_write_options(options, false, header.nodata())?.is_some() {
crate::dataset::ensure_writable_location(path)?;
let inner = match GeoTiffWriter::create(path, header, options)? {
#[cfg(not(target_arch = "wasm32"))]
mut w => {
let (tx, rx) = std::sync::mpsc::sync_channel::<(RasterData, usize)>(1);
let handle = std::thread::spawn(move || {
for (data, n) in rx {
w.write_rows(&data, n, None)?;
}
w.finish()
});
Inner::Thread { tx, handle: Some(handle) }
}
#[cfg(target_arch = "wasm32")]
w => Inner::Tiff(w),
};
return Ok(Self { inner: Some(inner), path: path.to_string(), rows, bands, width });
}
let mut ds = header.clone();
ds.raster.data = RasterData::new_filled(r.data_type, width * rows * bands, 0.0);
Ok(Self {
inner: Some(Inner::Whole { ds: Box::new(ds), options: Box::new(options.clone()), done: 0 }),
path: path.to_string(),
rows,
bands,
width,
})
}
pub fn write(&mut self, data: RasterData, rows: usize) -> Result<()> {
let Some(inner) = &mut self.inner else { return Err(Error::InvalidArgument("the raster is written".into())) };
match inner {
#[cfg(not(target_arch = "wasm32"))]
Inner::Thread { tx, handle } => {
if tx.send((data, rows)).is_ok() {
return Ok(());
}
match handle.take().map(|h| h.join()) {
Some(Ok(Err(e))) => Err(e),
_ => Err(Error::InvalidData("the GeoTIFF writer stopped".into())),
}
}
#[cfg(target_arch = "wasm32")]
Inner::Tiff(w) => w.write_rows(&data, rows, None),
Inner::Whole { ds, done, .. } => {
let (w, n) = (self.width, self.width * self.rows);
if *done + rows > self.rows || data.len() != w * rows * self.bands {
return Err(Error::InvalidArgument(format!("{rows} rows past the raster's {}", self.rows)));
}
macro_rules! copy {
($($v:ident),*) => {
match (&mut ds.raster.data, &data) {
$((RasterData::$v(out), RasterData::$v(d)) => {
for b in 0..self.bands {
out[b * n + *done * w..b * n + (*done + rows) * w].copy_from_slice(&d[b * w * rows..(b + 1) * w * rows]);
}
})*
_ => return Err(Error::InvalidArgument("the rows are not of the raster's data type".into())),
}
};
}
copy!(U8, I8, U16, I16, U32, I32, U64, I64, F32, F64);
*done += rows;
Ok(())
}
}
}
pub fn finish(mut self) -> Result<()> {
let Some(inner) = self.inner.take() else { return Ok(()) };
let result = match inner {
#[cfg(not(target_arch = "wasm32"))]
Inner::Thread { tx, handle } => {
drop(tx);
match handle.map(|h| h.join()) {
Some(Ok(result)) => result,
_ => Err(Error::InvalidData("the GeoTIFF writer stopped".into())),
}
}
#[cfg(target_arch = "wasm32")]
Inner::Tiff(w) => w.finish(),
Inner::Whole { ds, options, done } => {
if done != self.rows {
return Err(Error::InvalidArgument(format!("{done} rows written of {}", self.rows)));
}
return crate::write_raster_dataset(&ds, &self.path, &options);
}
};
if result.is_err() {
let _ = std::fs::remove_file(&self.path);
}
result
}
}
impl Drop for StripWriter {
fn drop(&mut self) {
let Some(inner) = self.inner.take() else { return };
match inner {
Inner::Whole { .. } => return,
#[cfg(not(target_arch = "wasm32"))]
Inner::Thread { tx, handle } => {
drop(tx);
if let Some(h) = handle {
let _ = h.join();
}
}
#[cfg(target_arch = "wasm32")]
Inner::Tiff(w) => drop(w),
}
let _ = std::fs::remove_file(&self.path);
}
}
pub(crate) fn pipelined<R: Send>(
n: usize,
mut read: impl FnMut(usize) -> Result<R> + Send,
mut work: impl FnMut(usize, R) -> Result<()>,
) -> Result<()> {
#[cfg(target_arch = "wasm32")]
{
for i in 0..n {
work(i, read(i)?)?;
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
std::thread::scope(|s| {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
s.spawn(move || {
for i in 0..n {
let r = read(i);
let failed = r.is_err();
if tx.send(r).is_err() || failed {
return;
}
}
});
for i in 0..n {
let r = rx.recv().map_err(|_| Error::InvalidData("the strip reader stopped".into()))??;
work(i, r)?;
}
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crs::SpatialRef;
use crate::ops::{Grid, raster_header};
use wbraster::DataType;
fn header(data_type: DataType) -> RasterDataset {
let grid = Grid { width: 4, height: 6, geo_transform: [0.0, 1.0, 0.0, 6.0, 0.0, -1.0] };
raster_header(grid, data_type, 1, None, &SpatialRef::parse("EPSG:4326").unwrap()).unwrap()
}
#[test]
fn unfinished_outputs_are_removed() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.tif");
let path = path.to_str().unwrap();
let rows = || RasterData::new_filled(DataType::U8, 4 * 3, 7.0);
let mut w = StripWriter::create(&header(DataType::U8), path, &Default::default()).unwrap();
w.write(rows(), 3).unwrap();
drop(w);
assert!(!std::path::Path::new(path).exists());
let mut w = StripWriter::create(&header(DataType::U8), path, &Default::default()).unwrap();
w.write(rows(), 3).unwrap();
w.write(rows(), 3).unwrap();
w.finish().unwrap();
assert!(std::path::Path::new(path).exists());
}
#[test]
fn invalid_options_fail_at_once() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("out.tif");
let options = RasterWriteOptions { compression: Some("JPEG".into()), ..Default::default() };
assert!(StripWriter::create(&header(DataType::F32), path.to_str().unwrap(), &options).is_err());
}
}