# 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
```

### 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).
