Skip to main content

flashraw_core/
metadata.rs

1//! Normalised camera/capture metadata and embedded preview descriptors.
2
3use crate::orientation::OrientationMetadata;
4use crate::types::{Compression, Orientation, RawFormat};
5
6/// Lens information.
7#[derive(Debug, Clone, Default, PartialEq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[non_exhaustive]
10pub struct LensInfo {
11    /// Lens manufacturer, when recorded.
12    pub make: Option<String>,
13    /// Lens model name, when recorded.
14    pub model: Option<String>,
15    /// Lens serial number, when recorded.
16    pub serial: Option<String>,
17    /// Min/max focal length in mm.
18    pub focal_range: Option<(f32, f32)>,
19    /// Max aperture (f-number) at min/max focal length.
20    pub aperture_range: Option<(f32, f32)>,
21}
22
23/// Exposure / capture parameters.
24#[derive(Debug, Clone, Default, PartialEq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[non_exhaustive]
27pub struct ExposureInfo {
28    /// ISO speed.
29    pub iso: Option<u32>,
30    /// Exposure time in seconds.
31    pub exposure_time: Option<f64>,
32    /// Aperture as an f-number (e.g. `2.8`).
33    pub f_number: Option<f32>,
34    /// Focal length in mm.
35    pub focal_length: Option<f32>,
36    /// 35 mm-equivalent focal length in mm (EXIF FocalLengthIn35mmFilm).
37    pub focal_length_35mm: Option<u32>,
38    /// Exposure compensation in EV.
39    pub exposure_bias: Option<f32>,
40    /// EXIF `Flash` tag value (bit field; bit 0 set when the flash fired).
41    pub flash: Option<u16>,
42    /// EXIF `MeteringMode` tag value.
43    pub metering_mode: Option<u16>,
44    /// EXIF `ExposureProgram` tag value.
45    pub exposure_program: Option<u16>,
46}
47
48/// GPS position.
49#[derive(Debug, Clone, Default, PartialEq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51#[non_exhaustive]
52pub struct GpsInfo {
53    /// Decimal degrees, negative for south.
54    pub latitude: Option<f64>,
55    /// Decimal degrees, negative for west.
56    pub longitude: Option<f64>,
57    /// Metres; negative below sea level.
58    pub altitude: Option<f64>,
59    /// GPS date and time in UTC, `YYYY-MM-DDTHH:MM:SSZ`.
60    pub timestamp: Option<String>,
61}
62
63/// Encoding of an embedded preview image.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66#[non_exhaustive]
67pub enum PreviewEncoding {
68    /// Baseline JPEG.
69    Jpeg,
70    /// Uncompressed 8-bit RGB, `width * height * 3` bytes.
71    Rgb8,
72    /// Uncompressed 8-bit YCbCr 4:2:2 (rare; some old Kodak/Sony files).
73    Ycbcr422,
74    /// JPEG XL (DNG 1.7 previews).
75    JpegXl,
76    /// HEIF/HEVC (e.g. CR3 HEIF previews, iPhone); exposed as bytes only.
77    Heif,
78}
79
80/// An embedded preview / thumbnail stored in the RAW container.
81#[derive(Debug, Clone, PartialEq)]
82#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
83#[non_exhaustive]
84pub struct PreviewInfo {
85    /// Byte offset of the preview data in the file.
86    pub offset: u64,
87    /// Length of the preview data in bytes.
88    pub length: u64,
89    /// Width in pixels as stored (before orientation); 0 if unknown.
90    pub width: u32,
91    /// Height in pixels as stored (before orientation); 0 if unknown.
92    pub height: u32,
93    /// How the preview bytes are encoded.
94    pub encoding: PreviewEncoding,
95    /// Orientation to apply to the preview's stored pixels: usually the file orientation.
96    /// Where vendor orientation records are resolved (Canon CR2, Nikon, Kodak) it is
97    /// resolved per preview: a preview carrying its own EXIF orientation uses it, and a
98    /// preview that was rendered already rotated is `Normal` even when the RAW image is not.
99    pub orientation: Orientation,
100    /// What the container labels this image as.
101    pub kind: PreviewKind,
102}
103
104impl PreviewInfo {
105    /// A preview of `length` bytes at `offset` in the file.
106    pub fn new(
107        offset: u64,
108        length: u64,
109        width: u32,
110        height: u32,
111        encoding: PreviewEncoding,
112        orientation: Orientation,
113        kind: PreviewKind,
114    ) -> Self {
115        PreviewInfo { offset, length, width, height, encoding, orientation, kind }
116    }
117}
118
119/// What the container labels a preview as.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122#[non_exhaustive]
123pub enum PreviewKind {
124    /// Small thumbnail.
125    Thumbnail,
126    /// Reduced-size preview.
127    Preview,
128    /// Full-resolution rendering of the image.
129    FullSize,
130}
131
132/// DNG camera-profile tags an editor needs to know about (presence and sizes; the tables
133/// themselves are not decoded).
134#[derive(Debug, Clone, Default, PartialEq)]
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
136#[non_exhaustive]
137pub struct DngProfileInfo {
138    /// ProfileName.
139    pub name: Option<String>,
140    /// ProfileEmbedPolicy (0 allow copying ... 3 no restrictions).
141    pub embed_policy: Option<u32>,
142    /// ProfileHueSatMapDims `[hue, sat, val]` when HueSatMap data exists.
143    pub hue_sat_map_dims: Option<[u32; 3]>,
144    /// Number of ProfileHueSatMapData tables present (0..=3).
145    pub hue_sat_map_count: u8,
146    /// ProfileLookTableDims `[hue, sat, val]` when ProfileLookTableData exists.
147    pub look_table_dims: Option<[u32; 3]>,
148    /// Whether ProfileToneCurve is present.
149    pub tone_curve: bool,
150    /// BaselineExposureOffset (stops).
151    pub baseline_exposure_offset: Option<f64>,
152}
153
154/// DNG rendering hints from IFD0 / the raw IFD that FlashRaw does not apply itself.
155#[derive(Debug, Clone, Default, PartialEq)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157#[non_exhaustive]
158pub struct DngExtras {
159    /// BaselineExposure in stops (signed).
160    pub baseline_exposure: Option<f64>,
161    /// BaselineNoise.
162    pub baseline_noise: Option<f64>,
163    /// BaselineSharpness.
164    pub baseline_sharpness: Option<f64>,
165    /// LinearResponseLimit (fraction of white).
166    pub linear_response_limit: Option<f64>,
167    /// AsShotNeutral as stored (camera RGB of the neutral).
168    pub as_shot_neutral: Option<Vec<f64>>,
169    /// AsShotWhiteXY chromaticity.
170    pub as_shot_white_xy: Option<(f64, f64)>,
171    /// DefaultUserCrop as fractions of the default-cropped image: top, left, bottom, right.
172    pub default_user_crop: Option<[f64; 4]>,
173    /// DefaultScale (horizontal, vertical).
174    pub default_scale: Option<(f64, f64)>,
175    /// NoiseProfile (pairs of scale/offset per plane).
176    pub noise_profile: Option<Vec<f64>>,
177    /// Camera profile tags.
178    pub profile: DngProfileInfo,
179}
180
181/// Normalised metadata for a RAW file, available without decoding sensor data.
182#[derive(Debug, Clone, Default, PartialEq)]
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184#[non_exhaustive]
185pub struct RawMetadata {
186    /// Make as written in the file.
187    pub make: String,
188    /// Model as written in the file.
189    pub model: String,
190    /// Normalised maker name ("Canon", "Nikon", "Sony", ...).
191    pub canonical_make: String,
192    /// Normalised model name (aliases resolved).
193    pub canonical_model: String,
194    /// DNG UniqueCameraModel or equivalent.
195    pub unique_camera_model: Option<String>,
196    /// Writing software (EXIF `Software` tag).
197    pub software: Option<String>,
198    /// Camera firmware version, from maker notes where available.
199    pub firmware: Option<String>,
200    /// Camera body serial number.
201    pub serial: Option<String>,
202    /// Artist / author.
203    pub artist: Option<String>,
204    /// Copyright notice.
205    pub copyright: Option<String>,
206    /// Lens information (fields are `None` when not recorded).
207    pub lens: LensInfo,
208    /// Exposure parameters (fields are `None` when not recorded).
209    pub exposure: ExposureInfo,
210    /// Capture timestamp, `YYYY-MM-DDTHH:MM:SS[.sss][±HH:MM]` when available.
211    pub capture_time: Option<String>,
212    /// GPS position; `None` unless a latitude or longitude is recorded.
213    pub gps: Option<GpsInfo>,
214    /// Image orientation (the resolved value of [`RawMetadata::orientation_info`]).
215    pub orientation: Orientation,
216    /// The orientation records found in the file (standard tag and vendor record) and
217    /// how the resolved [`RawMetadata::orientation`] was chosen.
218    pub orientation_info: OrientationMetadata,
219    /// Container format, when identified.
220    pub format: Option<RawFormat>,
221    /// Payload compression of the main RAW image, when identified.
222    pub compression: Option<Compression>,
223    /// Bits per sample of the encoded data.
224    pub bits_per_sample: u8,
225    /// Full sensor buffer width (including masked borders).
226    pub raw_width: u32,
227    /// Full sensor buffer height (including masked borders).
228    pub raw_height: u32,
229    /// Width after the default crop.
230    pub width: u32,
231    /// Height after the default crop.
232    pub height: u32,
233    /// DNG version bytes (e.g. `[1, 4, 0, 0]`); `None` for non-DNG files.
234    pub dng_version: Option<[u8; 4]>,
235    /// Free-form, vendor-specific values worth exposing (mode names, quality, crop mode...).
236    pub extra: Vec<(String, String)>,
237    /// Embedded previews and thumbnails.
238    pub previews: Vec<PreviewInfo>,
239    /// DNG rendering hints (`None` for non-DNG files).
240    pub dng: Option<DngExtras>,
241}
242
243/// [`RawMetadata::extra`] key: the camera's own image size as the file records it
244/// (`"<width>x<height>"`, from the EXIF pixel dimensions or a maker-note field).
245pub const EXTRA_IMAGE_SIZE: &str = "image_size";
246/// [`RawMetadata::extra`] key: where the default crop came from: `file` (the format's own
247/// crop field), `image-size` ([`EXTRA_IMAGE_SIZE`]), `preview` (the embedded full-size
248/// JPEG) or `camera-db`. Absent when the crop is the active area.
249pub const EXTRA_CROP_SOURCE: &str = "crop_source";
250
251impl RawMetadata {
252    /// The camera's own image size recorded by the file ([`EXTRA_IMAGE_SIZE`]).
253    pub fn image_size(&self) -> Option<(u32, u32)> {
254        let (w, h) = self.extra(EXTRA_IMAGE_SIZE)?.split_once('x')?;
255        Some((w.parse().ok()?, h.parse().ok()?)).filter(|&(w, h)| w > 0 && h > 0)
256    }
257    /// Record the camera's own image size ([`EXTRA_IMAGE_SIZE`]); zero sizes are ignored.
258    pub fn set_image_size(&mut self, width: u32, height: u32) {
259        if width > 0 && height > 0 {
260            self.set_extra(EXTRA_IMAGE_SIZE, format!("{width}x{height}"));
261        }
262    }
263
264    /// Metadata with the given make and model as written in the file; everything else
265    /// empty.
266    pub fn new(make: impl Into<String>, model: impl Into<String>) -> Self {
267        RawMetadata { make: make.into(), model: model.into(), ..Default::default() }
268    }
269
270    /// Set an entry of [`RawMetadata::extra`], replacing any existing value for `key`.
271    pub fn set_extra(&mut self, key: &str, value: impl ToString) {
272        let value = value.to_string();
273        if let Some(e) = self.extra.iter_mut().find(|(k, _)| k == key) {
274            e.1 = value;
275        } else {
276            self.extra.push((key.to_string(), value));
277        }
278    }
279    /// Look up an entry of [`RawMetadata::extra`] by key.
280    pub fn extra(&self, key: &str) -> Option<&str> {
281        self.extra.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
282    }
283    /// Human-friendly "Make Model" string using canonical names when known.
284    pub fn camera(&self) -> String {
285        let make = if self.canonical_make.is_empty() { &self.make } else { &self.canonical_make };
286        let model = if self.canonical_model.is_empty() { &self.model } else { &self.canonical_model };
287        format!("{make} {model}").trim().to_string()
288    }
289    /// Largest preview.
290    pub fn largest_preview(&self) -> Option<&PreviewInfo> {
291        self.previews.iter().max_by_key(|p| (p.width as u64 * p.height as u64, p.length))
292    }
293    /// Smallest preview (thumbnail).
294    pub fn smallest_preview(&self) -> Option<&PreviewInfo> {
295        self.previews.iter().min_by_key(|p| (p.width as u64 * p.height as u64, p.length))
296    }
297}