ExGdal

Elixir bindings for reading raster geospatial data (GeoTIFF, etc.) via GDAL.

The NIF layer is written in Rust using Rustler and wraps the gdal Rust crate. Dataset handles are held in ResourceArc<Mutex<Dataset>> so they are managed by the BEAM garbage collector and safe to pass between processes.

API

All functions return {:ok, result} or {:error, reason}.

{:ok, ds} = ExGdal.open("/path/to/raster.tif")

ds.driver       #=> "GTiff"
ds.raster_count #=> 3
ds.raster_size  #=> {120, 116}

{:ok, :float64}  = ExGdal.band_type(ds, 1)
{:ok, -9999.0}   = ExGdal.no_data_value(ds, 1)
{:ok, data}      = ExGdal.read_band(ds, 1)          # full band, raw native-endian bytes
{:ok, window}    = ExGdal.read_band_window(ds, 1, 0, 0, 10, 10)
{:ok, gt}        = ExGdal.geo_transform(ds)          # %ExGdal.GeoTransform{}
{:ok, wkt}       = ExGdal.spatial_ref_wkt(ds)
{:ok, proj4}     = ExGdal.spatial_ref_proj4(ds)
{:ok, val}       = ExGdal.metadata_item(ds, "AREA_OR_POINT")
{:ok, "GTiff"}   = ExGdal.driver_name(ds)

Band indices are 1-based, matching GDAL convention.

read_band/2 returns raw bytes in native endianness. For a float64 band on a 120x116 raster, that is 120 * 116 * 8 = 111_360 bytes. Use band_type/2 to know how to interpret the binary.

Structs

%ExGdal.Dataset{} holds the NIF resource reference along with cached raster_count, raster_size, path, and driver fields.

%ExGdal.GeoTransform{} has named fields: origin_x, origin_y, pixel_width, pixel_height, skew_x, skew_y. These correspond to GDAL's 6-element affine transform array.

Prerequisites

On Ubuntu/Debian:

sudo apt install libgdal-dev

On macOS:

brew install gdal

Setup

git clone <repo-url> && cd ex_gdal
mix deps.get
mix compile

The first compile builds the Rust NIF crate in release mode. Subsequent compiles are incremental and fast.

Running tests

mix test

Test fixtures (tinymarble.tif, dem-hills.tiff, gcp.tif) are in test/fixtures/, copied from the upstream gdal crate's fixture set.

Project structure

mix.exs                             # Elixir project config
lib/
  ex_gdal.ex                       # Public API
  ex_gdal/
    native.ex                      # NIF function stubs (use Rustler)
    dataset.ex                     # %ExGdal.Dataset{} struct
    geo_transform.ex               # %ExGdal.GeoTransform{} struct
native/ex_gdal_nif/
  Cargo.toml                       # Rust crate config
  src/lib.rs                       # NIF implementations
test/
  ex_gdal_test.exs                 # Integration tests
  fixtures/                        # Sample raster files

How the NIF works

The Rust NIF crate (native/ex_gdal_nif) depends on the gdal crate from crates.io (currently 0.19). It links against the system's libgdal shared library at compile time.

All I/O NIF functions (gdal_open, gdal_read_band, gdal_read_band_window) run on the BEAM dirty I/O scheduler so they do not block normal schedulers.

The Dataset from the gdal crate is Send but not Sync. It is wrapped in Mutex<Dataset> inside a ResourceArc to allow safe concurrent access from multiple BEAM processes.

Precompiled NIF builds

The project includes rustler_precompiled as a dependency for future use. To ship precompiled binaries:

  1. Set up a GitHub Actions workflow that builds the NIF for each target (x86_64-linux-gnu, aarch64-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin).
  2. Upload the .so/.dylib files to a GitHub release with checksum files.
  3. Switch lib/ex_gdal/native.ex from use Rustler to use RustlerPrecompiled.

For bundled builds that do not require system libgdal, add gdal-src to Cargo.toml dependencies with features = ["internal_drivers"]. This compiles GDAL from source (slow, but produces a fully static NIF).

License

MIT