use futures::stream::{self, StreamExt};
use oxigdal_core::error::{FormatError, IoError, OxiGdalError, Result};
use oxigdal_core::io::ByteRange;
use crate::fetch::FetchBackend;
const MAX_CONCURRENT_TILE_FETCHES: usize = 8;
const TAG_IMAGE_WIDTH: u16 = 256;
const TAG_IMAGE_LENGTH: u16 = 257;
const TAG_TILE_WIDTH: u16 = 322;
const TAG_TILE_LENGTH: u16 = 323;
const TAG_GEO_KEY_DIRECTORY: u16 = 34735;
const TAG_GEO_DOUBLE_PARAMS: u16 = 34736;
const GEOKEY_PROJECTED_CS_TYPE: u16 = 3072;
const GEOKEY_GEOGRAPHIC_TYPE: u16 = 2048;
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct OverviewMetadata {
pub width: u64,
pub height: u64,
pub tile_width: u32,
pub tile_height: u32,
}
#[derive(Debug, Clone)]
#[allow(dead_code)] pub struct IfdMetadata {
pub width: u64,
pub height: u64,
pub tile_width: u32,
pub tile_height: u32,
pub bits_per_sample: u16,
pub samples_per_pixel: u16,
pub sample_format: u16,
pub compression: u16,
pub photometric_interpretation: u16,
pub predictor: u16,
pub tile_offsets: Vec<u64>,
pub tile_byte_counts: Vec<u64>,
pub pixel_scale_x: Option<f64>,
pub pixel_scale_y: Option<f64>,
pub tiepoint_pixel_x: Option<f64>,
pub tiepoint_pixel_y: Option<f64>,
pub tiepoint_geo_x: Option<f64>,
pub tiepoint_geo_y: Option<f64>,
pub epsg_code: Option<u32>,
}
impl IfdMetadata {
fn from_cog(meta: &CogMetadata) -> Self {
Self {
width: meta.width,
height: meta.height,
tile_width: meta.tile_width,
tile_height: meta.tile_height,
bits_per_sample: meta.bits_per_sample,
samples_per_pixel: meta.samples_per_pixel,
sample_format: meta.sample_format,
compression: meta.compression,
photometric_interpretation: meta.photometric_interpretation,
predictor: meta.predictor,
tile_offsets: meta.tile_offsets.clone(),
tile_byte_counts: meta.tile_byte_counts.clone(),
pixel_scale_x: meta.pixel_scale_x,
pixel_scale_y: meta.pixel_scale_y,
tiepoint_pixel_x: meta.tiepoint_pixel_x,
tiepoint_pixel_y: meta.tiepoint_pixel_y,
tiepoint_geo_x: meta.tiepoint_geo_x,
tiepoint_geo_y: meta.tiepoint_geo_y,
epsg_code: meta.epsg_code,
}
}
}
#[derive(Debug, Clone)]
pub struct CogMetadata {
pub width: u64,
pub height: u64,
pub tile_width: u32,
pub tile_height: u32,
pub bits_per_sample: u16,
pub samples_per_pixel: u16,
pub sample_format: u16,
pub compression: u16,
#[allow(dead_code)]
pub photometric_interpretation: u16,
pub predictor: u16,
pub tile_offsets: Vec<u64>,
pub tile_byte_counts: Vec<u64>,
pub pixel_scale_x: Option<f64>,
pub pixel_scale_y: Option<f64>,
pub tiepoint_pixel_x: Option<f64>,
pub tiepoint_pixel_y: Option<f64>,
pub tiepoint_geo_x: Option<f64>,
pub tiepoint_geo_y: Option<f64>,
pub overview_count: usize,
pub overviews: Vec<OverviewMetadata>,
pub epsg_code: Option<u32>,
pub levels: Vec<IfdMetadata>,
}
pub struct WasmCogReader {
backend: FetchBackend,
metadata: CogMetadata,
#[allow(dead_code)]
byte_order: ByteOrder,
}
#[derive(Debug, Clone, Copy)]
pub enum ByteOrder {
LittleEndian,
BigEndian,
}
impl WasmCogReader {
pub async fn open(url: String) -> Result<Self> {
let backend = FetchBackend::new(url.clone()).await?;
let header_bytes = backend
.read_range_async(ByteRange::from_offset_length(0, 16))
.await?;
if header_bytes.len() < 16 {
return Err(OxiGdalError::Format(FormatError::InvalidHeader {
message: format!(
"truncated TIFF header: expected 16 bytes, got {}",
header_bytes.len()
),
}));
}
let byte_order = if &header_bytes[0..2] == b"II" {
ByteOrder::LittleEndian
} else if &header_bytes[0..2] == b"MM" {
ByteOrder::BigEndian
} else {
return Err(OxiGdalError::Format(FormatError::InvalidHeader {
message: "Invalid TIFF magic bytes".to_string(),
}));
};
let mut ifd_offset = match byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes([
header_bytes[4],
header_bytes[5],
header_bytes[6],
header_bytes[7],
]) as u64,
ByteOrder::BigEndian => u32::from_be_bytes([
header_bytes[4],
header_bytes[5],
header_bytes[6],
header_bytes[7],
]) as u64,
};
let ifd_data = backend
.read_range_async(ByteRange::from_offset_length(ifd_offset, 4096))
.await?;
let (metadata, next_offset) =
Self::parse_ifd(&ifd_data, byte_order, &backend, ifd_offset).await?;
let mut levels: Vec<IfdMetadata> = vec![IfdMetadata::from_cog(&metadata)];
let mut overviews = Vec::new();
ifd_offset = next_offset;
let mut ifd_count = 0;
while ifd_offset != 0 && ifd_count < 100 {
let ov_ifd_data = backend
.read_range_async(ByteRange::from_offset_length(ifd_offset, 4096))
.await?;
let (ov_meta, next_ov_offset) =
Self::parse_ifd(&ov_ifd_data, byte_order, &backend, ifd_offset).await?;
if ov_meta.width > 0 && ov_meta.height > 0 {
overviews.push(OverviewMetadata {
width: ov_meta.width,
height: ov_meta.height,
tile_width: ov_meta.tile_width,
tile_height: ov_meta.tile_height,
});
levels.push(IfdMetadata::from_cog(&ov_meta));
}
ifd_offset = next_ov_offset;
ifd_count += 1;
}
let mut final_metadata = metadata;
final_metadata.overview_count = overviews.len();
final_metadata.overviews = overviews;
final_metadata.levels = levels;
Ok(Self {
backend,
metadata: final_metadata,
byte_order,
})
}
async fn parse_ifd(
data: &[u8],
byte_order: ByteOrder,
backend: &FetchBackend,
_ifd_offset: u64,
) -> Result<(CogMetadata, u64)> {
if data.len() < 2 {
return Err(OxiGdalError::Format(FormatError::InvalidHeader {
message: format!(
"truncated IFD: expected at least 2 bytes, got {}",
data.len()
),
}));
}
let num_entries = match byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes([data[0], data[1]]),
ByteOrder::BigEndian => u16::from_be_bytes([data[0], data[1]]),
};
let mut width = 0u64;
let mut height = 0u64;
let mut tile_width = 256u32;
let mut tile_height = 256u32;
let mut rows_per_strip = 0u32;
let mut bits_per_sample = 8u16;
let mut samples_per_pixel = 1u16;
let mut sample_format = 1u16;
let mut compression = 1u16;
let mut predictor = 1u16;
let mut photometric = 1u16;
let mut tile_offsets = Vec::new();
let mut tile_byte_counts = Vec::new();
let mut pixel_scale_x: Option<f64> = None;
let mut pixel_scale_y: Option<f64> = None;
let mut tiepoint_pixel_x: Option<f64> = None;
let mut tiepoint_pixel_y: Option<f64> = None;
let mut tiepoint_geo_x: Option<f64> = None;
let mut tiepoint_geo_y: Option<f64> = None;
let mut geo_key_directory: Option<Vec<u16>> = None;
let mut geo_double_params: Vec<f64> = Vec::new();
for i in 0..num_entries {
let offset = 2 + (i as usize * 12);
if offset + 12 > data.len() {
break;
}
let entry = &data[offset..offset + 12];
let tag = match byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes([entry[0], entry[1]]),
ByteOrder::BigEndian => u16::from_be_bytes([entry[0], entry[1]]),
};
let field_type = match byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes([entry[2], entry[3]]),
ByteOrder::BigEndian => u16::from_be_bytes([entry[2], entry[3]]),
};
let count = match byte_order {
ByteOrder::LittleEndian => {
u32::from_le_bytes([entry[4], entry[5], entry[6], entry[7]])
}
ByteOrder::BigEndian => {
u32::from_be_bytes([entry[4], entry[5], entry[6], entry[7]])
}
};
let value_bytes = &entry[8..12];
match tag {
TAG_IMAGE_WIDTH => {
width = Self::read_value(value_bytes, field_type, byte_order);
}
TAG_IMAGE_LENGTH => {
height = Self::read_value(value_bytes, field_type, byte_order);
}
258 => {
bits_per_sample = if count <= 1 {
Self::read_value(value_bytes, field_type, byte_order) as u16
} else {
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?
.first()
.copied()
.unwrap_or(8) as u16
};
}
259 => {
compression = Self::read_value(value_bytes, field_type, byte_order) as u16;
}
262 => {
photometric = Self::read_value(value_bytes, field_type, byte_order) as u16;
}
317 => {
predictor = Self::read_value(value_bytes, field_type, byte_order) as u16;
}
277 => {
samples_per_pixel =
Self::read_value(value_bytes, field_type, byte_order) as u16;
}
339 => {
sample_format = if count <= 1 {
Self::read_value(value_bytes, field_type, byte_order) as u16
} else {
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?
.first()
.copied()
.unwrap_or(1) as u16
};
}
278 => {
rows_per_strip = Self::read_value(value_bytes, field_type, byte_order) as u32;
}
TAG_TILE_WIDTH => {
tile_width = Self::read_value(value_bytes, field_type, byte_order) as u32;
}
TAG_TILE_LENGTH => {
tile_height = Self::read_value(value_bytes, field_type, byte_order) as u32;
}
273 => {
tile_offsets =
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?;
}
279 => {
tile_byte_counts =
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?;
}
324 => {
tile_offsets =
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?;
}
325 => {
tile_byte_counts =
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?;
}
33550 if count >= 2 => {
let doubles = Self::read_double_array(
value_bytes,
field_type,
count,
byte_order,
backend,
)
.await?;
if !doubles.is_empty() {
pixel_scale_x = Some(doubles[0]);
}
if doubles.len() > 1 {
pixel_scale_y = Some(doubles[1]);
}
}
33550 => {
}
33922 if count >= 6 => {
let doubles = Self::read_double_array(
value_bytes,
field_type,
count,
byte_order,
backend,
)
.await?;
if doubles.len() >= 6 {
tiepoint_pixel_x = Some(doubles[0]);
tiepoint_pixel_y = Some(doubles[1]);
tiepoint_geo_x = Some(doubles[3]);
tiepoint_geo_y = Some(doubles[4]);
}
}
TAG_GEO_KEY_DIRECTORY => {
let values =
Self::read_array(value_bytes, field_type, count, byte_order, backend)
.await?;
geo_key_directory = Some(values.iter().map(|&v| v as u16).collect());
}
TAG_GEO_DOUBLE_PARAMS => {
geo_double_params = Self::read_double_array(
value_bytes,
field_type,
count,
byte_order,
backend,
)
.await?;
}
_ => {}
}
}
if rows_per_strip > 0 && !tile_offsets.is_empty() {
tile_width = width as u32; tile_height = rows_per_strip;
}
let next_ifd_offset_pos = 2 + (num_entries as usize * 12);
let next_ifd_offset = if next_ifd_offset_pos + 4 <= data.len() {
match byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes([
data[next_ifd_offset_pos],
data[next_ifd_offset_pos + 1],
data[next_ifd_offset_pos + 2],
data[next_ifd_offset_pos + 3],
]) as u64,
ByteOrder::BigEndian => u32::from_be_bytes([
data[next_ifd_offset_pos],
data[next_ifd_offset_pos + 1],
data[next_ifd_offset_pos + 2],
data[next_ifd_offset_pos + 3],
]) as u64,
}
} else {
0
};
let epsg_code = Self::parse_epsg_from_geokeys(&geo_key_directory, &geo_double_params);
let metadata = CogMetadata {
width,
height,
tile_width,
tile_height,
bits_per_sample,
samples_per_pixel,
sample_format,
compression,
photometric_interpretation: photometric,
predictor,
tile_offsets,
tile_byte_counts,
pixel_scale_x,
pixel_scale_y,
tiepoint_pixel_x,
tiepoint_pixel_y,
tiepoint_geo_x,
tiepoint_geo_y,
overview_count: 0, overviews: Vec::new(), epsg_code,
levels: Vec::new(), };
Ok((metadata, next_ifd_offset))
}
fn parse_epsg_from_geokeys(
geo_key_directory: &Option<Vec<u16>>,
_geo_double_params: &[f64],
) -> Option<u32> {
let directory = geo_key_directory.as_ref()?;
if directory.len() < 4 {
return None;
}
let key_count = directory[3] as usize;
if directory.len() < 4 + key_count * 4 {
return None;
}
for i in 0..key_count {
let base = 4 + i * 4;
let key_id = directory[base];
let tiff_tag_location = directory[base + 1];
let value_offset = directory[base + 3];
if tiff_tag_location == 0 {
if key_id == GEOKEY_PROJECTED_CS_TYPE && value_offset != 32767 {
return Some(u32::from(value_offset));
}
if key_id == GEOKEY_GEOGRAPHIC_TYPE && value_offset != 32767 {
return Some(u32::from(value_offset));
}
}
}
None
}
fn read_value(bytes: &[u8], field_type: u16, byte_order: ByteOrder) -> u64 {
match field_type {
3 => {
match byte_order {
ByteOrder::LittleEndian => u16::from_le_bytes([bytes[0], bytes[1]]) as u64,
ByteOrder::BigEndian => u16::from_be_bytes([bytes[0], bytes[1]]) as u64,
}
}
4 => {
match byte_order {
ByteOrder::LittleEndian => {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
ByteOrder::BigEndian => {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
}
}
_ => 0,
}
}
async fn read_array(
bytes: &[u8],
field_type: u16,
count: u32,
byte_order: ByteOrder,
backend: &FetchBackend,
) -> Result<Vec<u64>> {
let value_size = match field_type {
3 => 2, 4 => 4, _ => return Ok(Vec::new()),
};
let total_size = count as usize * value_size;
let data = if total_size <= 4 {
bytes.to_vec()
} else {
let offset = match byte_order {
ByteOrder::LittleEndian => {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
ByteOrder::BigEndian => {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
};
backend
.read_range_async(ByteRange::from_offset_length(offset, total_size as u64))
.await?
};
let mut values = Vec::with_capacity(count as usize);
for i in 0..count as usize {
let offset = i * value_size;
if offset + value_size > data.len() {
break;
}
let value = match field_type {
3 => {
match byte_order {
ByteOrder::LittleEndian => {
u16::from_le_bytes([data[offset], data[offset + 1]]) as u64
}
ByteOrder::BigEndian => {
u16::from_be_bytes([data[offset], data[offset + 1]]) as u64
}
}
}
4 => {
match byte_order {
ByteOrder::LittleEndian => u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as u64,
ByteOrder::BigEndian => u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as u64,
}
}
_ => 0,
};
values.push(value);
}
Ok(values)
}
async fn read_double_array(
bytes: &[u8],
field_type: u16,
count: u32,
byte_order: ByteOrder,
backend: &FetchBackend,
) -> Result<Vec<f64>> {
if field_type != 12 {
return Ok(Vec::new());
}
let value_size = 8; let total_size = count as usize * value_size;
let data = if total_size <= 4 {
bytes.to_vec()
} else {
let offset = match byte_order {
ByteOrder::LittleEndian => {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
ByteOrder::BigEndian => {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as u64
}
};
backend
.read_range_async(ByteRange::from_offset_length(offset, total_size as u64))
.await?
};
let mut values = Vec::with_capacity(count as usize);
for i in 0..count as usize {
let offset = i * value_size;
if offset + value_size > data.len() {
break;
}
let bytes_array = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
];
let value = match byte_order {
ByteOrder::LittleEndian => f64::from_le_bytes(bytes_array),
ByteOrder::BigEndian => f64::from_be_bytes(bytes_array),
};
values.push(value);
}
Ok(values)
}
pub async fn read_tile(&self, tile_x: u32, tile_y: u32) -> Result<Vec<u8>> {
self.read_tile_level(0, tile_x, tile_y).await
}
pub async fn read_tile_level(&self, level: usize, tile_x: u32, tile_y: u32) -> Result<Vec<u8>> {
let lvl = self
.metadata
.levels
.get(level)
.ok_or_else(|| OxiGdalError::OutOfBounds {
message: format!("Overview level {} out of range", level),
})?;
let tiles_across = lvl.width.div_ceil(lvl.tile_width as u64) as u32;
let tile_index = (tile_y * tiles_across + tile_x) as usize;
if tile_index >= lvl.tile_offsets.len() || tile_index >= lvl.tile_byte_counts.len() {
return Err(OxiGdalError::OutOfBounds {
message: format!("Tile index {} out of range at level {}", tile_index, level),
});
}
let offset = lvl.tile_offsets[tile_index];
let byte_count = lvl.tile_byte_counts[tile_index];
let compressed_data = self
.backend
.read_range_async(ByteRange::from_offset_length(offset, byte_count))
.await?;
let mut decompressed = decompress_tile(compressed_data, lvl.compression)?;
if lvl.predictor == 2 {
apply_horizontal_predictor(
&mut decompressed,
lvl.tile_width,
lvl.tile_height,
lvl.bits_per_sample,
lvl.samples_per_pixel,
self.is_little_endian(),
);
}
Ok(decompressed)
}
#[allow(dead_code)] pub async fn read_window_u16(
&self,
level: usize,
x0: u64,
y0: u64,
w: u32,
h: u32,
) -> Result<Vec<u16>> {
let lvl = self
.metadata
.levels
.get(level)
.ok_or_else(|| OxiGdalError::OutOfBounds {
message: format!("Overview level {} out of range", level),
})?;
if w == 0 || h == 0 {
return Ok(Vec::new());
}
let tile_width = lvl.tile_width;
let tile_height = lvl.tile_height;
let tw = tile_width as u64;
let th = tile_height as u64;
let tiles_across = lvl.width.div_ceil(tw);
let tiles_down = lvl.height.div_ceil(th);
let little_endian = self.is_little_endian();
let tx0 = x0 / tw;
let ty0 = y0 / th;
let tx1 = (x0 + w as u64 - 1) / tw;
let ty1 = (y0 + h as u64 - 1) / th;
let mut coords: Vec<(u32, u32)> = Vec::new();
for ty in ty0..=ty1 {
if ty >= tiles_down {
continue;
}
for tx in tx0..=tx1 {
if tx >= tiles_across {
continue;
}
coords.push((tx as u32, ty as u32));
}
}
let fetches = stream::iter(coords.into_iter().map(|(tx, ty)| async move {
let bytes = self.read_tile_level(level, tx, ty).await?;
Ok::<_, OxiGdalError>((tx, ty, bytes_to_u16(&bytes, little_endian)))
}))
.buffer_unordered(MAX_CONCURRENT_TILE_FETCHES)
.collect::<Vec<_>>()
.await;
let mut tiles: Vec<(u32, u32, Vec<u16>)> = Vec::with_capacity(fetches.len());
for result in fetches {
tiles.push(result?);
}
Ok(assemble_window(
&tiles,
tile_width,
tile_height,
x0,
y0,
w,
h,
))
}
#[allow(dead_code)] pub async fn read_window_rgb8(
&self,
level: usize,
x0: u64,
y0: u64,
w: u32,
h: u32,
) -> Result<Vec<u8>> {
let lvl = self
.metadata
.levels
.get(level)
.ok_or_else(|| OxiGdalError::OutOfBounds {
message: format!("Overview level {} out of range", level),
})?;
if w == 0 || h == 0 {
return Ok(Vec::new());
}
let tile_width = lvl.tile_width;
let tile_height = lvl.tile_height;
let tw = tile_width as u64;
let th = tile_height as u64;
let tiles_across = lvl.width.div_ceil(tw);
let tiles_down = lvl.height.div_ceil(th);
let tx0 = x0 / tw;
let ty0 = y0 / th;
let tx1 = (x0 + w as u64 - 1) / tw;
let ty1 = (y0 + h as u64 - 1) / th;
let mut coords: Vec<(u32, u32)> = Vec::new();
for ty in ty0..=ty1 {
if ty >= tiles_down {
continue;
}
for tx in tx0..=tx1 {
if tx >= tiles_across {
continue;
}
coords.push((tx as u32, ty as u32));
}
}
let fetches = stream::iter(coords.into_iter().map(|(tx, ty)| async move {
let bytes = self.read_tile_level(level, tx, ty).await?;
Ok::<_, OxiGdalError>((tx, ty, bytes))
}))
.buffer_unordered(MAX_CONCURRENT_TILE_FETCHES)
.collect::<Vec<_>>()
.await;
let mut tiles: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(fetches.len());
for result in fetches {
tiles.push(result?);
}
Ok(assemble_window_rgb8(
&tiles,
tile_width,
tile_height,
x0,
y0,
w,
h,
))
}
pub fn metadata(&self) -> &CogMetadata {
&self.metadata
}
pub fn is_little_endian(&self) -> bool {
matches!(self.byte_order, ByteOrder::LittleEndian)
}
}
fn decompress_tile(compressed: Vec<u8>, compression: u16) -> Result<Vec<u8>> {
match compression {
1 => Ok(compressed), 5 => Err(OxiGdalError::NotSupported {
operation: "LZW compression not yet supported in WASM".to_string(),
}),
8 => oxiarc_deflate::zlib_decompress(&compressed).map_err(|e| {
OxiGdalError::Io(IoError::Read {
message: format!("DEFLATE decompression failed: {}", e),
})
}),
other => Err(OxiGdalError::NotSupported {
operation: format!("Compression type {} not supported", other),
}),
}
}
#[allow(dead_code)] fn bytes_to_u16(bytes: &[u8], little_endian: bool) -> Vec<u16> {
bytes
.chunks_exact(2)
.map(|c| {
if little_endian {
u16::from_le_bytes([c[0], c[1]])
} else {
u16::from_be_bytes([c[0], c[1]])
}
})
.collect()
}
fn undo_horizontal_predictor_u16(row: &mut [u16]) {
for i in 1..row.len() {
row[i] = row[i].wrapping_add(row[i - 1]);
}
}
fn undo_horizontal_predictor_u8(row: &mut [u8], spp: usize) {
let spp = spp.max(1);
for i in spp..row.len() {
row[i] = row[i].wrapping_add(row[i - spp]);
}
}
fn apply_horizontal_predictor(
data: &mut [u8],
tile_width: u32,
tile_height: u32,
bits_per_sample: u16,
samples_per_pixel: u16,
little_endian: bool,
) {
let tw = tile_width as usize;
let th = tile_height as usize;
let spp = samples_per_pixel.max(1) as usize;
match bits_per_sample {
16 => {
let row_samples = tw * spp;
let row_bytes = row_samples * 2;
for r in 0..th {
let start = r * row_bytes;
let end = start + row_bytes;
if end > data.len() {
break;
}
let mut row: Vec<u16> = data[start..end]
.chunks_exact(2)
.map(|c| {
if little_endian {
u16::from_le_bytes([c[0], c[1]])
} else {
u16::from_be_bytes([c[0], c[1]])
}
})
.collect();
if spp == 1 {
undo_horizontal_predictor_u16(&mut row);
} else {
for i in spp..row.len() {
row[i] = row[i].wrapping_add(row[i - spp]);
}
}
for (i, &v) in row.iter().enumerate() {
let b = start + i * 2;
let out = if little_endian {
v.to_le_bytes()
} else {
v.to_be_bytes()
};
data[b] = out[0];
data[b + 1] = out[1];
}
}
}
8 => {
let row_bytes = tw * spp;
for r in 0..th {
let start = r * row_bytes;
let end = start + row_bytes;
if end > data.len() {
break;
}
undo_horizontal_predictor_u8(&mut data[start..end], spp);
}
}
_ => {}
}
}
#[allow(dead_code)] fn assemble_window(
tiles: &[(u32, u32, Vec<u16>)],
tile_width: u32,
tile_height: u32,
x0: u64,
y0: u64,
w: u32,
h: u32,
) -> Vec<u16> {
let w_usize = w as usize;
let mut out = vec![0u16; w_usize * h as usize];
let tw = tile_width as u64;
let th = tile_height as u64;
let x1 = x0 + w as u64;
let y1 = y0 + h as u64;
for (tx, ty, data) in tiles {
let origin_x = *tx as u64 * tw;
let origin_y = *ty as u64 * th;
for row in 0..th {
let gy = origin_y + row;
if gy < y0 || gy >= y1 {
continue;
}
let out_y = (gy - y0) as usize;
for col in 0..tw {
let gx = origin_x + col;
if gx < x0 || gx >= x1 {
continue;
}
let src_idx = (row * tw + col) as usize;
if src_idx < data.len() {
let out_x = (gx - x0) as usize;
out[out_y * w_usize + out_x] = data[src_idx];
}
}
}
}
out
}
#[allow(dead_code)] fn assemble_window_rgb8(
tiles: &[(u32, u32, Vec<u8>)],
tile_width: u32,
tile_height: u32,
x0: u64,
y0: u64,
w: u32,
h: u32,
) -> Vec<u8> {
let w_usize = w as usize;
let mut out = vec![0u8; w_usize * h as usize * 3];
let tw = tile_width as u64;
let th = tile_height as u64;
let x1 = x0 + w as u64;
let y1 = y0 + h as u64;
for (tx, ty, data) in tiles {
let origin_x = *tx as u64 * tw;
let origin_y = *ty as u64 * th;
for row in 0..th {
let gy = origin_y + row;
if gy < y0 || gy >= y1 {
continue;
}
let out_y = (gy - y0) as usize;
for col in 0..tw {
let gx = origin_x + col;
if gx < x0 || gx >= x1 {
continue;
}
let src_idx = ((row * tw + col) as usize) * 3;
if src_idx + 3 <= data.len() {
let out_x = (gx - x0) as usize;
let dst = (out_y * w_usize + out_x) * 3;
out[dst] = data[src_idx];
out[dst + 1] = data[src_idx + 1];
out[dst + 2] = data[src_idx + 2];
}
}
}
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn forward_predictor_u16(row: &mut [u16]) {
for i in (1..row.len()).rev() {
row[i] = row[i].wrapping_sub(row[i - 1]);
}
}
fn forward_predictor_u8(row: &mut [u8], spp: usize) {
for i in (spp..row.len()).rev() {
row[i] = row[i].wrapping_sub(row[i - spp]);
}
}
#[test]
fn predictor_u16_round_trip() {
let original: Vec<u16> = vec![100, 105, 103, 250, 60000, 60005, 1, 0, 65535];
let mut diffed = original.clone();
forward_predictor_u16(&mut diffed);
assert_ne!(diffed, original);
undo_horizontal_predictor_u16(&mut diffed);
assert_eq!(diffed, original);
}
#[test]
fn predictor_u16_wraps() {
let original: Vec<u16> = vec![65530, 5, 65535, 10];
let mut diffed = original.clone();
forward_predictor_u16(&mut diffed);
undo_horizontal_predictor_u16(&mut diffed);
assert_eq!(diffed, original);
}
#[test]
fn predictor_u8_rgb_round_trip() {
let original: Vec<u8> = vec![
10, 200, 30, 12, 205, 28, 15, 255, 25, 18, 2, 20, ];
let mut diffed = original.clone();
forward_predictor_u8(&mut diffed, 3);
assert_ne!(diffed, original);
undo_horizontal_predictor_u8(&mut diffed, 3);
assert_eq!(diffed, original);
}
#[test]
fn apply_predictor_over_tile_u16() {
let rows: [[u16; 2]; 2] = [[100, 150], [4000, 4020]];
let mut expected: Vec<u16> = Vec::new();
let mut bytes: Vec<u8> = Vec::new();
for row in &rows {
let mut diffed = row.to_vec();
forward_predictor_u16(&mut diffed);
for v in &diffed {
bytes.extend_from_slice(&v.to_le_bytes());
}
expected.extend_from_slice(row);
}
apply_horizontal_predictor(&mut bytes, 2, 2, 16, 1, true);
assert_eq!(bytes_to_u16(&bytes, true), expected);
}
fn synthetic_tile_u16(tx: u32, ty: u32, tw: u32, th: u32) -> (u32, u32, Vec<u16>) {
let mut data = Vec::with_capacity((tw * th) as usize);
for row in 0..th {
for col in 0..tw {
let gx = tx * tw + col;
let gy = ty * th + row;
data.push((gy * 1000 + gx) as u16);
}
}
(tx, ty, data)
}
#[test]
fn assemble_window_crosses_tiles() {
let tw = 2u32;
let th = 2u32;
let mut tiles = Vec::new();
for ty in 0..3 {
for tx in 0..3 {
tiles.push(synthetic_tile_u16(tx, ty, tw, th));
}
}
let (x0, y0, w, h) = (1u64, 1u64, 3u32, 3u32);
let out = assemble_window(&tiles, tw, th, x0, y0, w, h);
assert_eq!(out.len(), (w * h) as usize);
for oy in 0..h as u64 {
for ox in 0..w as u64 {
let gx = x0 + ox;
let gy = y0 + oy;
let expect = (gy * 1000 + gx) as u16;
assert_eq!(
out[(oy * w as u64 + ox) as usize],
expect,
"mismatch at window ({ox},{oy}) global ({gx},{gy})"
);
}
}
}
#[test]
fn assemble_window_off_grid_crop_zero_fills() {
let tw = 2u32;
let th = 2u32;
let tiles = vec![synthetic_tile_u16(0, 0, tw, th)];
let out = assemble_window(&tiles, tw, th, 0, 0, 3, 3);
assert_eq!(out.len(), 9);
assert_eq!(out[0], 0); assert_eq!(out[1], 1); assert_eq!(out[3], 1000); assert_eq!(out[4], 1001); assert_eq!(out[2], 0); assert_eq!(out[5], 0); assert_eq!(out[6], 0); assert_eq!(out[7], 0); assert_eq!(out[8], 0); }
#[test]
fn assemble_window_rgb8_crops() {
let tw = 2u32;
let th = 2u32;
let data: Vec<u8> = vec![
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, ];
let tiles = vec![(0u32, 0u32, data)];
let out = assemble_window_rgb8(&tiles, tw, th, 1, 1, 2, 2);
assert_eq!(out.len(), 2 * 2 * 3);
assert_eq!(&out[0..3], &[10, 11, 12]);
assert_eq!(&out[3..12], &[0u8; 9]);
}
#[test]
fn assemble_window_is_order_independent() {
let tw = 2u32;
let th = 2u32;
let mut tiles = Vec::new();
for ty in 0..3 {
for tx in 0..3 {
tiles.push(synthetic_tile_u16(tx, ty, tw, th));
}
}
let (x0, y0, w, h) = (1u64, 1u64, 3u32, 3u32);
let sequential = assemble_window(&tiles, tw, th, x0, y0, w, h);
let mut reversed = tiles.clone();
reversed.reverse();
assert_eq!(assemble_window(&reversed, tw, th, x0, y0, w, h), sequential);
let mut shuffled: Vec<_> = tiles.iter().skip(1).step_by(2).cloned().collect();
shuffled.extend(tiles.iter().step_by(2).cloned());
assert_eq!(assemble_window(&shuffled, tw, th, x0, y0, w, h), sequential);
}
#[test]
fn assemble_window_rgb8_is_order_independent() {
let tw = 2u32;
let th = 2u32;
let tiles: Vec<(u32, u32, Vec<u8>)> = vec![
(0, 0, vec![1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]),
(1, 0, vec![5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8]),
];
let sequential = assemble_window_rgb8(&tiles, tw, th, 0, 0, 4, 2);
let mut reversed = tiles.clone();
reversed.reverse();
assert_eq!(
assemble_window_rgb8(&reversed, tw, th, 0, 0, 4, 2),
sequential
);
}
#[test]
fn concurrent_tile_gather_preserves_coordinate_mapping() {
futures::executor::block_on(async {
let coords: Vec<(u32, u32)> = (0..3)
.rev()
.flat_map(|ty| (0..3).rev().map(move |tx| (tx, ty)))
.collect();
let fetches = stream::iter(coords.iter().copied().map(|(tx, ty)| async move {
Ok::<_, OxiGdalError>((tx, ty, vec![(ty * 1000 + tx) as u16]))
}))
.buffer_unordered(MAX_CONCURRENT_TILE_FETCHES)
.collect::<Vec<_>>()
.await;
let mut tiles: Vec<(u32, u32, Vec<u16>)> = Vec::with_capacity(fetches.len());
for result in fetches {
tiles.push(result.expect("fake fetch never errors"));
}
assert_eq!(tiles.len(), coords.len());
for (tx, ty) in coords {
let found = tiles
.iter()
.find(|(ttx, tty, _)| *ttx == tx && *tty == ty)
.unwrap_or_else(|| panic!("missing tile ({tx},{ty})"));
assert_eq!(found.2, vec![(ty * 1000 + tx) as u16]);
}
});
}
#[test]
fn bytes_to_u16_endianness() {
let bytes = [0x01, 0x02, 0xFF, 0x00];
assert_eq!(bytes_to_u16(&bytes, true), vec![0x0201, 0x00FF]);
assert_eq!(bytes_to_u16(&bytes, false), vec![0x0102, 0xFF00]);
}
#[test]
fn decompress_tile_passthrough_and_errors() {
let raw = vec![1u8, 2, 3, 4];
assert_eq!(decompress_tile(raw.clone(), 1).unwrap(), raw);
assert!(decompress_tile(raw.clone(), 5).is_err()); assert!(decompress_tile(raw, 99).is_err()); }
}