Skip to main content

exiftool_rs/
value.rs

1use std::fmt;
2
3/// Represents a metadata tag value, which can be of various types.
4#[derive(Debug, Clone, PartialEq)]
5pub enum Value {
6    /// ASCII/UTF-8 string
7    String(String),
8    /// Unsigned 8-bit integer
9    U8(u8),
10    /// Unsigned 16-bit integer
11    U16(u16),
12    /// Unsigned 32-bit integer
13    U32(u32),
14    /// Signed 16-bit integer
15    I16(i16),
16    /// Signed 32-bit integer
17    I32(i32),
18    /// Unsigned rational (numerator/denominator)
19    URational(u32, u32),
20    /// Signed rational (numerator/denominator)
21    IRational(i32, i32),
22    /// 32-bit float
23    F32(f32),
24    /// 64-bit float
25    F64(f64),
26    /// Raw binary data
27    Binary(Vec<u8>),
28    /// A list of values (e.g., GPS coordinates, color space arrays)
29    List(Vec<Value>),
30    /// Undefined/opaque bytes with a semantic type hint
31    Undefined(Vec<u8>),
32}
33
34impl Value {
35    /// Convert to string representation (PrintConv equivalent).
36    pub fn to_display_string(&self) -> String {
37        match self {
38            Value::String(s) => s.clone(),
39            Value::U8(v) => v.to_string(),
40            Value::U16(v) => v.to_string(),
41            Value::U32(v) => v.to_string(),
42            Value::I16(v) => v.to_string(),
43            Value::I32(v) => v.to_string(),
44            Value::URational(n, d) => {
45                if *d == 0 {
46                    if *n == 0 {
47                        "undef".to_string()
48                    } else {
49                        "inf".to_string()
50                    }
51                } else if *n % *d == 0 {
52                    (*n / *d).to_string()
53                } else {
54                    // EXIF rational64 is read via RoundFloat(val, 10) = %.10g (ExifTool
55                    // GetRational64u). XMP rationals use %.15g and are formatted separately.
56                    format_g_prec(*n as f64 / *d as f64, 10)
57                }
58            }
59            Value::IRational(n, d) => {
60                if *d == 0 {
61                    if *n >= 0 {
62                        "inf".to_string()
63                    } else {
64                        "-inf".to_string()
65                    }
66                } else if *n % *d == 0 {
67                    (*n / *d).to_string()
68                } else {
69                    format_g_prec(*n as f64 / *d as f64, 10)
70                }
71            }
72            Value::F32(v) => format!("{}", v),
73            // ExifTool stringifies doubles with Perl's %.15g.
74            Value::F64(v) => format_g15(*v),
75            Value::Binary(data) => {
76                format!(
77                    "(Binary data {} bytes, use -b option to extract)",
78                    data.len()
79                )
80            }
81            Value::List(items) => {
82                // ExifTool joins numeric arrays with a space, but string lists
83                // (e.g. IPTC Keywords) with ", ".
84                let numeric = items.iter().all(|v| {
85                    matches!(
86                        v,
87                        Value::U8(_)
88                            | Value::U16(_)
89                            | Value::U32(_)
90                            | Value::I16(_)
91                            | Value::I32(_)
92                            | Value::URational(..)
93                            | Value::IRational(..)
94                            | Value::F32(_)
95                            | Value::F64(_)
96                    )
97                });
98                let sep = if numeric { " " } else { ", " };
99                items
100                    .iter()
101                    .map(|v| v.to_display_string())
102                    .collect::<Vec<_>>()
103                    .join(sep)
104            }
105            Value::Undefined(data) => {
106                format!(
107                    "(Binary data {} bytes, use -b option to extract)",
108                    data.len()
109                )
110            }
111        }
112    }
113
114    /// Try to interpret the value as a float.
115    pub fn as_f64(&self) -> Option<f64> {
116        match self {
117            Value::U8(v) => Some(*v as f64),
118            Value::U16(v) => Some(*v as f64),
119            Value::U32(v) => Some(*v as f64),
120            Value::I16(v) => Some(*v as f64),
121            Value::I32(v) => Some(*v as f64),
122            Value::F32(v) => Some(*v as f64),
123            Value::F64(v) => Some(*v),
124            Value::URational(n, d) if *d != 0 => Some(*n as f64 / *d as f64),
125            Value::IRational(n, d) if *d != 0 => Some(*n as f64 / *d as f64),
126            _ => None,
127        }
128    }
129
130    /// Try to interpret the value as a string.
131    pub fn as_str(&self) -> Option<&str> {
132        match self {
133            Value::String(s) => Some(s),
134            _ => None,
135        }
136    }
137
138    /// Try to interpret the value as an unsigned integer.
139    pub fn as_u64(&self) -> Option<u64> {
140        match self {
141            Value::U8(v) => Some(*v as u64),
142            Value::U16(v) => Some(*v as u64),
143            Value::U32(v) => Some(*v as u64),
144            _ => None,
145        }
146    }
147}
148
149impl fmt::Display for Value {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        write!(f, "{}", self.to_display_string())
152    }
153}
154
155/// Format a float with Perl-style %.15g precision (15 significant digits, trailing zeros stripped).
156/// This matches ExifTool's default `%s` formatting for floating-point values.
157pub fn format_g15(v: f64) -> String {
158    format_g_prec(v, 15)
159}
160
161/// Format a float with Perl-style %.Ng precision (N significant digits, trailing zeros stripped).
162/// Mirrors C sprintf's %g: uses exponential if exponent < -4 or >= precision.
163pub fn format_g_prec(v: f64, prec: usize) -> String {
164    if v == 0.0 {
165        return "0".to_string();
166    }
167    let abs_v = v.abs();
168    let exp = abs_v.log10().floor() as i32;
169    if exp >= -4 && exp < prec as i32 {
170        // Fixed-point: need (prec-1 - exp) decimal places
171        let decimal_places = ((prec as i32 - 1 - exp).max(0)) as usize;
172        let s = format!("{:.prec$}", v, prec = decimal_places);
173        if s.contains('.') {
174            s.trim_end_matches('0').trim_end_matches('.').to_string()
175        } else {
176            s
177        }
178    } else {
179        // Exponential format: prec-1 decimal places
180        let decimal_places = prec - 1;
181        let s = format!("{:.prec$e}", v, prec = decimal_places);
182        // Rust produces e.g. "3.51360899930879e20", need "3.51360899930879e+20"
183        // and "-1.5e-6" → "-1.5e-06" (at least 2 digits in exponent)
184        // First strip trailing zeros from mantissa
185        let (mantissa_part, exp_part) = if let Some(e_pos) = s.find('e') {
186            (&s[..e_pos], &s[e_pos + 1..])
187        } else {
188            return s;
189        };
190        let mantissa_trimmed = if mantissa_part.contains('.') {
191            mantissa_part.trim_end_matches('0').trim_end_matches('.')
192        } else {
193            mantissa_part
194        };
195        // Parse exponent and reformat with sign and minimum 2 digits
196        let exp_val: i32 = exp_part.parse().unwrap_or(0);
197        let exp_str = if exp_val >= 0 {
198            format!("e+{:02}", exp_val)
199        } else {
200            format!("e-{:02}", -exp_val)
201        };
202        format!("{}{}", mantissa_trimmed, exp_str)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    // ── to_display_string ──────────────────────────────────────────
211
212    #[test]
213    fn display_string() {
214        assert_eq!(Value::String("hello".into()).to_display_string(), "hello");
215    }
216
217    #[test]
218    fn display_u8() {
219        assert_eq!(Value::U8(42).to_display_string(), "42");
220    }
221
222    #[test]
223    fn display_u16() {
224        assert_eq!(Value::U16(1024).to_display_string(), "1024");
225    }
226
227    #[test]
228    fn display_u32() {
229        assert_eq!(Value::U32(100_000).to_display_string(), "100000");
230    }
231
232    #[test]
233    fn display_i16() {
234        assert_eq!(Value::I16(-123).to_display_string(), "-123");
235    }
236
237    #[test]
238    fn display_i32() {
239        assert_eq!(Value::I32(-999_999).to_display_string(), "-999999");
240    }
241
242    #[test]
243    fn display_urational_exact_division() {
244        assert_eq!(Value::URational(100, 10).to_display_string(), "10");
245    }
246
247    #[test]
248    fn display_urational_non_exact() {
249        // ExifTool shows the decimal value (Perl %.15g), not the raw fraction.
250        assert_eq!(Value::URational(1, 3).to_display_string(), "0.3333333333");
251    }
252
253    #[test]
254    fn display_urational_zero_zero() {
255        assert_eq!(Value::URational(0, 0).to_display_string(), "undef");
256    }
257
258    #[test]
259    fn display_urational_n_over_zero() {
260        assert_eq!(Value::URational(5, 0).to_display_string(), "inf");
261    }
262
263    #[test]
264    fn display_irational_exact() {
265        assert_eq!(Value::IRational(-10, 5).to_display_string(), "-2");
266    }
267
268    #[test]
269    fn display_irational_non_exact() {
270        assert_eq!(Value::IRational(7, 3).to_display_string(), "2.333333333");
271    }
272
273    #[test]
274    fn display_irational_positive_inf() {
275        assert_eq!(Value::IRational(1, 0).to_display_string(), "inf");
276    }
277
278    #[test]
279    fn display_irational_zero_inf() {
280        // n=0 d=0 → n >= 0, so "inf"
281        assert_eq!(Value::IRational(0, 0).to_display_string(), "inf");
282    }
283
284    #[test]
285    fn display_irational_negative_inf() {
286        assert_eq!(Value::IRational(-3, 0).to_display_string(), "-inf");
287    }
288
289    #[test]
290    fn display_f32() {
291        let s = Value::F32(2.75).to_display_string();
292        assert!(s.starts_with("2.75"), "got: {}", s);
293    }
294
295    #[test]
296    fn display_f64() {
297        assert_eq!(Value::F64(2.5).to_display_string(), "2.5");
298    }
299
300    #[test]
301    fn display_binary() {
302        assert_eq!(
303            Value::Binary(vec![0, 1, 2]).to_display_string(),
304            "(Binary data 3 bytes, use -b option to extract)"
305        );
306    }
307
308    #[test]
309    fn display_list() {
310        // Numeric lists join with a space (ExifTool), string lists with ", ".
311        let list = Value::List(vec![Value::U16(640), Value::U16(480)]);
312        assert_eq!(list.to_display_string(), "640 480");
313    }
314
315    #[test]
316    fn display_undefined() {
317        assert_eq!(
318            Value::Undefined(vec![0xAB; 5]).to_display_string(),
319            "(Binary data 5 bytes, use -b option to extract)"
320        );
321    }
322
323    // ── as_f64 ─────────────────────────────────────────────────────
324
325    #[test]
326    fn as_f64_u8() {
327        assert_eq!(Value::U8(10).as_f64(), Some(10.0));
328    }
329
330    #[test]
331    fn as_f64_u16() {
332        assert_eq!(Value::U16(300).as_f64(), Some(300.0));
333    }
334
335    #[test]
336    fn as_f64_u32() {
337        assert_eq!(Value::U32(70_000).as_f64(), Some(70_000.0));
338    }
339
340    #[test]
341    fn as_f64_i16() {
342        assert_eq!(Value::I16(-50).as_f64(), Some(-50.0));
343    }
344
345    #[test]
346    fn as_f64_i32() {
347        assert_eq!(Value::I32(-1_000_000).as_f64(), Some(-1_000_000.0));
348    }
349
350    #[test]
351    fn as_f64_f32() {
352        let val = Value::F32(1.5).as_f64().unwrap();
353        assert!((val - 1.5).abs() < 1e-6);
354    }
355
356    #[test]
357    fn as_f64_f64() {
358        assert_eq!(Value::F64(9.99).as_f64(), Some(9.99));
359    }
360
361    #[test]
362    fn as_f64_urational() {
363        let val = Value::URational(1, 4).as_f64().unwrap();
364        assert!((val - 0.25).abs() < 1e-10);
365    }
366
367    #[test]
368    fn as_f64_urational_zero_denom() {
369        assert_eq!(Value::URational(5, 0).as_f64(), None);
370    }
371
372    #[test]
373    fn as_f64_irational() {
374        let val = Value::IRational(-3, 2).as_f64().unwrap();
375        assert!((val - -1.5).abs() < 1e-10);
376    }
377
378    #[test]
379    fn as_f64_irational_zero_denom() {
380        assert_eq!(Value::IRational(-1, 0).as_f64(), None);
381    }
382
383    #[test]
384    fn as_f64_string_none() {
385        assert_eq!(Value::String("hi".into()).as_f64(), None);
386    }
387
388    #[test]
389    fn as_f64_binary_none() {
390        assert_eq!(Value::Binary(vec![1]).as_f64(), None);
391    }
392
393    #[test]
394    fn as_f64_undefined_none() {
395        assert_eq!(Value::Undefined(vec![1]).as_f64(), None);
396    }
397
398    // ── as_str ─────────────────────────────────────────────────────
399
400    #[test]
401    fn as_str_string() {
402        assert_eq!(Value::String("test".into()).as_str(), Some("test"));
403    }
404
405    #[test]
406    fn as_str_non_string() {
407        assert_eq!(Value::U8(1).as_str(), None);
408        assert_eq!(Value::Binary(vec![]).as_str(), None);
409        assert_eq!(Value::F64(1.0).as_str(), None);
410    }
411
412    // ── as_u64 ─────────────────────────────────────────────────────
413
414    #[test]
415    fn as_u64_unsigned_types() {
416        assert_eq!(Value::U8(255).as_u64(), Some(255));
417        assert_eq!(Value::U16(65535).as_u64(), Some(65535));
418        assert_eq!(Value::U32(0xFFFFFFFF).as_u64(), Some(0xFFFFFFFF));
419    }
420
421    #[test]
422    fn as_u64_signed_none() {
423        assert_eq!(Value::I16(1).as_u64(), None);
424        assert_eq!(Value::I32(1).as_u64(), None);
425    }
426
427    #[test]
428    fn as_u64_other_none() {
429        assert_eq!(Value::String("42".into()).as_u64(), None);
430        assert_eq!(Value::F64(1.0).as_u64(), None);
431        assert_eq!(Value::Binary(vec![]).as_u64(), None);
432        assert_eq!(Value::Undefined(vec![]).as_u64(), None);
433    }
434
435    // ── Display trait ──────────────────────────────────────────────
436
437    #[test]
438    fn display_trait_delegates() {
439        let v = Value::URational(1, 3);
440        assert_eq!(format!("{}", v), "0.3333333333");
441    }
442
443    // ── format_g15 / format_g_prec ─────────────────────────────────
444
445    #[test]
446    fn format_g15_zero() {
447        assert_eq!(format_g15(0.0), "0");
448    }
449
450    #[test]
451    fn format_g15_integer() {
452        assert_eq!(format_g15(42.0), "42");
453    }
454
455    #[test]
456    fn format_g15_simple_decimal() {
457        assert_eq!(format_g15(3.5), "3.5");
458    }
459
460    #[test]
461    fn format_g15_negative() {
462        assert_eq!(format_g15(-1.25), "-1.25");
463    }
464
465    #[test]
466    fn format_g15_large_value_scientific() {
467        // 1e+20 should use exponential
468        let s = format_g15(1e20);
469        assert!(s.contains("e+"), "expected scientific notation, got: {}", s);
470    }
471
472    #[test]
473    fn format_g15_small_value_scientific() {
474        // 1e-5 is < 1e-4, should use exponential
475        let s = format_g15(1e-5);
476        assert!(s.contains("e-"), "expected scientific notation, got: {}", s);
477    }
478
479    #[test]
480    fn format_g15_borderline_fixed() {
481        // 0.0001 = 1e-4 → exp = -4, which is >= -4, so fixed format
482        let s = format_g15(0.0001);
483        assert_eq!(s, "0.0001");
484    }
485
486    #[test]
487    fn format_g_prec_low_precision() {
488        // 3 significant digits for pi
489        let s = format_g_prec(std::f64::consts::PI, 3);
490        assert_eq!(s, "3.14");
491    }
492
493    #[test]
494    fn format_g_prec_one_digit() {
495        let s = format_g_prec(7.7, 1);
496        assert_eq!(s, "8");
497    }
498
499    #[test]
500    fn format_g15_trailing_zeros_stripped() {
501        // 1.5 should not produce "1.500000..."
502        let s = format_g15(1.5);
503        assert!(!s.ends_with('0'), "trailing zeros not stripped: {}", s);
504    }
505
506    #[test]
507    fn format_g15_very_large() {
508        let s = format_g15(1.23456789e+100);
509        assert!(s.starts_with("1.23456789"), "got: {}", s);
510        assert!(s.contains("e+100"), "got: {}", s);
511    }
512
513    #[test]
514    fn format_g15_very_small_negative() {
515        let s = format_g15(-5.5e-10);
516        assert!(s.starts_with("-5.5e-"), "got: {}", s);
517    }
518}