Packages

Rustler NIF wrapping libraw for native camera RAW decoding in the BEAM

Current section

Files

Jump to
libraw README.md
Raw

README.md

# LibRaw
An Elixir library for native camera RAW decoding on the BEAM, powered by a
[Rustler](https://github.com/rusterlium/rustler) NIF that wraps
[libraw](https://www.libraw.org/).
## Prerequisites
**libraw must be installed** on every machine that runs `:libraw` — including
machines that use the precompiled NIF:
```bash
# 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](https://www.libraw.org/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:
```elixir
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:
| 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](https://rustup.rs) 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:
```elixir
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:
```elixir
config :rustler_precompiled, :force_build, libraw: true
```
## Usage
### Decode a RAW file
```elixir
{: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
```elixir
%{
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
```elixir
{: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
```bash
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](https://github.com/qweliant/libraw/blob/main/RELEASING.md) — the git tag
and the `mix.exs` version are tightly coupled and a mismatch 404s every
consumer.
## License
MIT — see [LICENSE](https://github.com/qweliant/libraw/blob/main/LICENSE).