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}