1use crate::decoding::parse::parse;
11use crate::file_format::{VsfField, VsfHeader};
12use crate::prelude::*;
13use crate::types::tensor::{BitPackedTensor, Tensor};
14use crate::types::VsfType;
15use crate::vsf_builder::VsfBuilder;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct SpectralCurve {
20 pub start_nm: f32,
21 pub step_nm: f32,
22 pub values: Vec<f32>,
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub struct SpectralChannel {
28 pub name: String,
29 pub curve: Option<SpectralCurve>,
30}
31
32#[derive(Debug, Clone, PartialEq)]
34pub enum PlaneLayout {
35 Mosaic { cfa: Tensor<u8> },
37 Planar,
39}
40
41#[derive(Debug, Clone, Default, PartialEq)]
43pub struct Provenance {
44 pub handle: String,
45 pub calibration_hash: Option<[u8; 32]>,
46 pub camera_ihi: Option<[u8; 32]>,
47 pub identity: Option<[u8; 32]>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum IdtClass {
54 Absolute,
55 Relative,
56 Creative,
57 Technical,
58}
59
60impl IdtClass {
61 pub fn as_str(self) -> &'static str {
62 match self {
63 IdtClass::Absolute => "absolute",
64 IdtClass::Relative => "relative",
65 IdtClass::Creative => "creative",
66 IdtClass::Technical => "technical",
67 }
68 }
69 fn parse(s: &str) -> Result<Self, SpectralImageError> {
70 match s {
71 "absolute" => Ok(IdtClass::Absolute),
72 "relative" => Ok(IdtClass::Relative),
73 "creative" => Ok(IdtClass::Creative),
74 "technical" => Ok(IdtClass::Technical),
75 other => Err(SpectralImageError::BadField(format!("unknown IDT class '{}'", other))),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ProfileGrade {
83 Unit,
84 Model,
85 Assumed,
86}
87
88impl ProfileGrade {
89 pub fn as_str(self) -> &'static str {
90 match self {
91 ProfileGrade::Unit => "unit",
92 ProfileGrade::Model => "model",
93 ProfileGrade::Assumed => "assumed",
94 }
95 }
96 fn parse(s: &str) -> Result<Self, SpectralImageError> {
97 match s {
98 "unit" => Ok(ProfileGrade::Unit),
99 "model" => Ok(ProfileGrade::Model),
100 "assumed" => Ok(ProfileGrade::Assumed),
101 other => Err(SpectralImageError::BadField(format!("unknown profile grade '{}'", other))),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum Transfer {
109 Linear,
110 Srgb,
111 Gamma2,
112 Gamma22,
113}
114
115impl Transfer {
116 pub fn as_str(self) -> &'static str {
117 match self {
118 Transfer::Linear => "linear",
119 Transfer::Srgb => "srgb",
120 Transfer::Gamma2 => "gamma2",
121 Transfer::Gamma22 => "gamma22",
122 }
123 }
124 fn parse(s: &str) -> Result<Self, SpectralImageError> {
125 match s {
126 "linear" => Ok(Transfer::Linear),
127 "srgb" => Ok(Transfer::Srgb),
128 "gamma2" => Ok(Transfer::Gamma2),
129 "gamma22" => Ok(Transfer::Gamma22),
130 other => Err(SpectralImageError::BadField(format!("unknown transfer '{}'", other))),
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq)]
137pub struct ProfileEntry {
138 pub matrix: [f32; 9],
139 pub source: String,
141 pub class: IdtClass,
142 pub grade: ProfileGrade,
143 pub illuminant: u16,
145 pub transfer: Transfer,
146}
147
148#[derive(Debug, Clone, PartialEq)]
150pub struct CalProvenance {
151 pub target_type: u32,
152 pub target_serial: u64,
153 pub timestamp: String,
154}
155
156#[derive(Debug, Clone, PartialEq)]
158pub struct ColourProfile {
159 pub target: String,
160 pub entries: Vec<ProfileEntry>,
161 pub dng_colormatrix: [Option<([f32; 9], u16)>; 2],
163 pub patches: Option<(Vec<f32>, Vec<f32>)>,
165 pub cal: Option<CalProvenance>,
166}
167
168#[derive(Debug, Clone, PartialEq)]
170pub struct ViewOp {
171 pub name: String,
173 pub class: IdtClass,
174 pub params: Vec<f32>,
175}
176
177#[derive(Debug, Clone, PartialEq)]
179pub struct ViewTransform {
180 pub space: String,
181 pub ops: Vec<ViewOp>,
182}
183
184#[derive(Debug, Clone, PartialEq)]
186pub struct SpectralImage {
187 pub width: usize,
188 pub height: usize,
189 pub channels: Vec<SpectralChannel>,
190 pub layout: PlaneLayout,
191 pub samples: BitPackedTensor,
193 pub black: Vec<f32>,
195 pub white: Vec<f32>,
197 pub make: String,
198 pub model: String,
199 pub provenance: Provenance,
200 pub profile: Option<ColourProfile>,
202 pub view: Option<ViewTransform>,
204}
205
206#[derive(Debug)]
207pub enum SpectralImageError {
208 Parse(String),
209 MissingSection(&'static str),
210 MissingField(&'static str),
211 BadField(String),
212}
213
214impl core::fmt::Display for SpectralImageError {
215 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
216 match self {
217 SpectralImageError::Parse(s) => write!(f, "VSF parse: {}", s),
218 SpectralImageError::MissingSection(s) => write!(f, "no '{}' section in file", s),
219 SpectralImageError::MissingField(s) => write!(f, "missing field '{}'", s),
220 SpectralImageError::BadField(s) => write!(f, "bad field: {}", s),
221 }
222 }
223}
224
225#[cfg(feature = "std")]
226impl std::error::Error for SpectralImageError {}
227
228impl SpectralImage {
229 pub fn channel_count(&self) -> usize {
231 self.channels.len()
232 }
233
234 pub fn bit_depth(&self) -> u8 {
236 self.samples.bit_depth
237 }
238}
239
240pub fn write(img: &SpectralImage) -> Result<Vec<u8>, String> {
242 let k = img.channels.len();
243 if img.black.len() != k || img.white.len() != k {
244 return Err(format!("black/white length {}/{} != channel count {}", img.black.len(), img.white.len(), k));
245 }
246
247 let mut fields: Vec<(String, VsfType)> = vec![
248 ("width".to_string(), VsfType::u(img.width, false)),
249 ("height".to_string(), VsfType::u(img.height, false)),
250 ("channel_count".to_string(), VsfType::u(k, false)),
251 ("black".to_string(), VsfType::t_f5(Tensor::new(vec![k], img.black.clone()))),
252 ("white".to_string(), VsfType::t_f5(Tensor::new(vec![k], img.white.clone()))),
253 ("channel_names".to_string(), VsfType::a(img.channels.iter().map(|c| c.name.as_str()).collect::<Vec<_>>().join("\n"))),
254 ];
255 match &img.layout {
256 PlaneLayout::Mosaic { cfa } => {
257 fields.push(("layout".to_string(), VsfType::a("mosaic".to_string())));
258 fields.push(("cfa".to_string(), VsfType::t_u3(cfa.clone())));
259 }
260 PlaneLayout::Planar => fields.push(("layout".to_string(), VsfType::a("planar".to_string()))),
261 }
262 if !img.make.is_empty() {
263 fields.push(("make".to_string(), VsfType::a(img.make.clone())));
264 }
265 if !img.model.is_empty() {
266 fields.push(("model".to_string(), VsfType::a(img.model.clone())));
267 }
268 fields.push(("samples".to_string(), VsfType::p(img.samples.clone())));
269
270 let mut builder = VsfBuilder::new().add_section("spectral_image", fields);
271
272 if img.channels.iter().any(|c| c.curve.is_some()) {
274 let mut starts = Vec::with_capacity(k);
275 let mut steps = Vec::with_capacity(k);
276 let mut counts: Vec<u32> = Vec::with_capacity(k);
277 let mut values: Vec<f32> = Vec::new();
278 for c in &img.channels {
279 match &c.curve {
280 Some(curve) => {
281 starts.push(curve.start_nm);
282 steps.push(curve.step_nm);
283 counts.push(curve.values.len() as u32);
284 values.extend_from_slice(&curve.values);
285 }
286 None => {
287 starts.push(0.0);
288 steps.push(0.0);
289 counts.push(0);
290 }
291 }
292 }
293 let total = values.len();
294 builder = builder.add_section(
295 "spectral_response",
296 vec![
297 ("curve_start".to_string(), VsfType::t_f5(Tensor::new(vec![k], starts))),
298 ("curve_step".to_string(), VsfType::t_f5(Tensor::new(vec![k], steps))),
299 ("curve_counts".to_string(), VsfType::t_u5(Tensor::new(vec![k], counts))),
300 ("curve_values".to_string(), VsfType::t_f5(Tensor::new(vec![total], values))),
301 ],
302 );
303 }
304
305 let p = &img.provenance;
307 let mut prov: Vec<(String, VsfType)> = Vec::new();
308 if !p.handle.is_empty() {
309 #[cfg(any(feature = "text", feature = "text-encode"))]
311 prov.push(("handle".to_string(), VsfType::x(p.handle.clone())));
312 #[cfg(not(any(feature = "text", feature = "text-encode")))]
313 {
314 if !p.handle.is_ascii() {
315 return Err("non-ASCII handle requires the 'text' or 'text-encode' feature".to_string());
316 }
317 prov.push(("handle".to_string(), VsfType::a(p.handle.clone())));
318 }
319 }
320 if let Some(h) = &p.calibration_hash {
321 prov.push(("calibration_hash".to_string(), VsfType::hp(h.to_vec())));
322 }
323 if let Some(h) = &p.camera_ihi {
324 prov.push(("camera_ihi".to_string(), VsfType::hs(h.to_vec())));
325 }
326 if let Some(h) = &p.identity {
327 prov.push(("identity".to_string(), VsfType::hs(h.to_vec())));
328 }
329 if !prov.is_empty() {
330 builder = builder.add_section("provenance", prov);
331 }
332
333 if let Some(profile) = &img.profile {
335 let n = profile.entries.len();
336 let mut matrices: Vec<f32> = Vec::with_capacity(n * 9);
337 let mut sources: Vec<&str> = Vec::with_capacity(n);
338 let mut classes: Vec<&str> = Vec::with_capacity(n);
339 let mut grades: Vec<&str> = Vec::with_capacity(n);
340 let mut illuminants: Vec<u16> = Vec::with_capacity(n);
341 let mut transfers: Vec<&str> = Vec::with_capacity(n);
342 for e in &profile.entries {
343 matrices.extend_from_slice(&e.matrix);
344 sources.push(&e.source);
345 classes.push(e.class.as_str());
346 grades.push(e.grade.as_str());
347 illuminants.push(e.illuminant);
348 transfers.push(e.transfer.as_str());
349 }
350 let mut fields: Vec<(String, VsfType)> = vec![
351 ("target".to_string(), VsfType::a(profile.target.clone())),
352 ("count".to_string(), VsfType::u(n, false)),
353 ("matrices".to_string(), VsfType::t_f5(Tensor::new(vec![n, 3, 3], matrices))),
354 ("sources".to_string(), VsfType::a(sources.join("\n"))),
355 ("classes".to_string(), VsfType::a(classes.join("\n"))),
356 ("grades".to_string(), VsfType::a(grades.join("\n"))),
357 ("illuminants".to_string(), VsfType::t_u4(Tensor::new(vec![n], illuminants))),
358 ("transfers".to_string(), VsfType::a(transfers.join("\n"))),
359 ];
360 for (i, name) in ["dng_colormatrix1", "dng_colormatrix2"].iter().enumerate() {
361 if let Some((m, code)) = &profile.dng_colormatrix[i] {
362 fields.push((name.to_string(), VsfType::t_f5(Tensor::new(vec![3, 3], m.to_vec()))));
363 let illum_name = if i == 0 { "dng_illuminant1" } else { "dng_illuminant2" };
364 fields.push((illum_name.to_string(), VsfType::u(*code as usize, false)));
365 }
366 }
367 if let Some((cam, reference)) = &profile.patches {
368 let p = cam.len() / 3;
369 fields.push(("patches_camera".to_string(), VsfType::t_f5(Tensor::new(vec![p, 3], cam.clone()))));
370 fields.push(("patches_reference".to_string(), VsfType::t_f5(Tensor::new(vec![p, 3], reference.clone()))));
371 }
372 if let Some(cal) = &profile.cal {
373 fields.push(("cal_target_type".to_string(), VsfType::u(cal.target_type as usize, false)));
374 fields.push(("cal_target_serial".to_string(), VsfType::u(cal.target_serial as usize, false)));
375 fields.push(("cal_timestamp".to_string(), VsfType::a(cal.timestamp.clone())));
376 }
377 builder = builder.add_section("colour_profile", fields);
378 }
379
380 if let Some(view) = &img.view {
382 let m = view.ops.len();
383 let mut names: Vec<&str> = Vec::with_capacity(m);
384 let mut classes: Vec<&str> = Vec::with_capacity(m);
385 let mut param_counts: Vec<u32> = Vec::with_capacity(m);
386 let mut params: Vec<f32> = Vec::new();
387 for op in &view.ops {
388 names.push(&op.name);
389 classes.push(op.class.as_str());
390 param_counts.push(op.params.len() as u32);
391 params.extend_from_slice(&op.params);
392 }
393 let total = params.len();
394 builder = builder.add_section(
395 "view_transform",
396 vec![
397 ("space".to_string(), VsfType::a(view.space.clone())),
398 ("ops".to_string(), VsfType::a(names.join("\n"))),
399 ("classes".to_string(), VsfType::a(classes.join("\n"))),
400 ("param_counts".to_string(), VsfType::t_u5(Tensor::new(vec![m], param_counts))),
401 ("params".to_string(), VsfType::t_f5(Tensor::new(vec![total], params))),
402 ],
403 );
404 }
405
406 builder.build()
407}
408
409pub fn read(data: &[u8]) -> Result<SpectralImage, SpectralImageError> {
411 let (header, _) = VsfHeader::decode(data).map_err(SpectralImageError::Parse)?;
412
413 let main = section_fields(data, &header, "spectral_image")?.ok_or(SpectralImageError::MissingSection("spectral_image"))?;
414
415 let width = take_usize(&main, "width")?;
416 let height = take_usize(&main, "height")?;
417 let k = take_usize(&main, "channel_count")?;
418 let black = take_f32_vec(&main, "black")?;
419 let white = take_f32_vec(&main, "white")?;
420 let names_joined = take_string(&main, "channel_names")?;
421 let layout_str = take_string(&main, "layout")?;
422 let make = take_string_opt(&main, "make").unwrap_or_default();
423 let model = take_string_opt(&main, "model").unwrap_or_default();
424
425 let samples = match find(&main, "samples") {
426 Some(VsfType::p(t)) => t.clone(),
427 Some(_) => return Err(SpectralImageError::BadField("samples is not a BitPackedTensor".into())),
428 None => return Err(SpectralImageError::MissingField("samples")),
429 };
430
431 let layout = match layout_str.as_str() {
432 "mosaic" => match find(&main, "cfa") {
433 Some(VsfType::t_u3(t)) => PlaneLayout::Mosaic { cfa: t.clone() },
434 Some(_) => return Err(SpectralImageError::BadField("cfa is not a u8 tensor".into())),
435 None => return Err(SpectralImageError::MissingField("cfa")),
436 },
437 "planar" => PlaneLayout::Planar,
438 other => return Err(SpectralImageError::BadField(format!("unknown layout '{}'", other))),
439 };
440
441 let names: Vec<String> = names_joined.split('\n').map(str::to_string).collect();
443 if names.len() != k {
444 return Err(SpectralImageError::BadField(format!("channel_names carries {} names for {} channels", names.len(), k)));
445 }
446
447 let mut curves: Vec<Option<SpectralCurve>> = vec![None; k];
449 if let Some(resp) = section_fields(data, &header, "spectral_response")? {
450 let starts = take_f32_vec(&resp, "curve_start")?;
451 let steps = take_f32_vec(&resp, "curve_step")?;
452 let counts = take_u32_vec(&resp, "curve_counts")?;
453 let values = take_f32_vec(&resp, "curve_values")?;
454 if starts.len() != k || steps.len() != k || counts.len() != k {
455 return Err(SpectralImageError::BadField(format!("spectral_response arrays sized {}/{}/{} for {} channels", starts.len(), steps.len(), counts.len(), k)));
456 }
457 let mut cursor = 0usize;
458 for i in 0..k {
459 let n = counts[i] as usize;
460 if n == 0 {
461 continue;
462 }
463 if cursor + n > values.len() {
464 return Err(SpectralImageError::BadField("curve_values shorter than curve_counts total".into()));
465 }
466 curves[i] = Some(SpectralCurve { start_nm: starts[i], step_nm: steps[i], values: values[cursor..cursor + n].to_vec() });
467 cursor += n;
468 }
469 }
470
471 let channels = names.into_iter().zip(curves).map(|(name, curve)| SpectralChannel { name, curve }).collect();
472
473 let mut provenance = Provenance::default();
475 if let Some(prov) = section_fields(data, &header, "provenance")? {
476 if let Some(h) = take_string_opt(&prov, "handle") {
477 provenance.handle = h;
478 }
479 provenance.calibration_hash = take_hash32(&prov, "calibration_hash");
480 provenance.camera_ihi = take_hash32(&prov, "camera_ihi");
481 provenance.identity = take_hash32(&prov, "identity");
482 }
483
484 if black.len() != k || white.len() != k {
485 return Err(SpectralImageError::BadField(format!("black/white length {}/{} != channel count {}", black.len(), white.len(), k)));
486 }
487
488 let profile = read_colour_profile(data, &header)?;
489 let view = read_view_transform(data, &header)?;
490
491 Ok(SpectralImage { width, height, channels, layout, samples, black, white, make, model, provenance, profile, view })
492}
493
494fn split_n(joined: &str, n: usize, what: &str) -> Result<Vec<String>, SpectralImageError> {
496 let parts: Vec<String> = joined.split('\n').map(str::to_string).collect();
497 if parts.len() != n {
498 return Err(SpectralImageError::BadField(format!("{} carries {} entries for {} rows", what, parts.len(), n)));
499 }
500 Ok(parts)
501}
502
503fn read_colour_profile(data: &[u8], header: &VsfHeader) -> Result<Option<ColourProfile>, SpectralImageError> {
505 let Some(sec) = section_fields(data, header, "colour_profile")? else {
506 return Ok(None);
507 };
508 let target = take_string(&sec, "target")?;
509 let n = take_usize(&sec, "count")?;
510 let matrices = take_f32_vec(&sec, "matrices")?;
511 if matrices.len() != n * 9 {
512 return Err(SpectralImageError::BadField(format!("colour_profile matrices carries {} values for {} entries", matrices.len(), n)));
513 }
514 let sources = split_n(&take_string(&sec, "sources")?, n, "colour_profile sources")?;
515 let classes = split_n(&take_string(&sec, "classes")?, n, "colour_profile classes")?;
516 let grades = split_n(&take_string(&sec, "grades")?, n, "colour_profile grades")?;
517 let transfers = split_n(&take_string(&sec, "transfers")?, n, "colour_profile transfers")?;
518 let illuminants = take_u32_vec(&sec, "illuminants")?;
519 if illuminants.len() != n {
520 return Err(SpectralImageError::BadField(format!("colour_profile illuminants carries {} values for {} entries", illuminants.len(), n)));
521 }
522 let mut entries = Vec::with_capacity(n);
523 for i in 0..n {
524 let mut matrix = [0f32; 9];
525 matrix.copy_from_slice(&matrices[i * 9..i * 9 + 9]);
526 entries.push(ProfileEntry {
527 matrix,
528 source: sources[i].clone(),
529 class: IdtClass::parse(&classes[i])?,
530 grade: ProfileGrade::parse(&grades[i])?,
531 illuminant: illuminants[i] as u16,
532 transfer: Transfer::parse(&transfers[i])?,
533 });
534 }
535
536 let mut dng_colormatrix: [Option<([f32; 9], u16)>; 2] = [None, None];
537 for (i, (mname, iname)) in [("dng_colormatrix1", "dng_illuminant1"), ("dng_colormatrix2", "dng_illuminant2")].iter().enumerate() {
538 if let Some(f) = sec.iter().find(|f| f.name == *mname) {
539 let flat = match f.values.first() {
540 Some(VsfType::t_f5(t)) => t.data.clone(),
541 Some(VsfType::v_f5(v)) => v.data.clone(),
542 _ => return Err(SpectralImageError::BadField(format!("{} is not an f32 tensor", mname))),
543 };
544 if flat.len() != 9 {
545 return Err(SpectralImageError::BadField(format!("{} carries {} values, need 9", mname, flat.len())));
546 }
547 let mut m = [0f32; 9];
548 m.copy_from_slice(&flat);
549 let code = take_usize(&sec, iname).unwrap_or(0) as u16;
550 dng_colormatrix[i] = Some((m, code));
551 }
552 }
553
554 let patches = match (sec.iter().any(|f| f.name == "patches_camera"), sec.iter().any(|f| f.name == "patches_reference")) {
555 (true, true) => Some((take_f32_vec(&sec, "patches_camera")?, take_f32_vec(&sec, "patches_reference")?)),
556 (false, false) => None,
557 _ => return Err(SpectralImageError::BadField("colour_profile has one of patches_camera/patches_reference without the other".into())),
558 };
559
560 let cal = if sec.iter().any(|f| f.name == "cal_target_type") {
561 Some(CalProvenance {
562 target_type: take_usize(&sec, "cal_target_type")? as u32,
563 target_serial: take_usize(&sec, "cal_target_serial")? as u64,
564 timestamp: take_string(&sec, "cal_timestamp")?,
565 })
566 } else {
567 None
568 };
569
570 Ok(Some(ColourProfile { target, entries, dng_colormatrix, patches, cal }))
571}
572
573fn read_view_transform(data: &[u8], header: &VsfHeader) -> Result<Option<ViewTransform>, SpectralImageError> {
575 let Some(sec) = section_fields(data, header, "view_transform")? else {
576 return Ok(None);
577 };
578 let space = take_string(&sec, "space")?;
579 let param_counts = take_u32_vec(&sec, "param_counts")?;
580 let m = param_counts.len();
581 let names = split_n(&take_string(&sec, "ops")?, m, "view_transform ops")?;
582 let classes = split_n(&take_string(&sec, "classes")?, m, "view_transform classes")?;
583 let params = take_f32_vec(&sec, "params")?;
584 let mut ops = Vec::with_capacity(m);
585 let mut cursor = 0usize;
586 for i in 0..m {
587 let count = param_counts[i] as usize;
588 if cursor + count > params.len() {
589 return Err(SpectralImageError::BadField("view_transform params shorter than param_counts total".into()));
590 }
591 ops.push(ViewOp {
592 name: names[i].clone(),
593 class: IdtClass::parse(&classes[i])?,
594 params: params[cursor..cursor + count].to_vec(),
595 });
596 cursor += count;
597 }
598 Ok(Some(ViewTransform { space, ops }))
599}
600
601fn section_fields(data: &[u8], header: &VsfHeader, name: &str) -> Result<Option<Vec<VsfField>>, SpectralImageError> {
603 let Some(field) = header.fields.iter().find(|f| f.name == name) else {
604 return Ok(None);
605 };
606 let mut p = field.offset_bytes;
607 if p >= data.len() {
608 return Err(SpectralImageError::Parse(format!("'{}' section offset {} beyond file length {}", name, p, data.len())));
609 }
610 if data[p] == b'>' {
611 p += 1;
612 }
613 if p >= data.len() || data[p] != b'[' {
614 return Err(SpectralImageError::Parse(format!("expected '[' at '{}' section start, got byte {:02x}", name, data.get(p).copied().unwrap_or(0))));
615 }
616 p += 1;
617 if p < data.len() && data[p] != b'(' {
618 parse(data, &mut p).map_err(|e| SpectralImageError::Parse(format!("section name: {:?}", e)))?;
619 parse(data, &mut p).map_err(|e| SpectralImageError::Parse(format!("section n: {:?}", e)))?;
620 parse(data, &mut p).map_err(|e| SpectralImageError::Parse(format!("section b: {:?}", e)))?;
621 }
622 let mut fields = Vec::with_capacity(field.child_count);
623 for _ in 0..field.child_count {
624 fields.push(VsfField::parse(data, &mut p).map_err(|e| SpectralImageError::Parse(format!("field parse: {}", e)))?);
625 }
626 Ok(Some(fields))
627}
628
629fn find<'a>(fields: &'a [VsfField], name: &str) -> Option<&'a VsfType> {
630 fields.iter().find(|f| f.name == name).and_then(|f| f.values.first())
631}
632
633fn take_usize(fields: &[VsfField], name: &'static str) -> Result<usize, SpectralImageError> {
634 match find(fields, name) {
635 Some(VsfType::u(v, _)) => Ok(*v),
636 Some(VsfType::u3(v)) => Ok(*v as usize),
637 Some(VsfType::u4(v)) => Ok(*v as usize),
638 Some(VsfType::u5(v)) => Ok(*v as usize),
639 Some(VsfType::u6(v)) => Ok(*v as usize),
640 Some(VsfType::n(v)) => Ok(*v),
641 Some(_) => Err(SpectralImageError::BadField(format!("'{}' is not an unsigned integer", name))),
642 None => Err(SpectralImageError::MissingField(name)),
643 }
644}
645
646fn take_string(fields: &[VsfField], name: &'static str) -> Result<String, SpectralImageError> {
647 take_string_opt(fields, name).ok_or(SpectralImageError::MissingField(name))
648}
649
650fn take_string_opt(fields: &[VsfField], name: &str) -> Option<String> {
651 match find(fields, name) {
652 Some(VsfType::a(s)) | Some(VsfType::x(s)) => Some(s.clone()),
653 _ => None,
654 }
655}
656
657fn take_f32_vec(fields: &[VsfField], name: &'static str) -> Result<Vec<f32>, SpectralImageError> {
658 match find(fields, name) {
660 Some(VsfType::t_f5(t)) => Ok(t.data.clone()),
661 Some(VsfType::v_f5(v)) => Ok(v.data.clone()),
662 Some(_) => Err(SpectralImageError::BadField(format!("'{}' is not an f32 tensor", name))),
663 None => Err(SpectralImageError::MissingField(name)),
664 }
665}
666
667fn take_u32_vec(fields: &[VsfField], name: &'static str) -> Result<Vec<u32>, SpectralImageError> {
668 match find(fields, name) {
670 Some(VsfType::t_u3(t)) => Ok(t.data.iter().map(|&v| v as u32).collect()),
671 Some(VsfType::t_u4(t)) => Ok(t.data.iter().map(|&v| v as u32).collect()),
672 Some(VsfType::t_u5(t)) => Ok(t.data.clone()),
673 Some(VsfType::v_u3(v)) => Ok(v.data.iter().map(|&v| v as u32).collect()),
674 Some(VsfType::v_u4(v)) => Ok(v.data.iter().map(|&v| v as u32).collect()),
675 Some(VsfType::v_u5(v)) => Ok(v.data.clone()),
676 Some(_) => Err(SpectralImageError::BadField(format!("'{}' is not an unsigned tensor", name))),
677 None => Err(SpectralImageError::MissingField(name)),
678 }
679}
680
681fn take_hash32(fields: &[VsfField], name: &str) -> Option<[u8; 32]> {
682 match find(fields, name) {
683 Some(VsfType::hp(v)) | Some(VsfType::hs(v)) | Some(VsfType::hb(v)) => v.as_slice().try_into().ok(),
684 _ => None,
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 fn bayer_test_image() -> SpectralImage {
693 let counts: Vec<u16> = (0..16).map(|i| (i * 200) as u16).collect();
695 SpectralImage {
696 width: 4,
697 height: 4,
698 channels: vec![
699 SpectralChannel { name: "R".into(), curve: None },
700 SpectralChannel { name: "G".into(), curve: None },
701 SpectralChannel { name: "B".into(), curve: None },
702 ],
703 layout: PlaneLayout::Mosaic { cfa: Tensor::new(vec![2, 2], vec![0, 1, 1, 2]) },
704 samples: BitPackedTensor::pack(12, vec![4, 4], &counts),
705 black: vec![512.0; 3],
706 white: vec![4095.0; 3],
707 make: "VERICHROME".into(),
708 model: "TestCam".into(),
709 provenance: Provenance::default(),
710 profile: None,
711 view: None,
712 }
713 }
714
715 #[test]
716 fn mosaic_round_trip() {
717 let img = bayer_test_image();
718 let bytes = write(&img).expect("write");
719 let back = read(&bytes).expect("read");
720 assert_eq!(back, img);
721 assert_eq!(back.samples.unpack_u16(), img.samples.unpack_u16());
722 assert_eq!(back.bit_depth(), 12);
723 }
724
725 #[test]
726 fn planar_with_curves_round_trip() {
727 let img = SpectralImage {
729 width: 2,
730 height: 2,
731 channels: vec![
732 SpectralChannel { name: "LED_450".into(), curve: Some(SpectralCurve { start_nm: 400.0, step_nm: 5.0, values: vec![0.1, 0.9, 0.4] }) },
733 SpectralChannel { name: "LED_650".into(), curve: None },
734 ],
735 layout: PlaneLayout::Planar,
736 samples: BitPackedTensor::pack(16, vec![2, 2, 2], &[1u16, 2, 3, 4, 5, 6, 7, 8]),
737 black: vec![0.0, 0.0],
738 white: vec![65535.0, 65535.0],
739 make: String::new(),
740 model: String::new(),
741 provenance: Provenance { handle: "nick".into(), calibration_hash: Some([7u8; 32]), camera_ihi: None, identity: Some([9u8; 32]) },
742 profile: None,
743 view: None,
744 };
745 let bytes = write(&img).expect("write");
746 let back = read(&bytes).expect("read");
747 assert_eq!(back, img);
748 }
749
750 #[test]
751 fn colour_profile_and_view_round_trip() {
752 let mut img = bayer_test_image();
754 img.profile = Some(ColourProfile {
755 target: "vsf_rgb".into(),
756 entries: vec![
757 ProfileEntry {
758 matrix: [1.1, 0.0, 0.0, 0.0, 0.9, 0.0, 0.0, 0.0, 1.2],
759 source: "magic9".into(),
760 class: IdtClass::Relative,
761 grade: ProfileGrade::Unit,
762 illuminant: 21,
763 transfer: Transfer::Linear,
764 },
765 ProfileEntry {
766 matrix: [0.8, 0.1, 0.05, 0.02, 0.95, 0.03, 0.01, 0.04, 1.1],
767 source: "dng_colormatrix1".into(),
768 class: IdtClass::Absolute,
769 grade: ProfileGrade::Model,
770 illuminant: 23,
771 transfer: Transfer::Linear,
772 },
773 ],
774 dng_colormatrix: [Some(([1.0, 0.1, 0.0, 0.0, 1.0, 0.1, 0.0, 0.0, 1.0], 23)), None],
775 patches: Some((vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], vec![0.11, 0.21, 0.31, 0.41, 0.51, 0.61])),
776 cal: Some(CalProvenance { target_type: 9, target_serial: 12345, timestamp: "eagle".into() }),
777 });
778 img.view = Some(ViewTransform {
779 space: "vsf_rgb_linear".into(),
780 ops: vec![ViewOp { name: "exposure".into(), class: IdtClass::Technical, params: vec![1.5] }],
781 });
782 let bytes = write(&img).expect("write");
783 let back = read(&bytes).expect("read");
784 assert_eq!(back, img);
785 }
786
787 #[test]
788 fn absent_profile_and_view_read_none() {
789 let img = bayer_test_image();
790 let back = read(&write(&img).expect("write")).expect("read");
791 assert!(back.profile.is_none());
792 assert!(back.view.is_none());
793 }
794
795 #[test]
796 fn empty_handle_is_omitted_and_reads_back_empty() {
797 let img = bayer_test_image();
798 let bytes = write(&img).expect("write");
799 assert!(!String::from_utf8_lossy(&bytes).contains("handle"));
800 assert_eq!(read(&bytes).expect("read").provenance.handle, "");
801 }
802}