A geospatial library that hands you the wrong pixels without telling you is worse than one that crashes.
Today we released OxiGeo 0.2.2 — a correctness release built around GitHub issue #14: Dataset::read_band silently ignored its band argument on multi-band rasters and returned the entire pixel-interleaved image instead. Root-causing it inside the GeoTIFF driver’s block-decode engine turned up the identical defect — wrong interleaving assumptions, or the wrong byte order — independently re-implemented in a dozen other crates. 192 files changed; 33 new issue_14_*-named regression tests, benchmarks, and examples guard against it coming back.
No C. No C++. No Fortran. OxiGeo 0.2.2 still compiles to a single static binary (or WASM) and runs everywhere Rust runs — and it ships a substantially faster DEFLATE decoder without picking up a single new native dependency to get there.
Why 0.2.2 is a game changer
A raster library’s one job is to hand back the pixels you asked for. When it doesn’t, the failure mode is the worst kind:
read_band(1)on a 3-band file silently returned bands 0, 1, and 2, interleaved — every caller that assumed a single-band result was quietly working with 3× too much data laid out wrong- The same “assume chunky interleaving” mistake had been independently reinvented in the QC scanner, the WMS/WCS server, mobile FFI, the WASM viewer, and half a dozen more crates — one root cause, ten symptoms
- A planar (
PlanarConfiguration=2) GeoTIFF decoded through the predictor path bled rows into each other with no error at all - Opening a malformed or unusually-laid-out file could silently hand back a 0×0, zero-band dataset instead of an error
OxiGeo 0.2.2 ends all of that:
Dataset::read_bandnow returns exactly that band’swidth × heightsamples — not the whole interleaved image. This is a BREAKING change: a 3-band file’sread_band(0)now returns a third as many samples as it used to, so a length check finds affected code immediately.- The GeoTIFF driver’s decode engine was rewritten from scratch (
band_read.rs/band_read/multi.rs): aReadPlan/LevelGeometryresolves each level’s real geometry and planar layout once, and either de-interleaves the requested band during the scatter (chunky) or reads only that band’s own blocks (planar) — the full interleaved plane is never materialized. - DEFLATE tile decoding is 1.45-1.79× faster. The
oxiarc-*suite moves 0.3.6 → 0.4.0 with a rewritten two-level-Huffman decoder; on 256×256 UInt16 DEM tiles withPREDICTOR=2(the layout SRTM/Copernicus DEM COGs use), throughput goes from 99.0 MiB/s to 143.7 MiB/s, and to 177.4 MiB/s through the newzlib_decompress_intopath that whole-band reads now take — with zero decode-side allocations. - Per-tile lookups went from O(n) to O(1). Re-parsing the whole
TileOffsets/TileByteCountsarray on every tile read measured at 77% of one band read on an 8000-strip file; a newBlockIndexparses each level’s offset/count arrays once atopen(). Dataset::opennow errors instead of silently opening empty. A GeoTIFF/GeoJSON/Shapefile/FlatGeobuf/GeoParquet file the old header peek couldn’t fully parse used to open “successfully” with zeroed-out metadata; every format probe is now a realResult.
Technical Deep Dive: what issue #14 actually touched
-
A purpose-built band-decode engine. The GeoTIFF driver gains a real band-aware, low-allocation read API:
read_band_into/read_band_into_typed,read_window_into/read_window_into_typed,read_bands_into_typed/read_window_bands_into_typed(one block decode shared across every requested band), and a new opt-inparallelfeature that fans block decode out across rayon workers — bit-identical to the serial path. -
A typed, zero-copy raster-element layer in
oxigeo-core. The sealedRasterElementtrait (implemented for every integer/float raster type) defines on-disk byte width,RasterDataTypetag, and native-endian conversion, plus exact integer-to-integer conversion through ani128bridge — replacing a per-pixelf64round-trip that silently lost precision above 2^53 onUInt64/Int64. It also closed a latent alignment-UB bug inas_slice/as_slice_mut/row_slicethat stayed invisible only because production allocators happen to over-align. -
New interleaved and zero-allocation readers.
read_interleaved/read_interleaved_intoandread_window_interleaved/read_window_interleaved_intoare the supported replacement for the old (buggy)read_bandbehavior —bands: Option<&[u32]>selects, reorders, or subsets bands, and the*_intoforms bound peak memory to one strip rather than the whole raster. A newdata_type()reads the on-disk pixel type before any raster read at all. -
The same defect pattern, hunted down workspace-wide. Once the root cause was named, it turned up independently reinvented in
oxigeo-qc(nodata/radiometric scanners),oxigeo-server(WMS/WMTS/XYZ handlers assuming power-of-two overview pyramids),oxigeo-services(WCSGetCoveragetruncating multi-band responses),oxigeo-mobile,oxigeo-wasm,oxigeo-node,oxigeo-cli,oxigeo-ml-foundation,oxigeo-jupyter, andoxigeo-drivers-vrt— ten crates, one shared correction.
Getting Started
[dependencies]
oxigeo = "0.2" # GeoTIFF + GeoJSON + Shapefile by default
use oxigeo::Dataset;
fn main() -> oxigeo::Result<()> {
let dataset = Dataset::open("scene.tif")?;
let (width, height) = (dataset.width() as usize, dataset.height() as usize);
// Single band: exactly width * height samples, always (as of 0.2.2).
let red: Vec<u8> = dataset.read_band(0)?;
assert_eq!(red.len(), width * height);
// Want every band together? Ask for it explicitly and pick the order.
let mut rgb = vec![0u8; width * height * 3];
dataset.read_interleaved_into(Some(&[2, 1, 0]), &mut rgb)?; // read as BGR
// Know the pixel type before you commit to a read.
println!("dtype: {:?}", dataset.data_type());
Ok(())
}
What’s New in 0.2.2
- BREAKING:
Dataset::read_bandnow returns exactly one band’s samples, not the whole interleaved image - BREAKING:
DatasetInfois now#[non_exhaustive](gainedimpl Defaultand a newdata_type: Option<RasterDataType>field) — struct-literal construction no longer compiles - New interleaved readers:
read_interleaved/read_interleaved_into,read_window_interleaved/read_window_interleaved_into, plus zero-allocationread_band_into/read_window_intoand a pre-readdata_type()query oxigeo-coregained a typedRasterElementzero-copy layer with exacti128-bridged integer conversion, andFileDataSource/MmapDataSourcegained real positional and zero-copy range reads- DEFLATE tile decoding 1.45-1.79× faster (
oxiarc-*0.3.6 → 0.4.0), with zero decode-side allocations on whole-band reads - GeoTIFF per-tile offset lookups are now O(1) via a new
BlockIndex, plus an opt-inparallel(rayon) block-decode feature - The read_band defect pattern fixed in ten downstream crates: qc, server, services (WCS), mobile, wasm, node, cli, ml-foundation, jupyter, drivers-vrt
- Unrelated fixes along the way:
oxigeo-streaming’sChunkedReaderfailing on its first read, anoxigeo-mbtilesspill-file leak, concurrent-corruption inoxigeo-mlpruning, and anoxigeo-compresswasm32 build fix - Quality gates: 18,133 tests passed / 0 failed / 101 skipped (
--all-features; 16,684/0/80 on default features), 402 doc tests, clippy 0 warnings,cargo deny checkpassing across all 75 crates
Tips
- Audit any code that relied on the old
read_bandbehavior. If you were readingread_band(0)and manually de-interleaving the result yourself (a common workaround), that code now double-processes an already-correct single band. Switch toread_interleaved/read_interleaved_intoif you actually wanted every band together. DatasetInfo { field, .. }no longer compiles. Build fromDatasetInfo::default()and set the fields you need — the struct is#[non_exhaustive]now specifically so a future field addition won’t break your code again.- Reach for the
*_intoreaders in hot loops.read_band_into,read_window_into, andread_interleaved_intodecode straight into a caller-owned buffer with no per-call allocation — the interleaved readers bound their scratch to one strip, not the whole raster, however large the image is. - Call
data_type()before you commit to a read. It reads the on-disk pixel type from the header alone, so you can pick your buffer’s element type without decoding a single block first. - Enable the new
parallelfeature on the GeoTIFF driver for rayon-based block decode across cores — output is bit-identical to the serial path, so it’s a free win for batch raster processing. - Already pinning
oxiarc-*? Bump to 0.4.0 to pick up the DEFLATE decoder rewrite — COG and DEM tile reads get faster with no code changes on your side.
This is the foundation
OxiGeo 0.2.2 leans on the same Pure Rust COOLJAPAN stack as every release before it: CRS transforms via OxiProj, HDF5/NetCDF read-write through oxih5 and oxinetcdf, SQLite via oxisql-sqlite-compat (Limbo), TLS via OxiTLS, compression across every format driver via the OxiArc family (now at 0.4.0 for the DEFLATE rewrite this release leans on), ML tensor math via SciRS2-Core, and model export validated against OxiONNX. Every one of those is itself Pure Rust — which is how a 75-crate, ~791K-SLoC workspace ships both a correctness fix and a decode-speed win without picking up a single new native dependency.
Repository: https://github.com/cool-japan/oxigeo
Star the repo if you’d rather your geospatial dependency return exactly the band you asked for than quietly hand you three times too much data laid out wrong. One root cause, ten crates fixed, and a faster decoder along the way — that’s what a correctness release is supposed to look like.
The era of “it probably decoded right” is over. Pure Rust geospatial is here — fast, safe, and sovereign.
— KitaSan at COOLJAPAN OÜ
July 30, 2026