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
103//!
104//! Some formats (HEIC Live Photos, RAF JPEG previews, …) embed media
105//! streams that `parse_exif` does not surface. The
106//! [`Exif::has_embedded_media`] / [`ExifIter::has_embedded_media`] /
107//! [`TrackInfo::has_embedded_media`] flags let callers detect this; the
108//! actual extraction API is a v3.x deliverable.
109
110pub use parser::{MediaKind, MediaParser, MediaSource};
111pub use video::{TrackInfo, TrackInfoTag};
112
113#[cfg(feature = "tokio")]
114pub use parser_async::AsyncMediaSource;
115
116pub use exif::gps::{Altitude, LatRef, LonRef, Speed, SpeedUnit};
117pub use exif::{
118    Exif, ExifEntry, ExifIter, ExifIterEntry, ExifTag, GPSInfo, IfdIndex, LatLng, TagOrCode,
119};
120pub use values::{EntryValue, ExifDateTime, IRational, Rational, URational};
121
122pub use error::{ConvertError, EntryError, Error, MalformedKind};
123
124/// Convenient one-line import of the most common v3 symbols.
125///
126/// ```rust
127/// use nom_exif::prelude::*;
128/// # fn main() -> Result<()> { Ok(()) }
129/// ```
130///
131/// Includes [`Error`] and [`MalformedKind`] so error-matching code does
132/// not need a second import. Cold-path types (e.g. `Rational`,
133/// `LatLng`, `ConvertError`, `ExifDateTime`) are intentionally **not**
134/// in the prelude — import them explicitly via `nom_exif::Type`.
135pub mod prelude {
136    pub use crate::{read_exif, read_metadata, read_track};
137    pub use crate::{
138        EntryValue, Error, Exif, ExifIter, ExifTag, GPSInfo, IfdIndex, MalformedKind, MediaKind,
139        MediaParser, MediaSource, Metadata, Result, TrackInfo, TrackInfoTag,
140    };
141}
142
143/// Crate-wide convenience alias for `std::result::Result<T, Error>`.
144pub type Result<T> = std::result::Result<T, Error>;
145
146/// One-shot result of [`read_metadata`]: either Exif (image) or TrackInfo
147/// (video/audio). Closed enum — see spec §8.6 for why there's no `Both`
148/// variant.
149#[derive(Debug, Clone)]
150pub enum Metadata {
151    Exif(Exif),
152    Track(TrackInfo),
153}
154
155use std::path::Path;
156
157/// Read EXIF metadata from a file in a single call.
158///
159/// For batch processing, prefer constructing a [`MediaParser`] once and
160/// reusing its parse buffer via [`MediaParser::parse_exif`].
161pub fn read_exif(path: impl AsRef<Path>) -> Result<Exif> {
162    let iter = read_exif_iter(path)?;
163    Ok(iter.into())
164}
165
166/// Read EXIF metadata from a file as a lazy iterator. Like [`read_exif`]
167/// but returns an [`ExifIter`] so per-entry errors can be inspected and
168/// values fetched without materializing the full [`Exif`] map.
169///
170/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_exif`].
171pub fn read_exif_iter(path: impl AsRef<Path>) -> Result<ExifIter> {
172    let file = std::fs::File::open(path)?;
173    let ms = MediaSource::seekable(file)?;
174    let mut parser = MediaParser::new();
175    parser.parse_exif(ms)
176}
177
178/// Read track metadata from a video / audio file in a single call.
179///
180/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_track`].
181pub fn read_track(path: impl AsRef<Path>) -> Result<TrackInfo> {
182    let file = std::fs::File::open(path)?;
183    let ms = MediaSource::seekable(file)?;
184    let mut parser = MediaParser::new();
185    parser.parse_track(ms)
186}
187
188/// Read metadata from a file, dispatching by detected [`MediaKind`]:
189/// images return [`Metadata::Exif`], video / audio containers return
190/// [`Metadata::Track`].
191///
192/// Use this when the caller does not know up-front whether the file is an
193/// image or a track. For batch processing, reuse a [`MediaParser`] and
194/// branch on [`MediaSource::kind`] manually.
195pub fn read_metadata(path: impl AsRef<Path>) -> Result<Metadata> {
196    let file = std::fs::File::open(path)?;
197    let ms = MediaSource::seekable(file)?;
198    let mut parser = MediaParser::new();
199    match ms.kind() {
200        MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
201        MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
202    }
203}
204
205/// Read EXIF metadata from an in-memory byte payload in a single call.
206/// Zero-copy: the underlying allocation is shared with the returned
207/// [`Exif`] via [`bytes::Bytes`] reference counting.
208///
209/// Accepts anything convertible into [`bytes::Bytes`] — `Vec<u8>`,
210/// `&'static [u8]`, an existing `Bytes`, or HTTP-body types that implement
211/// `Into<Bytes>` directly.
212///
213/// For batch processing or multiple parses against the same buffer, prefer
214/// constructing a [`MediaParser`] once and reusing it via
215/// [`MediaParser::parse_exif_from_bytes`].
216pub fn read_exif_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Exif> {
217    let iter = read_exif_iter_from_bytes(bytes)?;
218    Ok(iter.into())
219}
220
221/// Read EXIF metadata from an in-memory byte payload as a lazy iterator.
222/// Like [`read_exif_from_bytes`] but returns an [`ExifIter`].
223pub fn read_exif_iter_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<ExifIter> {
224    let ms = MediaSource::from_bytes(bytes)?;
225    let mut parser = MediaParser::new();
226    parser.parse_exif_from_bytes(ms)
227}
228
229/// Read track metadata from an in-memory video/audio payload.
230pub fn read_track_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<TrackInfo> {
231    let ms = MediaSource::from_bytes(bytes)?;
232    let mut parser = MediaParser::new();
233    parser.parse_track_from_bytes(ms)
234}
235
236/// Read metadata from an in-memory payload, dispatching by detected
237/// [`MediaKind`]: images return [`Metadata::Exif`], video/audio containers
238/// return [`Metadata::Track`].
239pub fn read_metadata_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Metadata> {
240    let ms = MediaSource::from_bytes(bytes)?;
241    let mut parser = MediaParser::new();
242    match ms.kind() {
243        MediaKind::Image => parser
244            .parse_exif_from_bytes(ms)
245            .map(|i| Metadata::Exif(i.into())),
246        MediaKind::Track => parser.parse_track_from_bytes(ms).map(Metadata::Track),
247    }
248}
249
250#[cfg(feature = "tokio")]
251mod tokio_top_level {
252    use super::*;
253
254    pub async fn read_exif_async(path: impl AsRef<std::path::Path>) -> Result<Exif> {
255        let iter = read_exif_iter_async(path).await?;
256        Ok(iter.into())
257    }
258
259    pub async fn read_exif_iter_async(path: impl AsRef<std::path::Path>) -> Result<ExifIter> {
260        let file = tokio::fs::File::open(path).await?;
261        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
262        let mut parser = MediaParser::new();
263        parser.parse_exif_async(ms).await
264    }
265
266    pub async fn read_track_async(path: impl AsRef<std::path::Path>) -> Result<TrackInfo> {
267        let file = tokio::fs::File::open(path).await?;
268        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
269        let mut parser = MediaParser::new();
270        parser.parse_track_async(ms).await
271    }
272
273    pub async fn read_metadata_async(path: impl AsRef<std::path::Path>) -> Result<Metadata> {
274        let file = tokio::fs::File::open(path).await?;
275        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
276        let mut parser = MediaParser::new();
277        match ms.kind() {
278            MediaKind::Image => parser
279                .parse_exif_async(ms)
280                .await
281                .map(|i| Metadata::Exif(i.into())),
282            MediaKind::Track => parser.parse_track_async(ms).await.map(Metadata::Track),
283        }
284    }
285}
286
287#[cfg(feature = "tokio")]
288pub use tokio_top_level::{
289    read_exif_async, read_exif_iter_async, read_metadata_async, read_track_async,
290};
291
292mod bbox;
293mod cr3;
294mod ebml;
295mod error;
296mod exif;
297mod file;
298mod heif;
299mod jpeg;
300mod mov;
301mod parser;
302#[cfg(feature = "tokio")]
303mod parser_async;
304mod raf;
305mod slice;
306mod utils;
307mod values;
308mod video;
309
310#[cfg(test)]
311mod testkit;
312
313#[cfg(test)]
314mod v3_top_level_tests {
315    use super::*;
316
317    #[test]
318    fn read_exif_jpg() {
319        let exif = read_exif("testdata/exif.jpg").unwrap();
320        assert!(exif.get(ExifTag::Make).is_some());
321    }
322
323    #[test]
324    fn read_track_mov() {
325        let info = read_track("testdata/meta.mov").unwrap();
326        assert!(info.get(TrackInfoTag::Make).is_some());
327    }
328
329    #[test]
330    fn read_metadata_dispatches_image() {
331        match read_metadata("testdata/exif.jpg").unwrap() {
332            Metadata::Exif(_) => {}
333            Metadata::Track(_) => panic!("expected Exif variant"),
334        }
335    }
336
337    #[test]
338    fn read_metadata_dispatches_track() {
339        match read_metadata("testdata/meta.mov").unwrap() {
340            Metadata::Track(_) => {}
341            Metadata::Exif(_) => panic!("expected Track variant"),
342        }
343    }
344
345    #[cfg(feature = "tokio")]
346    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
347    async fn read_exif_async_jpg() {
348        let exif = read_exif_async("testdata/exif.jpg").await.unwrap();
349        assert!(exif.get(ExifTag::Make).is_some());
350    }
351
352    #[cfg(feature = "tokio")]
353    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
354    async fn read_track_async_mov() {
355        let info = read_track_async("testdata/meta.mov").await.unwrap();
356        assert!(info.get(TrackInfoTag::Make).is_some());
357    }
358
359    #[test]
360    fn read_exif_from_bytes_jpg() {
361        let raw = std::fs::read("testdata/exif.jpg").unwrap();
362        let exif = read_exif_from_bytes(raw).unwrap();
363        assert!(exif.get(ExifTag::Make).is_some());
364    }
365
366    #[test]
367    fn read_exif_iter_from_bytes_jpg() {
368        let raw = std::fs::read("testdata/exif.jpg").unwrap();
369        let iter = read_exif_iter_from_bytes(raw).unwrap();
370        assert!(iter.into_iter().count() > 0);
371    }
372
373    #[test]
374    fn read_track_from_bytes_mov() {
375        let raw = std::fs::read("testdata/meta.mov").unwrap();
376        let info = read_track_from_bytes(raw).unwrap();
377        assert!(info.get(TrackInfoTag::Make).is_some());
378    }
379
380    #[test]
381    fn read_metadata_from_bytes_dispatches_image() {
382        let raw = std::fs::read("testdata/exif.jpg").unwrap();
383        match read_metadata_from_bytes(raw).unwrap() {
384            Metadata::Exif(_) => {}
385            Metadata::Track(_) => panic!("expected Exif variant"),
386        }
387    }
388
389    #[test]
390    fn read_metadata_from_bytes_dispatches_track() {
391        let raw = std::fs::read("testdata/meta.mov").unwrap();
392        match read_metadata_from_bytes(raw).unwrap() {
393            Metadata::Track(_) => {}
394            Metadata::Exif(_) => panic!("expected Track variant"),
395        }
396    }
397
398    #[test]
399    fn read_exif_from_bytes_static_slice() {
400        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
401        let exif = read_exif_from_bytes(raw).unwrap();
402        assert!(exif.get(ExifTag::Make).is_some());
403    }
404
405    #[test]
406    fn prelude_imports_compile() {
407        use crate::prelude::*;
408        fn _consume(_: Option<Exif>, _: Option<TrackInfo>, _: Option<MediaParser>) {}
409        // Verify the function symbols are in scope (compilation is the test).
410        let _e = read_exif("testdata/exif.jpg");
411        let _t = read_track("testdata/meta.mov");
412        let _m = read_metadata("testdata/exif.jpg");
413    }
414}