nom-exif 3.0.0

Exif/metadata parsing library written in pure Rust, both image (jpeg/heif/heic/jpg/tiff etc.) and video/audio (mov/mp4/3gp/webm/mkv/mka, etc.) files are supported.
Documentation

Nom-Exif

crates.io Documentation LICENSE CI

nom-exif is a pure Rust library for both image EXIF and video / audio track metadata through a single unified API. Built on nom.

Highlights

  • Pure Rust — no FFmpeg, no libexif, no system deps; cross-compiles cleanly.
  • Image and video / audio in one crate — MediaParser dispatches to the right backend by detected MIME, no per-format wrappers.
  • RAW format support — Canon CR3, Fujifilm RAF, Phase One IIQ, alongside JPEG / HEIC / TIFF.
  • Three input modes — files, arbitrary Read / Read + Seek (network streams, pipes), or in-RAM bytes (WASM, mobile, HTTP proxies).
  • Sync and async unified under one MediaParser.
  • Eager (Exif, get-by-tag) or lazy (ExifIter, parse-on-demand) — per-entry errors surface in both modes (Exif::errors() / per-iter Result), so one bad tag doesn't poison the parse.
  • Allocation-frugal — parser buffer is recycled across calls; sub-IFDs share the same allocation (no deep copies).
  • Fuzz-tested with cargo-fuzz against malformed and adversarial input.

Supported File Types

  • Image
    • .heic, .heif, etc.
    • .jpg, .jpeg
    • .tiff, .tif, .iiq (Phase One IIQ images), etc.
    • .RAF (Fujifilm RAW)
    • .CR3 (Canon RAW)
  • Video/Audio
    • ISO base media file format (ISOBMFF): .mp4, .mov, .3gp, etc.
    • Matroska based file format: .webm, .mkv, .mka, etc.

Quick Start

use nom_exif::{read_exif, read_track, read_metadata, ExifTag, TrackInfoTag, Metadata};

// One image:
let exif = read_exif("./testdata/exif.jpg")?;
let make = exif.get(ExifTag::Make).and_then(|v| v.as_str());

// One video:
let info = read_track("./testdata/meta.mov")?;
let model = info.get(TrackInfoTag::Model).and_then(|v| v.as_str());

// Auto-detect:
match read_metadata("./testdata/exif.jpg")? {
    Metadata::Exif(_)  => { /* image */ }
    Metadata::Track(_) => { /* video/audio */ }
}
# Ok::<(), nom_exif::Error>(())

Reusable Parser

For batch processing, build a MediaParser once and reuse its buffer across calls:

use nom_exif::{MediaKind, MediaParser, MediaSource, ExifTag, TrackInfoTag};

let mut parser = MediaParser::new();

let files = [
    "./testdata/exif.heic",
    "./testdata/exif.jpg",
    "./testdata/meta.mov",
];

for f in files {
    let ms = MediaSource::open(f)?;
    match ms.kind() {
        MediaKind::Image => {
            let iter = parser.parse_exif(ms)?;
            let exif: nom_exif::Exif = iter.into();
            let _ = exif.get(ExifTag::Make);
        }
        MediaKind::Track => {
            let info = parser.parse_track(ms)?;
            let _ = info.get(TrackInfoTag::Make);
        }
    }
}
# Ok::<(), nom_exif::Error>(())

MediaSource accepts any Read (or Read + Seek):

  • MediaSource::open(path) — convenience for files.
  • MediaSource::seekable(reader) — any Read + Seek source.
  • MediaSource::unseekable(reader)Read-only source (e.g. a network stream); slower for formats that store metadata at the end of the file (such as .mov).

In-Memory Bytes

When the payload is already in RAM (decoded HTTP body, WASM-loaded asset, mobile-cached blob), use the *_from_bytes helpers to skip the File / Read round-trip. Memory mode is zero-copy: the underlying allocation is shared with the returned Exif / ExifIter / TrackInfo via bytes::Bytes reference counting.

use nom_exif::{read_exif_from_bytes, ExifTag};

let raw: Vec<u8> = std::fs::read("./testdata/exif.jpg")?;
let exif = read_exif_from_bytes(raw)?;
let make = exif.get(ExifTag::Make).and_then(|v| v.as_str());
# let _ = make; Ok::<(), nom_exif::Error>(())

For batch processing many in-memory payloads, reuse a MediaParser:

use nom_exif::{MediaParser, MediaSource};

let mut parser = MediaParser::new();
let raw = std::fs::read("./testdata/exif.jpg")?;
let ms = MediaSource::from_bytes(raw)?;
let iter = parser.parse_exif_from_bytes(ms)?;
# let _ = iter; Ok::<(), nom_exif::Error>(())

MediaSource::from_bytes accepts anything convertible into bytes::Bytes: Vec<u8>, &'static [u8], Bytes, and HTTP-body types that implement Into<Bytes> directly.

Two API styles for Exif

The library exposes both eager and lazy views of EXIF metadata.

use nom_exif::{read_exif, read_exif_iter, ExifTag};

