use crate::range_reader::{create_range_reader, RangeReader};
use crate::tile_cache;
use crate::tiff_utils::AnyResult;
use std::collections::HashMap;
use std::sync::Arc;
const TAG_IMAGE_WIDTH: u16 = 256;
const TAG_IMAGE_LENGTH: u16 = 257;
const TAG_BITS_PER_SAMPLE: u16 = 258;
const TAG_COMPRESSION: u16 = 259;
const TAG_SAMPLES_PER_PIXEL: u16 = 277;
const TAG_PREDICTOR: u16 = 317;
const TAG_ROWS_PER_STRIP: u16 = 278;
const TAG_STRIP_OFFSETS: u16 = 273;
const TAG_STRIP_BYTE_COUNTS: u16 = 279;
const TAG_TILE_WIDTH: u16 = 322;
const TAG_TILE_LENGTH: u16 = 323;
const TAG_TILE_OFFSETS: u16 = 324;
const TAG_TILE_BYTE_COUNTS: u16 = 325;
const TAG_SAMPLE_FORMAT: u16 = 339;
const TAG_MODEL_PIXEL_SCALE: u16 = 33550;
const TAG_MODEL_TIEPOINT: u16 = 33922;
const TAG_GEO_KEY_DIRECTORY: u16 = 34735;
const TAG_GDAL_METADATA: u16 = 42112;
const TAG_GDAL_NODATA: u16 = 42113;
const GEO_KEY_GEOGRAPHIC_TYPE: u16 = 2048;
const GEO_KEY_PROJECTED_CRS: u16 = 3072;
const COMPRESSION_NONE: u16 = 1;
const COMPRESSION_LZW: u16 = 5;
const COMPRESSION_JPEG: u16 = 7;
const COMPRESSION_DEFLATE: u16 = 8;
const COMPRESSION_WEBP: u16 = 50001;
const COMPRESSION_ZSTD: u16 = 50000;
const SAMPLE_FORMAT_UINT: u16 = 1;
const SAMPLE_FORMAT_INT: u16 = 2;
const SAMPLE_FORMAT_FLOAT: u16 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CogDataType {
UInt8,
UInt16,
UInt32,
UInt64,
Int8,
Int16,
Int32,
Int64,
Float32,
Float64,
}
impl CogDataType {
#[must_use] pub fn bytes_per_sample(&self) -> usize {
match self {
CogDataType::UInt8 | CogDataType::Int8 => 1,
CogDataType::UInt16 | CogDataType::Int16 => 2,
CogDataType::UInt32 | CogDataType::Int32 | CogDataType::Float32 => 4,
CogDataType::UInt64 | CogDataType::Int64 | CogDataType::Float64 => 8,
}
}
#[must_use]
#[allow(clippy::match_same_arms)] pub fn from_tags(bits_per_sample: u16, sample_format: u16) -> Option<Self> {
match (sample_format, bits_per_sample) {
(SAMPLE_FORMAT_UINT, 8) => Some(CogDataType::UInt8),
(SAMPLE_FORMAT_UINT, 16) => Some(CogDataType::UInt16),
(SAMPLE_FORMAT_UINT, 32) => Some(CogDataType::UInt32),
(SAMPLE_FORMAT_UINT, 64) => Some(CogDataType::UInt64),
(SAMPLE_FORMAT_INT, 8) => Some(CogDataType::Int8),
(SAMPLE_FORMAT_INT, 16) => Some(CogDataType::Int16),
(SAMPLE_FORMAT_INT, 32) => Some(CogDataType::Int32),
(SAMPLE_FORMAT_INT, 64) => Some(CogDataType::Int64),
(SAMPLE_FORMAT_FLOAT, 32) => Some(CogDataType::Float32),
(SAMPLE_FORMAT_FLOAT, 64) => Some(CogDataType::Float64),
(_, 8) => Some(CogDataType::UInt8),
(_, 16) => Some(CogDataType::UInt16),
(_, 32) => Some(CogDataType::UInt32),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
None,
Lzw,
Jpeg,
Deflate,
Zstd,
Webp,
}
impl Compression {
#[must_use] pub fn from_tag(value: u16) -> Option<Self> {
match value {
COMPRESSION_NONE => Some(Compression::None),
COMPRESSION_LZW => Some(Compression::Lzw),
COMPRESSION_JPEG => Some(Compression::Jpeg),
COMPRESSION_DEFLATE | 32946 => Some(Compression::Deflate), COMPRESSION_ZSTD => Some(Compression::Zstd),
COMPRESSION_WEBP => Some(Compression::Webp),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct GeoTransform {
pub pixel_scale: Option<[f64; 3]>,
pub tiepoint: Option<[f64; 6]>,
pub is_point_registered: bool,
}
impl GeoTransform {
#[must_use] pub fn pixel_to_world(&self, px: f64, py: f64) -> Option<(f64, f64)> {
let scale = self.pixel_scale?;
let tie = self.tiepoint?;
let offset = if self.is_point_registered { 0.5 } else { 0.0 };
let world_x = tie[3] + (px + offset - tie[0]) * scale[0];
let world_y = tie[4] - (py + offset - tie[1]) * scale[1];
Some((world_x, world_y))
}
#[must_use] pub fn world_to_pixel(&self, wx: f64, wy: f64) -> Option<(f64, f64)> {
let scale = self.pixel_scale?;
let tie = self.tiepoint?;
if scale[0] == 0.0 || scale[1] == 0.0 {
return None;
}
let offset = if self.is_point_registered { 0.5 } else { 0.0 };
let px = tie[0] + (wx - tie[3]) / scale[0] + offset;
let py = tie[1] + (tie[4] - wy) / scale[1] + offset;
Some((px, py))
}
#[must_use] pub fn get_extent(&self, width: usize, height: usize) -> Option<(f64, f64, f64, f64)> {
let (min_x, max_y) = self.pixel_to_world(0.0, 0.0)?;
#[allow(clippy::cast_precision_loss)]
let (max_x, min_y) = self.pixel_to_world(width as f64, height as f64)?;
Some((min_x, min_y, max_x, max_y))
}
}
#[derive(Debug, Clone)]
pub struct CogMetadata {
pub width: usize,
pub height: usize,
pub tile_width: usize,
pub tile_height: usize,
pub bands: usize,
pub data_type: CogDataType,
pub compression: Compression,
pub predictor: u16,
pub little_endian: bool,
pub tile_offsets: Vec<u64>,
pub tile_byte_counts: Vec<u64>,
pub tiles_across: usize,
pub tiles_down: usize,
pub is_tiled: bool,
pub geo_transform: GeoTransform,
pub crs_code: Option<i32>,
pub stats_min: Option<f32>,
pub stats_max: Option<f32>,
pub nodata: Option<f64>,
}
impl CogMetadata {
#[must_use] pub fn is_tiled(&self) -> bool {
self.tile_width > 0 && self.tile_height > 0
}
#[must_use] pub fn tile_index_for_pixel(&self, px: usize, py: usize) -> Option<usize> {
if px >= self.width || py >= self.height {
return None;
}
let tile_col = px / self.tile_width;
let tile_row = py / self.tile_height;
Some(tile_row * self.tiles_across + tile_col)
}
#[must_use] pub fn pixel_range_in_tile(&self, tile_index: usize) -> (usize, usize, usize, usize) {
let tile_col = tile_index % self.tiles_across;
let tile_row = tile_index / self.tiles_across;
let start_x = tile_col * self.tile_width;
let start_y = tile_row * self.tile_height;
let end_x = (start_x + self.tile_width).min(self.width);
let end_y = (start_y + self.tile_height).min(self.height);
(start_x, start_y, end_x, end_y)
}
#[must_use] pub fn tile_pixel_count(&self, tile_index: usize) -> usize {
let (start_x, start_y, end_x, end_y) = self.pixel_range_in_tile(tile_index);
(end_x - start_x) * (end_y - start_y) * self.bands
}
}
#[derive(Debug, Clone)]
pub struct OverviewMetadata {
pub width: usize,
pub height: usize,
pub tile_width: usize,
pub tile_height: usize,
pub tiles_across: usize,
pub tiles_down: usize,
pub tile_offsets: Vec<u64>,
pub tile_byte_counts: Vec<u64>,
pub scale: usize,
}
impl OverviewMetadata {
#[must_use] pub fn tile_index_for_pixel(&self, px: usize, py: usize) -> Option<usize> {
if px >= self.width || py >= self.height {
return None;
}
let tile_col = px / self.tile_width;
let tile_row = py / self.tile_height;
Some(tile_row * self.tiles_across + tile_col)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum OverviewQualityHint {
#[default]
ComputeAtRuntime,
AllUsable,
NoneUsable,
MinUsable(usize),
}
impl OverviewQualityHint {
#[must_use]
pub fn from_db_value(value: Option<i32>) -> Self {
match value {
Some(-2) => Self::AllUsable,
Some(-1) => Self::NoneUsable,
Some(n) if n >= 0 => {
#[allow(clippy::cast_sign_loss)]
Self::MinUsable(n as usize)
}
None | Some(_) => Self::ComputeAtRuntime, }
}
#[must_use]
pub fn to_db_value(&self) -> Option<i32> {
match self {
Self::ComputeAtRuntime => None,
Self::NoneUsable => Some(-1),
Self::AllUsable => Some(-2),
Self::MinUsable(n) => {
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
Some(*n as i32)
}
}
}
}
pub struct CogReader {
reader: Arc<dyn RangeReader>,
pub metadata: CogMetadata,
pub overviews: Vec<OverviewMetadata>,
pub min_usable_overview: Option<usize>,
}
impl CogReader {
pub fn open(source: &str) -> AnyResult<Self> {
let reader = create_range_reader(source)?;
Self::from_reader(reader)
}
pub fn open_with_hint(source: &str, hint: OverviewQualityHint) -> AnyResult<Self> {
let reader = create_range_reader(source)?;
Self::from_reader_with_hint(reader, hint)
}
pub fn from_reader(reader: Arc<dyn RangeReader>) -> AnyResult<Self> {
Self::from_reader_with_hint(reader, OverviewQualityHint::ComputeAtRuntime)
}
pub fn from_reader_with_hint(reader: Arc<dyn RangeReader>, hint: OverviewQualityHint) -> AnyResult<Self> {
let header_bytes = reader.read_range(0, 8)?;
let little_endian = match &header_bytes[0..2] {
b"II" => true,
b"MM" => false,
_ => return Err("Invalid TIFF signature".into()),
};
let version = read_u16(&header_bytes[2..4], little_endian);
if version != 42 {
return Err(format!("Invalid TIFF version: {version}").into());
}
let ifd_offset = read_u32(&header_bytes[4..8], little_endian);
let file_size = reader.size();
#[allow(clippy::cast_possible_truncation)]
let ifd_size_estimate = 4096.min((file_size - u64::from(ifd_offset)) as usize);
let ifd_bytes = reader.read_range(u64::from(ifd_offset), ifd_size_estimate)?;
let (metadata, next_ifd_offset) = parse_ifd_with_next(&ifd_bytes, &reader, u64::from(ifd_offset), little_endian)?;
let mut overviews = Vec::new();
let mut current_ifd_offset = next_ifd_offset;
let full_width = metadata.width;
while current_ifd_offset != 0 {
#[allow(clippy::cast_possible_truncation)]
let ovr_ifd_size = 4096.min((file_size - u64::from(current_ifd_offset)) as usize);
let ovr_ifd_bytes = reader.read_range(u64::from(current_ifd_offset), ovr_ifd_size)?;
if let Ok((ovr_meta, next_offset)) = parse_overview_ifd(&ovr_ifd_bytes, &reader, u64::from(current_ifd_offset), little_endian, &metadata) {
let actual_scale = full_width / ovr_meta.width;
overviews.push(OverviewMetadata {
width: ovr_meta.width,
height: ovr_meta.height,
tile_width: ovr_meta.tile_width,
tile_height: ovr_meta.tile_height,
tiles_across: ovr_meta.tiles_across,
tiles_down: ovr_meta.tiles_down,
tile_offsets: ovr_meta.tile_offsets,
tile_byte_counts: ovr_meta.tile_byte_counts,
scale: actual_scale,
});
current_ifd_offset = next_offset;
} else {
break;
}
if overviews.len() > 10 {
break;
}
}
let min_usable_overview = match hint {
OverviewQualityHint::AllUsable => {
if overviews.is_empty() {
None
} else {
Some(overviews.len() - 1)
}
}
OverviewQualityHint::NoneUsable => {
None
}
OverviewQualityHint::MinUsable(n) => Some(n),
OverviewQualityHint::ComputeAtRuntime => {
None
}
};
let mut cog_reader = Self {
reader,
metadata,
overviews,
min_usable_overview,
};
if matches!(hint, OverviewQualityHint::ComputeAtRuntime) {
cog_reader.analyze_overview_quality();
}
Ok(cog_reader)
}
#[must_use]
pub fn compute_overview_quality_hint(&self) -> OverviewQualityHint {
if self.overviews.is_empty() {
return OverviewQualityHint::AllUsable;
}
let result = self.analyze_overview_quality_impl();
match result {
None => {
if self.overviews.is_empty() {
OverviewQualityHint::AllUsable
} else {
OverviewQualityHint::NoneUsable
}
}
Some(idx) => OverviewQualityHint::MinUsable(idx),
}
}
fn analyze_overview_quality(&mut self) {
self.min_usable_overview = self.analyze_overview_quality_impl();
}
fn analyze_overview_quality_impl(&self) -> Option<usize> {
if self.overviews.is_empty() {
return None;
}
let min_density_threshold = 0.05;
for (idx, ovr) in self.overviews.iter().enumerate().rev() {
let num_tiles = ovr.tile_offsets.len();
let sample_indices: Vec<usize> = if num_tiles <= 3 {
(0..num_tiles).collect()
} else {
vec![0, num_tiles / 2, num_tiles - 1]
};
let mut total_pixels = 0usize;
let mut valid_pixels = 0usize;
for &tile_idx in &sample_indices {
if let Ok(data) = self.read_overview_tile(idx, tile_idx) {
total_pixels += data.len();
valid_pixels += data.iter().filter(|v| !v.is_nan() && **v != 0.0).count();
}
}
let density = if total_pixels > 0 {
#[allow(clippy::cast_precision_loss)]
let density_value = valid_pixels as f64 / total_pixels as f64;
density_value
} else {
0.0
};
if density >= min_density_threshold {
return Some(idx);
}
}
None
}
#[must_use] pub fn best_overview_for_resolution(&self, extent_src_width: usize, extent_src_height: usize) -> Option<usize> {
if self.min_usable_overview.is_none() && !self.overviews.is_empty() {
return None;
}
let output_size = 256.0;
#[allow(clippy::cast_precision_loss)]
let scale_x = extent_src_width as f64 / output_size;
#[allow(clippy::cast_precision_loss)]
let scale_y = extent_src_height as f64 / output_size;
let needed_scale = scale_x.max(scale_y);
if needed_scale < 1.5 {
return None;
}
let mut best_idx = None;
let mut best_scale = 0usize;
for (idx, ovr) in self.overviews.iter().enumerate() {
if let Some(min_usable) = self.min_usable_overview
&& idx > min_usable {
continue;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
if ovr.scale <= (needed_scale as usize) && ovr.scale > best_scale {
best_scale = ovr.scale;
best_idx = Some(idx);
}
}
best_idx
}
pub fn read_overview_tile(&self, overview_idx: usize, tile_index: usize) -> AnyResult<Vec<f32>> {
let source_id = self.reader.identifier();
if let Some(cached) = tile_cache::get(source_id, tile_index, Some(overview_idx)) {
return Ok((*cached).clone());
}
let ovr = self.overviews.get(overview_idx)
.ok_or_else(|| format!("Overview index {overview_idx} out of range"))?;
if tile_index >= ovr.tile_offsets.len() {
return Err(format!(
"Tile index {} out of range (max {})",
tile_index,
ovr.tile_offsets.len()
).into());
}
let offset = ovr.tile_offsets[tile_index];
#[allow(clippy::cast_possible_truncation)]
let byte_count = ovr.tile_byte_counts[tile_index] as usize;
if byte_count == 0 {
let pixel_count = ovr.tile_width * ovr.tile_height * self.metadata.bands;
return Ok(vec![f32::NAN; pixel_count]);
}
let compressed = self.reader.read_range(offset, byte_count)?;
let decompressed = decompress_tile(
&compressed,
self.metadata.compression,
ovr.tile_width,
ovr.tile_height,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let unpredicted = apply_predictor(
&decompressed,
self.metadata.predictor,
ovr.tile_width,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let result = convert_to_f32(
&unpredicted,
self.metadata.data_type,
self.metadata.little_endian,
);
tile_cache::insert(source_id, tile_index, Some(overview_idx), Arc::new(result.clone()));
Ok(result)
}
pub fn read_tile(&self, tile_index: usize) -> AnyResult<Vec<f32>> {
let source_id = self.reader.identifier();
if let Some(cached) = tile_cache::get(source_id, tile_index, None) {
return Ok((*cached).clone());
}
if tile_index >= self.metadata.tile_offsets.len() {
return Err(format!(
"Tile index {} out of range (max {})",
tile_index,
self.metadata.tile_offsets.len()
)
.into());
}
let offset = self.metadata.tile_offsets[tile_index];
#[allow(clippy::cast_possible_truncation)]
let byte_count = self.metadata.tile_byte_counts[tile_index] as usize;
if byte_count == 0 {
let pixel_count = self.metadata.tile_width * self.metadata.tile_height * self.metadata.bands;
return Ok(vec![f32::NAN; pixel_count]);
}
let compressed = self.reader.read_range(offset, byte_count)?;
let decompressed = decompress_tile(
&compressed,
self.metadata.compression,
self.metadata.tile_width,
self.metadata.tile_height,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let unpredicted = apply_predictor(
&decompressed,
self.metadata.predictor,
self.metadata.tile_width,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let result = convert_to_f32(
&unpredicted,
self.metadata.data_type,
self.metadata.little_endian,
);
tile_cache::insert(source_id, tile_index, None, Arc::new(result.clone()));
Ok(result)
}
pub fn read_tile_with_bytes(&self, tile_index: usize) -> AnyResult<(Vec<f32>, usize)> {
let source_id = self.reader.identifier();
if let Some(cached) = tile_cache::get(source_id, tile_index, None) {
return Ok(((*cached).clone(), 0)); }
if tile_index >= self.metadata.tile_offsets.len() {
return Err(format!(
"Tile index {} out of range (max {})",
tile_index,
self.metadata.tile_offsets.len()
).into());
}
let offset = self.metadata.tile_offsets[tile_index];
#[allow(clippy::cast_possible_truncation)]
let byte_count = self.metadata.tile_byte_counts[tile_index] as usize;
if byte_count == 0 {
let pixel_count = self.metadata.tile_width * self.metadata.tile_height * self.metadata.bands;
return Ok((vec![f32::NAN; pixel_count], 0));
}
let compressed = self.reader.read_range(offset, byte_count)?;
let decompressed = decompress_tile(
&compressed,
self.metadata.compression,
self.metadata.tile_width,
self.metadata.tile_height,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let unpredicted = apply_predictor(
&decompressed,
self.metadata.predictor,
self.metadata.tile_width,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let result = convert_to_f32(
&unpredicted,
self.metadata.data_type,
self.metadata.little_endian,
);
tile_cache::insert(source_id, tile_index, None, Arc::new(result.clone()));
Ok((result, byte_count))
}
pub fn read_overview_tile_with_bytes(&self, overview_idx: usize, tile_index: usize) -> AnyResult<(Vec<f32>, usize)> {
let source_id = self.reader.identifier();
if let Some(cached) = tile_cache::get(source_id, tile_index, Some(overview_idx)) {
return Ok(((*cached).clone(), 0)); }
let ovr = self.overviews.get(overview_idx)
.ok_or_else(|| format!("Overview index {overview_idx} out of range"))?;
if tile_index >= ovr.tile_offsets.len() {
return Err(format!(
"Tile index {} out of range (max {})",
tile_index,
ovr.tile_offsets.len()
).into());
}
let offset = ovr.tile_offsets[tile_index];
#[allow(clippy::cast_possible_truncation)]
let byte_count = ovr.tile_byte_counts[tile_index] as usize;
if byte_count == 0 {
let pixel_count = ovr.tile_width * ovr.tile_height * self.metadata.bands;
return Ok((vec![f32::NAN; pixel_count], 0));
}
let compressed = self.reader.read_range(offset, byte_count)?;
let decompressed = decompress_tile(
&compressed,
self.metadata.compression,
ovr.tile_width,
ovr.tile_height,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let unpredicted = apply_predictor(
&decompressed,
self.metadata.predictor,
ovr.tile_width,
self.metadata.bands,
self.metadata.data_type.bytes_per_sample(),
)?;
let result = convert_to_f32(
&unpredicted,
self.metadata.data_type,
self.metadata.little_endian,
);
tile_cache::insert(source_id, tile_index, Some(overview_idx), Arc::new(result.clone()));
Ok((result, byte_count))
}
pub fn sample(&self, band: usize, x: usize, y: usize) -> AnyResult<Option<f32>> {
let Some(tile_index) = self.metadata.tile_index_for_pixel(x, y) else {
return Ok(None);
};
let tile_data = self.read_tile(tile_index)?;
let tile_col = tile_index % self.metadata.tiles_across;
let tile_row = tile_index / self.metadata.tiles_across;
let local_x = x - tile_col * self.metadata.tile_width;
let local_y = y - tile_row * self.metadata.tile_height;
let idx = (local_y * self.metadata.tile_width + local_x) * self.metadata.bands + band;
Ok(tile_data.get(idx).copied())
}
pub fn estimate_min_max(&self) -> AnyResult<(f32, f32)> {
if let (Some(min), Some(max)) = (self.metadata.stats_min, self.metadata.stats_max) {
return Ok((min, max));
}
if self.reader.is_local() {
self.estimate_min_max_full_scan()
} else {
self.estimate_min_max_fast()
}
}
pub fn estimate_min_max_fast(&self) -> AnyResult<(f32, f32)> {
if let (Some(min), Some(max)) = (self.metadata.stats_min, self.metadata.stats_max) {
return Ok((min, max));
}
if !self.overviews.is_empty() {
let smallest_ovr_idx = self.overviews.len() - 1;
let ovr = &self.overviews[smallest_ovr_idx];
let total_tiles = ovr.tile_offsets.len();
let sample_indices: Vec<usize> = if total_tiles <= 5 {
(0..total_tiles).collect()
} else {
vec![
0,
ovr.tiles_across.saturating_sub(1),
total_tiles / 2,
total_tiles.saturating_sub(ovr.tiles_across),
total_tiles.saturating_sub(1),
]
};
return self.scan_tiles_for_minmax(&sample_indices, Some(smallest_ovr_idx));
}
let total_tiles = self.metadata.tile_offsets.len();
let sample_indices: Vec<usize> = if total_tiles <= 5 {
(0..total_tiles).collect()
} else {
vec![
0, self.metadata.tiles_across.saturating_sub(1), total_tiles / 2, total_tiles.saturating_sub(self.metadata.tiles_across), total_tiles.saturating_sub(1), ]
};
self.scan_tiles_for_minmax(&sample_indices, None)
}
fn estimate_min_max_full_scan(&self) -> AnyResult<(f32, f32)> {
if !self.overviews.is_empty() {
let smallest_ovr_idx = self.overviews.len() - 1;
let ovr = &self.overviews[smallest_ovr_idx];
let all_indices: Vec<usize> = (0..ovr.tile_offsets.len()).collect();
return self.scan_tiles_for_minmax(&all_indices, Some(smallest_ovr_idx));
}
let all_indices: Vec<usize> = (0..self.metadata.tile_offsets.len()).collect();
self.scan_tiles_for_minmax(&all_indices, None)
}
fn scan_tiles_for_minmax(&self, indices: &[usize], overview_idx: Option<usize>) -> AnyResult<(f32, f32)> {
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
let nodata = self.metadata.nodata;
for &tile_idx in indices {
let tile_data = if let Some(ovr_idx) = overview_idx {
self.read_overview_tile(ovr_idx, tile_idx)?
} else {
self.read_tile(tile_idx)?
};
for &val in &tile_data {
if val.is_nan() {
continue;
}
if let Some(nd) = nodata
&& (f64::from(val) - nd).abs() < 0.001 {
continue;
}
if val < min {
min = val;
}
if val > max {
max = val;
}
}
}
if min.is_infinite() || max.is_infinite() {
Ok((0.0, 1.0)) } else {
Ok((min, max))
}
}
#[must_use]
pub fn clone_for_async(&self) -> Self {
Self {
reader: Arc::clone(&self.reader),
metadata: self.metadata.clone(),
overviews: self.overviews.clone(),
min_usable_overview: self.min_usable_overview,
}
}
}
#[inline]
fn read_u16(bytes: &[u8], little_endian: bool) -> u16 {
if little_endian {
u16::from_le_bytes([bytes[0], bytes[1]])
} else {
u16::from_be_bytes([bytes[0], bytes[1]])
}
}
#[inline]
fn read_u32(bytes: &[u8], little_endian: bool) -> u32 {
if little_endian {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
} else {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
}
#[inline]
fn read_u64(bytes: &[u8], little_endian: bool) -> u64 {
if little_endian {
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
])
} else {
u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
])
}
}
#[inline]
fn read_f64(bytes: &[u8], little_endian: bool) -> f64 {
if little_endian {
f64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
])
} else {
f64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
])
}
}
#[allow(clippy::type_complexity)] fn parse_tile_layout(
tags: &HashMap<u16, IfdEntry>,
reader: &Arc<dyn RangeReader>,
ifd_offset: u64,
little_endian: bool,
width: usize,
height: usize,
) -> AnyResult<(usize, usize, usize, usize, bool, Vec<u64>, Vec<u64>)> {
let has_tile_tags = tags.contains_key(&TAG_TILE_OFFSETS);
let has_strip_tags = tags.contains_key(&TAG_STRIP_OFFSETS);
let is_tiled = has_tile_tags;
if is_tiled {
#[allow(clippy::cast_possible_truncation)]
let tw = get_tag_value(tags, TAG_TILE_WIDTH, little_endian).unwrap_or(width as u32) as usize;
#[allow(clippy::cast_possible_truncation)]
let th = get_tag_value(tags, TAG_TILE_LENGTH, little_endian).unwrap_or(height as u32) as usize;
let ta = width.div_ceil(tw);
let td = height.div_ceil(th);
let total_tiles = ta * td;
let offsets = read_tag_array_u64(
tags,
TAG_TILE_OFFSETS,
reader,
ifd_offset,
little_endian,
total_tiles,
)?;
let byte_counts = read_tag_array_u64(
tags,
TAG_TILE_BYTE_COUNTS,
reader,
ifd_offset,
little_endian,
total_tiles,
)?;
Ok((tw, th, ta, td, is_tiled, offsets, byte_counts))
} else if has_strip_tags {
#[allow(clippy::cast_possible_truncation)]
let rows_per_strip = get_tag_value(tags, TAG_ROWS_PER_STRIP, little_endian)
.unwrap_or(height as u32) as usize;
let tw = width; let th = rows_per_strip;
let ta = 1; let td = height.div_ceil(rows_per_strip);
let total_strips = td;
let offsets = read_tag_array_u64(
tags,
TAG_STRIP_OFFSETS,
reader,
ifd_offset,
little_endian,
total_strips,
)?;
let byte_counts = read_tag_array_u64(
tags,
TAG_STRIP_BYTE_COUNTS,
reader,
ifd_offset,
little_endian,
total_strips,
)?;
Ok((tw, th, ta, td, false, offsets, byte_counts))
} else {
Err("TIFF has neither tile nor strip tags".into())
}
}
fn parse_ifd(
ifd_bytes: &[u8],
reader: &Arc<dyn RangeReader>,
ifd_offset: u64,
little_endian: bool,
) -> AnyResult<CogMetadata> {
let entry_count = read_u16(&ifd_bytes[0..2], little_endian) as usize;
let mut tags: HashMap<u16, IfdEntry> = HashMap::new();
for i in 0..entry_count {
let offset = 2 + i * 12;
if offset + 12 > ifd_bytes.len() {
break;
}
let tag = read_u16(&ifd_bytes[offset..offset + 2], little_endian);
let field_type = read_u16(&ifd_bytes[offset + 2..offset + 4], little_endian);
let count = read_u32(&ifd_bytes[offset + 4..offset + 8], little_endian);
let value_offset = read_u32(&ifd_bytes[offset + 8..offset + 12], little_endian);
tags.insert(
tag,
IfdEntry {
field_type,
count,
value_offset,
raw_bytes: [
ifd_bytes[offset + 8],
ifd_bytes[offset + 9],
ifd_bytes[offset + 10],
ifd_bytes[offset + 11],
],
},
);
}
let width = get_tag_value(&tags, TAG_IMAGE_WIDTH, little_endian)
.ok_or("Missing ImageWidth tag")? as usize;
let height = get_tag_value(&tags, TAG_IMAGE_LENGTH, little_endian)
.ok_or("Missing ImageLength tag")? as usize;
#[allow(clippy::cast_possible_truncation)]
let bits_per_sample = get_tag_value(&tags, TAG_BITS_PER_SAMPLE, little_endian).unwrap_or(8) as u16;
#[allow(clippy::cast_possible_truncation)]
let sample_format = get_tag_value(&tags, TAG_SAMPLE_FORMAT, little_endian).unwrap_or(1) as u16;
#[allow(clippy::cast_possible_truncation)]
let bands = get_tag_value(&tags, TAG_SAMPLES_PER_PIXEL, little_endian).unwrap_or(1) as usize;
#[allow(clippy::cast_possible_truncation)]
let compression_val = get_tag_value(&tags, TAG_COMPRESSION, little_endian).unwrap_or(1) as u16;
#[allow(clippy::cast_possible_truncation)]
let predictor = get_tag_value(&tags, TAG_PREDICTOR, little_endian).unwrap_or(1) as u16;
let data_type = CogDataType::from_tags(bits_per_sample, sample_format)
.ok_or_else(|| format!("Unsupported data type: bits={bits_per_sample}, format={sample_format}"))?;
let compression = Compression::from_tag(compression_val)
.ok_or_else(|| format!("Unsupported compression: {compression_val}"))?;
let (tile_width, tile_height, tiles_across, tiles_down, is_tiled, tile_offsets, tile_byte_counts) =
parse_tile_layout(&tags, reader, ifd_offset, little_endian, width, height)?;
let pixel_scale = read_tag_f64_array(&tags, TAG_MODEL_PIXEL_SCALE, reader, ifd_offset, little_endian, 3)?;
let tiepoint = read_tag_f64_array(&tags, TAG_MODEL_TIEPOINT, reader, ifd_offset, little_endian, 6)?;
let crs_code = read_crs_from_geokeys(&tags, reader, ifd_offset, little_endian)?;
let is_point_from_geokey = read_raster_type_from_geokeys(&tags, reader, little_endian)?;
let (stats_min, stats_max, is_point_from_gdal) = read_gdal_metadata_info(&tags, reader, ifd_offset, little_endian)?;
let is_point_registered = is_point_from_geokey || is_point_from_gdal;
let geo_transform = GeoTransform {
pixel_scale: pixel_scale.map(|v| [v[0], v[1], v[2]]),
tiepoint: tiepoint.map(|v| [v[0], v[1], v[2], v[3], v[4], v[5]]),
is_point_registered,
};
let nodata = read_gdal_nodata(&tags, reader, ifd_offset, little_endian)?;
Ok(CogMetadata {
width,
height,
tile_width,
tile_height,
bands,
data_type,
compression,
predictor,
little_endian,
tile_offsets,
tile_byte_counts,
tiles_across,
tiles_down,
is_tiled,
geo_transform,
crs_code,
stats_min,
stats_max,
nodata,
})
}
fn parse_ifd_with_next(
ifd_bytes: &[u8],
reader: &Arc<dyn RangeReader>,
ifd_offset: u64,
little_endian: bool,
) -> AnyResult<(CogMetadata, u32)> {
let entry_count = read_u16(&ifd_bytes[0..2], little_endian) as usize;
let next_ifd_pos = 2 + entry_count * 12;
let next_ifd_offset = if next_ifd_pos + 4 <= ifd_bytes.len() {
read_u32(&ifd_bytes[next_ifd_pos..next_ifd_pos + 4], little_endian)
} else {
0
};
let metadata = parse_ifd(ifd_bytes, reader, ifd_offset, little_endian)?;
Ok((metadata, next_ifd_offset))
}
struct OverviewIfdData {
width: usize,
height: usize,
tile_width: usize,
tile_height: usize,
tiles_across: usize,
tiles_down: usize,
tile_offsets: Vec<u64>,
tile_byte_counts: Vec<u64>,
}
fn parse_overview_ifd(
ifd_bytes: &[u8],
reader: &Arc<dyn RangeReader>,
ifd_offset: u64,
little_endian: bool,
_full_meta: &CogMetadata, ) -> AnyResult<(OverviewIfdData, u32)> {
let entry_count = read_u16(&ifd_bytes[0..2], little_endian) as usize;
let mut tags: HashMap<u16, IfdEntry> = HashMap::new();
for i in 0..entry_count {
let offset = 2 + i * 12;
if offset + 12 > ifd_bytes.len() {
break;
}
let tag = read_u16(&ifd_bytes[offset..offset + 2], little_endian);
let field_type = read_u16(&ifd_bytes[offset + 2..offset + 4], little_endian);
let count = read_u32(&ifd_bytes[offset + 4..offset + 8], little_endian);
let value_offset = read_u32(&ifd_bytes[offset + 8..offset + 12], little_endian);
tags.insert(
tag,
IfdEntry {
field_type,
count,
value_offset,
raw_bytes: [
ifd_bytes[offset + 8],
ifd_bytes[offset + 9],
ifd_bytes[offset + 10],
ifd_bytes[offset + 11],
],
},
);
}
let next_ifd_pos = 2 + entry_count * 12;
let next_ifd_offset = if next_ifd_pos + 4 <= ifd_bytes.len() {
read_u32(&ifd_bytes[next_ifd_pos..next_ifd_pos + 4], little_endian)
} else {
0
};
let width = get_tag_value(&tags, TAG_IMAGE_WIDTH, little_endian)
.ok_or("Overview missing ImageWidth tag")? as usize;
let height = get_tag_value(&tags, TAG_IMAGE_LENGTH, little_endian)
.ok_or("Overview missing ImageLength tag")? as usize;
let tile_width = get_tag_value(&tags, TAG_TILE_WIDTH, little_endian)
.ok_or("Overview missing TileWidth tag")? as usize;
let tile_height = get_tag_value(&tags, TAG_TILE_LENGTH, little_endian)
.ok_or("Overview missing TileLength tag")? as usize;
let tiles_across = width.div_ceil(tile_width);
let tiles_down = height.div_ceil(tile_height);
let total_tiles = tiles_across * tiles_down;
let tile_offsets = read_tag_array_u64(
&tags,
TAG_TILE_OFFSETS,
reader,
ifd_offset,
little_endian,
total_tiles,
)?;
let tile_byte_counts = read_tag_array_u64(
&tags,
TAG_TILE_BYTE_COUNTS,
reader,
ifd_offset,
little_endian,
total_tiles,
)?;
Ok((OverviewIfdData {
width,
height,
tile_width,
tile_height,
tiles_across,
tiles_down,
tile_offsets,
tile_byte_counts,
}, next_ifd_offset))
}
struct IfdEntry {
field_type: u16,
count: u32,
value_offset: u32,
raw_bytes: [u8; 4],
}
fn get_tag_value(tags: &HashMap<u16, IfdEntry>, tag: u16, little_endian: bool) -> Option<u32> {
let entry = tags.get(&tag)?;
let type_size = match entry.field_type {
1 => 1, 3 => 2, 4 => 4, _ => return None,
};
if entry.count == 1 && type_size <= 4 {
match entry.field_type {
1 => Some(u32::from(entry.raw_bytes[0])),
3 => Some(u32::from(read_u16(&entry.raw_bytes, little_endian))),
4 => Some(read_u32(&entry.raw_bytes, little_endian)),
_ => None,
}
} else {
None }
}
fn read_tag_array_u64(
tags: &HashMap<u16, IfdEntry>,
tag: u16,
reader: &Arc<dyn RangeReader>,
_ifd_offset: u64,
little_endian: bool,
expected_count: usize,
) -> AnyResult<Vec<u64>> {
let entry = tags.get(&tag).ok_or_else(|| format!("Missing tag {tag}"))?;
let type_size = match entry.field_type {
3 => 2, 4 => 4, 16 => 8, _ => return Err(format!("Unsupported type {} for tag {}", entry.field_type, tag).into()),
};
let total_bytes = entry.count as usize * type_size;
let raw_bytes = if total_bytes <= 4 {
entry.raw_bytes[..total_bytes].to_vec()
} else {
reader.read_range(u64::from(entry.value_offset), total_bytes)?
};
let mut values = Vec::with_capacity(entry.count as usize);
for i in 0..entry.count as usize {
let offset = i * type_size;
let value = match entry.field_type {
3 => u64::from(read_u16(&raw_bytes[offset..], little_endian)),
4 => u64::from(read_u32(&raw_bytes[offset..], little_endian)),
16 => read_u64(&raw_bytes[offset..], little_endian),
_ => 0,
};
values.push(value);
}
while values.len() < expected_count {
values.push(0);
}
Ok(values)
}
fn read_tag_f64_array(
tags: &HashMap<u16, IfdEntry>,
tag: u16,
reader: &Arc<dyn RangeReader>,
_ifd_offset: u64,
little_endian: bool,
min_count: usize,
) -> AnyResult<Option<Vec<f64>>> {
let Some(entry) = tags.get(&tag) else {
return Ok(None);
};
if entry.field_type != 12 {
return Ok(None);
}
if (entry.count as usize) < min_count {
return Ok(None);
}
let total_bytes = entry.count as usize * 8;
let raw_bytes = reader.read_range(u64::from(entry.value_offset), total_bytes)?;
let mut values = Vec::with_capacity(entry.count as usize);
for i in 0..entry.count as usize {
let offset = i * 8;
values.push(read_f64(&raw_bytes[offset..], little_endian));
}
Ok(Some(values))
}
const GEO_KEY_RASTER_TYPE: u16 = 1025;
fn read_crs_from_geokeys(
tags: &HashMap<u16, IfdEntry>,
reader: &Arc<dyn RangeReader>,
_ifd_offset: u64,
little_endian: bool,
) -> AnyResult<Option<i32>> {
let Some(raw_bytes) = read_geokey_directory(tags, reader, little_endian)? else {
return Ok(None);
};
if raw_bytes.len() < 8 {
return Ok(None);
}
let num_keys = read_u16(&raw_bytes[6..8], little_endian) as usize;
for i in 0..num_keys {
let offset = 8 + i * 8;
if offset + 8 > raw_bytes.len() {
break;
}
let key_id = read_u16(&raw_bytes[offset..], little_endian);
let _tiff_tag_location = read_u16(&raw_bytes[offset + 2..], little_endian);
let _count = read_u16(&raw_bytes[offset + 4..], little_endian);
let value = read_u16(&raw_bytes[offset + 6..], little_endian);
if key_id == GEO_KEY_PROJECTED_CRS && value > 0 {
return Ok(Some(i32::from(value)));
}
if key_id == GEO_KEY_GEOGRAPHIC_TYPE && value > 0 {
return Ok(Some(i32::from(value)));
}
}
Ok(None)
}
fn read_raster_type_from_geokeys(
tags: &HashMap<u16, IfdEntry>,
reader: &Arc<dyn RangeReader>,
little_endian: bool,
) -> AnyResult<bool> {
let Some(raw_bytes) = read_geokey_directory(tags, reader, little_endian)? else {
return Ok(false);
};
if raw_bytes.len() < 8 {
return Ok(false);
}
let num_keys = read_u16(&raw_bytes[6..8], little_endian) as usize;
for i in 0..num_keys {
let offset = 8 + i * 8;
if offset + 8 > raw_bytes.len() {
break;
}
let key_id = read_u16(&raw_bytes[offset..], little_endian);
let tiff_tag_location = read_u16(&raw_bytes[offset + 2..], little_endian);
let _count = read_u16(&raw_bytes[offset + 4..], little_endian);
let value = read_u16(&raw_bytes[offset + 6..], little_endian);
if key_id == GEO_KEY_RASTER_TYPE && tiff_tag_location == 0 {
return Ok(value == 2); }
}
Ok(false)
}
fn read_geokey_directory(
tags: &HashMap<u16, IfdEntry>,
reader: &Arc<dyn RangeReader>,
_little_endian: bool,
) -> AnyResult<Option<Vec<u8>>> {
let Some(entry) = tags.get(&TAG_GEO_KEY_DIRECTORY) else {
return Ok(None);
};
if entry.field_type != 3 {
return Ok(None);
}
let total_bytes = entry.count as usize * 2;
let raw_bytes = if total_bytes <= 4 {
entry.raw_bytes[..total_bytes].to_vec()
} else {
reader.read_range(u64::from(entry.value_offset), total_bytes)?
};
Ok(Some(raw_bytes))
}
#[allow(dead_code)]
fn read_geokey_directory_unused(_little_endian: bool) {}
fn read_gdal_metadata_info(
tags: &HashMap<u16, IfdEntry>,
reader: &Arc<dyn RangeReader>,
_ifd_offset: u64,
_little_endian: bool,
) -> AnyResult<(Option<f32>, Option<f32>, bool)> {
let Some(entry) = tags.get(&TAG_GDAL_METADATA) else {
return Ok((None, None, false));
};
let total_bytes = entry.count as usize;
let raw_bytes = if total_bytes <= 4 {
entry.raw_bytes[..total_bytes].to_vec()
} else {
reader.read_range(u64::from(entry.value_offset), total_bytes)?
};
let metadata_str = String::from_utf8_lossy(&raw_bytes);
let min = extract_gdal_stat(&metadata_str, "STATISTICS_MINIMUM");
let max = extract_gdal_stat(&metadata_str, "STATISTICS_MAXIMUM");
let is_point_registered = extract_gdal_str(&metadata_str, "AREA_OR_POINT")
.map(|s| s == "Point")
.unwrap_or(false);
Ok((min, max, is_point_registered))
}
fn extract_gdal_stat(metadata: &str, key: &str) -> Option<f32> {
extract_gdal_str(metadata, key)?.parse().ok()
}
fn extract_gdal_str(metadata: &str, key: &str) -> Option<String> {
let needle = format!("name=\"{key}\"");
let pos = metadata.find(&needle)?;
let rest = &metadata[pos..];
let start = rest.find('>')? + 1;
let rest = &rest[start..];
let end = rest.find('<')?;
Some(rest[..end].trim().to_string())
}
fn read_gdal_nodata(
tags: &HashMap<u16, IfdEntry>,
reader: &Arc<dyn RangeReader>,
_ifd_offset: u64,
_little_endian: bool,
) -> AnyResult<Option<f64>> {
let Some(entry) = tags.get(&TAG_GDAL_NODATA) else {
return Ok(None);
};
let total_bytes = entry.count as usize;
let raw_bytes = if total_bytes <= 4 {
entry.raw_bytes[..total_bytes].to_vec()
} else {
reader.read_range(u64::from(entry.value_offset), total_bytes)?
};
let nodata_str = String::from_utf8_lossy(&raw_bytes);
let nodata_str = nodata_str.trim_end_matches('\0').trim();
Ok(nodata_str.parse().ok())
}
fn decompress_tile(
compressed: &[u8],
compression: Compression,
tile_width: usize,
tile_height: usize,
bands: usize,
bytes_per_sample: usize,
) -> AnyResult<Vec<u8>> {
let expected_size = tile_width * tile_height * bands * bytes_per_sample;
match compression {
Compression::None => {
if compressed.len() >= expected_size {
Ok(compressed[..expected_size].to_vec())
} else {
let mut result = compressed.to_vec();
result.resize(expected_size, 0);
Ok(result)
}
}
Compression::Deflate => {
use std::io::Read;
let mut decoder = flate2::read::ZlibDecoder::new(compressed);
let mut decompressed = Vec::with_capacity(expected_size);
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
Compression::Lzw => {
let mut decoder = weezl::decode::Decoder::with_tiff_size_switch(weezl::BitOrder::Msb, 8);
let decompressed = decoder.decode(compressed)?;
Ok(decompressed)
}
Compression::Jpeg => {
use image::ImageReader;
use std::io::Cursor;
let cursor = Cursor::new(compressed);
let reader = ImageReader::with_format(cursor, image::ImageFormat::Jpeg);
let img = reader.decode()
.map_err(|e| format!("JPEG decode error: {e}"))?;
let raw = match img {
image::DynamicImage::ImageRgb8(rgb) => rgb.into_raw(),
image::DynamicImage::ImageRgba8(rgba) => rgba.into_raw(),
image::DynamicImage::ImageLuma8(gray) => gray.into_raw(),
image::DynamicImage::ImageLumaA8(gray_alpha) => gray_alpha.into_raw(),
other => {
other.to_rgb8().into_raw()
}
};
Ok(raw)
}
Compression::Zstd => {
let decompressed = zstd::stream::decode_all(compressed)?;
Ok(decompressed)
}
Compression::Webp => {
use image::ImageReader;
use std::io::Cursor;
let cursor = Cursor::new(compressed);
let reader = ImageReader::with_format(cursor, image::ImageFormat::WebP);
let img = reader.decode()
.map_err(|e| format!("WebP decode error: {e}"))?;
let raw = match img {
image::DynamicImage::ImageRgb8(rgb) => rgb.into_raw(),
image::DynamicImage::ImageRgba8(rgba) => rgba.into_raw(),
image::DynamicImage::ImageLuma8(gray) => gray.into_raw(),
image::DynamicImage::ImageLumaA8(gray_alpha) => gray_alpha.into_raw(),
other => {
other.to_rgb8().into_raw()
}
};
Ok(raw)
}
}
}
fn apply_predictor(
data: &[u8],
predictor: u16,
tile_width: usize,
bands: usize,
bytes_per_sample: usize,
) -> AnyResult<Vec<u8>> {
match predictor {
1 => Ok(data.to_vec()),
2 => {
let mut result = data.to_vec();
let row_bytes = tile_width * bands * bytes_per_sample;
let samples_per_row = tile_width * bands;
for row in result.chunks_mut(row_bytes) {
match bytes_per_sample {
1 => {
for i in bands..row.len() {
row[i] = row[i].wrapping_add(row[i - bands]);
}
}
2 => {
for i in bands..samples_per_row {
let prev_offset = (i - bands) * 2;
let curr_offset = i * 2;
let prev = u16::from_le_bytes([row[prev_offset], row[prev_offset + 1]]);
let curr = u16::from_le_bytes([row[curr_offset], row[curr_offset + 1]]);
let sum = curr.wrapping_add(prev);
row[curr_offset..curr_offset + 2].copy_from_slice(&sum.to_le_bytes());
}
}
4 => {
for i in bands..samples_per_row {
let prev_offset = (i - bands) * 4;
let curr_offset = i * 4;
let prev = u32::from_le_bytes([
row[prev_offset], row[prev_offset + 1],
row[prev_offset + 2], row[prev_offset + 3],
]);
let curr = u32::from_le_bytes([
row[curr_offset], row[curr_offset + 1],
row[curr_offset + 2], row[curr_offset + 3],
]);
let sum = curr.wrapping_add(prev);
row[curr_offset..curr_offset + 4].copy_from_slice(&sum.to_le_bytes());
}
}
8 => {
for i in bands..samples_per_row {
let prev_offset = (i - bands) * 8;
let curr_offset = i * 8;
let prev = u64::from_le_bytes([
row[prev_offset], row[prev_offset + 1],
row[prev_offset + 2], row[prev_offset + 3],
row[prev_offset + 4], row[prev_offset + 5],
row[prev_offset + 6], row[prev_offset + 7],
]);
let curr = u64::from_le_bytes([
row[curr_offset], row[curr_offset + 1],
row[curr_offset + 2], row[curr_offset + 3],
row[curr_offset + 4], row[curr_offset + 5],
row[curr_offset + 6], row[curr_offset + 7],
]);
let sum = curr.wrapping_add(prev);
row[curr_offset..curr_offset + 8].copy_from_slice(&sum.to_le_bytes());
}
}
_ => {
for i in bytes_per_sample..row.len() {
row[i] = row[i].wrapping_add(row[i - bytes_per_sample]);
}
}
}
}
Ok(result)
}
3 => {
let samples = bands; let row_bytes = tile_width * bands * bytes_per_sample;
let floats_per_row = tile_width * bands;
let tile_height = data.len() / row_bytes;
let mut output = vec![0u8; data.len()];
for row_idx in 0..tile_height {
let row_start = row_idx * row_bytes;
let row_end = row_start + row_bytes;
let mut row_data: Vec<u8> = data[row_start..row_end].to_vec();
for i in samples..row_data.len() {
row_data[i] = row_data[i].wrapping_add(row_data[i - samples]);
}
let output_row_start = row_idx * floats_per_row * bytes_per_sample;
match bytes_per_sample {
4 => {
for i in 0..floats_per_row {
let b0 = row_data[i];
let b1 = row_data[floats_per_row + i];
let b2 = row_data[floats_per_row * 2 + i];
let b3 = row_data[floats_per_row * 3 + i];
let val = u32::from_be_bytes([b0, b1, b2, b3]);
let out_offset = output_row_start + i * 4;
output[out_offset..out_offset + 4].copy_from_slice(&val.to_ne_bytes());
}
}
8 => {
for i in 0..floats_per_row {
let b0 = row_data[i];
let b1 = row_data[floats_per_row + i];
let b2 = row_data[floats_per_row * 2 + i];
let b3 = row_data[floats_per_row * 3 + i];
let b4 = row_data[floats_per_row * 4 + i];
let b5 = row_data[floats_per_row * 5 + i];
let b6 = row_data[floats_per_row * 6 + i];
let b7 = row_data[floats_per_row * 7 + i];
let val = u64::from_be_bytes([b0, b1, b2, b3, b4, b5, b6, b7]);
let out_offset = output_row_start + i * 8;
output[out_offset..out_offset + 8].copy_from_slice(&val.to_ne_bytes());
}
}
2 => {
for i in 0..floats_per_row {
let b0 = row_data[i];
let b1 = row_data[floats_per_row + i];
let val = u16::from_be_bytes([b0, b1]);
let out_offset = output_row_start + i * 2;
output[out_offset..out_offset + 2].copy_from_slice(&val.to_ne_bytes());
}
}
_ => {
return Err(format!(
"Predictor 3 not supported for {}-byte samples",
bytes_per_sample
).into());
}
}
}
Ok(output)
}
_ => Err(format!("Unsupported predictor: {predictor}").into()),
}
}
fn convert_to_f32(data: &[u8], data_type: CogDataType, little_endian: bool) -> Vec<f32> {
let bytes_per_sample = data_type.bytes_per_sample();
let sample_count = data.len() / bytes_per_sample;
let mut result = Vec::with_capacity(sample_count);
for i in 0..sample_count {
let offset = i * bytes_per_sample;
let bytes = &data[offset..offset + bytes_per_sample];
let value = match data_type {
CogDataType::UInt8 => f32::from(bytes[0]),
CogDataType::Int8 => {
#[allow(clippy::cast_possible_wrap)]
f32::from(bytes[0] as i8)
}
CogDataType::UInt16 => {
if little_endian {
f32::from(u16::from_le_bytes([bytes[0], bytes[1]]))
} else {
f32::from(u16::from_be_bytes([bytes[0], bytes[1]]))
}
}
CogDataType::Int16 => {
if little_endian {
f32::from(i16::from_le_bytes([bytes[0], bytes[1]]))
} else {
f32::from(i16::from_be_bytes([bytes[0], bytes[1]]))
}
}
CogDataType::UInt32 => {
#[allow(clippy::cast_precision_loss)]
if little_endian {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f32
} else {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f32
}
}
CogDataType::Int32 => {
#[allow(clippy::cast_precision_loss)]
if little_endian {
i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f32
} else {
i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f32
}
}
CogDataType::Float32 => {
if little_endian {
f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
} else {
f32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
}
CogDataType::UInt64 => {
#[allow(clippy::cast_precision_loss)]
if little_endian {
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
]) as f32
} else {
u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
]) as f32
}
}
CogDataType::Int64 => {
#[allow(clippy::cast_precision_loss)]
if little_endian {
i64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
]) as f32
} else {
i64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
]) as f32
}
}
CogDataType::Float64 => {
#[allow(clippy::cast_possible_truncation)]
if little_endian {
f64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
]) as f32
} else {
f64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5], bytes[6], bytes[7],
]) as f32
}
}
};
result.push(value);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compare_with_tiff_crate() {
use std::io::BufReader;
use std::fs::File;
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/copernicus_dem_san_francisco.tif");
if !std::path::Path::new(path).exists() {
println!("Skipping: test file not found");
return;
}
let file = File::open(path).unwrap();
let mut decoder = tiff::decoder::Decoder::new(BufReader::new(file)).unwrap();
let dims = decoder.dimensions().unwrap();
println!("TIFF dimensions: {}x{}", dims.0, dims.1);
use tiff::tags::Tag;
let has_tile_offsets = decoder.get_tag(Tag::TileOffsets).is_ok();
let has_strip_offsets = decoder.get_tag(Tag::StripOffsets).is_ok();
println!("Has tile offsets: {}, Has strip offsets: {}", has_tile_offsets, has_strip_offsets);
if has_tile_offsets {
if let Ok(tw) = decoder.get_tag_unsigned::<u32>(Tag::TileWidth) {
println!("Tile width from tiff crate: {}", tw);
}
if let Ok(th) = decoder.get_tag_unsigned::<u32>(Tag::TileLength) {
println!("Tile height from tiff crate: {}", th);
}
}
let tiff_data = match decoder.read_image().unwrap() {
tiff::decoder::DecodingResult::F32(data) => {
println!("TIFF crate first 8 values: {:?}", &data[0..8]);
data
}
_ => panic!("Expected f32 data"),
};
let reader = crate::LocalRangeReader::new(path).unwrap();
let cog = CogReader::from_reader(std::sync::Arc::new(reader)).unwrap();
println!("COG metadata: {} x {}, {} bands, predictor={}",
cog.metadata.width, cog.metadata.height,
cog.metadata.bands, cog.metadata.predictor);
println!("Tile size: {} x {}", cog.metadata.tile_width, cog.metadata.tile_height);
println!("Compression: {:?}", cog.metadata.compression);
println!("is_point_registered: {}", cog.metadata.geo_transform.is_point_registered);
println!("tiepoint: {:?}", cog.metadata.geo_transform.tiepoint);
println!("pixel_scale: {:?}", cog.metadata.geo_transform.pixel_scale);
let lon = -122.24_f64;
let lat = 37.88_f64;
let (px, py) = cog.metadata.geo_transform.world_to_pixel(lon, lat).unwrap();
println!("\nBerkeley Hills ({}, {}):", lon, lat);
println!(" pixel: ({}, {})", px, py);
println!(" truncated: ({}, {})", px as usize, py as usize);
let our_data = cog.read_tile(0).unwrap();
println!("Our first 8 values: {:?}", &our_data[0..8]);
let tile_size = cog.metadata.tile_width * cog.metadata.tile_height;
println!("Our tile has {} values", our_data.len());
println!("TIFF tile would have {} values", tile_size);
let expected_bytes: [u8; 4] = 101.38305_f32.to_ne_bytes();
let got_bytes: [u8; 4] = our_data[0].to_ne_bytes();
println!("Expected first float bytes (native): {:02x} {:02x} {:02x} {:02x}",
expected_bytes[0], expected_bytes[1], expected_bytes[2], expected_bytes[3]);
println!("Got first float bytes (native): {:02x} {:02x} {:02x} {:02x}",
got_bytes[0], got_bytes[1], got_bytes[2], got_bytes[3]);
let tolerance = 0.001;
let mut mismatches = 0;
for i in 0..std::cmp::min(8, our_data.len()) {
let diff = (our_data[i] - tiff_data[i]).abs();
if diff > tolerance {
println!("Mismatch at {}: ours={} vs tiff={}", i, our_data[i], tiff_data[i]);
mismatches += 1;
}
}
assert_eq!(mismatches, 0, "Found {} value mismatches vs tiff crate", mismatches);
}
#[test]
fn test_data_type_detection() {
assert_eq!(CogDataType::from_tags(8, 1), Some(CogDataType::UInt8));
assert_eq!(CogDataType::from_tags(16, 1), Some(CogDataType::UInt16));
assert_eq!(CogDataType::from_tags(32, 3), Some(CogDataType::Float32));
assert_eq!(CogDataType::from_tags(64, 3), Some(CogDataType::Float64));
}
#[test]
fn test_compression_detection() {
assert_eq!(Compression::from_tag(1), Some(Compression::None));
assert_eq!(Compression::from_tag(5), Some(Compression::Lzw));
assert_eq!(Compression::from_tag(7), Some(Compression::Jpeg));
assert_eq!(Compression::from_tag(8), Some(Compression::Deflate));
assert_eq!(Compression::from_tag(50000), Some(Compression::Zstd));
assert_eq!(Compression::from_tag(50001), Some(Compression::Webp));
assert_eq!(Compression::from_tag(999), None);
}
#[test]
fn test_geo_transform() {
let transform = GeoTransform {
pixel_scale: Some([10.0, 10.0, 0.0]),
tiepoint: Some([0.0, 0.0, 0.0, 100.0, 200.0, 0.0]),
is_point_registered: false,
};
let (wx, wy) = transform.pixel_to_world(0.0, 0.0).unwrap();
assert!((wx - 100.0).abs() < 0.001);
assert!((wy - 200.0).abs() < 0.001);
let (wx, wy) = transform.pixel_to_world(10.0, 5.0).unwrap();
assert!((wx - 200.0).abs() < 0.001);
assert!((wy - 150.0).abs() < 0.001);
}
#[test]
fn test_geo_transform_point_registered() {
let transform = GeoTransform {
pixel_scale: Some([10.0, 10.0, 0.0]),
tiepoint: Some([0.0, 0.0, 0.0, 100.0, 200.0, 0.0]),
is_point_registered: true,
};
let (px, py) = transform.world_to_pixel(100.0, 200.0).unwrap();
assert!((px - 0.5).abs() < 0.001, "Expected px=0.5, got {}", px);
assert!((py - 0.5).abs() < 0.001, "Expected py=0.5, got {}", py);
let (px, py) = transform.world_to_pixel(105.0, 195.0).unwrap();
assert!((px - 1.0).abs() < 0.001, "Expected px=1.0, got {}", px);
assert!((py - 1.0).abs() < 0.001, "Expected py=1.0, got {}", py);
}
#[test]
fn test_real_cog_file() {
let path = "data/viridis/output_cog.tif";
if !std::path::Path::new(path).exists() {
println!("Skipping test - file not found: {}", path);
return;
}
let reader = CogReader::open(path).expect("Failed to open COG");
let m = &reader.metadata;
println!("Testing real COG: {}", path);
println!(" Width: {}, Height: {}", m.width, m.height);
println!(" Tile size: {}x{}", m.tile_width, m.tile_height);
println!(" CRS code: {:?}", m.crs_code);
println!(" Bands: {}", m.bands);
println!(" Compression: {:?}", m.compression);
println!(" Extent: {:?}", m.geo_transform.get_extent(m.width, m.height));
println!(" Pixel scale: {:?}", m.geo_transform.pixel_scale);
println!(" Tiepoint: {:?}", m.geo_transform.tiepoint);
assert!(m.width > 0, "Width should be positive");
assert!(m.height > 0, "Height should be positive");
assert!(m.is_tiled(), "Should be a tiled TIFF");
if let Some((px, py)) = m.geo_transform.world_to_pixel(0.0, 0.0) {
println!(" Lon 0, Lat 0 -> pixel ({}, {})", px, py);
assert!(px > 0.0 && px < m.width as f64, "X pixel should be in range");
assert!(py > 0.0 && py < m.height as f64, "Y pixel should be in range");
}
let tile_data = reader.read_tile(0).expect("Failed to read tile 0");
assert!(!tile_data.is_empty(), "Tile data should not be empty");
let non_nan = tile_data.iter().filter(|v| !v.is_nan()).count();
println!(" Tile 0: {} values, {} non-NaN", tile_data.len(), non_nan);
assert!(non_nan > 0, "Tile should have some valid pixels");
let (min, max) = reader.estimate_min_max().expect("Failed to estimate min/max");
println!(" Estimated min: {}, max: {}", min, max);
assert!(min >= 0.0, "Min should be >= 0");
assert!(max <= 255.0, "Max should be <= 255 for 8-bit data");
}
#[test]
fn test_predictor2_16bit_samples() {
let input: Vec<u8> = vec![0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00];
let result = apply_predictor(&input, 2, 4, 1, 2).unwrap();
let s0 = u16::from_le_bytes([result[0], result[1]]);
let s1 = u16::from_le_bytes([result[2], result[3]]);
let s2 = u16::from_le_bytes([result[4], result[5]]);
let s3 = u16::from_le_bytes([result[6], result[7]]);
assert_eq!(s0, 256, "Sample 0 should be 256");
assert_eq!(s1, 257, "Sample 1 should be 256 + 1 = 257");
assert_eq!(s2, 258, "Sample 2 should be 257 + 1 = 258");
assert_eq!(s3, 259, "Sample 3 should be 258 + 1 = 259");
}
#[test]
fn test_predictor2_32bit_samples() {
let input: Vec<u8> = vec![
0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ];
let result = apply_predictor(&input, 2, 4, 1, 4).unwrap();
let s0 = u32::from_le_bytes([result[0], result[1], result[2], result[3]]);
let s1 = u32::from_le_bytes([result[4], result[5], result[6], result[7]]);
let s2 = u32::from_le_bytes([result[8], result[9], result[10], result[11]]);
let s3 = u32::from_le_bytes([result[12], result[13], result[14], result[15]]);
assert_eq!(s0, 0x40000000, "Sample 0 should be 0x40000000");
assert_eq!(s1, 0x40000001, "Sample 1 should be 0x40000001");
assert_eq!(s2, 0x40000002, "Sample 2 should be 0x40000002");
assert_eq!(s3, 0x40000003, "Sample 3 should be 0x40000003");
}
#[test]
fn test_predictor2_64bit_samples() {
let input: Vec<u8> = vec![
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
let result = apply_predictor(&input, 2, 3, 1, 8).unwrap();
let s0 = u64::from_le_bytes([
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
]);
let s1 = u64::from_le_bytes([
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15],
]);
let s2 = u64::from_le_bytes([
result[16], result[17], result[18], result[19],
result[20], result[21], result[22], result[23],
]);
assert_eq!(s0, 0x1000, "Sample 0 should be 0x1000 (4096)");
assert_eq!(s1, 0x1001, "Sample 1 should be 0x1000 + 1 = 0x1001 (4097)");
assert_eq!(s2, 0x1002, "Sample 2 should be 0x1001 + 1 = 0x1002 (4098)");
}
#[test]
fn test_predictor2_wrapping_overflow() {
let input: Vec<u8> = vec![
0xFF, 0xFF, 0x01, 0x00, ];
let result = apply_predictor(&input, 2, 2, 1, 2).unwrap();
let s0 = u16::from_le_bytes([result[0], result[1]]);
let s1 = u16::from_le_bytes([result[2], result[3]]);
assert_eq!(s0, 65535, "Sample 0 should be 65535");
assert_eq!(s1, 0, "Sample 1 should wrap to 0 (65535 + 1)");
}
#[test]
fn test_predictor2_multiple_rows() {
let input: Vec<u8> = vec![
0x64, 0x00, 0x01, 0x00, 0x01, 0x00,
0xC8, 0x00, 0x02, 0x00, 0x02, 0x00,
];
let result = apply_predictor(&input, 2, 3, 1, 2).unwrap();
let r1s0 = u16::from_le_bytes([result[0], result[1]]);
let r1s1 = u16::from_le_bytes([result[2], result[3]]);
let r1s2 = u16::from_le_bytes([result[4], result[5]]);
let r2s0 = u16::from_le_bytes([result[6], result[7]]);
let r2s1 = u16::from_le_bytes([result[8], result[9]]);
let r2s2 = u16::from_le_bytes([result[10], result[11]]);
assert_eq!(r1s0, 100, "Row 1 Sample 0");
assert_eq!(r1s1, 101, "Row 1 Sample 1");
assert_eq!(r1s2, 102, "Row 1 Sample 2");
assert_eq!(r2s0, 200, "Row 2 Sample 0 - fresh start");
assert_eq!(r2s1, 202, "Row 2 Sample 1");
assert_eq!(r2s2, 204, "Row 2 Sample 2");
}
#[test]
fn test_predictor2_multiband_8bit() {
let input: Vec<u8> = vec![10, 20, 1, 2];
let result = apply_predictor(&input, 2, 2, 2, 1).unwrap();
assert_eq!(result[0], 10, "Pixel 0 Band 0");
assert_eq!(result[1], 20, "Pixel 0 Band 1");
assert_eq!(result[2], 11, "Pixel 1 Band 0 = 10 + 1");
assert_eq!(result[3], 22, "Pixel 1 Band 1 = 20 + 2");
}
#[test]
fn test_predictor2_multiband_16bit() {
let input: Vec<u8> = vec![
100, 0, 200, 0, 1, 0, 2, 0, ];
let result = apply_predictor(&input, 2, 2, 2, 2).unwrap();
let s0 = u16::from_le_bytes([result[0], result[1]]);
let s1 = u16::from_le_bytes([result[2], result[3]]);
let s2 = u16::from_le_bytes([result[4], result[5]]);
let s3 = u16::from_le_bytes([result[6], result[7]]);
assert_eq!(s0, 100, "Pixel 0 Band 0");
assert_eq!(s1, 200, "Pixel 0 Band 1");
assert_eq!(s2, 101, "Pixel 1 Band 0 = 100 + 1");
assert_eq!(s3, 202, "Pixel 1 Band 1 = 200 + 2");
}
#[test]
fn test_overview_hint_from_db_value() {
assert!(matches!(
OverviewQualityHint::from_db_value(None),
OverviewQualityHint::ComputeAtRuntime
));
assert!(matches!(
OverviewQualityHint::from_db_value(Some(-1)),
OverviewQualityHint::NoneUsable
));
assert!(matches!(
OverviewQualityHint::from_db_value(Some(-2)),
OverviewQualityHint::AllUsable
));
assert!(matches!(
OverviewQualityHint::from_db_value(Some(0)),
OverviewQualityHint::MinUsable(0)
));
assert!(matches!(
OverviewQualityHint::from_db_value(Some(3)),
OverviewQualityHint::MinUsable(3)
));
}
#[test]
fn test_overview_hint_to_db_value() {
assert_eq!(OverviewQualityHint::NoneUsable.to_db_value(), Some(-1));
assert_eq!(OverviewQualityHint::AllUsable.to_db_value(), Some(-2));
assert_eq!(OverviewQualityHint::MinUsable(0).to_db_value(), Some(0));
assert_eq!(OverviewQualityHint::MinUsable(5).to_db_value(), Some(5));
assert_eq!(OverviewQualityHint::ComputeAtRuntime.to_db_value(), None);
}
#[test]
fn test_webp_decompression() {
use image::{ImageBuffer, ImageFormat, Rgb, DynamicImage};
use std::io::Cursor;
let mut img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(2, 2);
img.put_pixel(0, 0, Rgb([255, 0, 0])); img.put_pixel(1, 0, Rgb([0, 255, 0])); img.put_pixel(0, 1, Rgb([0, 0, 255])); img.put_pixel(1, 1, Rgb([255, 255, 0]));
let mut webp_data = Cursor::new(Vec::new());
DynamicImage::ImageRgb8(img)
.write_to(&mut webp_data, ImageFormat::WebP)
.expect("Failed to encode WebP");
let result = decompress_tile(
webp_data.get_ref(),
Compression::Webp,
2, 2, 3, 1, ).expect("WebP decompression failed");
assert_eq!(result.len(), 12, "Expected 12 bytes for 2x2 RGB image");
assert!(result[0] > 200, "Red channel of red pixel should be high");
assert!(result[1] < 50, "Green channel of red pixel should be low");
assert!(result[2] < 50, "Blue channel of red pixel should be low");
assert!(result[3] < 50, "Red channel of green pixel should be low");
assert!(result[4] > 200, "Green channel of green pixel should be high");
assert!(result[5] < 50, "Blue channel of green pixel should be low");
assert!(result[6] < 50, "Red channel of blue pixel should be low");
assert!(result[7] < 50, "Green channel of blue pixel should be low");
assert!(result[8] > 200, "Blue channel of blue pixel should be high");
assert!(result[9] > 200, "Red channel of yellow pixel should be high");
assert!(result[10] > 200, "Green channel of yellow pixel should be high");
assert!(result[11] < 50, "Blue channel of yellow pixel should be low");
}
}
#[test]
fn test_gray_3857_crs_detection() {
let path = "data/grayscale/gray_3857-cog.tif";
if !std::path::Path::new(path).exists() {
println!("Skipping - file not found: {}", path);
return;
}
let reader = CogReader::open(path).expect("Failed to open COG");
assert_eq!(reader.metadata.crs_code, Some(3857), "CRS should be detected as 3857");
}
#[test]
fn test_overview_scale_uses_floor_division() {
let path = "data/grayscale/gray_3857-cog.tif";
if !std::path::Path::new(path).exists() {
println!("Skipping - file not found: {}", path);
return;
}
let reader = CogReader::open(path).expect("Failed to open COG");
assert!(!reader.overviews.is_empty(), "Should have overviews");
let full_width = reader.metadata.width;
for (i, ovr) in reader.overviews.iter().enumerate() {
let expected_scale = full_width / ovr.width;
assert_eq!(
ovr.scale, expected_scale,
"Overview {} scale mismatch: got {}, expected {} (floor of {}/{})",
i, ovr.scale, expected_scale, full_width, ovr.width
);
let ceiling_scale = full_width.div_ceil(ovr.width);
if ceiling_scale != expected_scale {
assert_ne!(
ovr.scale, ceiling_scale,
"Overview {} appears to use ceiling division (got {}), should use floor ({})",
i, ceiling_scale, expected_scale
);
}
}
if reader.overviews.len() > 3 {
let ovr3 = &reader.overviews[3];
assert_eq!(
ovr3.scale, 16,
"Overview 3 (1310x1310) scale should be 16 (floor), not 17 (ceiling)"
);
}
}
#[test]
fn test_overview_pixel_values_match_gdal() {
let path = "data/grayscale/gray_3857-cog.tif";
if !std::path::Path::new(path).exists() {
println!("Skipping - file not found: {}", path);
return;
}
let reader = CogReader::open(path).expect("Failed to open COG");
if reader.overviews.len() > 3 {
let ovr_idx = 3;
let ovr = &reader.overviews[ovr_idx];
assert_eq!(ovr.width, 1310, "Overview 3 should be 1310 wide");
assert_eq!(ovr.height, 1310, "Overview 3 should be 1310 tall");
assert_eq!(ovr.scale, 16, "Overview 3 scale should be 16");
let tile_data = reader.read_overview_tile(ovr_idx, 0).expect("Failed to read overview tile 0");
let expected_size = ovr.tile_width * ovr.tile_height;
assert_eq!(tile_data.len(), expected_size, "Tile data should be {}x{} pixels", ovr.tile_width, ovr.tile_height);
let valid_count = tile_data.iter().filter(|v| !v.is_nan()).count();
assert!(valid_count > 0, "Tile should have valid (non-NaN) pixels");
let min_val = tile_data.iter().filter(|v| !v.is_nan()).copied().fold(f32::INFINITY, f32::min);
let max_val = tile_data.iter().filter(|v| !v.is_nan()).copied().fold(f32::NEG_INFINITY, f32::max);
assert!(min_val >= 0.0, "Min value should be >= 0, got {}", min_val);
assert!(max_val <= 255.0, "Max value should be <= 255, got {}", max_val);
let corner_value = tile_data[0];
assert!(
!corner_value.is_nan() && (100.0..=255.0).contains(&corner_value),
"Corner value should be valid grayscale, got {}",
corner_value
);
}
}
#[test]
fn test_scale_factor_coordinate_mapping() {
let path = "data/grayscale/gray_3857-cog.tif";
if !std::path::Path::new(path).exists() {
println!("Skipping - file not found: {}", path);
return;
}
let reader = CogReader::open(path).expect("Failed to open COG");
if let (Some(pixel_scale), Some(_tiepoint)) = (
reader.metadata.geo_transform.pixel_scale,
reader.metadata.geo_transform.tiepoint,
) {
let base_scale_x = pixel_scale[0];
for (i, ovr) in reader.overviews.iter().enumerate() {
let effective_scale_x = base_scale_x * (ovr.scale as f64);
let full_extent_x = base_scale_x * (reader.metadata.width as f64);
let expected_effective_scale = full_extent_x / (ovr.width as f64);
let tolerance = expected_effective_scale * 0.01;
assert!(
(effective_scale_x - expected_effective_scale).abs() < tolerance,
"Overview {} effective scale mismatch: got {}, expected {} (within {})",
i, effective_scale_x, expected_effective_scale, tolerance
);
}
}
}
#[test]
fn test_best_overview_selection() {
let path = "data/grayscale/gray_3857-cog.tif";
if !std::path::Path::new(path).exists() {
println!("Skipping - file not found: {}", path);
return;
}
let reader = CogReader::open(path).expect("Failed to open COG");
let _full_res = reader.best_overview_for_resolution(256, 256);
let large_extent = reader.best_overview_for_resolution(20000, 20000);
assert!(
large_extent.is_some() || reader.overviews.is_empty(),
"Large extent should use an overview"
);
let medium_extent = reader.best_overview_for_resolution(5000, 5000);
if let Some(idx) = medium_extent {
assert!(
idx < reader.overviews.len(),
"Overview index {} should be valid",
idx
);
}
}
#[test]
fn test_predictor2_multibyte_samples() {
let input_16: Vec<u8> = vec![100, 0, 5, 0, 10, 0]; let result_16 = apply_predictor(&input_16, 2, 3, 1, 2).expect("predictor failed");
let s0 = u16::from_le_bytes([result_16[0], result_16[1]]);
let s1 = u16::from_le_bytes([result_16[2], result_16[3]]);
let s2 = u16::from_le_bytes([result_16[4], result_16[5]]);
assert_eq!(s0, 100, "First sample should be unchanged");
assert_eq!(s1, 105, "Second sample should be 100 + 5 = 105");
assert_eq!(s2, 115, "Third sample should be 105 + 10 = 115");
let mut input_32: Vec<u8> = Vec::new();
input_32.extend_from_slice(&1000u32.to_le_bytes());
input_32.extend_from_slice(&50u32.to_le_bytes());
input_32.extend_from_slice(&100u32.to_le_bytes());
let result_32 = apply_predictor(&input_32, 2, 3, 1, 4).expect("predictor failed");
let s0_32 = u32::from_le_bytes([result_32[0], result_32[1], result_32[2], result_32[3]]);
let s1_32 = u32::from_le_bytes([result_32[4], result_32[5], result_32[6], result_32[7]]);
let s2_32 = u32::from_le_bytes([result_32[8], result_32[9], result_32[10], result_32[11]]);
assert_eq!(s0_32, 1000, "First u32 sample should be unchanged");
assert_eq!(s1_32, 1050, "Second u32 sample should be 1000 + 50 = 1050");
assert_eq!(s2_32, 1150, "Third u32 sample should be 1050 + 100 = 1150");
let mut input_64: Vec<u8> = Vec::new();
input_64.extend_from_slice(&10000u64.to_le_bytes());
input_64.extend_from_slice(&500u64.to_le_bytes());
input_64.extend_from_slice(&1000u64.to_le_bytes());
let result_64 = apply_predictor(&input_64, 2, 3, 1, 8).expect("predictor failed");
let s0_64 = u64::from_le_bytes(result_64[0..8].try_into().unwrap());
let s1_64 = u64::from_le_bytes(result_64[8..16].try_into().unwrap());
let s2_64 = u64::from_le_bytes(result_64[16..24].try_into().unwrap());
assert_eq!(s0_64, 10000, "First u64 sample should be unchanged");
assert_eq!(s1_64, 10500, "Second u64 sample should be 10000 + 500 = 10500");
assert_eq!(s2_64, 11500, "Third u64 sample should be 10500 + 1000 = 11500");
}
#[test]
fn test_predictor2_wrapping_behavior() {
let mut input: Vec<u8> = Vec::new();
input.extend_from_slice(&65535u16.to_le_bytes()); input.extend_from_slice(&1u16.to_le_bytes());
let result = apply_predictor(&input, 2, 2, 1, 2).expect("predictor failed");
let s0 = u16::from_le_bytes([result[0], result[1]]);
let s1 = u16::from_le_bytes([result[2], result[3]]);
assert_eq!(s0, 65535, "First sample unchanged");
assert_eq!(s1, 0, "Second sample should wrap: 65535 + 1 = 0");
let mut input_32: Vec<u8> = Vec::new();
input_32.extend_from_slice(&0xFFFFFFFFu32.to_le_bytes());
input_32.extend_from_slice(&2u32.to_le_bytes());
let result_32 = apply_predictor(&input_32, 2, 2, 1, 4).expect("predictor failed");
let s1_32 = u32::from_le_bytes([result_32[4], result_32[5], result_32[6], result_32[7]]);
assert_eq!(s1_32, 1, "u32 should wrap: 0xFFFFFFFF + 2 = 1");
}
#[test]
fn test_predictor2_multirow() {
let mut input: Vec<u8> = Vec::new();
input.extend_from_slice(&100u16.to_le_bytes());
input.extend_from_slice(&10u16.to_le_bytes());
input.extend_from_slice(&20u16.to_le_bytes());
input.extend_from_slice(&200u16.to_le_bytes());
input.extend_from_slice(&5u16.to_le_bytes());
input.extend_from_slice(&15u16.to_le_bytes());
let result = apply_predictor(&input, 2, 3, 1, 2).expect("predictor failed");
let r1_s0 = u16::from_le_bytes([result[0], result[1]]);
let r1_s1 = u16::from_le_bytes([result[2], result[3]]);
let r1_s2 = u16::from_le_bytes([result[4], result[5]]);
assert_eq!(r1_s0, 100, "Row 1, sample 0");
assert_eq!(r1_s1, 110, "Row 1, sample 1: 100 + 10 = 110");
assert_eq!(r1_s2, 130, "Row 1, sample 2: 110 + 20 = 130");
let r2_s0 = u16::from_le_bytes([result[6], result[7]]);
let r2_s1 = u16::from_le_bytes([result[8], result[9]]);
let r2_s2 = u16::from_le_bytes([result[10], result[11]]);
assert_eq!(r2_s0, 200, "Row 2, sample 0 (fresh start)");
assert_eq!(r2_s1, 205, "Row 2, sample 1: 200 + 5 = 205");
assert_eq!(r2_s2, 220, "Row 2, sample 2: 205 + 15 = 220");
}
#[cfg(test)]
mod gdal_verification_tests {
use super::*;
use crate::point_query::PointQuery;
use gdal::Metadata;
use std::sync::Arc;
const TEST_COG_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/copernicus_dem_san_francisco.tif");
fn get_test_cog() -> Option<CogReader> {
if !std::path::Path::new(TEST_COG_PATH).exists() {
println!("Skipping: test file not found at {}", TEST_COG_PATH);
return None;
}
let reader = crate::LocalRangeReader::new(TEST_COG_PATH).ok()?;
CogReader::from_reader(Arc::new(reader)).ok()
}
#[test]
fn test_gdal_geotransform_comparison() {
let Some(cog) = get_test_cog() else { return };
let gdal_ds = gdal::Dataset::open(TEST_COG_PATH).expect("GDAL failed to open");
let gdal_gt = gdal_ds.geo_transform().expect("Failed to get geotransform");
let area_or_point = gdal_ds.metadata_item("AREA_OR_POINT", "").unwrap_or_default();
println!("GDAL AREA_OR_POINT: {:?}", area_or_point);
println!("Our is_point_registered: {}", cog.metadata.geo_transform.is_point_registered);
let gdal_origin_x = gdal_gt[0];
let gdal_origin_y = gdal_gt[3];
let gdal_pixel_width = gdal_gt[1];
let gdal_pixel_height = -gdal_gt[5];
println!("GDAL geotransform: {:?}", gdal_gt);
println!("Our tiepoint: {:?}", cog.metadata.geo_transform.tiepoint);
println!("Our pixel_scale: {:?}", cog.metadata.geo_transform.pixel_scale);
if let Some(scale) = &cog.metadata.geo_transform.pixel_scale {
assert!((scale[0] - gdal_pixel_width).abs() < 1e-12,
"Pixel width mismatch: ours={}, gdal={}", scale[0], gdal_pixel_width);
assert!((scale[1] - gdal_pixel_height).abs() < 1e-12,
"Pixel height mismatch: ours={}, gdal={}", scale[1], gdal_pixel_height);
}
if cog.metadata.geo_transform.is_point_registered {
println!("\nPixelIsPoint dataset - verifying coordinate transform matches GDAL");
let test_points = [
(-122.4, 37.78), (-122.24, 37.88), (-122.5965, 37.9236), ];
for (lon, lat) in test_points {
let (our_px, our_py) = cog.metadata.geo_transform.world_to_pixel(lon, lat).unwrap();
let gdal_px = (lon - gdal_origin_x) / gdal_pixel_width;
let gdal_py = (gdal_origin_y - lat) / gdal_pixel_height;
println!("Point ({}, {}): ours=({:.6}, {:.6}), gdal=({:.6}, {:.6})",
lon, lat, our_px, our_py, gdal_px, gdal_py);
assert!((our_px - gdal_px).abs() < 0.001,
"X pixel mismatch at ({}, {}): ours={}, gdal={}", lon, lat, our_px, gdal_px);
assert!((our_py - gdal_py).abs() < 0.001,
"Y pixel mismatch at ({}, {}): ours={}, gdal={}", lon, lat, our_py, gdal_py);
}
}
}
#[test]
fn test_gdal_pixel_value_comparison() {
let Some(cog) = get_test_cog() else { return };
let gdal_ds = gdal::Dataset::open(TEST_COG_PATH).expect("GDAL failed to open");
let band = gdal_ds.rasterband(1).expect("Failed to get band 1");
let test_coords = [
(-122.4, 37.78, "SF Downtown"),
(-122.24, 37.88, "Berkeley Hills"),
(-122.5965, 37.9236, "Mt Tam"),
(-122.38, 37.79, "SF Bay"),
];
let gdal_gt = gdal_ds.geo_transform().expect("Failed to get geotransform");
for (lon, lat, name) in test_coords {
let gdal_px = ((lon - gdal_gt[0]) / gdal_gt[1]) as isize;
let gdal_py = ((gdal_gt[3] - lat) / (-gdal_gt[5])) as isize;
let gdal_buf: gdal::raster::Buffer<f32> = band.read_as((gdal_px, gdal_py), (1, 1), (1, 1), None)
.expect("GDAL read failed");
let gdal_value = gdal_buf.data()[0];
let our_result = cog.sample_lonlat(lon, lat).expect("Our read failed");
let our_value = our_result.get(0).unwrap_or(f32::NAN);
println!("{}: GDAL pixel=({}, {}) value={}, Our pixel={:?} value={}",
name, gdal_px, gdal_py, gdal_value, our_result.pixel_coords, our_value);
assert!((our_value - gdal_value).abs() < 0.001,
"{}: Value mismatch - ours={}, gdal={}", name, our_value, gdal_value);
}
}
#[test]
fn test_gdal_tile_value_comparison() {
let Some(cog) = get_test_cog() else { return };
let gdal_ds = gdal::Dataset::open(TEST_COG_PATH).expect("GDAL failed to open");
let band = gdal_ds.rasterband(1).expect("Failed to get band 1");
let test_pixels = [
(0, 0), (1023, 0), (1024, 0), (0, 1024), (2736, 432), ];
for (px, py) in test_pixels {
let gdal_buf: gdal::raster::Buffer<f32> = band.read_as((px as isize, py as isize), (1, 1), (1, 1), None)
.expect("GDAL read failed");
let gdal_value = gdal_buf.data()[0];
let our_value = cog.sample(0, px, py).expect("Our read failed").unwrap_or(f32::NAN);
println!("Pixel ({}, {}): GDAL={}, Ours={}", px, py, gdal_value, our_value);
assert!((our_value - gdal_value).abs() < 0.001,
"Pixel ({}, {}): mismatch - ours={}, gdal={}", px, py, our_value, gdal_value);
}
}
const RGB_COG_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/data/natural_earth_rgb.tif");
fn get_rgb_cog() -> Option<CogReader> {
if !std::path::Path::new(RGB_COG_PATH).exists() {
println!("Skipping: RGB test file not found at {}", RGB_COG_PATH);
return None;
}
let reader = crate::LocalRangeReader::new(RGB_COG_PATH).ok()?;
CogReader::from_reader(Arc::new(reader)).ok()
}
#[test]
fn test_rgb_cog_metadata() {
let Some(cog) = get_rgb_cog() else { return };
assert_eq!(cog.metadata.bands, 3, "Should have 3 bands (RGB)");
assert_eq!(cog.metadata.crs_code, Some(4326));
assert_eq!(cog.metadata.data_type, crate::CogDataType::UInt8);
assert!(!cog.overviews.is_empty(), "Should have overviews");
println!("RGB COG: {}x{}, {} bands, {} overviews",
cog.metadata.width, cog.metadata.height,
cog.metadata.bands, cog.overviews.len());
}
#[test]
fn test_rgb_cog_gdal_metadata_comparison() {
let Some(cog) = get_rgb_cog() else { return };
let gdal_ds = gdal::Dataset::open(RGB_COG_PATH).expect("GDAL failed to open RGB COG");
let (gdal_width, gdal_height) = gdal_ds.raster_size();
assert_eq!(cog.metadata.width, gdal_width);
assert_eq!(cog.metadata.height, gdal_height);
assert_eq!(cog.metadata.bands, gdal_ds.raster_count());
let gdal_gt = gdal_ds.geo_transform().expect("Failed to get geotransform");
if let Some(scale) = &cog.metadata.geo_transform.pixel_scale {
assert!((scale[0] - gdal_gt[1]).abs() < 1e-10,
"X pixel scale mismatch: ours={}, gdal={}", scale[0], gdal_gt[1]);
}
}
#[test]
fn test_rgb_cog_multiband_pixel_values() {
let Some(cog) = get_rgb_cog() else { return };
let gdal_ds = gdal::Dataset::open(RGB_COG_PATH).expect("GDAL failed to open RGB COG");
let (width, height) = gdal_ds.raster_size();
let test_pixels = [
(0, 0), (width / 2, height / 2), (width - 1, height - 1), (width / 4, height / 4), ];
for (px, py) in test_pixels {
for band_idx in 1..=cog.metadata.bands {
let band = gdal_ds.rasterband(band_idx).expect("Failed to get band");
let gdal_buf: gdal::raster::Buffer<u8> = band.read_as((px as isize, py as isize), (1, 1), (1, 1), None)
.expect("GDAL read failed");
let gdal_value = gdal_buf.data()[0] as f32;
let our_value = cog.sample(band_idx - 1, px, py).expect("Our read failed").unwrap_or(f32::NAN);
assert!((our_value - gdal_value).abs() < 0.01,
"Pixel ({}, {}) band {}: mismatch - ours={}, gdal={}",
px, py, band_idx, our_value, gdal_value);
}
}
}
#[test]
fn test_rgb_cog_overview_dimensions() {
let Some(cog) = get_rgb_cog() else { return };
assert!(!cog.overviews.is_empty(), "Should have overviews");
let mut prev_width = cog.metadata.width;
let mut prev_height = cog.metadata.height;
for (i, overview) in cog.overviews.iter().enumerate() {
assert!(overview.width < prev_width,
"Overview {} width should be smaller than previous", i);
assert!(overview.height < prev_height,
"Overview {} height should be smaller than previous", i);
prev_width = overview.width;
prev_height = overview.height;
}
}
}