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
- .png
- .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).
Motion Photos
Pixel and Samsung 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).
In-Memory Bytes
When the payload is already in RAM (decoded HTTP body, WASM-loaded
asset, mobile-cached blob), use MediaSource::from_memory 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 ms = from_memory?;
let mut parser = new;
let iter = parser.parse_exif?;
let exif: Exif = iter.into;
let make = exif.get.and_then;
# let _ = make; Ok::
MediaSource::from_memory accepts anything convertible into
bytes::Bytes: Vec<u8>, &'static [u8], Bytes, and HTTP-body types
that implement Into<Bytes> directly.
Format-Specific Metadata (parse_image_metadata)
Some image formats carry metadata that doesn't fit the EXIF/IFD model
— PNG tEXt chunks are the headline example. The new (v3.3+)
MediaParser::parse_image_metadata returns a structured
ImageMetadata { exif, format } covering both:
use ;
let mut parser = new;
let ms = open?;
let img = parser.parse_image_metadata?;
if let Some = img.format
# Ok::
img.exif is the standard Option<ExifIter> — convert to Exif
with .into() and read tags as in any other example.
For PNG specifically, this also captures legacy EXIF embedded in
Raw profile type exif / Raw profile type APP1 tEXt chunks
(ImageMagick / Photoshop pattern) — those are transparently
hex-decoded and merged into img.exif. The original tEXt entry
is still visible via img.format.
parse_image_metadata accepts the same source types as parse_exif:
files, in-memory bytes (via MediaSource::from_memory), and async
sources. The top-level read_image_metadata convenience helper is
deferred to v4 (alongside the planned Metadata enum redesign).
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
...
Contributing
Enable the repository's pre-commit hook once per clone so commits that
would fail cargo fmt --check in CI are rejected locally:
The hook lives in .githooks/pre-commit and runs cargo fmt --check
(sub-second). Bypass with git commit --no-verify for emergencies.
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: