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