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:

PlatformRequired librawShipped by
Linux x86_64 / ARM64libraw.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

Testing the 0.3.0 release candidate? Pin it exactly: {:libraw, "0.3.0-rc1"}. Hex's resolver excludes prereleases from ordinary requirements, so ~> 0.3 will not select it — and until 0.3.0 final is published, ~> 0.3 resolves to nothing at all.

Supported Platforms

Precompiled NIF binaries are downloaded automatically for:

TargetPlatform
aarch64-apple-darwinmacOS (Apple Silicon)
x86_64-apple-darwinmacOS (Intel)
x86_64-unknown-linux-gnuLinux x86_64 (glibc, libraw 0.21.x)
aarch64-unknown-linux-gnuLinux 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

OptionTypeDefaultDescription
use_camera_wbbooleantrueUse white balance stored in the file
no_auto_brightbooleanfalseDisable automatic brightening
output_bps8 | 168Bits per sample in the output
gamma:srgb | :linear | {g0, g1}:srgbGamma 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.