1use crate::tiff::{Ifd, Tiff, TiffOptions, tags};
9use crate::types::{ColorMatrix, Illuminant};
10use crate::{Error, Result};
11
12mod extra {
14 pub const PROFILE_COPYRIGHT: u16 = 0xC6FE;
15 pub const PROFILE_HUE_SAT_MAP_ENCODING: u16 = 0xC7A3;
16 pub const PROFILE_LOOK_TABLE_ENCODING: u16 = 0xC7A4;
17 pub const DEFAULT_BLACK_RENDER: u16 = 0xC7A6;
18}
19
20const MAX_TABLE_ENTRIES: usize = 1 << 22;
22const MAX_CURVE_POINTS: usize = 1 << 14;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29#[non_exhaustive]
30pub enum TableEncoding {
31 #[default]
33 Linear,
34 Srgb,
37}
38
39#[derive(Debug, Clone, PartialEq)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[non_exhaustive]
46pub struct ProfileTable {
47 pub dims: [u32; 3],
49 pub data: Vec<f32>,
52 pub data2: Option<Vec<f32>>,
54 pub encoding: TableEncoding,
56}
57
58impl ProfileTable {
59 pub fn new(dims: [u32; 3], data: Vec<f32>, encoding: TableEncoding) -> Option<Self> {
62 let n = dims.iter().try_fold(1usize, |a, &d| a.checked_mul(d as usize))?;
63 let ok = dims.iter().all(|&d| d >= 1) && n <= MAX_TABLE_ENTRIES / 3 && data.len() == n * 3;
64 (ok && data.iter().all(|v| v.is_finite())).then_some(ProfileTable { dims, data, data2: None, encoding })
65 }
66
67 pub fn len(&self) -> usize {
69 self.data.len() / 3
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.data.is_empty()
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Default)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85#[non_exhaustive]
86pub struct CameraProfile {
87 pub name: Option<String>,
89 pub unique_camera_model: Option<String>,
91 pub copyright: Option<String>,
93 pub embed_policy: Option<u32>,
95 pub color_matrices: Vec<ColorMatrix>,
97 pub forward_matrices: Vec<ColorMatrix>,
99 pub hue_sat_map: Option<ProfileTable>,
101 pub look_table: Option<ProfileTable>,
103 pub tone_curve: Option<Vec<[f32; 2]>>,
105 pub baseline_exposure_offset: f64,
107 pub default_black_render: u32,
109}
110
111impl CameraProfile {
112 pub fn from_dcp(bytes: &[u8]) -> Result<Self> {
118 if bytes.len() < 8 || &bytes[0..4] != b"IIRC" {
119 return Err(Error::UnsupportedFormat("not a DNG camera profile (missing IIRC magic)".into()));
120 }
121 let tiff = Tiff::parse_with(bytes, TiffOptions { follow_sub_ifds: false, ..TiffOptions::default() })?;
122 let ifd = tiff.ifds.first().ok_or_else(|| Error::corrupt("camera profile without an IFD"))?;
123 let p = Self::from_ifd(ifd, true)?;
124 if p.is_empty() {
125 return Err(Error::invalid_metadata("camera profile without matrices, tables or a tone curve"));
126 }
127 Ok(p)
128 }
129
130 pub fn from_dng_ifd(ifd: &Ifd) -> Option<Self> {
134 let p = Self::from_ifd(ifd, false).ok()?;
135 let has_profile_data = !p.forward_matrices.is_empty()
136 || p.hue_sat_map.is_some()
137 || p.look_table.is_some()
138 || p.tone_curve.is_some();
139 has_profile_data.then_some(p)
140 }
141
142 fn from_ifd(ifd: &Ifd, strict: bool) -> Result<Self> {
143 let mut p = CameraProfile {
144 name: ifd.string(tags::PROFILE_NAME),
145 unique_camera_model: ifd.string(tags::UNIQUE_CAMERA_MODEL),
146 copyright: ifd.string(extra::PROFILE_COPYRIGHT),
147 embed_policy: ifd.u32(tags::PROFILE_EMBED_POLICY),
148 baseline_exposure_offset: ifd.f64(tags::BASELINE_EXPOSURE_OFFSET).filter(|v| v.is_finite()).unwrap_or(0.0),
149 default_black_render: ifd.u32(extra::DEFAULT_BLACK_RENDER).unwrap_or(0),
150 ..Default::default()
151 };
152 let illuminant = |tag: u16| Illuminant(ifd.u32(tag).unwrap_or(0) as u16);
153 for (m, i) in
154 [(tags::COLOR_MATRIX1, tags::CALIBRATION_ILLUMINANT1), (tags::COLOR_MATRIX2, tags::CALIBRATION_ILLUMINANT2)]
155 {
156 if let Some(e) = ifd.get(m) {
157 let v = e.f32s();
158 if v.len() == 9 && v.iter().all(|x| x.is_finite()) {
159 p.color_matrices.extend(ColorMatrix::new(illuminant(i), 3, v));
160 }
161 }
162 }
163 for (m, i) in [
164 (tags::FORWARD_MATRIX1, tags::CALIBRATION_ILLUMINANT1),
165 (tags::FORWARD_MATRIX2, tags::CALIBRATION_ILLUMINANT2),
166 ] {
167 if let Some(e) = ifd.get(m) {
168 let v = e.f32s();
169 if v.len() == 9 && v.iter().all(|x| x.is_finite()) {
170 p.forward_matrices.extend(ColorMatrix::new(illuminant(i), 3, v));
171 }
172 }
173 }
174 let encoding = |tag: u16| match ifd.u32(tag) {
175 Some(1) => TableEncoding::Srgb,
176 _ => TableEncoding::Linear,
177 };
178 let dims = |tag: u16| -> Option<[u32; 3]> {
179 let v = ifd.get(tag)?.u32s();
180 (v.len() >= 3).then(|| [v[0], v[1], v[2]])
181 };
182 let mut bad = None;
183 if let (Some(d), Some(e1)) = (dims(tags::PROFILE_HUE_SAT_MAP_DIMS), ifd.get(tags::PROFILE_HUE_SAT_MAP_DATA1)) {
184 let enc = encoding(extra::PROFILE_HUE_SAT_MAP_ENCODING);
185 match ProfileTable::new(d, e1.f32s(), enc) {
186 Some(mut t) => {
187 if let Some(e2) = ifd.get(tags::PROFILE_HUE_SAT_MAP_DATA2) {
188 let v = e2.f32s();
189 if v.len() == t.data.len() && v.iter().all(|x| x.is_finite()) {
190 t.data2 = Some(v);
191 } else {
192 bad = Some("ProfileHueSatMapData2 does not match ProfileHueSatMapDims");
193 }
194 }
195 p.hue_sat_map = Some(t);
196 }
197 None => bad = Some("ProfileHueSatMapData1 does not match ProfileHueSatMapDims"),
198 }
199 }
200 if let (Some(d), Some(e)) = (dims(tags::PROFILE_LOOK_TABLE_DIMS), ifd.get(tags::PROFILE_LOOK_TABLE_DATA)) {
201 match ProfileTable::new(d, e.f32s(), encoding(extra::PROFILE_LOOK_TABLE_ENCODING)) {
202 Some(t) => p.look_table = Some(t),
203 None => bad = Some("ProfileLookTableData does not match ProfileLookTableDims"),
204 }
205 }
206 if let Some(e) = ifd.get(tags::PROFILE_TONE_CURVE) {
207 let v = e.f32s();
208 let pts: Vec<[f32; 2]> = v.chunks_exact(2).map(|c| [c[0], c[1]]).collect();
209 let sane = pts.len() >= 2
210 && pts.len() <= MAX_CURVE_POINTS
211 && v.len() % 2 == 0
212 && pts.iter().all(|c| c[0].is_finite() && c[1].is_finite())
213 && pts.windows(2).all(|w| w[1][0] > w[0][0]);
214 if sane {
215 p.tone_curve = Some(pts);
216 } else {
217 bad = Some("ProfileToneCurve is not an ascending list of (input, output) pairs");
218 }
219 }
220 match (bad, strict) {
221 (Some(why), true) => Err(Error::invalid_metadata(why)),
222 _ => Ok(p),
223 }
224 }
225
226 pub fn is_empty(&self) -> bool {
228 self.color_matrices.is_empty()
229 && self.forward_matrices.is_empty()
230 && self.hue_sat_map.is_none()
231 && self.look_table.is_none()
232 && self.tone_curve.is_none()
233 && self.baseline_exposure_offset == 0.0
234 }
235
236 pub fn to_dcp(&self) -> Vec<u8> {
239 type Entry = (u16, u16, u32, Vec<u8>);
240 fn floats(e: &mut Vec<Entry>, tag: u16, v: &[f32]) {
241 e.push((tag, 11, v.len() as u32, v.iter().flat_map(|x| x.to_le_bytes()).collect()));
242 }
243 fn longs(e: &mut Vec<Entry>, tag: u16, v: &[u32]) {
244 e.push((tag, 4, v.len() as u32, v.iter().flat_map(|x| x.to_le_bytes()).collect()));
245 }
246 fn ascii(e: &mut Vec<Entry>, tag: u16, s: &Option<String>) {
247 if let Some(s) = s {
248 let mut b = s.as_bytes().to_vec();
249 b.push(0);
250 e.push((tag, 2, b.len() as u32, b));
251 }
252 }
253 let mut entries: Vec<Entry> = Vec::new();
254 ascii(&mut entries, tags::UNIQUE_CAMERA_MODEL, &self.unique_camera_model);
255 ascii(&mut entries, tags::PROFILE_NAME, &self.name);
256 ascii(&mut entries, extra::PROFILE_COPYRIGHT, &self.copyright);
257 let color = [tags::COLOR_MATRIX1, tags::COLOR_MATRIX2];
258 let forward = [tags::FORWARD_MATRIX1, tags::FORWARD_MATRIX2];
259 let illuminants = [tags::CALIBRATION_ILLUMINANT1, tags::CALIBRATION_ILLUMINANT2];
260 for (i, m) in self.color_matrices.iter().take(2).enumerate() {
261 floats(&mut entries, color[i], &m.values);
262 entries.push((illuminants[i], 3, 1, m.illuminant.0.to_le_bytes().to_vec()));
263 }
264 for (i, m) in self.forward_matrices.iter().take(2).enumerate() {
265 floats(&mut entries, forward[i], &m.values);
266 if self.color_matrices.len() <= i {
267 entries.push((illuminants[i], 3, 1, m.illuminant.0.to_le_bytes().to_vec()));
268 }
269 }
270 if let Some(t) = &self.hue_sat_map {
271 longs(&mut entries, tags::PROFILE_HUE_SAT_MAP_DIMS, &t.dims);
272 floats(&mut entries, tags::PROFILE_HUE_SAT_MAP_DATA1, &t.data);
273 if let Some(d2) = &t.data2 {
274 floats(&mut entries, tags::PROFILE_HUE_SAT_MAP_DATA2, d2);
275 }
276 if t.encoding == TableEncoding::Srgb {
277 longs(&mut entries, extra::PROFILE_HUE_SAT_MAP_ENCODING, &[1]);
278 }
279 }
280 if let Some(t) = &self.look_table {
281 longs(&mut entries, tags::PROFILE_LOOK_TABLE_DIMS, &t.dims);
282 floats(&mut entries, tags::PROFILE_LOOK_TABLE_DATA, &t.data);
283 if t.encoding == TableEncoding::Srgb {
284 longs(&mut entries, extra::PROFILE_LOOK_TABLE_ENCODING, &[1]);
285 }
286 }
287 if let Some(c) = &self.tone_curve {
288 let flat: Vec<f32> = c.iter().flatten().copied().collect();
289 floats(&mut entries, tags::PROFILE_TONE_CURVE, &flat);
290 }
291 if let Some(e) = self.embed_policy {
292 longs(&mut entries, tags::PROFILE_EMBED_POLICY, &[e]);
293 }
294 if self.baseline_exposure_offset != 0.0 {
295 entries.push((tags::BASELINE_EXPOSURE_OFFSET, 12, 1, self.baseline_exposure_offset.to_le_bytes().to_vec()));
296 }
297 if self.default_black_render != 0 {
298 longs(&mut entries, extra::DEFAULT_BLACK_RENDER, &[self.default_black_render]);
299 }
300 entries.sort_by_key(|e| e.0);
301 let n = entries.len();
302 let mut out = b"IIRC\x08\0\0\0".to_vec();
303 out.extend((n as u16).to_le_bytes());
304 let mut extra_off = 8 + 2 + n * 12 + 4;
305 let mut extra: Vec<u8> = Vec::new();
306 for (tag, typ, count, data) in &entries {
307 out.extend(tag.to_le_bytes());
308 out.extend(typ.to_le_bytes());
309 out.extend(count.to_le_bytes());
310 if data.len() <= 4 {
311 let mut d = data.clone();
312 d.resize(4, 0);
313 out.extend(d);
314 } else {
315 out.extend((extra_off as u32).to_le_bytes());
316 extra_off += data.len();
317 extra.extend(data);
318 }
319 }
320 out.extend(0u32.to_le_bytes());
321 out.extend(extra);
322 out
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn sample() -> CameraProfile {
331 let mut hs = ProfileTable::new([2, 2, 1], (0..12).map(|i| i as f32 * 0.5).collect(), TableEncoding::Linear)
332 .expect("table");
333 hs.data2 = Some((0..12).map(|i| i as f32).collect());
334 CameraProfile {
335 name: Some("Test profile".into()),
336 unique_camera_model: Some("Test Camera".into()),
337 copyright: Some("CC0".into()),
338 embed_policy: Some(3),
339 color_matrices: vec![
340 ColorMatrix::new(Illuminant::STANDARD_A, 3, vec![1.0, 0.1, 0.0, 0.0, 1.0, 0.1, 0.1, 0.0, 1.0]).unwrap(),
341 ColorMatrix::new(Illuminant::D65, 3, vec![0.9, 0.0, 0.1, 0.0, 0.9, 0.1, 0.0, 0.1, 0.9]).unwrap(),
342 ],
343 forward_matrices: vec![
344 ColorMatrix::new(Illuminant::STANDARD_A, 3, vec![0.8, 0.1, 0.1, 0.3, 0.6, 0.1, 0.0, 0.1, 0.7]).unwrap(),
345 ColorMatrix::new(Illuminant::D65, 3, vec![0.7, 0.2, 0.1, 0.3, 0.7, 0.0, 0.0, 0.0, 0.8]).unwrap(),
346 ],
347 hue_sat_map: Some(hs),
348 look_table: Some(
349 ProfileTable::new([1, 2, 2], (0..12).map(|i| i as f32 + 0.25).collect(), TableEncoding::Srgb).unwrap(),
350 ),
351 tone_curve: Some(vec![[0.0, 0.0], [0.5, 0.7], [1.0, 1.0]]),
352 baseline_exposure_offset: 0.5,
353 default_black_render: 1,
354 }
355 }
356
357 #[test]
358 fn dcp_round_trips() {
359 let p = sample();
360 let bytes = p.to_dcp();
361 assert_eq!(&bytes[..4], b"IIRC");
362 let q = CameraProfile::from_dcp(&bytes).expect("parse");
363 assert_eq!(q, p);
364 }
365
366 #[test]
367 fn dcp_rejects_other_files() {
368 let p = sample().to_dcp();
369 let mut tiff = p.clone();
370 tiff[..4].copy_from_slice(b"II*\0");
371 assert!(matches!(CameraProfile::from_dcp(&tiff), Err(Error::UnsupportedFormat(_))));
372 assert!(CameraProfile::from_dcp(b"IIRC").is_err());
373 for n in 0..p.len() {
375 let _ = CameraProfile::from_dcp(&p[..n]);
376 }
377 }
378
379 #[test]
380 fn mismatched_table_is_an_error() {
381 let mut p = sample();
382 p.hue_sat_map.as_mut().unwrap().dims = [3, 3, 3];
383 let err = CameraProfile::from_dcp(&p.to_dcp()).unwrap_err();
385 assert!(err.to_string().contains("ProfileHueSatMapData1"), "{err}");
386 }
387
388 #[test]
389 fn table_validation() {
390 assert!(ProfileTable::new([0, 1, 1], vec![], TableEncoding::Linear).is_none());
391 assert!(ProfileTable::new([1, 1, 1], vec![0.0, 1.0], TableEncoding::Linear).is_none());
392 assert!(ProfileTable::new([1, 1, 1], vec![0.0, f32::NAN, 1.0], TableEncoding::Linear).is_none());
393 assert!(ProfileTable::new([u32::MAX, u32::MAX, 2], vec![0.0; 3], TableEncoding::Linear).is_none());
394 assert!(ProfileTable::new([1, 1, 1], vec![0.0, 1.0, 1.0], TableEncoding::Linear).is_some());
395 }
396}