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
- Erlang/OTP 27+
- Elixir 1.15+
- Rust 1.80+ (via rustup)
libgdal-dev(system GDAL headers and shared library)
On Ubuntu/Debian:
sudo apt install libgdal-devOn macOS:
brew install gdalSetup
git clone <repo-url> && cd ex_gdal
mix deps.get
mix compileThe 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 filesHow 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:
- 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).
-
Upload the
.so/.dylibfiles to a GitHub release with checksum files. -
Switch
lib/ex_gdal/native.exfromuse Rustlertouse 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