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 { "undef".to_string() } else { "inf".to_string() }
47                } else if *n % *d == 0 {
48                    (*n / *d).to_string()
49                } else {
50                    format!("{}/{}", n, d)
51                }
52            }
53            Value::IRational(n, d) => {
54                if *d == 0 {
55                    if *n >= 0 { "inf".to_string() } else { "-inf".to_string() }
56                } else if *n % *d == 0 {
57                    (*n / *d).to_string()
58                } else {
59                    format!("{}/{}", n, d)
60                }
61            }
62            Value::F32(v) => format!("{}", v),
63            Value::F64(v) => format!("{}", v),
64            Value::Binary(data) => format!("(Binary data {} bytes)", data.len()),
65            Value::List(items) => {
66                items
67                    .iter()
68                    .map(|v| v.to_display_string())
69                    .collect::<Vec<_>>()
70                    .join(", ")
71            }
72            Value::Undefined(data) => format!("(Undefined {} bytes)", data.len()),
73        }
74    }
75
76    /// Try to interpret the value as a float.
77    pub fn as_f64(&self) -> Option<f64> {
78        match self {
79            Value::U8(v) => Some(*v as f64),
80            Value::U16(v) => Some(*v as f64),
81            Value::U32(v) => Some(*v as f64),
82            Value::I16(v) => Some(*v as f64),
83            Value::I32(v) => Some(*v as f64),
84            Value::F32(v) => Some(*v as f64),
85            Value::F64(v) => Some(*v),
86            Value::URational(n, d) if *d != 0 => Some(*n as f64 / *d as f64),
87            Value::IRational(n, d) if *d != 0 => Some(*n as f64 / *d as f64),
88            _ => None,
89        }
90    }
91
92    /// Try to interpret the value as a string.
93    pub fn as_str(&self) -> Option<&str> {
94        match self {
95            Value::String(s) => Some(s),
96            _ => None,
97        }
98    }
99
100    /// Try to interpret the value as an unsigned integer.
101    pub fn as_u64(&self) -> Option<u64> {
102        match self {
103            Value::U8(v) => Some(*v as u64),
104            Value::U16(v) => Some(*v as u64),
105            Value::U32(v) => Some(*v as u64),
106            _ => None,
107        }
108    }
109}
110
111impl fmt::Display for Value {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "{}", self.to_display_string())
114    }
115}
116
117/// Format a float with Perl-style %.15g precision (15 significant digits, trailing zeros stripped).
118/// This matches ExifTool's default `%s` formatting for floating-point values.
119pub fn format_g15(v: f64) -> String {
120    format_g_prec(v, 15)
121}
122
123/// Format a float with Perl-style %.Ng precision (N significant digits, trailing zeros stripped).
124/// Mirrors C sprintf's %g: uses exponential if exponent < -4 or >= precision.
125pub fn format_g_prec(v: f64, prec: usize) -> String {
126    if v == 0.0 {
127        return "0".to_string();
128    }
129    let abs_v = v.abs();
130    let exp = abs_v.log10().floor() as i32;
131    if exp >= -4 && exp < prec as i32 {
132        // Fixed-point: need (prec-1 - exp) decimal places
133        let decimal_places = ((prec as i32 - 1 - exp).max(0)) as usize;
134        let s = format!("{:.prec$}", v, prec = decimal_places);
135        if s.contains('.') {
136            s.trim_end_matches('0').trim_end_matches('.').to_string()
137        } else {
138            s
139        }
140    } else {
141        // Exponential format: prec-1 decimal places
142        let decimal_places = prec - 1;
143        let s = format!("{:.prec$e}", v, prec = decimal_places);
144        // Rust produces e.g. "3.51360899930879e20", need "3.51360899930879e+20"
145        // and "-1.5e-6" → "-1.5e-06" (at least 2 digits in exponent)
146        // First strip trailing zeros from mantissa
147        let (mantissa_part, exp_part) = if let Some(e_pos) = s.find('e') {
148            (&s[..e_pos], &s[e_pos+1..])
149        } else {
150            return s;
151        };
152        let mantissa_trimmed = if mantissa_part.contains('.') {
153            mantissa_part.trim_end_matches('0').trim_end_matches('.')
154        } else {
155            mantissa_part
156        };
157        // Parse exponent and reformat with sign and minimum 2 digits
158        let exp_val: i32 = exp_part.parse().unwrap_or(0);
159        let exp_str = if exp_val >= 0 {
160            format!("e+{:02}", exp_val)
161        } else {
162            format!("e-{:02}", -exp_val)
163        };
164        format!("{}{}", mantissa_trimmed, exp_str)
165    }
166}