// Eager — easiest. Get-by-tag, parsed up front.
let exif = read_exif("./testdata/exif.jpg")?;
let make = exif.get(ExifTag::Make).and_then(|v| v.as_str());

// Lazy — finer-grained. Parse-on-demand, per-entry errors visible.
let iter = read_exif_iter("./testdata/exif.jpg")?;
for entry in iter {
    let _tag = entry.tag();          // TagOrCode (Tag(...) or Unknown(code))
    let _ifd = entry.ifd();          // IfdIndex
    let _ = entry.into_result();      // Result<EntryValue, EntryError>
}
# Ok::<(), nom_exif::Error>(())

Async API

Enable the tokio feature in your Cargo.toml:

[dependencies]
nom-exif = { version = "3", features = ["tokio"] }

Then use the _async helpers, or call parse_exif_async / parse_track_async on a MediaParser directly:

# #[cfg(feature = "tokio")]
# async fn demo() -> nom_exif::Result<()> {
use nom_exif::{read_exif_async, MediaParser, AsyncMediaSource};

// One-shot:
let exif = read_exif_async("./testdata/exif.jpg").await?;

// Reusable:
let mut parser = MediaParser::new();
let ms = AsyncMediaSource::open("./testdata/exif.jpg").await?;
let iter = parser.parse_exif_async(ms).await?;
# let _ = (exif, iter); Ok(())
# }

GPS Info

Exif and TrackInfo both expose gps_info(). ExifIter adds parse_gps() for early termination once GPS tags have been read.

use nom_exif::{read_exif, LatRef, LonRef, Altitude};

let exif = read_exif("./testdata/exif.heic")?;
if let Some(g) = exif.gps_info() {
    let _ = matches!(g.latitude_ref, LatRef::North | LatRef::South);
    let _ = matches!(g.longitude_ref, LonRef::East | LonRef::West);
    let _ = matches!(g.altitude, Altitude::AboveSeaLevel(_) | Altitude::BelowSeaLevel(_));
    let _iso = g.to_iso6709();
}
# Ok::<(), nom_exif::Error>(())

Migration from v2

v3.0.0 reshapes the public API end-to-end. The full migration guide lives in docs/MIGRATION.md — every row there is exercised by tests/migration_guide.rs. A few high-traffic items:

  • MediaSource::file_path(p)MediaSource::open(p) or read_exif(p).
  • parser.parse::<_,_,ExifIter>(ms)parser.parse_exif(ms).
  • parser.parse::<_,_,TrackInfo>(ms)parser.parse_track(ms).
  • entry.take_result() (panicky) → entry.into_result() (consumes self).
  • iter.parse_gps_info()iter.parse_gps().
  • info.get_gps_info()info.gps_info() (returns Option<&GPSInfo>).
  • g.latitude_ref == 'N'matches!(g.latitude_ref, LatRef::North).
  • Cargo features: asynctokio, json_dumpserde.

CLI Tool rexiftool

Human Readable Output

cargo run --example rexiftool testdata/meta.mov:

Make                            => Apple
Model                           => iPhone X
Software                        => 12.1.2
CreateDate                      => 2024-02-02T08:09:57+00:00
DurationMs                      => 500
Width                           => 720
Height                          => 1280
GpsIso6709                      => +27.1281+100.2508+000.000/

Pass --debug to enable tracing logs:

cargo run --example rexiftool -- --debug ./testdata/meta.mov

JSON Dump

cargo run --features serde --example rexiftool testdata/meta.mov -j:

{
  "Width": "720",
  "Software": "12.1.2",
  "Height": "1280",
  "Make": "Apple",
  "GpsIso6709": "+27.1281+100.2508+000.000/",
  "CreateDate": "2024-02-02T08:09:57+00:00",
  "Model": "iPhone X",
  "DurationMs": "500"
}

Parsing Files in a Directory

rexiftool also supports batch parsing of all files in a folder (non-recursive).

cargo run --example rexiftool testdata/:

File: "testdata/embedded-in-heic.mov"
------------------------------------------------
Make                            => Apple
Model                           => iPhone 15 Pro
Software                        => 17.1
CreateDate                      => 2023-11-02T12:01:02+00:00
DurationMs                      => 2795
Width                           => 1920
Height                          => 1440
GpsIso6709                      => +22.5797+113.9380+028.396/

File: "testdata/exif.jpg"
------------------------------------------------
ImageWidth                      => 3072
Model                           => vivo X90 Pro+
ImageHeight                     => 4096
ModifyDate                      => 2023-07-09T20:36:33+08:00
...

Fuzz Testing

The project uses cargo-fuzz (libFuzzer) for fuzz testing. Requires nightly Rust.

Run the fuzzer:

# Use testdata/ as seed corpus, write new corpus to fuzz/corpus/media_parser/
cargo +nightly fuzz run media_parser fuzz/corpus/media_parser/ testdata/

Reproduce a crash:

cargo +nightly fuzz run media_parser fuzz/artifacts/media_parser/<crash-file>

Minimize a crash input:

cargo +nightly fuzz tmin media_parser fuzz/artifacts/media_parser/<crash-file>

Changelog

CHANGELOG.md