nom_exif/
exif.rs

1use crate::error::{nom_error_to_parsing_error_with_state, ParsingError, ParsingErrorState};
2use crate::file::MimeImage;
3use crate::parser::{BufParser, ParsingState, ShareBuf};
4use crate::raf::RafInfo;
5use crate::skip::Skip;
6use crate::slice::SubsliceRange;
7use crate::{cr3, heif, jpeg, MediaParser, MediaSource};
8#[allow(deprecated)]
9use crate::{partial_vec::PartialVec, FileFormat};
10pub use exif_exif::Exif;
11use exif_exif::TIFF_HEADER_LEN;
12use exif_iter::input_into_iter;
13pub use exif_iter::{ExifIter, ParsedExifEntry};
14pub use gps::{GPSInfo, LatLng};
15pub use tags::ExifTag;
16
17use std::io::Read;
18use std::ops::Range;
19
20pub(crate) mod ifd;
21pub(crate) use exif_exif::{check_exif_header, check_exif_header2, TiffHeader};
22pub(crate) use travel::IfdHeaderTravel;
23
24mod exif_exif;
25mod exif_iter;
26mod gps;
27mod tags;
28mod travel;
29
30/// *Deprecated*: Please use [`crate::MediaParser`] instead.
31///
32/// Read exif data from `reader`, and build an [`ExifIter`] for it.
33///
34/// ~~If `format` is None, the parser will detect the file format automatically.~~
35/// *The `format` param will be ignored from v2.0.0.*
36///
37/// Currently supported file formats are:
38///
39/// - *.heic, *.heif, etc.
40/// - *.jpg, *.jpeg, etc.
41///
42/// *.tiff/*.tif is not supported by this function, please use `MediaParser`
43/// instead.
44///
45/// All entries are lazy-parsed. That is, only when you iterate over
46/// [`ExifIter`] will the IFD entries be parsed one by one.
47///
48/// The one exception is the time zone entries. The parser will try to find and
49/// parse the time zone data first, so we can correctly parse all time
50/// information in subsequent iterates.
51///
52/// Please note that the parsing routine itself provides a buffer, so the
53/// `reader` may not need to be wrapped with `BufRead`.
54///
55/// Returns:
56///
57/// - An `Ok<Some<ExifIter>>` if Exif data is found and parsed successfully.
58/// - An `Ok<None>` if Exif data is not found.
59/// - An `Err` if Exif data is found but parsing failed.
60#[deprecated(since = "2.0.0")]
61#[allow(deprecated)]
62pub fn parse_exif<T: Read>(reader: T, _: Option<FileFormat>) -> crate::Result<Option<ExifIter>> {
63    let mut parser = MediaParser::new();
64    let iter: ExifIter = parser.parse(MediaSource::unseekable(reader)?)?;
65    let iter = iter.to_owned();
66    Ok(Some(iter))
67}
68
69#[tracing::instrument(skip(reader))]
70pub(crate) fn parse_exif_iter<R: Read, S: Skip<R>>(
71    parser: &mut MediaParser,
72    mime_img: MimeImage,
73    reader: &mut R,
74) -> Result<ExifIter, crate::Error> {
75    // For CR3 files, we need special handling to get all CMT blocks
76    if mime_img == MimeImage::Cr3 {
77        return parse_cr3_exif_iter::<R, S>(parser, reader);
78    }
79
80    let out = parser.load_and_parse::<R, S, _, _>(reader, |buf, state| {
81        extract_exif_range(mime_img, buf, state)
82    })?;
83
84    range_to_iter(parser, out)
85}
86
87/// Special parser for CR3 files that extracts all CMT blocks (CMT1, CMT2, CMT3)
88/// and adds them as additional TIFF blocks to the ExifIter.
89#[tracing::instrument(skip(reader))]
90fn parse_cr3_exif_iter<R: Read, S: Skip<R>>(
91    parser: &mut MediaParser,
92    reader: &mut R,
93) -> Result<ExifIter, crate::Error> {
94    use crate::parser::Buf;
95
96    // First, parse to get all CMT ranges
97    let cmt_ranges = parser
98        .load_and_parse::<R, S, _, _>(reader, |buf, _state| cr3::extract_all_cmt_ranges(buf))?;
99
100    let Some(cmt_ranges) = cmt_ranges else {
101        return Err("CR3: No CMT data found".into());
102    };
103
104    if cmt_ranges.ranges.is_empty() {
105        return Err("CR3: No CMT ranges available".into());
106    }
107
108    tracing::debug!(
109        cmt_count = cmt_ranges.ranges.len(),
110        "Found CMT ranges in CR3 file"
111    );
112
113    // Get the parser position offset - share_buf will add this to ranges
114    let position_offset = parser.position();
115
116    // Get the first CMT range (CMT1) to create the primary ExifIter
117    let (first_block_id, first_range) = &cmt_ranges.ranges[0];
118    tracing::debug!(
119        block_id = first_block_id,
120        range = ?first_range,
121        position_offset,
122        "Creating primary ExifIter from first CMT block"
123    );
124
125    // Share the buffer and create the primary ExifIter
126    // Note: share_buf adds position_offset to the range internally
127    let input: PartialVec = parser.share_buf(first_range.clone());
128    let mut iter = input_into_iter(input, None)?;
129
130    // Add remaining CMT blocks as additional TIFF blocks
131    // We need to adjust the ranges by position_offset since the PartialVec.data
132    // contains the full buffer and ranges need to be absolute
133    // Note: We skip CMT3 (MakerNotes) as it has a proprietary format that requires
134    // special handling and would produce garbage data if parsed as standard EXIF
135    for (block_id, range) in cmt_ranges.ranges.iter().skip(1) {
136        // Skip CMT3 (MakerNotes) - it has a proprietary Canon format
137        if *block_id == "CMT3" {
138            tracing::debug!(block_id, "Skipping CMT3 (MakerNotes) - proprietary format");
139            continue;
140        }
141
142        let adjusted_range = (range.start + position_offset)..(range.end + position_offset);
143        tracing::debug!(
144            block_id,
145            original_range = ?range,
146            adjusted_range = ?adjusted_range,
147            "Adding additional CMT block"
148        );
149        iter.add_tiff_block(block_id.to_string(), adjusted_range, None);
150    }
151
152    Ok(iter)
153}
154
155type ExifRangeResult = Result<Option<(Range<usize>, Option<TiffHeader>)>, ParsingErrorState>;
156
157fn extract_exif_range(img: MimeImage, buf: &[u8], state: Option<ParsingState>) -> ExifRangeResult {
158    let (exif_data, state) = extract_exif_with_mime(img, buf, state)?;
159    let header = state.and_then(|x| match x {
160        ParsingState::TiffHeader(h) => Some(h),
161        ParsingState::HeifExifSize(_) => None,
162        ParsingState::Cr3ExifSize(_) => None,
163    });
164    Ok(exif_data
165        .and_then(|x| buf.subslice_in_range(x))
166        .map(|x| (x, header)))
167}
168
169fn range_to_iter(
170    parser: &mut impl ShareBuf,
171    out: Option<(Range<usize>, Option<TiffHeader>)>,
172) -> Result<ExifIter, crate::Error> {
173    if let Some((range, header)) = out {
174        tracing::debug!(?range, ?header, "Got Exif data");
175        let input: PartialVec = parser.share_buf(range);
176        let iter = input_into_iter(input, header)?;
177
178        Ok(iter)
179    } else {
180        tracing::debug!("Exif not found");
181        Err("Exif not found".into())
182    }
183}
184
185#[cfg(feature = "async")]
186#[tracing::instrument(skip(reader))]
187pub(crate) async fn parse_exif_iter_async<
188    R: AsyncRead + Unpin + Send,
189    S: crate::skip::AsyncSkip<R>,
190>(
191    parser: &mut crate::AsyncMediaParser,
192    mime_img: MimeImage,
193    reader: &mut R,
194) -> Result<ExifIter, crate::Error> {
195    use crate::parser_async::AsyncBufParser;
196
197    let out = parser
198        .load_and_parse::<R, S, _, _>(reader, |buf, state| {
199            extract_exif_range(mime_img, buf, state)
200        })
201        .await?;
202
203    range_to_iter(parser, out)
204}
205
206#[tracing::instrument(skip(buf))]
207pub(crate) fn extract_exif_with_mime(
208    img_type: crate::file::MimeImage,
209    buf: &[u8],
210    state: Option<ParsingState>,
211) -> Result<(Option<&[u8]>, Option<ParsingState>), ParsingErrorState> {
212    let (exif_data, state) = match img_type {
213        MimeImage::Jpeg => jpeg::extract_exif_data(buf)
214            .map(|res| (res.1, state.clone()))
215            .map_err(|e| nom_error_to_parsing_error_with_state(e, state))?,
216        MimeImage::Heic | crate::file::MimeImage::Heif => heif_extract_exif(state, buf)?,
217        MimeImage::Tiff => {
218            let header = match state {
219                Some(ParsingState::TiffHeader(ref h)) => h.to_owned(),
220                None => {
221                    let (_, header) = TiffHeader::parse(buf)
222                        .map_err(|e| nom_error_to_parsing_error_with_state(e, None))?;
223                    if header.ifd0_offset as usize > buf.len() {
224                        let clear_and_skip =
225                            ParsingError::Need(header.ifd0_offset as usize - TIFF_HEADER_LEN + 2);
226                        let state = Some(ParsingState::TiffHeader(header));
227                        return Err(ParsingErrorState::new(clear_and_skip, state));
228                    }
229                    header
230                }
231                _ => unreachable!(),
232            };
233
234            // full fill TIFF data
235            tracing::debug!("full fill TIFF data");
236            let mut iter = IfdHeaderTravel::new(
237                buf,
238                header.ifd0_offset as usize,
239                tags::ExifTagCode::Code(0x2a),
240                header.endian,
241            );
242            iter.travel_ifd(0)
243                .map_err(|e| ParsingErrorState::new(e, state.clone()))?;
244            tracing::debug!("full fill TIFF data done");
245
246            (Some(buf), state)
247        }
248        MimeImage::Raf => RafInfo::parse(buf)
249            .map(|res| (res.1.exif_data, state.clone()))
250            .map_err(|e| nom_error_to_parsing_error_with_state(e, state))?,
251        MimeImage::Cr3 => cr3_extract_exif(state, buf)?,
252    };
253    Ok((exif_data, state))
254}
255
256fn heif_extract_exif(
257    state: Option<ParsingState>,
258    buf: &[u8],
259) -> Result<(Option<&[u8]>, Option<ParsingState>), ParsingErrorState> {
260    heif::extract_exif_data(state, buf)
261}
262
263fn cr3_extract_exif(
264    state: Option<ParsingState>,
265    buf: &[u8],
266) -> Result<(Option<&[u8]>, Option<ParsingState>), ParsingErrorState> {
267    cr3::extract_exif_data(state, buf)
268}
269
270#[cfg(feature = "async")]
271use tokio::io::AsyncRead;
272
273/// *Deprecated*: Please use [`crate::MediaParser`] instead.
274///
275/// `async` version of [`parse_exif`].
276#[allow(deprecated)]
277#[cfg(feature = "async")]
278#[deprecated(since = "2.0.0")]
279pub async fn parse_exif_async<T: AsyncRead + Unpin + Send>(
280    reader: T,
281    _: Option<FileFormat>,
282) -> crate::Result<Option<ExifIter>> {
283    use crate::{AsyncMediaParser, AsyncMediaSource};
284
285    let mut parser = AsyncMediaParser::new();
286    let exif: ExifIter = parser
287        .parse(AsyncMediaSource::unseekable(reader).await?)
288        .await?;
289    Ok(Some(exif))
290}
291
292#[cfg(test)]
293#[allow(deprecated)]
294mod tests {
295    use std::{sync::mpsc, thread, time::Duration};
296
297    use crate::{
298        file::MimeImage,
299        testkit::{open_sample, read_sample},
300        values::URational,
301    };
302    use test_case::test_case;
303
304    use super::*;
305
306    #[test_case("exif.heic", "+43.29013+084.22713+1595.950CRSWGS_84/")]
307    #[test_case("exif.jpg", "+22.53113+114.02148/")]
308    #[test_case("invalid-gps", "-")]
309    fn gps(path: &str, gps_str: &str) {
310        let f = open_sample(path).unwrap();
311        let iter = parse_exif(f, None)
312            .expect("should be Ok")
313            .expect("should not be None");
314
315        if gps_str == "-" {
316            assert!(iter.parse_gps_info().expect("should be ok").is_none());
317        } else {
318            let gps_info = iter
319                .parse_gps_info()
320                .expect("should be parsed Ok")
321                .expect("should not be None");
322
323            // let gps_info = iter
324            //     .consume_parse_gps_info()
325            //     .expect("should be parsed Ok")
326            //     .expect("should not be None");
327            assert_eq!(gps_info.format_iso6709(), gps_str);
328        }
329    }
330
331    #[cfg(feature = "async")]
332    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
333    #[test_case("exif.heic", "+43.29013+084.22713+1595.950CRSWGS_84/")]
334    #[test_case("exif.jpg", "+22.53113+114.02148/")]
335    async fn gps_async(path: &str, gps_str: &str) {
336        use std::path::Path;
337        use tokio::fs::File;
338
339        let f = File::open(Path::new("testdata").join(path)).await.unwrap();
340        let iter = parse_exif_async(f, None)
341            .await
342            .expect("should be Ok")
343            .expect("should not be None");
344
345        let gps_str = gps_str.to_owned();
346        let _ = tokio::spawn(async move {
347            let exif: Exif = iter.into();
348            let gps_info = exif.get_gps_info().expect("ok").expect("some");
349            assert_eq!(gps_info.format_iso6709(), gps_str);
350        })
351        .await;
352    }
353
354    #[test_case(
355        "exif.jpg",
356        'N',
357        [(22, 1), (31, 1), (5208, 100)].into(),
358        'E',
359        [(114, 1), (1, 1), (1733, 100)].into(),
360        0u8,
361        (0, 1).into(),
362        None,
363        None
364    )]
365    #[allow(clippy::too_many_arguments)]
366    fn gps_info(
367        path: &str,
368        latitude_ref: char,
369        latitude: LatLng,
370        longitude_ref: char,
371        longitude: LatLng,
372        altitude_ref: u8,
373        altitude: URational,
374        speed_ref: Option<char>,
375        speed: Option<URational>,
376    ) {
377        let _ = tracing_subscriber::fmt().with_test_writer().try_init();
378
379        let buf = read_sample(path).unwrap();
380        let (data, _) = extract_exif_with_mime(MimeImage::Jpeg, &buf, None).unwrap();
381        let data = data.unwrap();
382
383        let subslice_in_range = buf.subslice_in_range(data).unwrap();
384        let iter = input_into_iter((buf, subslice_in_range), None).unwrap();
385        let exif: Exif = iter.into();
386
387        let gps = exif.get_gps_info().unwrap().unwrap();
388        assert_eq!(
389            gps,
390            GPSInfo {
391                latitude_ref,
392                latitude,
393                longitude_ref,
394                longitude,
395                altitude_ref,
396                altitude,
397                speed_ref,
398                speed,
399            }
400        )
401    }
402
403    #[test_case("exif.heic")]
404    fn tag_values(path: &str) {
405        let f = open_sample(path).unwrap();
406        let iter = parse_exif(f, None).unwrap().unwrap();
407        let tags = [ExifTag::Make, ExifTag::Model];
408        let res: Vec<String> = iter
409            .clone()
410            .filter(|e| e.tag().is_some_and(|t| tags.contains(&t)))
411            .filter(|e| e.has_value())
412            .map(|e| format!("{} => {}", e.tag().unwrap(), e.get_value().unwrap()))
413            .collect();
414        assert_eq!(res.join(", "), "Make => Apple, Model => iPhone 12 Pro");
415    }
416
417    #[test]
418    fn endless_loop() {
419        let (sender, receiver) = mpsc::channel();
420
421        thread::spawn(move || {
422            let name = "endless_loop.jpg";
423            let f = open_sample(name).unwrap();
424            let iter = parse_exif(f, None).unwrap().unwrap();
425            let _: Exif = iter.into();
426            sender.send(()).unwrap();
427        });
428
429        receiver
430            .recv_timeout(Duration::from_secs(1))
431            .expect("There is an infinite loop in the parsing process!");
432    }
433}