Skip to main content

nom_exif/
lib.rs

1//! `nom-exif` is a pure Rust library for **both image EXIF and
2//! video / audio track metadata** through a single unified API.
3//!
4//! # Highlights
5//!
6//! - Pure Rust — no FFmpeg, no libexif, no system deps; cross-compiles
7//!   cleanly.
8//! - Image **and** video / audio in one crate — [`MediaParser`] dispatches
9//!   to the right backend by detected MIME, no per-format wrappers.
10//! - RAW format support — Canon CR3, Fujifilm RAF, Phase One IIQ,
11//!   alongside JPEG / HEIC / TIFF.
12//! - Three input modes — files, arbitrary `Read` / `Read + Seek`
13//!   (network streams, pipes), or in-RAM bytes (WASM, mobile, HTTP
14//!   proxies).
15//! - Sync and async unified under one [`MediaParser`].
16//! - Eager ([`Exif`], get-by-tag) or lazy ([`ExifIter`], parse-on-demand)
17//!   — per-entry errors surface in both modes ([`Exif::errors`] /
18//!   per-iter `Result`), so one bad tag doesn't poison the parse.
19//! - Allocation-frugal — parser buffer is recycled across calls;
20//!   sub-IFDs share the same allocation (no deep copies).
21//! - Fuzz-tested with `cargo-fuzz` against malformed and adversarial input.
22//!
23//! # Quick start
24//!
25//! For a one-shot read, use the helpers:
26//!
27//! ```rust
28//! use nom_exif::{read_exif, ExifTag};
29//!
30//! let exif = read_exif("./testdata/exif.jpg")?;
31//! let make = exif.get(ExifTag::Make).and_then(|v| v.as_str());
32//! assert_eq!(make, Some("vivo"));
33//! # Ok::<(), nom_exif::Error>(())
34//! ```
35//!
36//! For batch processing, build a [`MediaParser`] once and reuse its
37//! buffer:
38//!
39//! ```rust
40//! use nom_exif::{MediaKind, MediaParser, MediaSource};
41//!
42//! let mut parser = MediaParser::new();
43//! for path in ["./testdata/exif.jpg", "./testdata/meta.mov"] {
44//!     let ms = MediaSource::open(path)?;
45//!     match ms.kind() {
46//!         MediaKind::Image => { let _ = parser.parse_exif(ms)?; }
47//!         MediaKind::Track => { let _ = parser.parse_track(ms)?; }
48//!     }
49//! }
50//! # Ok::<(), nom_exif::Error>(())
51//! ```
52//!
53//! Async variants live behind `feature = "tokio"`:
54//! [`read_exif_async`], [`read_track_async`], [`read_metadata_async`],
55//! plus [`MediaParser::parse_exif_async`] / [`MediaParser::parse_track_async`].
56//!
57//! # Reading from in-memory bytes
58//!
59//! When the payload is already in RAM (WASM, mobile, HTTP proxy, decoded
60//! response body), use the `*_from_bytes` helpers to skip the `File` /
61//! `Read` round-trip entirely. Memory mode is **zero-copy**: the underlying
62//! allocation is shared with the returned [`Exif`] / [`ExifIter`] /
63//! [`TrackInfo`] via [`bytes::Bytes`] reference counting.
64//!
65//! ```rust
66//! use nom_exif::{read_exif_from_bytes, ExifTag};
67//!
68//! let raw = std::fs::read("./testdata/exif.jpg")?;
69//! let exif = read_exif_from_bytes(raw)?;
70//! assert_eq!(exif.get(ExifTag::Make).and_then(|v| v.as_str()), Some("vivo"));
71//! # Ok::<(), nom_exif::Error>(())
72//! ```
73//!
74//! For batch processing of many in-memory payloads, build a [`MediaParser`]
75//! once and call [`MediaParser::parse_exif_from_bytes`] /
76//! [`MediaParser::parse_track_from_bytes`] per payload.
77//!
78//! # API surface
79//!
80//! - **One-shot helpers**: [`read_exif`], [`read_exif_iter`], [`read_track`], [`read_metadata`]
81//!   for files; [`read_exif_from_bytes`], [`read_exif_iter_from_bytes`],
82//!   [`read_track_from_bytes`], [`read_metadata_from_bytes`] for in-memory bytes.
83//! - **Reusable parser**: [`MediaParser`] + [`MediaSource`] (or [`AsyncMediaSource`])
84//!   + [`MediaKind`].
85//! - **Image metadata**: [`Exif`] (eager, get-by-tag) or [`ExifIter`]
86//!   (lazy iterator with per-entry errors). Convert: `let exif: Exif = iter.into();`.
87//! - **Track metadata**: [`TrackInfo`] (audio/video container metadata).
88//! - **Discriminated union**: [`Metadata`] returned by [`read_metadata`].
89//! - **Errors**: [`Error`] for parse-level, [`EntryError`] for per-entry
90//!   IFD errors, [`ConvertError`] for type-conversion peer errors.
91//! - **Convenience**: [`prelude`] re-exports the symbols you most often need.
92//!
93//! See `docs/MIGRATION.md` for the v2 → v3 migration guide and
94//! `docs/V3_API_DESIGN.md` for the internal design contract.
95//!
96//! # Cargo features
97//!
98//! - `tokio` — async API via tokio (`AsyncMediaSource`, `read_*_async`,
99//!   `MediaParser::parse_*_async`).
100//! - `serde` — derives `Serialize`/`Deserialize` on the public types.
101//!
102//! # Embedded media tracks
103//!
104//! Some images embed a media track that `parse_exif` doesn't surface —
105//! most commonly **Pixel/Google Motion Photo** JPEGs, which carry a short
106//! MP4 video appended after the JPEG image data. The
107//! [`Exif::has_embedded_track`] / [`ExifIter::has_embedded_track`] flags
108//! are set by `parse_exif` when it observes a concrete content signal
109//! (e.g. the `GCamera:MotionPhoto="1"` XMP attribute). When the flag is
110//! `true`, call [`MediaParser::parse_track`] on the same source to
111//! extract the embedded MP4's metadata — `parse_track` automatically
112//! locates and parses the trailer.
113//!
114//! ```no_run
115//! use nom_exif::{MediaParser, MediaSource};
116//! let mut parser = MediaParser::new();
117//! let path = "PXL_20240101_120000000.MP.jpg";
118//! let iter = parser.parse_exif(MediaSource::open(path)?)?;
119//! if iter.has_embedded_track() {
120//!     // Re-open: MediaSource is consumed by parse_exif.
121//!     let track = parser.parse_track(MediaSource::open(path)?)?;
122//!     // ...
123//! }
124//! # Ok::<(), nom_exif::Error>(())
125//! ```
126//!
127//! **Coverage**: Pixel/Google Motion Photos and Samsung Galaxy Motion
128//! Photos that use the Adobe XMP Container directory format (modern
129//! Pixel including Ultra HDR, modern Galaxy JPEGs).
130//!
131//! The pre-3.1 names (`has_embedded_media`) are `#[deprecated]` aliases
132//! that forward to the new methods.
133
134pub use parser::{MediaKind, MediaParser, MediaSource};
135pub use video::{TrackInfo, TrackInfoTag};
136
137#[cfg(feature = "tokio")]
138pub use parser_async::AsyncMediaSource;
139
140pub use exif::gps::{Altitude, LatRef, LonRef, Speed, SpeedUnit};
141pub use exif::{
142    Exif, ExifEntry, ExifIter, ExifIterEntry, ExifTag, GPSInfo, IfdIndex, LatLng, TagOrCode,
143};
144pub use values::{EntryValue, ExifDateTime, IRational, Rational, URational};
145
146pub use error::{ConvertError, EntryError, Error, MalformedKind};
147
148/// Convenient one-line import of the most common v3 symbols.
149///
150/// ```rust
151/// use nom_exif::prelude::*;
152/// # fn main() -> Result<()> { Ok(()) }
153/// ```
154///
155/// Includes [`Error`] and [`MalformedKind`] so error-matching code does
156/// not need a second import. Cold-path types (e.g. `Rational`,
157/// `LatLng`, `ConvertError`, `ExifDateTime`) are intentionally **not**
158/// in the prelude — import them explicitly via `nom_exif::Type`.
159pub mod prelude {
160    pub use crate::{read_exif, read_metadata, read_track};
161    pub use crate::{
162        EntryValue, Error, Exif, ExifIter, ExifTag, GPSInfo, IfdIndex, MalformedKind, MediaKind,
163        MediaParser, MediaSource, Metadata, Result, TrackInfo, TrackInfoTag,
164    };
165}
166
167/// Crate-wide convenience alias for `std::result::Result<T, Error>`.
168pub type Result<T> = std::result::Result<T, Error>;
169
170/// One-shot result of [`read_metadata`]: either Exif (image) or TrackInfo
171/// (video/audio). Closed enum — see spec §8.6 for why there's no `Both`
172/// variant.
173#[derive(Debug, Clone)]
174pub enum Metadata {
175    Exif(Exif),
176    Track(TrackInfo),
177}
178
179use std::path::Path;
180
181/// Read EXIF metadata from a file in a single call.
182///
183/// For batch processing, prefer constructing a [`MediaParser`] once and
184/// reusing its parse buffer via [`MediaParser::parse_exif`].
185pub fn read_exif(path: impl AsRef<Path>) -> Result<Exif> {
186    let iter = read_exif_iter(path)?;
187    Ok(iter.into())
188}
189
190/// Read EXIF metadata from a file as a lazy iterator. Like [`read_exif`]
191/// but returns an [`ExifIter`] so per-entry errors can be inspected and
192/// values fetched without materializing the full [`Exif`] map.
193///
194/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_exif`].
195pub fn read_exif_iter(path: impl AsRef<Path>) -> Result<ExifIter> {
196    let file = std::fs::File::open(path)?;
197    let ms = MediaSource::seekable(file)?;
198    let mut parser = MediaParser::new();
199    parser.parse_exif(ms)
200}
201
202/// Read track metadata from a video / audio file in a single call.
203///
204/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_track`].
205pub fn read_track(path: impl AsRef<Path>) -> Result<TrackInfo> {
206    let file = std::fs::File::open(path)?;
207    let ms = MediaSource::seekable(file)?;
208    let mut parser = MediaParser::new();
209    parser.parse_track(ms)
210}
211
212/// Read metadata from a file, dispatching by detected [`MediaKind`]:
213/// images return [`Metadata::Exif`], video / audio containers return
214/// [`Metadata::Track`].
215///
216/// Use this when the caller does not know up-front whether the file is an
217/// image or a track. For batch processing, reuse a [`MediaParser`] and
218/// branch on [`MediaSource::kind`] manually.
219pub fn read_metadata(path: impl AsRef<Path>) -> Result<Metadata> {
220    let file = std::fs::File::open(path)?;
221    let ms = MediaSource::seekable(file)?;
222    let mut parser = MediaParser::new();
223    match ms.kind() {
224        MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
225        MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
226    }
227}
228
229/// Read EXIF metadata from an in-memory byte payload in a single call.
230/// Zero-copy: the underlying allocation is shared with the returned
231/// [`Exif`] via [`bytes::Bytes`] reference counting.
232///
233/// Accepts anything convertible into [`bytes::Bytes`] — `Vec<u8>`,
234/// `&'static [u8]`, an existing `Bytes`, or HTTP-body types that implement
235/// `Into<Bytes>` directly.
236///
237/// For batch processing or multiple parses against the same buffer, prefer
238/// constructing a [`MediaParser`] once and reusing it via
239/// [`MediaParser::parse_exif_from_bytes`].
240pub fn read_exif_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Exif> {
241    let iter = read_exif_iter_from_bytes(bytes)?;
242    Ok(iter.into())
243}
244
245/// Read EXIF metadata from an in-memory byte payload as a lazy iterator.
246/// Like [`read_exif_from_bytes`] but returns an [`ExifIter`].
247pub fn read_exif_iter_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<ExifIter> {
248    let ms = MediaSource::from_bytes(bytes)?;
249    let mut parser = MediaParser::new();
250    parser.parse_exif_from_bytes(ms)
251}
252
253/// Read track metadata from an in-memory video/audio payload.
254pub fn read_track_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<TrackInfo> {
255    let ms = MediaSource::from_bytes(bytes)?;
256    let mut parser = MediaParser::new();
257    parser.parse_track_from_bytes(ms)
258}
259
260/// Read metadata from an in-memory payload, dispatching by detected
261/// [`MediaKind`]: images return [`Metadata::Exif`], video/audio containers
262/// return [`Metadata::Track`].
263pub fn read_metadata_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Metadata> {
264    let ms = MediaSource::from_bytes(bytes)?;
265    let mut parser = MediaParser::new();
266    match ms.kind() {
267        MediaKind::Image => parser
268            .parse_exif_from_bytes(ms)
269            .map(|i| Metadata::Exif(i.into())),
270        MediaKind::Track => parser.parse_track_from_bytes(ms).map(Metadata::Track),
271    }
272}
273
274#[cfg(feature = "tokio")]
275mod tokio_top_level {
276    use super::*;
277
278    pub async fn read_exif_async(path: impl AsRef<std::path::Path>) -> Result<Exif> {
279        let iter = read_exif_iter_async(path).await?;
280        Ok(iter.into())
281    }
282
283    pub async fn read_exif_iter_async(path: impl AsRef<std::path::Path>) -> Result<ExifIter> {
284        let file = tokio::fs::File::open(path).await?;
285        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
286        let mut parser = MediaParser::new();
287        parser.parse_exif_async(ms).await
288    }
289
290    pub async fn read_track_async(path: impl AsRef<std::path::Path>) -> Result<TrackInfo> {
291        let file = tokio::fs::File::open(path).await?;
292        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
293        let mut parser = MediaParser::new();
294        parser.parse_track_async(ms).await
295    }
296
297    pub async fn read_metadata_async(path: impl AsRef<std::path::Path>) -> Result<Metadata> {
298        let file = tokio::fs::File::open(path).await?;
299        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
300        let mut parser = MediaParser::new();
301        match ms.kind() {
302            MediaKind::Image => parser
303                .parse_exif_async(ms)
304                .await
305                .map(|i| Metadata::Exif(i.into())),
306            MediaKind::Track => parser.parse_track_async(ms).await.map(Metadata::Track),
307        }
308    }
309}
310
311#[cfg(feature = "tokio")]
312pub use tokio_top_level::{
313    read_exif_async, read_exif_iter_async, read_metadata_async, read_track_async,
314};
315
316mod bbox;
317mod cr3;
318mod ebml;
319mod error;
320mod exif;
321mod file;
322mod heif;
323mod jpeg;
324mod mov;
325mod parser;
326#[cfg(feature = "tokio")]
327mod parser_async;
328mod raf;
329mod slice;
330mod utils;
331mod values;
332mod video;
333
334#[cfg(test)]
335mod testkit;
336
337#[cfg(test)]
338mod v3_top_level_tests {
339    use super::*;
340
341    #[test]
342    fn read_exif_jpg() {
343        let exif = read_exif("testdata/exif.jpg").unwrap();
344        assert!(exif.get(ExifTag::Make).is_some());
345    }
346
347    #[test]
348    fn read_track_mov() {
349        let info = read_track("testdata/meta.mov").unwrap();
350        assert!(info.get(TrackInfoTag::Make).is_some());
351    }
352
353    #[test]
354    fn read_metadata_dispatches_image() {
355        match read_metadata("testdata/exif.jpg").unwrap() {
356            Metadata::Exif(_) => {}
357            Metadata::Track(_) => panic!("expected Exif variant"),
358        }
359    }
360
361    #[test]
362    fn read_metadata_dispatches_track() {
363        match read_metadata("testdata/meta.mov").unwrap() {
364            Metadata::Track(_) => {}
365            Metadata::Exif(_) => panic!("expected Track variant"),
366        }
367    }
368
369    #[cfg(feature = "tokio")]
370    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
371    async fn read_exif_async_jpg() {
372        let exif = read_exif_async("testdata/exif.jpg").await.unwrap();
373        assert!(exif.get(ExifTag::Make).is_some());
374    }
375
376    #[cfg(feature = "tokio")]
377    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
378    async fn read_track_async_mov() {
379        let info = read_track_async("testdata/meta.mov").await.unwrap();
380        assert!(info.get(TrackInfoTag::Make).is_some());
381    }
382
383    #[test]
384    fn read_exif_from_bytes_jpg() {
385        let raw = std::fs::read("testdata/exif.jpg").unwrap();
386        let exif = read_exif_from_bytes(raw).unwrap();
387        assert!(exif.get(ExifTag::Make).is_some());
388    }
389
390    #[test]
391    fn read_exif_iter_from_bytes_jpg() {
392        let raw = std::fs::read("testdata/exif.jpg").unwrap();
393        let iter = read_exif_iter_from_bytes(raw).unwrap();
394        assert!(iter.into_iter().count() > 0);
395    }
396
397    #[test]
398    fn read_track_from_bytes_mov() {
399        let raw = std::fs::read("testdata/meta.mov").unwrap();
400        let info = read_track_from_bytes(raw).unwrap();
401        assert!(info.get(TrackInfoTag::Make).is_some());
402    }
403
404    #[test]
405    fn read_metadata_from_bytes_dispatches_image() {
406        let raw = std::fs::read("testdata/exif.jpg").unwrap();
407        match read_metadata_from_bytes(raw).unwrap() {
408            Metadata::Exif(_) => {}
409            Metadata::Track(_) => panic!("expected Exif variant"),
410        }
411    }
412
413    #[test]
414    fn read_metadata_from_bytes_dispatches_track() {
415        let raw = std::fs::read("testdata/meta.mov").unwrap();
416        match read_metadata_from_bytes(raw).unwrap() {
417            Metadata::Track(_) => {}
418            Metadata::Exif(_) => panic!("expected Track variant"),
419        }
420    }
421
422    #[test]
423    fn read_exif_from_bytes_static_slice() {
424        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
425        let exif = read_exif_from_bytes(raw).unwrap();
426        assert!(exif.get(ExifTag::Make).is_some());
427    }
428
429    #[test]
430    fn prelude_imports_compile() {
431        use crate::prelude::*;
432        fn _consume(_: Option<Exif>, _: Option<TrackInfo>, _: Option<MediaParser>) {}
433        // Verify the function symbols are in scope (compilation is the test).
434        let _e = read_exif("testdata/exif.jpg");
435        let _t = read_track("testdata/meta.mov");
436        let _m = read_metadata("testdata/exif.jpg");
437    }
438}