Nom-Exif
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 —
MediaParserdispatches to the right backend by detected MIME, no per-format wrappers. - Motion Photo support — Pixel and Samsung Motion Photos (JPEG with
an embedded MP4) are detected automatically;
parse_trackextracts the embedded video's track metadata. - 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-iterResult), 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-fuzzagainst 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 ;
// One image:
let exif = read_exif?;
let make = exif.get.and_then;
// One video:
let info = read_track?;
let model = info.get.and_then;
// Auto-detect:
match read_metadata?
# Ok::
Reusable Parser
For batch processing, build a MediaParser once and reuse its buffer
across calls:
use ;
let mut parser = new;
let files = ;
for f in files
# Ok::
MediaSource accepts any Read (or Read + Seek):
MediaSource::open(path)— convenience for files.MediaSource::seekable(reader)— anyRead + Seeksource.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 ;
let raw: = read?;
let exif = read_exif_from_bytes?;
let make = exif.get.and_then;
# let _ = make; Ok::
For batch processing many in-memory payloads, reuse a MediaParser:
use ;
let mut parser = new;
let raw = read?;
let ms = from_bytes?;
let iter = parser.parse_exif_from_bytes?;
# let _ = iter; Ok::
MediaSource::from_bytes accepts anything convertible into
bytes::Bytes: Vec<u8>, &'static [u8], Bytes, and HTTP-body types
that implement Into<Bytes> directly.
Embedded Media Tracks (Motion Photos)
Pixel and Google phones store Motion Photos as a single JPEG with a
short MP4 video appended after the image data. parse_exif reads the
photo's EXIF as usual and sets a flag when it sees the
GCamera:MotionPhoto="1" XMP signal; parse_track on the same source
then extracts the embedded MP4's metadata.
use ;
let path = "PXL_20240101_120000000.MP.jpg";
let mut parser = new;
// 1. Parse the still image as usual.
let iter = parser.parse_exif?;
println!;
// 2. If true, re-open the source (parse_exif consumed it) and call
// parse_track to extract the embedded MP4's metadata.
if iter.has_embedded_track
# Ok::
has_embedded_track is content-detected, not a MIME-level guess — a
plain JPEG without the Motion Photo XMP returns false and parse_track
returns Error::TrackNotFound.
Coverage: Pixel/Google Motion Photos and Samsung Galaxy Motion Photos that use the Adobe XMP Container directory format (modern Pixel including Ultra HDR, modern Galaxy JPEGs).
Two API styles for Exif
The library exposes both eager and lazy views of EXIF metadata.
use ;
// Eager — easiest. Get-by-tag, parsed up front.
let exif = read_exif?;
let make = exif.get.and_then;
// Lazy — finer-grained. Parse-on-demand, per-entry errors visible.
let iter = read_exif_iter?;
for entry in iter
# Ok::
Async API
Enable the tokio feature in your Cargo.toml:
[]
= { = "3", = ["tokio"] }
Then use the _async helpers, or call parse_exif_async /
parse_track_async on a MediaParser directly:
#
# async
GPS Info
Exif and TrackInfo both expose gps_info(). ExifIter adds
parse_gps() for early termination once GPS tags have been read.
use ;
let exif = read_exif?;
if let Some = exif.gps_info
# Ok::
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)orread_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()(returnsOption<&GPSInfo>).g.latitude_ref == 'N'→matches!(g.latitude_ref, LatRef::North).- Cargo features:
async→tokio,json_dump→serde.
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
When the source carries an embedded media track (e.g. a Pixel Motion
Photo MP4 trailer), its metadata is appended after the EXIF entries
under an -- Embedded Track -- separator. Pass --no-track to skip
this and show only EXIF.
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"
}
For images with embedded tracks (Pixel Motion Photo etc.), the track's
metadata appears under a nested _embedded_track key. Pass --no-track
to omit it.
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/
Reproduce a crash:
Minimize a crash input: