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, ExifEntryRef, ExifIter, ExifIterEntry, ExifTag, GPSInfo, IfdIndex, IfdKind,
178    LatLng, TagOrCode,
179};
180pub use image_metadata::{ExifRepr, ImageFormatMetadata, ImageMetadata};
181pub use values::{EntryValue, ExifDateTime, IRational, Rational, URational};
182
183pub use error::{ConvertError, EntryError, Error, MalformedKind};
184
185/// Convenient one-line import of the most common v3 symbols.
186///
187/// ```rust
188/// use nom_exif::prelude::*;
189/// # fn main() -> Result<()> { Ok(()) }
190/// ```
191///
192/// Includes [`Error`] and [`MalformedKind`] so error-matching code does
193/// not need a second import. Cold-path types (e.g. `Rational`,
194/// `LatLng`, `ConvertError`, `ExifDateTime`) are intentionally **not**
195/// in the prelude — import them explicitly via `nom_exif::Type`.
196pub mod prelude {
197    pub use crate::{read_exif, read_metadata, read_track};
198    pub use crate::{
199        EntryValue, Error, Exif, ExifIter, ExifTag, GPSInfo, IfdIndex, IfdKind, MalformedKind,
200        MediaKind, MediaParser, MediaSource, Metadata, Result, TrackInfo, TrackInfoTag,
201    };
202}
203
204/// Crate-wide convenience alias for `std::result::Result<T, Error>`.
205pub type Result<T> = std::result::Result<T, Error>;
206
207/// One-shot result of [`read_metadata`]: either Exif (image) or TrackInfo
208/// (video/audio). Closed enum — see spec §8.6 for why there's no `Both`
209/// variant.
210#[derive(Debug, Clone)]
211pub enum Metadata {
212    Exif(Exif),
213    Track(TrackInfo),
214}
215
216use std::path::Path;
217
218/// Read EXIF metadata from a file in a single call.
219///
220/// For batch processing, prefer constructing a [`MediaParser`] once and
221/// reusing its parse buffer via [`MediaParser::parse_exif`].
222pub fn read_exif(path: impl AsRef<Path>) -> Result<Exif> {
223    let iter = read_exif_iter(path)?;
224    Ok(iter.into())
225}
226
227/// Read EXIF metadata from a file as a lazy iterator. Like [`read_exif`]
228/// but returns an [`ExifIter`] so per-entry errors can be inspected and
229/// values fetched without materializing the full [`Exif`] map.
230///
231/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_exif`].
232pub fn read_exif_iter(path: impl AsRef<Path>) -> Result<ExifIter> {
233    let file = std::fs::File::open(path)?;
234    let ms = MediaSource::seekable(file)?;
235    let mut parser = MediaParser::new();
236    parser.parse_exif(ms)
237}
238
239/// Read track metadata from a video / audio file in a single call.
240///
241/// For batch processing, reuse a [`MediaParser`] via [`MediaParser::parse_track`].
242pub fn read_track(path: impl AsRef<Path>) -> Result<TrackInfo> {
243    let file = std::fs::File::open(path)?;
244    let ms = MediaSource::seekable(file)?;
245    let mut parser = MediaParser::new();
246    parser.parse_track(ms)
247}
248
249/// Read metadata from a file, dispatching by detected [`MediaKind`]:
250/// images return [`Metadata::Exif`], video / audio containers return
251/// [`Metadata::Track`].
252///
253/// Use this when the caller does not know up-front whether the file is an
254/// image or a track. For batch processing, reuse a [`MediaParser`] and
255/// branch on [`MediaSource::kind`] manually.
256pub fn read_metadata(path: impl AsRef<Path>) -> Result<Metadata> {
257    let file = std::fs::File::open(path)?;
258    let ms = MediaSource::seekable(file)?;
259    let mut parser = MediaParser::new();
260    match ms.kind() {
261        MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
262        MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
263    }
264}
265
266/// **Deprecated since v3.3.0**: use [`read_exif`] with
267/// [`MediaSource::from_memory`] directly.
268#[deprecated(
269    since = "3.3.0",
270    note = "Use `read_exif` with `MediaSource::from_memory`."
271)]
272pub fn read_exif_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Exif> {
273    #[allow(deprecated)]
274    let iter = read_exif_iter_from_bytes(bytes)?;
275    Ok(iter.into())
276}
277
278#[deprecated(
279    since = "3.3.0",
280    note = "Use `read_exif_iter` with `MediaSource::from_memory`."
281)]
282pub fn read_exif_iter_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<ExifIter> {
283    let ms = MediaSource::from_memory(bytes)?;
284    let mut parser = MediaParser::new();
285    parser.parse_exif(ms)
286}
287
288#[deprecated(
289    since = "3.3.0",
290    note = "Use `read_track` with `MediaSource::from_memory`."
291)]
292pub fn read_track_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<TrackInfo> {
293    let ms = MediaSource::from_memory(bytes)?;
294    let mut parser = MediaParser::new();
295    parser.parse_track(ms)
296}
297
298#[deprecated(
299    since = "3.3.0",
300    note = "Use `read_metadata` with `MediaSource::from_memory`."
301)]
302pub fn read_metadata_from_bytes(bytes: impl Into<bytes::Bytes>) -> Result<Metadata> {
303    let ms = MediaSource::from_memory(bytes)?;
304    let mut parser = MediaParser::new();
305    match ms.kind() {
306        MediaKind::Image => parser.parse_exif(ms).map(|i| Metadata::Exif(i.into())),
307        MediaKind::Track => parser.parse_track(ms).map(Metadata::Track),
308    }
309}
310
311#[cfg(feature = "tokio-fs")]
312mod tokio_top_level {
313    use super::*;
314
315    pub async fn read_exif_async(path: impl AsRef<std::path::Path>) -> Result<Exif> {
316        let iter = read_exif_iter_async(path).await?;
317        Ok(iter.into())
318    }
319
320    pub async fn read_exif_iter_async(path: impl AsRef<std::path::Path>) -> Result<ExifIter> {
321        let file = tokio::fs::File::open(path).await?;
322        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
323        let mut parser = MediaParser::new();
324        parser.parse_exif_async(ms).await
325    }
326
327    pub async fn read_track_async(path: impl AsRef<std::path::Path>) -> Result<TrackInfo> {
328        let file = tokio::fs::File::open(path).await?;
329        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
330        let mut parser = MediaParser::new();
331        parser.parse_track_async(ms).await
332    }
333
334    pub async fn read_metadata_async(path: impl AsRef<std::path::Path>) -> Result<Metadata> {
335        let file = tokio::fs::File::open(path).await?;
336        let ms = parser_async::AsyncMediaSource::seekable(file).await?;
337        let mut parser = MediaParser::new();
338        match ms.kind() {
339            MediaKind::Image => parser
340                .parse_exif_async(ms)
341                .await
342                .map(|i| Metadata::Exif(i.into())),
343            MediaKind::Track => parser.parse_track_async(ms).await.map(Metadata::Track),
344        }
345    }
346}
347
348#[cfg(feature = "tokio-fs")]
349pub use tokio_top_level::{
350    read_exif_async, read_exif_iter_async, read_metadata_async, read_track_async,
351};
352
353mod bbox;
354mod cr3;
355mod ebml;
356mod error;
357mod exif;
358mod file;
359mod heif;
360mod image_metadata;
361mod jpeg;
362mod mov;
363mod parser;
364#[cfg(feature = "tokio")]
365mod parser_async;
366mod png;
367mod raf;
368mod slice;
369mod utils;
370mod values;
371mod video;
372mod webp;
373
374#[cfg(test)]
375mod testkit;
376
377#[cfg(test)]
378mod v3_top_level_tests {
379    use super::*;
380
381    #[test]
382    fn read_exif_jpg() {
383        let exif = read_exif("testdata/exif.jpg").unwrap();
384        assert!(exif.get(ExifTag::Make).is_some());
385    }
386
387    #[test]
388    fn read_track_mov() {
389        let info = read_track("testdata/meta.mov").unwrap();
390        assert!(info.get(TrackInfoTag::Make).is_some());
391    }
392
393    #[test]
394    fn read_metadata_dispatches_image() {
395        match read_metadata("testdata/exif.jpg").unwrap() {
396            Metadata::Exif(_) => {}
397            Metadata::Track(_) => panic!("expected Exif variant"),
398        }
399    }
400
401    #[test]
402    fn read_metadata_dispatches_track() {
403        match read_metadata("testdata/meta.mov").unwrap() {
404            Metadata::Track(_) => {}
405            Metadata::Exif(_) => panic!("expected Track variant"),
406        }
407    }
408
409    #[cfg(feature = "tokio-fs")]
410    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
411    async fn read_exif_async_jpg() {
412        let exif = read_exif_async("testdata/exif.jpg").await.unwrap();
413        assert!(exif.get(ExifTag::Make).is_some());
414    }
415
416    #[cfg(feature = "tokio-fs")]
417    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
418    async fn read_track_async_mov() {
419        let info = read_track_async("testdata/meta.mov").await.unwrap();
420        assert!(info.get(TrackInfoTag::Make).is_some());
421    }
422
423    #[test]
424    #[allow(deprecated)]
425    fn read_exif_from_bytes_jpg() {
426        let raw = std::fs::read("testdata/exif.jpg").unwrap();
427        let exif = read_exif_from_bytes(raw).unwrap();
428        assert!(exif.get(ExifTag::Make).is_some());
429    }
430
431    #[test]
432    #[allow(deprecated)]
433    fn read_exif_iter_from_bytes_jpg() {
434        let raw = std::fs::read("testdata/exif.jpg").unwrap();
435        let iter = read_exif_iter_from_bytes(raw).unwrap();
436        assert!(iter.into_iter().count() > 0);
437    }
438
439    #[test]
440    #[allow(deprecated)]
441    fn read_track_from_bytes_mov() {
442        let raw = std::fs::read("testdata/meta.mov").unwrap();
443        let info = read_track_from_bytes(raw).unwrap();
444        assert!(info.get(TrackInfoTag::Make).is_some());
445    }
446
447    #[test]
448    #[allow(deprecated)]
449    fn read_metadata_from_bytes_dispatches_image() {
450        let raw = std::fs::read("testdata/exif.jpg").unwrap();
451        match read_metadata_from_bytes(raw).unwrap() {
452            Metadata::Exif(_) => {}
453            Metadata::Track(_) => panic!("expected Exif variant"),
454        }
455    }
456
457    #[test]
458    #[allow(deprecated)]
459    fn read_metadata_from_bytes_dispatches_track() {
460        let raw = std::fs::read("testdata/meta.mov").unwrap();
461        match read_metadata_from_bytes(raw).unwrap() {
462            Metadata::Track(_) => {}
463            Metadata::Exif(_) => panic!("expected Track variant"),
464        }
465    }
466
467    #[test]
468    #[allow(deprecated)]
469    fn read_exif_from_bytes_static_slice() {
470        let raw: &'static [u8] = include_bytes!("../testdata/exif.jpg");
471        let exif = read_exif_from_bytes(raw).unwrap();
472        assert!(exif.get(ExifTag::Make).is_some());
473    }
474
475    #[test]
476    fn prelude_imports_compile() {
477        use crate::prelude::*;
478        fn _consume(_: Option<Exif>, _: Option<TrackInfo>, _: Option<MediaParser>) {}
479        // Verify the function symbols are in scope (compilation is the test).
480        let _e = read_exif("testdata/exif.jpg");
481        let _t = read_track("testdata/meta.mov");
482        let _m = read_metadata("testdata/exif.jpg");
483    }
484}