# geotiff-rust

[![tiff-core crates.io](https://img.shields.io/crates/v/tiff-core.svg)](https://crates.io/crates/tiff-core)
[![tiff-core docs.rs](https://docs.rs/tiff-core/badge.svg)](https://docs.rs/tiff-core)
[![tiff-reader crates.io](https://img.shields.io/crates/v/tiff-reader.svg)](https://crates.io/crates/tiff-reader)
[![tiff-reader docs.rs](https://docs.rs/tiff-reader/badge.svg)](https://docs.rs/tiff-reader)
[![tiff-writer crates.io](https://img.shields.io/crates/v/tiff-writer.svg)](https://crates.io/crates/tiff-writer)
[![tiff-writer docs.rs](https://docs.rs/tiff-writer/badge.svg)](https://docs.rs/tiff-writer)
[![geotiff-core crates.io](https://img.shields.io/crates/v/geotiff-core.svg)](https://crates.io/crates/geotiff-core)
[![geotiff-core docs.rs](https://docs.rs/geotiff-core/badge.svg)](https://docs.rs/geotiff-core)
[![geotiff-reader crates.io](https://img.shields.io/crates/v/geotiff-reader.svg)](https://crates.io/crates/geotiff-reader)
[![geotiff-reader docs.rs](https://docs.rs/geotiff-reader/badge.svg)](https://docs.rs/geotiff-reader)
[![geotiff-writer crates.io](https://img.shields.io/crates/v/geotiff-writer.svg)](https://crates.io/crates/geotiff-writer)
[![geotiff-writer docs.rs](https://docs.rs/geotiff-writer/badge.svg)](https://docs.rs/geotiff-writer)

Pure-Rust TIFF/BigTIFF and GeoTIFF/COG readers and writers. No C libraries, no build scripts, no unsafe beyond `memmap2`.

## Crates

| Crate | Description |
|---|---|
| `tiff-core` | Shared TIFF types: ByteOrder, tags, sample traits, compression/predictor enums, and color-model metadata |
| `tiff-reader` | TIFF/BigTIFF decoder with mmap, strip/tile reads, storage-domain reads, and explicit decoded pixel access |
| `tiff-writer` | TIFF/BigTIFF encoder with streaming writes, compression, predictors, and BigTIFF |
| `geotiff-core` | Shared GeoTIFF types: GeoKeyDirectory, CRS, GeoTransform, tag constants |
| `geotiff-reader` | GeoTIFF reader with CRS/transform extraction, overview discovery, and optional HTTP COG access |
| `geotiff-writer` | GeoTIFF/COG writer with fluent builder, tile-wise GeoTIFF writes, and overview generation |

## Reading

```rust
use geotiff_reader::GeoTiffFile;

let file = GeoTiffFile::open("dem.tif")?;
println!("EPSG: {:?}, bounds: {:?}", file.epsg(), file.geo_bounds());
let raster: ndarray::ArrayD<f32> = file.read_raster()?;
```

Use `read_decoded_raster` / `read_decoded_window` on `GeoTiffFile` and
`read_decoded_image` / `read_decoded_window` on `TiffFile` when you want
palette expansion or color-space conversion (for example palette TIFF,
YCbCr, or CMYK) instead of storage-domain samples.

Use `read_band` / `read_band_window` on `GeoTiffFile` and `TiffFile` when
you only need one storage-domain band as a `[rows, cols]` array.

## Writing

```rust
use geotiff_writer::{GeoTiffBuilder, Compression};
use ndarray::Array2;

let data = Array2::<f32>::zeros((256, 256));
GeoTiffBuilder::new(256, 256)
    .epsg(4326)
    .pixel_scale(0.01, 0.01)
    .origin(-180.0, 90.0)
    .nodata("-9999")
    .compression(Compression::Deflate)
    .write_2d("output.tif", data.view())?;
```

For separate-planar multiband output, set
`planar_configuration(PlanarConfiguration::Planar)` on `ImageBuilder` or
`GeoTiffBuilder`.

### Streaming tile writes

```rust
use geotiff_writer::GeoTiffBuilder;
use ndarray::Array2;

let builder = GeoTiffBuilder::new(512, 512)
    .tile_size(256, 256)
    .epsg(4326);
let mut writer = builder.tile_writer_file::<f32, _>("large.tif")?;
for (x, y, tile) in tiles {
    writer.write_tile(x, y, &tile.view())?;
}
writer.finish()?;
```

### COG with overviews

```rust
use geotiff_writer::{GeoTiffBuilder, CogBuilder, Resampling, Compression};
use ndarray::Array2;

let data = Array2::<u8>::zeros((1024, 1024));
CogBuilder::new(
    GeoTiffBuilder::new(1024, 1024)
        .tile_size(256, 256)
        .compression(Compression::Deflate)
        .epsg(4326)
)
.overview_levels(vec![2, 4, 8])
.resampling(Resampling::Average)
.write_2d("output.tif", data.view())?;
```

For multi-band COG output, use `write_3d`/`write_3d_to` or `write_tile_3d`
with `bands(...)` and optional
`planar_configuration(PlanarConfiguration::Planar)`.

## Features

**Read**
- Classic TIFF and BigTIFF
- Little-endian and big-endian byte orders
- Strip and tile data access with windowed reads
- Chunky and separate planar sample layouts
- Full-raster and windowed single-band reads, optimized for separate-planar rasters
- Compression: Deflate, LZW, PackBits, LERC, LERC+DEFLATE, JPEG (optional), ZSTD (optional), LERC+ZSTD (optional)
- Bounded IFD parsing and block decompression budgets for untrusted input
- Parallel decompression via Rayon
- Storage-domain typed sample reads via `read_*` / `read_*_samples`
- Explicit decoded pixel reads via `read_decoded_*` for standard TIFF color models, including palette expansion, YCbCr/CMYK conversion, and sub-byte grayscale/palette decode
- Structured photometric/color-model metadata: palette `ColorMap`, `ExtraSamples`, CMYK, and YCbCr
- GeoKey directory, structured CRS metadata (projected, geographic, geocentric, vertical, compound), transforms, NoData
- Overview discovery from both reduced-resolution top-level IFDs and recursive base-image SubIFD-backed overview trees
- Optional HTTP range-backed remote COG access

**Write**
- Classic TIFF and BigTIFF with auto-detection
- Strip and tile layouts
- Compression: Deflate, LZW, JPEG (optional), LERC, LERC+DEFLATE, ZSTD (optional), LERC+ZSTD (optional)
- Predictors: horizontal differencing, floating-point byte interleaving
- Chunky and separate planar multi-band layouts and all sample types (u8 through f64)
- Photometric/color-model tags: palette `ColorMap`, `ExtraSamples` alpha, CMYK (`Separated` + `InkSet`), and YCbCr 4:4:4
- Streaming tile-by-tile GeoTIFF writes for large rasters
- GeoTIFF metadata: GeoTIFF 1.1 key-directory emission, projected/geographic/geocentric/vertical compound CRS keys, pixel scale, origin, affine transforms, NoData
- COG output with GDAL-compatible ghost-area metadata, overview generation (nearest-neighbor, average), top-level or SubIFD-backed overview IFDs, and multi-band chunky/planar rasters
- Disk-backed tile-wise COG assembly via `CogTileWriter` (base tiles are staged in a temporary raw tile store before final emission)

## Codec Notes

`JPEG`-in-TIFF write uses standard compression code `7` with full JPEG
interchange streams per strip/tile. The supported interoperable layouts are
single-band chunky output and multi-band separate-planar output, which keeps
TIFF, GeoTIFF, and COG files compatible with GDAL/libtiff without requiring
TIFF-side shared `JPEGTables`.

TIFF `LERC` writing records the registered LERC2 2.4 parameter version used by
GDAL/libtiff. If the encoder produces a different LERC2 container version, the
writer rejects the block before writing incompatible TIFF metadata.

## Robustness Notes

`TiffFile::open_with_options`, `from_bytes_with_options`, and
`from_source_with_options` accept `OpenOptions` with `ParseBudgets` for bounding
IFD chain length, tag counts, and metadata payload bytes. `GeoTiffFile`
exposes the same settings as `GeoTiffOpenOptions`, and HTTP COG opens pass them
through `HttpOpenOptions::tiff_options`. The reader also derives encoded
strip/tile read limits and decompressed output limits from the raster layout
before reading block payloads.

## Feature flags

| Flag | Default | Description |
|---|---|---|
| `local` | yes | Local file reading via `tiff-reader` (geotiff-reader) |
| `rayon` | yes | Parallel strip/tile decompression (tiff-reader, geotiff-reader) |
| `jpeg` | yes | JPEG-in-TIFF read/write support (tiff-reader, tiff-writer) |
| `zstd` | yes | Pure-Rust ZSTD compression via `ruzstd`, including TIFF `LERC+ZSTD` read/write support (tiff-reader, tiff-writer) |
| `cog` | no | HTTP range-backed remote COG open (geotiff-reader) |

## Testing

```sh
cargo test --workspace --all-features
cargo clippy --workspace --all-targets --all-features -- -D warnings
```

Reference-library parity tests are included for `tiff-reader` and
`geotiff-reader`. They compare this workspace against GDAL/libtiff when those
tools are available locally; otherwise they self-skip. Lossless codecs use
exact byte and hash parity. The JPEG fixture uses a strict bounded-delta check
because compliant decoders can differ by +/-1 in a small number of samples.

For a reproducible reference environment, run the Docker harness:

```sh
./scripts/run-reference-parity.sh
```

For reference comparisons and current benchmark results against GDAL/libtiff,
see [docs/benchmark-report.md](docs/benchmark-report.md).

For the workspace release order and package verification notes, see
[docs/publishing.md](docs/publishing.md).

## License

MIT OR Apache-2.0
