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}