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