Skip to main content

oxigeo_streaming/raster/reader/
mod.rs

1//! Async raster stream reader for large datasets.
2//!
3//! Reads real GeoTIFF files via the `oxigeo-geotiff` driver, streaming
4//! data in configurable chunks.
5
6use super::{RasterChunk, RasterStream, RasterStreamConfig, RasterStreaming};
7use crate::error::{Result, StreamingError};
8use async_trait::async_trait;
9use oxigeo_core::{
10    buffer::RasterBuffer,
11    io::FileDataSource,
12    types::{BoundingBox, GeoTransform, RasterMetadata},
13};
14use oxigeo_geotiff::GeoTiffReader;
15use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use tokio::sync::Semaphore;
18use tokio::task;
19use tracing::{debug, error, info};
20
21/// Supported raster formats detected from file extension or magic bytes.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RasterFormat {
24    /// GeoTIFF / Cloud Optimized GeoTIFF
25    GeoTiff,
26}
27
28/// Detects the raster format from file extension.
29pub(crate) fn detect_format_from_extension(path: &Path) -> Option<RasterFormat> {
30    let ext = path.extension()?.to_str()?.to_ascii_lowercase();
31    match ext.as_str() {
32        "tif" | "tiff" | "geotiff" | "gtiff" => Some(RasterFormat::GeoTiff),
33        _ => None,
34    }
35}
36
37/// Detects the raster format from magic bytes.
38pub(crate) fn detect_format_from_magic(path: &Path) -> Option<RasterFormat> {
39    let data = std::fs::read(path).ok()?;
40    if data.len() >= 4 && oxigeo_geotiff::is_tiff(&data[..4.min(data.len())]) {
41        return Some(RasterFormat::GeoTiff);
42    }
43    None
44}
45
46/// Detects the raster format from a file path using both extension and magic bytes.
47pub(crate) fn detect_format(path: &Path) -> Result<RasterFormat> {
48    if let Some(fmt) = detect_format_from_extension(path) {
49        return Ok(fmt);
50    }
51    if let Some(fmt) = detect_format_from_magic(path) {
52        return Ok(fmt);
53    }
54    Err(StreamingError::Other(format!(
55        "Unsupported raster format for file: {}",
56        path.display()
57    )))
58}
59
60/// Async raster stream reader.
61///
62/// Reads real GeoTIFF files, extracting metadata from IFD tags and
63/// reading actual tile/strip data for each chunk.
64pub struct RasterStreamReader {
65    /// Path to the raster file
66    path: PathBuf,
67
68    /// Stream configuration
69    config: RasterStreamConfig,
70
71    /// Raster metadata
72    metadata: RasterMetadata,
73
74    /// The underlying stream
75    stream: Option<RasterStream>,
76
77    /// Prefetch semaphore for limiting concurrent operations
78    prefetch_semaphore: Arc<Semaphore>,
79
80    /// Band indices to read
81    bands: Vec<usize>,
82
83    /// Detected file format
84    format: RasterFormat,
85}
86
87impl RasterStreamReader {
88    /// Create a new raster stream reader.
89    ///
90    /// Opens the GeoTIFF file and reads its metadata (dimensions, data type,
91    /// geotransform, CRS, nodata value) from the TIFF IFD.
92    pub async fn new<P: AsRef<Path>>(path: P, config: RasterStreamConfig) -> Result<Self> {
93        let path = path.as_ref().to_path_buf();
94
95        // Validate file exists
96        if !path.exists() {
97            return Err(StreamingError::Io(std::io::Error::new(
98                std::io::ErrorKind::NotFound,
99                format!("File not found: {}", path.display()),
100            )));
101        }
102
103        // Detect format
104        let format = detect_format(&path)?;
105
106        // Read metadata from the file
107        let metadata = Self::read_metadata_from_file(&path, format).await?;
108
109        info!(
110            "Created raster stream reader for {}x{} raster with {} bands ({})",
111            metadata.width,
112            metadata.height,
113            metadata.band_count,
114            path.display()
115        );
116
117        let prefetch_semaphore = Arc::new(Semaphore::new(config.prefetch_count));
118
119        Ok(Self {
120            path,
121            config,
122            metadata,
123            stream: None,
124            prefetch_semaphore,
125            bands: vec![0], // Default to first band
126            format,
127        })
128    }
129
130    /// Read metadata from a raster file using the GeoTIFF driver.
131    async fn read_metadata_from_file(path: &Path, format: RasterFormat) -> Result<RasterMetadata> {
132        let path = path.to_path_buf();
133        task::spawn_blocking(move || match format {
134            RasterFormat::GeoTiff => Self::read_geotiff_metadata(&path),
135        })
136        .await
137        .map_err(|e| StreamingError::Other(format!("Task join error: {}", e)))?
138    }
139
140    /// Read metadata from a GeoTIFF file using the oxigeo-geotiff driver.
141    fn read_geotiff_metadata(path: &Path) -> Result<RasterMetadata> {
142        let source = FileDataSource::open(path).map_err(|e| {
143            StreamingError::Other(format!(
144                "Failed to open GeoTIFF file '{}': {}",
145                path.display(),
146                e
147            ))
148        })?;
149
150        let reader = GeoTiffReader::open(source).map_err(|e| {
151            StreamingError::Other(format!(
152                "Failed to parse GeoTIFF '{}': {}",
153                path.display(),
154                e
155            ))
156        })?;
157
158        Ok(reader.metadata())
159    }
160
161    /// Set which bands to read.
162    pub fn with_bands(mut self, bands: Vec<usize>) -> Self {
163        self.bands = bands;
164        self
165    }
166
167    /// Start the streaming process.
168    pub async fn start(&mut self) -> Result<()> {
169        let stream = RasterStream::new(self.config.clone(), self.metadata.clone())?;
170
171        // Start prefetch workers if enabled
172        if self.config.parallel {
173            self.start_prefetch_workers().await?;
174        }
175
176        self.stream = Some(stream);
177        Ok(())
178    }
179
180    /// Start prefetch workers for parallel chunk loading.
181    async fn start_prefetch_workers(&self) -> Result<()> {
182        let num_workers = self.config.num_workers;
183
184        for worker_id in 0..num_workers {
185            let _path = self.path.clone();
186            let _config = self.config.clone();
187            let _metadata = self.metadata.clone();
188            let _bands = self.bands.clone();
189            let _semaphore = Arc::clone(&self.prefetch_semaphore);
190
191            tokio::spawn(async move {
192                debug!("Prefetch worker {} started", worker_id);
193                // Workers acquire semaphore permits before reading chunks
194                debug!("Prefetch worker {} finished", worker_id);
195            });
196        }
197
198        Ok(())
199    }
200
201    /// Read a specific chunk from the raster.
202    ///
203    /// This actually reads tile/strip data from the GeoTIFF file for the
204    /// given chunk coordinates.
205    pub async fn read_chunk(&self, row: usize, col: usize) -> Result<RasterChunk> {
206        let _permit = self
207            .prefetch_semaphore
208            .acquire()
209            .await
210            .map_err(|e| StreamingError::Other(e.to_string()))?;
211
212        let path = self.path.clone();
213        let config = self.config.clone();
214        let metadata = self.metadata.clone();
215        let bands = self.bands.clone();
216        let format = self.format;
217
218        task::spawn_blocking(move || {
219            Self::read_chunk_blocking(path, row, col, config, metadata, bands, format)
220        })
221        .await
222        .map_err(|e| StreamingError::Other(e.to_string()))?
223    }
224
225    /// Read a chunk in blocking mode using the real GeoTIFF driver.
226    fn read_chunk_blocking(
227        path: PathBuf,
228        row: usize,
229        col: usize,
230        config: RasterStreamConfig,
231        metadata: RasterMetadata,
232        _bands: Vec<usize>,
233        format: RasterFormat,
234    ) -> Result<RasterChunk> {
235        let chunk_width = config.chunk_size.0;
236        let chunk_height = config.chunk_size.1;
237        let overlap = config.overlap;
238
239        let effective_width = chunk_width.saturating_sub(overlap).max(1);
240        let effective_height = chunk_height.saturating_sub(overlap).max(1);
241
242        let x_start = col * effective_width;
243        let y_start = row * effective_height;
244        let x_end = (x_start + chunk_width).min(metadata.width as usize);
245        let y_end = (y_start + chunk_height).min(metadata.height as usize);
246
247        let actual_width = x_end.saturating_sub(x_start);
248        let actual_height = y_end.saturating_sub(y_start);
249
250        if actual_width == 0 || actual_height == 0 {
251            return Err(StreamingError::InvalidOperation(format!(
252                "Empty chunk at ({}, {}): {}x{}",
253                row, col, actual_width, actual_height
254            )));
255        }
256
257        // Read actual data from the file
258        let buffer = match format {
259            RasterFormat::GeoTiff => Self::read_geotiff_chunk(
260                &path,
261                &metadata,
262                x_start,
263                y_start,
264                actual_width,
265                actual_height,
266            )?,
267        };
268
269        // Calculate bounding box
270        let gt = metadata
271            .geo_transform
272            .as_ref()
273            .ok_or_else(|| StreamingError::InvalidState("No geotransform available".to_string()))?;
274
275        let min_x = gt.origin_x + (x_start as f64) * gt.pixel_width;
276        let max_y = gt.origin_y + (y_start as f64) * gt.pixel_height;
277        let max_x = gt.origin_x + (x_end as f64) * gt.pixel_width;
278        let min_y = gt.origin_y + (y_end as f64) * gt.pixel_height;
279
280        let bbox = BoundingBox::new(min_x, min_y, max_x, max_y).map_err(StreamingError::Core)?;
281
282        // Calculate chunk geotransform
283        let chunk_gt = GeoTransform {
284            origin_x: min_x,
285            origin_y: max_y,
286            pixel_width: gt.pixel_width,
287            pixel_height: gt.pixel_height,
288            row_rotation: gt.row_rotation,
289            col_rotation: gt.col_rotation,
290        };
291
292        Ok(RasterChunk::new(buffer, bbox, chunk_gt, (row, col)))
293    }
294
295    /// Read a rectangular region from a GeoTIFF file by reading relevant
296    /// tiles/strips and extracting the overlapping pixels.
297    fn read_geotiff_chunk(
298        path: &Path,
299        metadata: &RasterMetadata,
300        x_start: usize,
301        y_start: usize,
302        width: usize,
303        height: usize,
304    ) -> Result<RasterBuffer> {
305        let source = FileDataSource::open(path).map_err(|e| {
306            StreamingError::Other(format!("Failed to open GeoTIFF for chunk read: {}", e))
307        })?;
308
309        let reader = GeoTiffReader::open(source).map_err(|e| {
310            StreamingError::Other(format!("Failed to parse GeoTIFF for chunk read: {}", e))
311        })?;
312
313        let info = reader.metadata();
314        let data_type = info.data_type;
315        let bytes_per_pixel = data_type.size_bytes() * info.band_count as usize;
316        let img_width = info.width as usize;
317        let img_height = info.height as usize;
318
319        // Determine tile/strip layout.
320        //
321        // `GeoTiffReader::metadata().layout` is *always* `PixelLayout::Tiled`
322        // (it substitutes 256x256 when the file carries no TileWidth tag), so
323        // it cannot be used to tell a striped file from a tiled one. Ask
324        // `tile_size()` instead, which returns `None` for striped files, and
325        // fall back to the per-band window reader for those -- the tile-grid
326        // arithmetic below would otherwise index a fabricated 256x256 grid.
327        let Some((tile_w, tile_h)) = reader.tile_size() else {
328            return Self::read_geotiff_chunk_full_band(
329                path, metadata, x_start, y_start, width, height,
330            );
331        };
332        let (tile_w, tile_h) = (tile_w as usize, tile_h as usize);
333
334        // Allocate output buffer
335        let out_size = width * height * bytes_per_pixel;
336        let mut out_data = vec![0u8; out_size];
337
338        // Calculate which tiles overlap our window
339        let tile_col_start = x_start / tile_w;
340        let tile_col_end = (x_start + width).min(img_width).div_ceil(tile_w);
341        let tile_row_start = y_start / tile_h;
342        let tile_row_end = (y_start + height).min(img_height).div_ceil(tile_h);
343
344        for ty in tile_row_start..tile_row_end {
345            for tx in tile_col_start..tile_col_end {
346                // Read tile data
347                let tile_data = reader.read_tile(0, tx as u32, ty as u32).map_err(|e| {
348                    StreamingError::Other(format!("Failed to read tile ({}, {}): {}", tx, ty, e))
349                })?;
350
351                // Calculate overlap between tile and our window
352                let tile_x0 = tx * tile_w;
353                let tile_y0 = ty * tile_h;
354                let tile_x1 = (tile_x0 + tile_w).min(img_width);
355                let tile_y1 = (tile_y0 + tile_h).min(img_height);
356
357                let overlap_x0 = x_start.max(tile_x0);
358                let overlap_y0 = y_start.max(tile_y0);
359                let overlap_x1 = (x_start + width).min(tile_x1);
360                let overlap_y1 = (y_start + height).min(tile_y1);
361
362                if overlap_x0 >= overlap_x1 || overlap_y0 >= overlap_y1 {
363                    continue;
364                }
365
366                // Copy overlapping region
367                let copy_width = overlap_x1 - overlap_x0;
368                for row_idx in overlap_y0..overlap_y1 {
369                    let src_row_in_tile = row_idx - tile_y0;
370                    let src_col_in_tile = overlap_x0 - tile_x0;
371                    let src_offset = (src_row_in_tile * tile_w + src_col_in_tile) * bytes_per_pixel;
372
373                    let dst_row = row_idx - y_start;
374                    let dst_col = overlap_x0 - x_start;
375                    let dst_offset = (dst_row * width + dst_col) * bytes_per_pixel;
376
377                    let copy_bytes = copy_width * bytes_per_pixel;
378
379                    if src_offset + copy_bytes <= tile_data.len()
380                        && dst_offset + copy_bytes <= out_data.len()
381                    {
382                        out_data[dst_offset..dst_offset + copy_bytes]
383                            .copy_from_slice(&tile_data[src_offset..src_offset + copy_bytes]);
384                    }
385                }
386            }
387        }
388
389        // RasterBuffer validates size as width * height * data_type.size_bytes().
390        // For multi-band interleaved data, the total size is width * height * bytes_per_pixel,
391        // where bytes_per_pixel = data_type.size_bytes() * band_count.
392        // We encode the effective width as width * band_count so that the buffer can hold
393        // all interleaved band data correctly.
394        //
395        // The band count must come from the same source `bytes_per_pixel` above
396        // was derived from (the file), not from the cached `metadata` struct: if
397        // the two ever disagree the payload length and the declared width would
398        // disagree too, and `RasterBuffer::new` would reject a buffer that is in
399        // fact correct.
400        let band_count = info.band_count as u64;
401        let effective_width = width as u64 * band_count;
402        RasterBuffer::new(
403            out_data,
404            effective_width,
405            height as u64,
406            data_type,
407            metadata.nodata,
408        )
409        .map_err(|e| StreamingError::Other(format!("Failed to create RasterBuffer: {}", e)))
410    }
411
412    /// Fallback for striped GeoTIFFs: read the requested window of every band
413    /// and weave the planes back into the chunky (band-interleaved-by-pixel)
414    /// layout the rest of the streaming pipeline expects.
415    ///
416    /// `GeoTiffReader::read_window(level, band, ..)` returns *one* band plane --
417    /// `w * h * bytes_per_sample` bytes, row-major -- and touches only the
418    /// strips that overlap the window, so this no longer decodes the whole
419    /// image to serve one chunk. The interleave here is deliberate: the chunk
420    /// buffer is consumed by `raster::writer`, which recovers the pixel width as
421    /// `buffer.width() / band_count` and reads samples at a `band_count` stride.
422    /// See <https://github.com/cool-japan/oxigeo/issues/14>.
423    fn read_geotiff_chunk_full_band(
424        path: &Path,
425        metadata: &RasterMetadata,
426        x_start: usize,
427        y_start: usize,
428        width: usize,
429        height: usize,
430    ) -> Result<RasterBuffer> {
431        let source = FileDataSource::open(path).map_err(|e| {
432            StreamingError::Other(format!("Failed to open GeoTIFF for band read: {}", e))
433        })?;
434
435        let reader = GeoTiffReader::open(source).map_err(|e| {
436            StreamingError::Other(format!("Failed to parse GeoTIFF for band read: {}", e))
437        })?;
438
439        let info = reader.metadata();
440        let data_type = info.data_type;
441        let bytes_per_sample = data_type.size_bytes();
442        let band_count = info.band_count.max(1) as usize;
443        let bytes_per_pixel = bytes_per_sample * band_count;
444        let img_width = info.width as usize;
445        let img_height = info.height as usize;
446
447        let out_size = width * height * bytes_per_pixel;
448        let mut out_data = vec![0u8; out_size];
449
450        // Clamp the window to the raster extent: chunk grids routinely overhang
451        // the right/bottom edge, and `read_window` rejects an out-of-bounds
452        // window rather than silently truncating. The overhang stays zeroed,
453        // matching the tiled path.
454        let copy_w = width.min(img_width.saturating_sub(x_start));
455        let copy_h = height.min(img_height.saturating_sub(y_start));
456
457        if copy_w > 0 && copy_h > 0 {
458            for band in 0..band_count {
459                let plane = reader
460                    .read_window(
461                        0,
462                        band,
463                        x_start as u64,
464                        y_start as u64,
465                        copy_w as u64,
466                        copy_h as u64,
467                    )
468                    .map_err(|e| {
469                        StreamingError::Other(format!("Failed to read band {}: {}", band, e))
470                    })?;
471
472                for row in 0..copy_h {
473                    for col in 0..copy_w {
474                        let src = (row * copy_w + col) * bytes_per_sample;
475                        let dst = ((row * width + col) * band_count + band) * bytes_per_sample;
476                        out_data[dst..dst + bytes_per_sample]
477                            .copy_from_slice(&plane[src..src + bytes_per_sample]);
478                    }
479                }
480            }
481        }
482
483        // Keep the encoded width consistent with the band count the payload was
484        // actually built from, so `RasterBuffer::new`'s `w * h * size_bytes`
485        // check cannot fail on a metadata/file disagreement.
486        let effective_width = width as u64 * band_count as u64;
487        RasterBuffer::new(
488            out_data,
489            effective_width,
490            height as u64,
491            data_type,
492            metadata.nodata,
493        )
494        .map_err(|e| StreamingError::Other(format!("Failed to create RasterBuffer: {}", e)))
495    }
496
497    /// Read multiple chunks in parallel.
498    pub async fn read_chunks(&self, chunks: Vec<(usize, usize)>) -> Result<Vec<RasterChunk>> {
499        let mut handles = Vec::with_capacity(chunks.len());
500
501        for (row, col) in chunks {
502            let path = self.path.clone();
503            let config = self.config.clone();
504            let metadata = self.metadata.clone();
505            let bands = self.bands.clone();
506            let semaphore = Arc::clone(&self.prefetch_semaphore);
507            let format = self.format;
508
509            let handle = tokio::spawn(async move {
510                let _permit = semaphore
511                    .acquire()
512                    .await
513                    .map_err(|e| StreamingError::Other(e.to_string()))?;
514
515                task::spawn_blocking(move || {
516                    Self::read_chunk_blocking(path, row, col, config, metadata, bands, format)
517                })
518                .await
519                .map_err(|e| StreamingError::Other(e.to_string()))?
520            });
521
522            handles.push(handle);
523        }
524
525        let mut results = Vec::with_capacity(handles.len());
526        for handle in handles {
527            match handle.await {
528                Ok(Ok(chunk)) => results.push(chunk),
529                Ok(Err(e)) => {
530                    error!("Failed to read chunk: {}", e);
531                    return Err(e);
532                }
533                Err(e) => {
534                    error!("Task panicked: {}", e);
535                    return Err(StreamingError::Other(e.to_string()));
536                }
537            }
538        }
539
540        Ok(results)
541    }
542
543    /// Get the metadata for this raster.
544    pub fn metadata(&self) -> &RasterMetadata {
545        &self.metadata
546    }
547
548    /// Get the stream configuration.
549    pub fn config(&self) -> &RasterStreamConfig {
550        &self.config
551    }
552
553    /// Get the detected file format.
554    pub fn format(&self) -> RasterFormat {
555        self.format
556    }
557}
558
559#[async_trait]
560impl RasterStreaming for RasterStreamReader {
561    async fn next_chunk(&mut self) -> Result<Option<RasterChunk>> {
562        let stream = self
563            .stream
564            .as_mut()
565            .ok_or_else(|| StreamingError::InvalidState("Stream not started".to_string()))?;
566        stream.next_chunk().await
567    }
568
569    async fn next_chunks(&mut self, count: usize) -> Result<Vec<RasterChunk>> {
570        let stream = self
571            .stream
572            .as_mut()
573            .ok_or_else(|| StreamingError::InvalidState("Stream not started".to_string()))?;
574        stream.next_chunks(count).await
575    }
576
577    async fn seek_to_chunk(&mut self, row: usize, col: usize) -> Result<()> {
578        let stream = self
579            .stream
580            .as_mut()
581            .ok_or_else(|| StreamingError::InvalidState("Stream not started".to_string()))?;
582        stream.seek_to_chunk(row, col).await
583    }
584
585    fn total_chunks(&self) -> (usize, usize) {
586        self.stream
587            .as_ref()
588            .map(|s| s.total_chunks())
589            .unwrap_or((0, 0))
590    }
591
592    fn current_position(&self) -> (usize, usize) {
593        self.stream
594            .as_ref()
595            .map(|s| s.current_position())
596            .unwrap_or((0, 0))
597    }
598
599    fn has_more_chunks(&self) -> bool {
600        self.stream
601            .as_ref()
602            .map(|s| s.has_more_chunks())
603            .unwrap_or(false)
604    }
605}
606
607/// Builder for configuring a raster stream reader.
608pub struct RasterStreamReaderBuilder {
609    path: PathBuf,
610    config: RasterStreamConfig,
611    bands: Vec<usize>,
612}
613
614impl RasterStreamReaderBuilder {
615    /// Create a new builder.
616    pub fn new<P: AsRef<Path>>(path: P) -> Self {
617        Self {
618            path: path.as_ref().to_path_buf(),
619            config: RasterStreamConfig::default(),
620            bands: vec![0],
621        }
622    }
623
624    /// Set the chunk size.
625    pub fn chunk_size(mut self, width: usize, height: usize) -> Self {
626        self.config = self.config.with_chunk_size(width, height);
627        self
628    }
629
630    /// Set the overlap size.
631    pub fn overlap(mut self, overlap: usize) -> Self {
632        self.config = self.config.with_overlap(overlap);
633        self
634    }
635
636    /// Enable compression.
637    pub fn compression(mut self, level: u8) -> Self {
638        self.config = self.config.with_compression(level);
639        self
640    }
641
642    /// Set the bands to read.
643    pub fn bands(mut self, bands: Vec<usize>) -> Self {
644        self.bands = bands;
645        self
646    }
647
648    /// Set the number of parallel workers.
649    pub fn parallel(mut self, num_workers: usize) -> Self {
650        self.config = self.config.with_parallel(true, num_workers);
651        self
652    }
653
654    /// Build the reader.
655    pub async fn build(self) -> Result<RasterStreamReader> {
656        let reader = RasterStreamReader::new(self.path, self.config).await?;
657        Ok(reader.with_bands(self.bands))
658    }
659}
660
661#[cfg(test)]
662mod tests;