LibRaw
An Elixir library for native camera RAW decoding on the BEAM, powered by a Rustler NIF that wraps libraw.
Prerequisites
libraw must be installed on every machine that runs :libraw — including
machines that use the precompiled NIF:
# macOS
brew install libraw
# Debian / Ubuntu
apt install libraw-dev
LGPL compliance: libraw is dynamically linked at runtime. Distributing your application requires that end users can replace the libraw shared library. See the libraw license for details.
Elixir 1.15 or later is required (rustler_precompiled itself declares
~> 1.15).
A Rust toolchain is not required for the targets listed under Supported
Platforms below. For unsupported targets the package falls back to compiling
the NIF from source, which requires Rust and the libraw headers above. You can
also opt into a source build explicitly with LIBRAW_BUILD=1.
libraw version matters
The precompiled NIFs are dynamically linked against a specific libraw ABI, baked in when the release is built:
| Platform | Required libraw | Shipped by |
|---|---|---|
| Linux x86_64 / ARM64 | libraw.so.23 (libraw 0.21.x) | Ubuntu 24.04+, Debian 13+ |
| macOS (both arches) | libraw.25.dylib (libraw 0.22.x) | current Homebrew libraw |
If your system provides a different major version — Ubuntu 22.04 and Debian 12
ship libraw.so.20 — the precompiled NIF will fail to load with an error like
libraw.so.23: cannot open shared object file. Install a newer libraw, or
build from source with LIBRAW_BUILD=1 to link against whatever you have.
On macOS the NIF references Homebrew's default prefix (/opt/homebrew on
Apple Silicon, /usr/local on Intel) by absolute path. Non-default Homebrew
prefixes, MacPorts, and Nix installs need a source build.
Installation
Add :libraw to your mix.exs dependencies:
def deps do
[
{:libraw, "~> 0.3"}
]
end
Supported Platforms
Precompiled NIF binaries are downloaded automatically for:
| Target | Platform |
|---|---|
aarch64-apple-darwin | macOS (Apple Silicon) |
x86_64-apple-darwin | macOS (Intel) |
x86_64-unknown-linux-gnu | Linux x86_64 (glibc, libraw 0.21.x) |
aarch64-unknown-linux-gnu | Linux ARM64 (glibc, libraw 0.21.x) |
MUSL / Alpine Linux is not supported — libraw is not reliable on musl libc.
Forcing a source build: set LIBRAW_BUILD=1 before mix deps.compile.
You will also need Rust installed and libraw-dev headers.
:rustler is an optional dependency of :libraw, so it is not installed on
the precompiled path. A source build needs it in your dependency list:
def deps do
[
{:libraw, "~> 0.3"},
{:rustler, "~> 0.37", runtime: false}
]
end
Without it the build fails with Rustler dependency is needed to force the build.
The same thing can be done from config, which is useful when the env var is awkward to thread through a release build:
config :rustler_precompiled, :force_build, libraw: true
Usage
Decode a RAW file
{:ok, image} = LibRaw.decode("/path/to/photo.CR3")
# => %{pixels: <<...>>, width: 6000, height: 4000, colors: 3, bps: 8}
# 16-bit output with linear gamma
{:ok, image16} = LibRaw.decode("/path/to/photo.CR3",
output_bps: 16,
gamma: :linear
)
# Custom gamma curve
{:ok, image_custom} = LibRaw.decode("/path/to/photo.NEF",
gamma: {2.4, 12.92},
use_camera_wb: true,
no_auto_bright: true
)
Options
| Option | Type | Default | Description |
|---|---|---|---|
use_camera_wb | boolean | true | Use white balance stored in the file |
no_auto_bright | boolean | false | Disable automatic brightening |
output_bps | 8 | 16 | 8 | Bits per sample in the output |
gamma | :srgb | :linear | {g0, g1} | :srgb | Gamma curve |
Return value
%{
pixels: binary(), # raw pixel bytes (interleaved RGB or RGBA)
width: non_neg_integer(),
height: non_neg_integer(),
colors: non_neg_integer(), # number of color channels (typically 3)
bps: non_neg_integer() # bits per sample of the output
}
Read metadata without decoding
{:ok, meta} = LibRaw.metadata("/path/to/photo.CR3")
# => %{
# camera_make: "Canon",
# camera_model: "EOS R5",
# captured_at: ~U[2023-06-15 10:32:11Z], # DateTime UTC, or nil
# iso: 400.0,
# shutter: 0.002, # seconds
# aperture: 2.8, # f-number
# orientation: 0 # EXIF flip code
# }
Architecture
lib/
lib_raw.ex Public API: decode/2, metadata/1, gamma resolution, timestamp parsing
lib_raw/
nif.ex use RustlerPrecompiled + NIF stubs (nif_not_loaded fallbacks)
native/
libraw_nif/
Cargo.toml deps: rustler = "0.37"; build-deps: cc = "1", pkg-config = "0.3"
build.rs pkg_config::probe("libraw") for dynamic linking; cc::Build compiles wrapper.c
src/
lib.rs rustler::init! and two #[rustler::nif(schedule = "DirtyCpu")] functions
wrapper.c thin C shim — C compiler resolves all struct field offsets
raw.rs safe Rust RAII wrappers around libraw_data_t / libraw_processed_image_t
error.rs LibRawError enum + helpers
Why a C shim?
Direct bindgen / libraw-sys approaches embed struct field offsets at compile
time, which can break across libraw versions 0.20, 0.21, and 0.22 as the
struct layout evolves. wrapper.c is compiled with the same headers as the
installed libraw, so the C compiler always uses the correct offsets. Rust
calls only opaque C functions and never touches libraw structs directly.
Dirty CPU Schedulers
Both NIFs (decode_nif and metadata_nif) are annotated with
schedule = "DirtyCpu". Decoding a RAW file typically takes 100–500 ms,
which is far beyond the 1 ms NIF time budget for normal schedulers. Running
on dirty schedulers prevents blocking the BEAM scheduler threads.
Development
mix deps.get
mix test # unit tests (no RAW file required)
mix test.smoke # end-to-end decode test; requires test/fixtures/sample.raw
Working on this repo always builds the NIF from source, so a Rust
toolchain and the libraw headers are required — config/config.exs sets
force_build for the :dev and :test environments. This is deliberate: a
checkout is normally ahead of the last published release, so downloading a
precompiled NIF would either 404 or hand you a stale binary that ignores your
Rust changes. That config is never seen by applications that depend on
:libraw.
To run the smoke test, drop any RAW file (CR2, CR3, NEF, ARW, DNG, RAF,
etc.) at test/fixtures/sample.raw. The path is gitignored.
Cutting a release is documented in RELEASING.md — the git tag
and the mix.exs version are tightly coupled and a mismatch 404s every
consumer.
License
MIT — see LICENSE.