Skip to main content

vsf/
builders.rs

1//! High-level builders for common VSF use cases
2//!
3//! This module provides constructors for complex data types that require packaging multiple VSF primitives together:
4//! - GPS coordinate conversions (lat/lon → WorldCoord)
5//! - RAW camera images with metadata
6//!
7//! For simple types, use VsfType directly:
8//! - Text: `VsfType::x("Hello".to_string())`
9//! - Images: `VsfType::p(BitPackedTensor::pack(12, vec![w, h], &samples))`
10//! - Tensors: `VsfType::t_u3(Tensor::new(vec![w, h], data))`
11//!
12//! # Examples ∞
13//! ```ignore
14//! use vsf::builders::*; use vsf::types::*;
15//!
16//! // RAW camera image (12-bit sensor) let raw = raw_image(12, 4096, 3072, pixel_data);
17
18use crate::prelude::*;
19use crate::types::{BitPackedTensor, Tensor, VsfType, WorldCoord};
20use crate::vsf_builder::VsfBuilder;
21
22// ==================== NEWTYPE WRAPPERS FOR TYPE SAFETY ====================
23
24/// CFA (Colour Filter Array) pattern with validation
25/// - Bayer 2×2: 4 bytes like `[b'R', b'G', b'G', b'B']`
26/// - X-Trans 6×6: 36 bytes
27/// - Valid colours: R, G, B, C (Cyan), Y (Yellow), W (White), E (Emerald)
28#[derive(Debug, Clone)]
29pub struct CfaPattern(VsfType);
30
31impl CfaPattern {
32    pub fn new(pattern: Vec<u8>) -> Result<Self, String> {
33        // Validate pattern length (common sizes: 4 for Bayer, 36 for X-Trans)
34        if pattern.is_empty() {
35            return Err("CFA pattern cannot be empty".to_string());
36        }
37
38        // Validate colour codes
39        for &byte in &pattern {
40            match byte {
41                b'R' | b'G' | b'B' | b'C' | b'Y' | b'W' | b'E' => {}
42                _ => return Err(format!("Invalid CFA colour code: {}", byte as char)),
43            }
44        }
45
46        Ok(CfaPattern(VsfType::t_u3(Tensor {
47            shape: vec![pattern.len()],
48            data: pattern,
49        })))
50    }
51
52    pub fn to_vsf_type(self) -> VsfType {
53        self.0
54    }
55
56    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
57        match vsf {
58            VsfType::t_u3(ref tensor) => {
59                // Validate the pattern
60                for &byte in &tensor.data {
61                    match byte {
62                        b'R' | b'G' | b'B' | b'C' | b'Y' | b'W' | b'E' => {}
63                        _ => return Err(format!("Invalid CFA colour code: {}", byte as char)),
64                    }
65                }
66                Ok(CfaPattern(vsf))
67            }
68            _ => Err("Expected t_u3 type for CFA pattern".to_string()),
69        }
70    }
71}
72
73/// Sensor black level (digital zero point)
74#[derive(Debug, Clone)]
75pub struct BlackLevel(VsfType);
76
77impl BlackLevel {
78    pub fn new(level: f32) -> Result<Self, String> {
79        if level < 0.0 {
80            return Err("Black level cannot be negative".to_string());
81        }
82        Ok(BlackLevel(VsfType::f5(level)))
83    }
84
85    pub fn to_vsf_type(self) -> VsfType {
86        self.0
87    }
88
89    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
90        match vsf {
91            VsfType::f5(v) if v >= 0.0 => Ok(BlackLevel(vsf)),
92            VsfType::f5(_) => Err("Black level cannot be negative".to_string()),
93            _ => Err("Expected f5 type for black level".to_string()),
94        }
95    }
96}
97
98/// Sensor white level (saturation point)
99#[derive(Debug, Clone)]
100pub struct WhiteLevel(VsfType);
101
102impl WhiteLevel {
103    pub fn new(level: f32) -> Result<Self, String> {
104        if level <= 0.0 {
105            return Err("White level must be positive".to_string());
106        }
107        Ok(WhiteLevel(VsfType::f5(level)))
108    }
109
110    pub fn to_vsf_type(self) -> VsfType {
111        self.0
112    }
113
114    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
115        match vsf {
116            VsfType::f5(v) if v > 0.0 => Ok(WhiteLevel(vsf)),
117            VsfType::f5(_) => Err("White level must be positive".to_string()),
118            _ => Err("Expected f5 type for white level".to_string()),
119        }
120    }
121}
122
123/// Hash reference to a calibration frame (dark frame, flat field, etc.)
124#[derive(Debug, Clone)]
125pub struct CalibrationHash(VsfType);
126
127impl CalibrationHash {
128    pub fn new(algorithm: u8, hash: Vec<u8>) -> Result<Self, String> {
129        use crate::crypto_algorithms::{HASH_BLAKE3, HASH_SHA256, HASH_SHA512};
130
131        if hash.is_empty() {
132            return Err("Hash cannot be empty".to_string());
133        }
134
135        let vsf_type = match algorithm {
136            HASH_BLAKE3 => VsfType::hb(hash),
137            HASH_SHA256 | HASH_SHA512 => VsfType::hs(hash),
138            _ => return Err(format!("Unsupported hash algorithm: {}", algorithm as char)),
139        };
140
141        Ok(CalibrationHash(vsf_type))
142    }
143
144    pub fn to_vsf_type(self) -> VsfType {
145        self.0
146    }
147
148    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
149        match vsf {
150            VsfType::hb(ref hash) | VsfType::hs(ref hash) if !hash.is_empty() => {
151                Ok(CalibrationHash(vsf))
152            }
153            VsfType::hb(_) | VsfType::hs(_) => Err("Hash cannot be empty".to_string()),
154            _ => Err("Expected hash type for calibration hash".to_string()),
155        }
156    }
157}
158
159/// Magic 9: 3×3 colour transformation matrix (Sensor RGB → LMS) Must contain exactly 9 elements in row-major order
160#[derive(Debug, Clone)]
161pub struct Magic9(VsfType);
162
163impl Magic9 {
164    pub fn new(values: Vec<f32>) -> Result<Self, String> {
165        if values.len() != 9 {
166            return Err(format!(
167                "Magic 9 matrix must have exactly 9 elements, got {}",
168                values.len()
169            ));
170        }
171        Ok(Magic9(VsfType::t_f5(Tensor {
172            shape: vec![3, 3],
173            data: values,
174        })))
175    }
176
177    pub fn to_vsf_type(self) -> VsfType {
178        self.0
179    }
180
181    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
182        match vsf {
183            VsfType::t_f5(ref tensor) => {
184                if tensor.data.len() != 9 {
185                    return Err(format!(
186                        "Magic 9 matrix must have exactly 9 elements, got {}",
187                        tensor.data.len()
188                    ));
189                }
190                Ok(Magic9(vsf))
191            }
192            _ => Err("Expected t_f5 type for Magic 9 matrix".to_string()),
193        }
194    }
195}
196
197/// ISO speed (sensitivity)
198#[derive(Debug, Clone)]
199pub struct IsoSpeed(VsfType);
200
201impl IsoSpeed {
202    pub fn new(iso: f32) -> Result<Self, String> {
203        if iso <= 0.0 {
204            return Err("ISO speed must be positive".to_string());
205        }
206        Ok(IsoSpeed(VsfType::f5(iso)))
207    }
208
209    pub fn to_vsf_type(self) -> VsfType {
210        self.0
211    }
212
213    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
214        match vsf {
215            VsfType::f5(v) if v > 0.0 => Ok(IsoSpeed(vsf)),
216            VsfType::f5(_) => Err("ISO speed must be positive".to_string()),
217            _ => Err("Expected f5 type for ISO speed".to_string()),
218        }
219    }
220}
221
222/// Shutter time in seconds
223#[derive(Debug, Clone)]
224pub struct ShutterTime(VsfType);
225
226impl ShutterTime {
227    pub fn new(seconds: f32) -> Result<Self, String> {
228        if seconds <= 0.0 {
229            return Err("Shutter time must be positive".to_string());
230        }
231        Ok(ShutterTime(VsfType::f5(seconds)))
232    }
233
234    pub fn to_vsf_type(self) -> VsfType {
235        self.0
236    }
237
238    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
239        match vsf {
240            VsfType::f5(v) if v > 0.0 => Ok(ShutterTime(vsf)),
241            VsfType::f5(_) => Err("Shutter time must be positive".to_string()),
242            _ => Err("Expected f5 type for shutter time".to_string()),
243        }
244    }
245}
246
247/// Aperture (f-number)
248#[derive(Debug, Clone)]
249pub struct Aperture(VsfType);
250
251impl Aperture {
252    pub fn new(f_number: f32) -> Result<Self, String> {
253        if f_number <= 0.0 {
254            return Err("Aperture f-number must be positive".to_string());
255        }
256        Ok(Aperture(VsfType::f5(f_number)))
257    }
258
259    pub fn to_vsf_type(self) -> VsfType {
260        self.0
261    }
262
263    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
264        match vsf {
265            VsfType::f5(v) if v > 0.0 => Ok(Aperture(vsf)),
266            VsfType::f5(_) => Err("Aperture f-number must be positive".to_string()),
267            _ => Err("Expected f5 type for aperture".to_string()),
268        }
269    }
270}
271
272/// Focal length in meters
273#[derive(Debug, Clone)]
274pub struct FocalLength(VsfType);
275
276impl FocalLength {
277    pub fn new(meters: f32) -> Result<Self, String> {
278        if meters <= 0.0 {
279            return Err("Focal length must be positive".to_string());
280        }
281        Ok(FocalLength(VsfType::f5(meters)))
282    }
283
284    pub fn to_vsf_type(self) -> VsfType {
285        self.0
286    }
287
288    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
289        match vsf {
290            VsfType::f5(v) if v > 0.0 => Ok(FocalLength(vsf)),
291            VsfType::f5(_) => Err("Focal length must be positive".to_string()),
292            _ => Err("Expected f5 type for focal length".to_string()),
293        }
294    }
295}
296
297/// Exposure compensation in EV
298#[derive(Debug, Clone)]
299pub struct ExposureCompensation(VsfType);
300
301impl ExposureCompensation {
302    pub fn new(ev: f32) -> Result<Self, String> {
303        Ok(ExposureCompensation(VsfType::f5(ev))) // Can be negative
304    }
305
306    pub fn to_vsf_type(self) -> VsfType {
307        self.0
308    }
309
310    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
311        match vsf {
312            VsfType::f5(_) => Ok(ExposureCompensation(vsf)),
313            _ => Err("Expected f5 type for exposure compensation".to_string()),
314        }
315    }
316}
317
318/// Focus distance in meters
319#[derive(Debug, Clone)]
320pub struct FocusDistance(VsfType);
321
322impl FocusDistance {
323    pub fn new(meters: f32) -> Result<Self, String> {
324        if meters < 0.0 {
325            return Err("Focus distance cannot be negative".to_string());
326        }
327        Ok(FocusDistance(VsfType::f5(meters)))
328    }
329
330    pub fn to_vsf_type(self) -> VsfType {
331        self.0
332    }
333
334    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
335        match vsf {
336            VsfType::f5(v) if v >= 0.0 => Ok(FocusDistance(vsf)),
337            VsfType::f5(_) => Err("Focus distance cannot be negative".to_string()),
338            _ => Err("Expected f5 type for focus distance".to_string()),
339        }
340    }
341}
342
343/// Flash status
344#[derive(Debug, Clone)]
345pub struct FlashFired(VsfType);
346
347impl FlashFired {
348    pub fn new(fired: bool) -> Result<Self, String> {
349        Ok(FlashFired(VsfType::u0(fired)))
350    }
351
352    pub fn to_vsf_type(self) -> VsfType {
353        self.0
354    }
355
356    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
357        match vsf {
358            VsfType::u0(_) => Ok(FlashFired(vsf)),
359            _ => Err("Expected u0 type for flash fired".to_string()),
360        }
361    }
362}
363
364/// Metering mode (spot, center, matrix, etc.)
365#[derive(Debug, Clone)]
366pub struct MeteringMode(VsfType);
367
368impl MeteringMode {
369    pub fn new(mode: String) -> Result<Self, String> {
370        if mode.is_empty() {
371            return Err("Metering mode cannot be empty".to_string());
372        }
373        Ok(MeteringMode(VsfType::x(mode)))
374    }
375
376    pub fn to_vsf_type(self) -> VsfType {
377        self.0
378    }
379
380    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
381        match vsf {
382            VsfType::x(ref s) if !s.is_empty() => Ok(MeteringMode(vsf)),
383            VsfType::x(_) => Err("Metering mode cannot be empty".to_string()),
384            _ => Err("Expected x type for metering mode".to_string()),
385        }
386    }
387}
388
389/// Manufacturer name
390#[derive(Debug, Clone)]
391pub struct Manufacturer(VsfType);
392
393impl Manufacturer {
394    pub fn new(name: String) -> Result<Self, String> {
395        if name.is_empty() {
396            return Err("Manufacturer name cannot be empty".to_string());
397        }
398        Ok(Manufacturer(VsfType::x(name)))
399    }
400
401    pub fn to_vsf_type(self) -> VsfType {
402        self.0
403    }
404
405    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
406        match vsf {
407            VsfType::x(ref s) if !s.is_empty() => Ok(Manufacturer(vsf)),
408            VsfType::x(_) => Err("Manufacturer name cannot be empty".to_string()),
409            _ => Err("Expected x type for manufacturer".to_string()),
410        }
411    }
412}
413
414/// Model name
415#[derive(Debug, Clone)]
416pub struct ModelName(VsfType);
417
418impl ModelName {
419    pub fn new(name: String) -> Result<Self, String> {
420        if name.is_empty() {
421            return Err("Model name cannot be empty".to_string());
422        }
423        Ok(ModelName(VsfType::x(name)))
424    }
425
426    pub fn to_vsf_type(self) -> VsfType {
427        self.0
428    }
429
430    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
431        match vsf {
432            VsfType::x(ref s) if !s.is_empty() => Ok(ModelName(vsf)),
433            VsfType::x(_) => Err("Model name cannot be empty".to_string()),
434            _ => Err("Expected x type for model name".to_string()),
435        }
436    }
437}
438
439/// Serial number
440#[derive(Debug, Clone)]
441pub struct SerialNumber(VsfType);
442
443impl SerialNumber {
444    pub fn new(serial: String) -> Result<Self, String> {
445        if serial.is_empty() {
446            return Err("Serial number cannot be empty".to_string());
447        }
448        Ok(SerialNumber(VsfType::x(serial)))
449    }
450
451    pub fn to_vsf_type(self) -> VsfType {
452        self.0
453    }
454
455    pub fn from_vsf_type(vsf: VsfType) -> Result<Self, String> {
456        match vsf {
457            VsfType::x(ref s) if !s.is_empty() => Ok(SerialNumber(vsf)),
458            VsfType::x(_) => Err("Serial number cannot be empty".to_string()),
459            _ => Err("Expected x type for serial number".to_string()),
460        }
461    }
462}
463
464// ==================== RAW IMAGE METADATA STRUCTURES ====================
465
466/// Metadata for RAW image captures
467#[derive(Debug, Clone)]
468pub struct RawMetadata {
469    // Sensor characteristics
470    pub cfa_pattern: Option<CfaPattern>,
471    pub black_level: Option<BlackLevel>,
472    pub white_level: Option<WhiteLevel>,
473
474    // Calibration frames (by hash reference, not embedded)
475    pub dark_frame_hash: Option<CalibrationHash>,
476    pub flat_field_hash: Option<CalibrationHash>,
477    pub bias_frame_hash: Option<CalibrationHash>,
478    pub vignette_correction_hash: Option<CalibrationHash>,
479    pub distortion_correction_hash: Option<CalibrationHash>,
480
481    // Magic 9 (3×3 colour matrix: Sensor RGB → LMS)
482    pub magic_9: Option<Magic9>,
483}
484
485/// Camera settings at time of capture
486#[derive(Debug, Clone)]
487pub struct CameraSettings {
488    pub make: Option<Manufacturer>,
489    pub model: Option<ModelName>,
490    pub serial_number: Option<SerialNumber>,
491    pub iso_speed: Option<IsoSpeed>,
492    pub shutter_time_s: Option<ShutterTime>,
493    pub aperture_f_number: Option<Aperture>,
494    pub focal_length_m: Option<FocalLength>,
495    pub exposure_compensation: Option<ExposureCompensation>,
496    pub focus_distance_m: Option<FocusDistance>,
497    pub flash_fired: Option<FlashFired>,
498    pub metering_mode: Option<MeteringMode>,
499    // No white_balance - use magic_9 for Sensor→LMS conversion
500}
501
502/// Lens information
503#[derive(Debug, Clone)]
504pub struct LensInfo {
505    pub make: Option<Manufacturer>,
506    pub model: Option<ModelName>,
507    pub serial_number: Option<SerialNumber>,
508    pub min_focal_length_m: Option<FocalLength>,
509    pub max_focal_length_m: Option<FocalLength>,
510    pub min_aperture_f: Option<Aperture>, // Smallest aperture (largest f-number, e.g. f/22)
511    pub max_aperture_f: Option<Aperture>, // Largest aperture (smallest f-number, e.g. f/1.4)
512}
513
514// ==================== BUILDER PATTERN API ====================
515
516/// Builder for RawMetadata with convenient field access
517#[derive(Debug, Clone, Default)]
518pub struct RawMetadataBuilder {
519    pub cfa_pattern: Option<Vec<u8>>,
520    pub black_level: Option<f32>,
521    pub white_level: Option<f32>,
522    pub dark_frame_hash: Option<(u8, Vec<u8>)>,
523    pub flat_field_hash: Option<(u8, Vec<u8>)>,
524    pub bias_frame_hash: Option<(u8, Vec<u8>)>,
525    pub vignette_correction_hash: Option<(u8, Vec<u8>)>,
526    pub distortion_correction_hash: Option<(u8, Vec<u8>)>,
527    pub magic_9: Option<Vec<f32>>,
528}
529
530impl RawMetadataBuilder {
531    /// Convert builder to RawMetadata (returns None if all fields are None)
532    fn build(self) -> Result<Option<RawMetadata>, String> {
533        if self.cfa_pattern.is_none()
534            && self.black_level.is_none()
535            && self.white_level.is_none()
536            && self.dark_frame_hash.is_none()
537            && self.flat_field_hash.is_none()
538            && self.bias_frame_hash.is_none()
539            && self.vignette_correction_hash.is_none()
540            && self.distortion_correction_hash.is_none()
541            && self.magic_9.is_none()
542        {
543            return Ok(None);
544        }
545
546        Ok(Some(RawMetadata {
547            cfa_pattern: self.cfa_pattern.map(CfaPattern::new).transpose()?,
548            black_level: self.black_level.map(BlackLevel::new).transpose()?,
549            white_level: self.white_level.map(WhiteLevel::new).transpose()?,
550            dark_frame_hash: self
551                .dark_frame_hash
552                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
553                .transpose()?,
554            flat_field_hash: self
555                .flat_field_hash
556                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
557                .transpose()?,
558            bias_frame_hash: self
559                .bias_frame_hash
560                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
561                .transpose()?,
562            vignette_correction_hash: self
563                .vignette_correction_hash
564                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
565                .transpose()?,
566            distortion_correction_hash: self
567                .distortion_correction_hash
568                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
569                .transpose()?,
570            magic_9: self.magic_9.map(Magic9::new).transpose()?,
571        }))
572    }
573}
574
575/// Builder for CameraSettings with convenient field access
576#[derive(Debug, Clone, Default)]
577pub struct CameraBuilder {
578    pub make: Option<String>,
579    pub model: Option<String>,
580    pub serial_number: Option<String>,
581    pub iso_speed: Option<f32>,
582    pub shutter_time_s: Option<f32>,
583    pub aperture_f_number: Option<f32>,
584    pub focal_length_m: Option<f32>,
585    pub exposure_compensation: Option<f32>,
586    pub focus_distance_m: Option<f32>,
587    pub flash_fired: Option<bool>,
588    pub metering_mode: Option<String>,
589}
590
591impl CameraBuilder {
592    /// Convert builder to CameraSettings (returns None if all fields are None)
593    fn build(self) -> Result<Option<CameraSettings>, String> {
594        if self.make.is_none()
595            && self.model.is_none()
596            && self.serial_number.is_none()
597            && self.iso_speed.is_none()
598            && self.shutter_time_s.is_none()
599            && self.aperture_f_number.is_none()
600            && self.focal_length_m.is_none()
601            && self.exposure_compensation.is_none()
602            && self.focus_distance_m.is_none()
603            && self.flash_fired.is_none()
604            && self.metering_mode.is_none()
605        {
606            return Ok(None);
607        }
608
609        Ok(Some(CameraSettings {
610            make: self.make.map(Manufacturer::new).transpose()?,
611            model: self.model.map(ModelName::new).transpose()?,
612            serial_number: self.serial_number.map(SerialNumber::new).transpose()?,
613            iso_speed: self.iso_speed.map(IsoSpeed::new).transpose()?,
614            shutter_time_s: self.shutter_time_s.map(ShutterTime::new).transpose()?,
615            aperture_f_number: self.aperture_f_number.map(Aperture::new).transpose()?,
616            focal_length_m: self.focal_length_m.map(FocalLength::new).transpose()?,
617            exposure_compensation: self
618                .exposure_compensation
619                .map(ExposureCompensation::new)
620                .transpose()?,
621            focus_distance_m: self.focus_distance_m.map(FocusDistance::new).transpose()?,
622            flash_fired: self.flash_fired.map(FlashFired::new).transpose()?,
623            metering_mode: self.metering_mode.map(MeteringMode::new).transpose()?,
624        }))
625    }
626}
627
628/// Builder for LensInfo with convenient field access
629#[derive(Debug, Clone, Default)]
630pub struct LensBuilder {
631    pub make: Option<String>,
632    pub model: Option<String>,
633    pub serial_number: Option<String>,
634    pub min_focal_length_m: Option<f32>,
635    pub max_focal_length_m: Option<f32>,
636    pub min_aperture_f: Option<f32>,
637    pub max_aperture_f: Option<f32>,
638}
639
640impl LensBuilder {
641    /// Convert builder to LensInfo (returns None if all fields are None)
642    fn build(self) -> Result<Option<LensInfo>, String> {
643        if self.make.is_none()
644            && self.model.is_none()
645            && self.serial_number.is_none()
646            && self.min_focal_length_m.is_none()
647            && self.max_focal_length_m.is_none()
648            && self.min_aperture_f.is_none()
649            && self.max_aperture_f.is_none()
650        {
651            return Ok(None);
652        }
653
654        Ok(Some(LensInfo {
655            make: self.make.map(Manufacturer::new).transpose()?,
656            model: self.model.map(ModelName::new).transpose()?,
657            serial_number: self.serial_number.map(SerialNumber::new).transpose()?,
658            min_focal_length_m: self.min_focal_length_m.map(FocalLength::new).transpose()?,
659            max_focal_length_m: self.max_focal_length_m.map(FocalLength::new).transpose()?,
660            min_aperture_f: self.min_aperture_f.map(Aperture::new).transpose()?,
661            max_aperture_f: self.max_aperture_f.map(Aperture::new).transpose()?,
662        }))
663    }
664}
665
666/// Builder pattern for creating RAW images with ergonomic dot notation
667///
668/// # Example
669/// ```ignore
670/// use vsf::builders::RawImageBuilder; use vsf::types::BitPackedTensor;
671///
672/// let samples: Vec<u64> = vec![2048; 4096 * 3072]; let image = BitPackedTensor::pack(12, vec![4096, 3072], &samples);
673///
674/// let mut raw = RawImageBuilder::new(image); raw.camera.iso_speed = Some(800.0); raw.camera.shutter_time_s = Some(1.0 / 60.0); raw.raw.cfa_pattern = Some(vec![b'R', b'G', b'G', b'B']); raw.lens.make = Some("Sony".to_string());
675///
676/// let bytes = raw.build()?;
677/// ```
678#[derive(Debug, Clone)]
679pub struct RawImageBuilder {
680    image: BitPackedTensor,
681    pub raw: RawMetadataBuilder,
682    pub camera: CameraBuilder,
683    pub lens: LensBuilder,
684}
685
686impl RawImageBuilder {
687    /// Create a new RawImageBuilder with the image data
688    pub fn new(image: BitPackedTensor) -> Self {
689        Self {
690            image,
691            raw: RawMetadataBuilder::default(),
692            camera: CameraBuilder::default(),
693            lens: LensBuilder::default(),
694        }
695    }
696
697    /// Build the complete VSF RAW image file
698    pub fn build(self) -> Result<Vec<u8>, String> {
699        let metadata = self.raw.build()?;
700        let camera = self.camera.build()?;
701        let lens = self.lens.build()?;
702
703        build_raw_image(self.image, metadata, camera, lens)
704    }
705}
706
707// ==================== SIMPLE HELPER FUNCTIONS ====================
708
709/// Create a RAW camera image with arbitrary bit depth
710///
711/// Supports 1-256 bits per sample
712///
713/// # Arguments
714/// * `bit_depth` - Bits per sample (1-256, where 0 = 256) * `width` - Image width in samples * `height` - Image height in samples * `samples` - RAW sensor sample values (unreferenced, single-plane)
715///
716/// # Example
717/// ```ignore
718/// let samples: Vec<u64> = vec![2048; 4096 * 3072]; // 12-bit mid-gray let raw = raw_image(12, 4096, 3072, samples);
719/// ```
720pub fn raw_image(bit_depth: u8, width: usize, height: usize, samples: Vec<u64>) -> VsfType {
721    let tensor = BitPackedTensor::pack(bit_depth, vec![width, height], &samples);
722    VsfType::p(tensor)
723}
724
725/// Create a GPS track from lat/lon coordinates
726///
727/// Returns a 1D tensor of WorldCoord values
728///
729/// # Example
730/// ```ignore
731/// let track = gps_track(vec![ (40.7128, -74.0060),  // NYC (51.5074, -0.1278),   // London (35.6762, 139.6503),  // Tokyo ]);
732/// ```
733pub fn gps_track(coords: Vec<(f64, f64)>) -> Vec<WorldCoord> {
734    coords
735        .into_iter()
736        .map(|(lat, lon)| WorldCoord::from_lat_lon(lat, lon))
737        .collect()
738}
739
740/// Create a single GPS waypoint
741///
742/// # Example
743/// ```ignore
744/// let nyc = gps_waypoint(40.7128, -74.0060); let encoded = VsfType::w(nyc).flatten();
745/// ```
746pub fn gps_waypoint(lat: f64, lon: f64) -> WorldCoord {
747    WorldCoord::from_lat_lon(lat, lon)
748}
749
750/// Create a geotagged image with location metadata
751///
752/// Returns (image, location) tuple
753///
754/// # Example
755/// ```ignore
756/// let (img, loc) = geotagged_photo( 1920, 1080, rgb_data, 40.7128, -74.0060  // Photo taken in NYC );
757/// ```
758pub fn geotagged_photo(
759    width: usize,
760    height: usize,
761    rgb_data: Vec<u8>,
762    lat: f64,
763    lon: f64,
764) -> (VsfType, WorldCoord) {
765    let tensor = Tensor::new(vec![width, height, 3], rgb_data);
766    let img = VsfType::t_u3(tensor);
767    let loc = WorldCoord::from_lat_lon(lat, lon);
768    (img, loc)
769}
770
771// ==================== COMPLETE RAW IMAGE BUILDERS ====================
772
773/// Build a complete RAW image file with full metadata and calibration
774///
775/// **IMPORTANT:** The `image` parameter is a `BitPackedTensor` which is SELF-DESCRIBING. It already contains:
776/// - `bit_depth` (8, 10, 12, 14, 16, etc.)
777/// - `shape` ([width, height] like [4096, 3072])
778/// - `data` (the actual bitpacked pixels)
779///
780/// **DO NOT** add redundant width/height/bits_per_pixel fields! The `p` type has it all.
781///
782/// # VSF Structure Created
783/// ```text
784/// RÅ<...n1 or n2 labels...> [(dimage:p[bitdepth, shape, pixels])    ← Image is FIRST field (self-describing!) (diso speed:u...)                      ← Optional metadata follows (dshutter time ns:u...) (dcfa pattern:t_u3['R','G','G','B'])   ← ASCII characters for readability (dcolour matrix:t_f6[...])]
785/// ```
786///
787/// If TOKEN auth is provided, creates TWO labels: "token auth" and "raw" If no TOKEN auth, creates ONE label: "raw" only
788///
789/// # Arguments
790/// * `image` - BitPackedTensor (use `BitPackedTensor::pack(bit_depth, shape, samples)`) * `metadata` - Optional sensor metadata (CFA pattern, black/white levels, calibration hashes) * `camera` - Optional camera settings (ISO, shutter, aperture, etc.)
791/// * `lens` - Optional lens info (make, model, focal range, aperture range)
792///
793/// # Returns
794/// Complete VSF file bytes ready to write to disk
795///
796/// # Note
797/// To add cryptographic verification, use the verification module functions:
798/// - `verification::add_file_hash()` for full file integrity
799/// - `verification::sign_section()` for per-section signatures
800pub fn build_raw_image(
801    image: BitPackedTensor,
802    metadata: Option<RawMetadata>,
803    camera: Option<CameraSettings>,
804    lens: Option<LensInfo>,
805) -> Result<Vec<u8>, String> {
806    let mut builder = VsfBuilder::new();
807
808    // Build raw section - start with the image (p type has width, height, bit_depth)
809    let mut raw_items = vec![("image".to_string(), VsfType::p(image))];
810
811    // Add optional metadata
812    if let Some(meta) = metadata {
813        if let Some(cfa) = meta.cfa_pattern {
814            raw_items.push(("cfa_pattern".to_string(), cfa.to_vsf_type()));
815        }
816
817        if let Some(black) = meta.black_level {
818            raw_items.push(("black_level".to_string(), black.to_vsf_type()));
819        }
820
821        if let Some(white) = meta.white_level {
822            raw_items.push(("white_level".to_string(), white.to_vsf_type()));
823        }
824
825        // Calibration hashes (algorithm + hash bytes)
826        if let Some(hash) = meta.dark_frame_hash {
827            raw_items.push(("dark_frame_hash".to_string(), hash.to_vsf_type()));
828        }
829
830        if let Some(hash) = meta.flat_field_hash {
831            raw_items.push(("flat_field_hash".to_string(), hash.to_vsf_type()));
832        }
833
834        if let Some(hash) = meta.bias_frame_hash {
835            raw_items.push(("bias_frame_hash".to_string(), hash.to_vsf_type()));
836        }
837
838        if let Some(hash) = meta.vignette_correction_hash {
839            raw_items.push(("vignette_correction_hash".to_string(), hash.to_vsf_type()));
840        }
841
842        if let Some(hash) = meta.distortion_correction_hash {
843            raw_items.push(("distortion_correction_hash".to_string(), hash.to_vsf_type()));
844        }
845
846        // Magic 9 (3×3 colour matrix: Sensor RGB → LMS)
847        if let Some(matrix) = meta.magic_9 {
848            raw_items.push(("magic_9".to_string(), matrix.to_vsf_type()));
849        }
850    }
851
852    // Camera settings
853    if let Some(cam) = camera {
854        if let Some(make) = cam.make {
855            raw_items.push(("camera_make".to_string(), make.to_vsf_type()));
856        }
857
858        if let Some(model) = cam.model {
859            raw_items.push(("camera_model".to_string(), model.to_vsf_type()));
860        }
861
862        if let Some(serial) = cam.serial_number {
863            raw_items.push(("camera_serial".to_string(), serial.to_vsf_type()));
864        }
865
866        if let Some(iso) = cam.iso_speed {
867            raw_items.push(("iso_speed".to_string(), iso.to_vsf_type()));
868        }
869
870        if let Some(shutter) = cam.shutter_time_s {
871            raw_items.push(("shutter_time_s".to_string(), shutter.to_vsf_type()));
872        }
873
874        if let Some(aperture) = cam.aperture_f_number {
875            raw_items.push(("aperture_f_number".to_string(), aperture.to_vsf_type()));
876        }
877
878        if let Some(focal) = cam.focal_length_m {
879            raw_items.push(("focal_length_m".to_string(), focal.to_vsf_type()));
880        }
881
882        if let Some(comp) = cam.exposure_compensation {
883            raw_items.push(("exposure_compensation".to_string(), comp.to_vsf_type()));
884        }
885
886        if let Some(focus) = cam.focus_distance_m {
887            raw_items.push(("focus_distance_m".to_string(), focus.to_vsf_type()));
888        }
889
890        if let Some(flash) = cam.flash_fired {
891            raw_items.push(("flash_fired".to_string(), flash.to_vsf_type()));
892        }
893
894        if let Some(metering) = cam.metering_mode {
895            raw_items.push(("metering_mode".to_string(), metering.to_vsf_type()));
896        }
897    }
898
899    // Lens info
900    if let Some(l) = lens {
901        if let Some(make) = l.make {
902            raw_items.push(("lens_make".to_string(), make.to_vsf_type()));
903        }
904
905        if let Some(model) = l.model {
906            raw_items.push(("lens_model".to_string(), model.to_vsf_type()));
907        }
908
909        if let Some(serial) = l.serial_number {
910            raw_items.push(("lens_serial".to_string(), serial.to_vsf_type()));
911        }
912
913        if let Some(min_focal) = l.min_focal_length_m {
914            raw_items.push(("lens_min_focal_m".to_string(), min_focal.to_vsf_type()));
915        }
916
917        if let Some(max_focal) = l.max_focal_length_m {
918            raw_items.push(("lens_max_focal_m".to_string(), max_focal.to_vsf_type()));
919        }
920
921        if let Some(min_ap) = l.min_aperture_f {
922            raw_items.push(("lens_min_aperture".to_string(), min_ap.to_vsf_type()));
923        }
924
925        if let Some(max_ap) = l.max_aperture_f {
926            raw_items.push(("lens_max_aperture".to_string(), max_ap.to_vsf_type()));
927        }
928    }
929
930    builder = builder.add_section("raw", raw_items);
931
932    builder.build()
933}
934
935/// Convenience function for Lumis 12-bit captures
936///
937/// **Lumis sensor specs:**
938/// - Resolution: 4096×3072 (12.6 megapixels)
939/// - Bit depth: 12-bit (values 0-4095)
940/// - Bayer pattern: RGGB
941/// - Black level: 64
942/// - White level: 4095
943///
944/// **What this function does:**
945/// 1. Creates a `BitPackedTensor::pack(12, [4096, 3072], samples)` - this packs your 12-bit samples into the minimal bitpacked representation
946/// 2. Adds sensor metadata (CFA pattern, black/white levels)
947/// 3. Adds camera settings (ISO, shutter speed)
948///
949/// **The resulting p type contains EVERYTHING about the image:**
950/// - No separate width field (shape has it: [4096, 3072])
951/// - No separate bit_depth field (p encoding has it: 12)
952/// - No separate sample data section (p has the bitpacked bytes)
953///
954/// # Arguments
955/// * `samples` - RAW sensor sample values as u64 (0-4095 for 12-bit), will be bitpacked * `iso` - ISO speed (e.g., 100, 200, 400, 800, 1600, 3200) * `shutter_s` - Shutter time in seconds (e.g., 1./60. = 0.0167 for 1/60 second)
956pub fn lumis_raw_capture(samples: Vec<u64>, iso: f32, shutter_s: f32) -> Result<Vec<u8>, String> {
957    // Create BitPackedTensor for 12-bit Lumis sensor
958    let image = BitPackedTensor::pack(12, vec![4096, 3072], &samples);
959
960    build_raw_image(
961        image,
962        Some(RawMetadata {
963            cfa_pattern: Some(CfaPattern::new(vec![b'R', b'G', b'G', b'B'])?), // RGGB Bayer pattern
964            black_level: Some(BlackLevel::new(64.0)?),
965            white_level: Some(WhiteLevel::new(4095.0)?),
966            dark_frame_hash: None,
967            flat_field_hash: None,
968            bias_frame_hash: None,
969            vignette_correction_hash: None,
970            distortion_correction_hash: None,
971            magic_9: None,
972        }),
973        Some(CameraSettings {
974            make: None,
975            model: None,
976            serial_number: None,
977            iso_speed: Some(IsoSpeed::new(iso)?),
978            shutter_time_s: Some(ShutterTime::new(shutter_s)?),
979            aperture_f_number: None,
980            focal_length_m: None,
981            exposure_compensation: None,
982            focus_distance_m: None,
983            flash_fired: Some(FlashFired::new(false)?),
984            metering_mode: None,
985        }),
986        None, // No lens info (phone camera)
987    )
988}
989
990// ==================== COMPRESSED IMAGE ====================
991
992/// AV1 encoding marker for v-wrapped data
993pub const ENCODING_AV1: u8 = b'a';
994
995/// Build a compressed image (AV1 payload in VSF RGB colourspace)
996///
997/// Creates a minimal VSF file with:
998/// - Provenance hash only (no rolling hash, no signature)
999/// - AV1-compressed pixel data wrapped in v type (`va`)
1000///
1001/// The `va` encoding tells us it's AV1, and AV1 bitstream contains dimensions. Provenance hash ensures integrity. No redundant metadata needed.
1002///
1003/// Assumes VSF RGB colourspace (gamma 2, Rec.2020 primaries).
1004///
1005/// # Arguments
1006/// * `av1_data` - AV1-encoded pixel data
1007///
1008/// # Returns
1009/// Complete VSF file bytes ready to write to disk
1010pub fn compressed_image(av1_data: Vec<u8>) -> Result<Vec<u8>, String> {
1011    VsfBuilder::new()
1012        .provenance_only()
1013        .add_section(
1014            "image",
1015            vec![("pixels".to_string(), VsfType::v(ENCODING_AV1, av1_data))],
1016        )
1017        .build()
1018}
1019
1020/// Parsed compressed image from a VSF file
1021pub struct ParsedCompressedImage {
1022    pub encoding: u8,
1023    pub data: Vec<u8>,
1024}
1025
1026/// Parse a compressed image VSF file
1027///
1028/// Extracts encoding type and compressed pixel data. Dimensions come from decoding the AV1 bitstream.
1029///
1030/// # Arguments
1031/// * `data` - Complete VSF file bytes
1032///
1033/// # Returns
1034/// ParsedCompressedImage or error
1035pub fn parse_compressed_image(data: &[u8]) -> Result<ParsedCompressedImage, String> {
1036    use crate::file_format::{VsfHeader, VsfSection};
1037
1038    // Parse header using library function
1039    let (header, _) = VsfHeader::decode(data)?;
1040
1041    // Find the "image" section
1042    let image_field = header
1043        .fields
1044        .iter()
1045        .find(|f| f.name == "image")
1046        .ok_or("Required 'image' section not found")?;
1047
1048    // Parse section at offset
1049    let mut ptr = image_field.offset_bytes;
1050    let section = VsfSection::parse(data, &mut ptr)?;
1051
1052    // Extract pixels field
1053    let pixels_field = section
1054        .get_field("pixels")
1055        .ok_or("Missing 'pixels' field in image section")?;
1056
1057    // Get first value (the v-wrapped data)
1058    let value = pixels_field.values.first().ok_or("Empty 'pixels' field")?;
1059
1060    match value {
1061        VsfType::v(encoding, pixel_data) => Ok(ParsedCompressedImage {
1062            encoding: *encoding,
1063            data: pixel_data.clone(),
1064        }),
1065        _ => Err("Expected v type for pixels field".to_string()),
1066    }
1067}
1068
1069// ==================== RAW IMAGE PARSER ====================
1070
1071/// Parsed RAW image data from a VSF file
1072pub struct ParsedRawImage {
1073    pub image: BitPackedTensor,
1074    pub metadata: Option<RawMetadata>,
1075    pub camera: Option<CameraSettings>,
1076    pub lens: Option<LensInfo>,
1077}
1078
1079// Helper to convert any VsfType unsigned variant to Rust usize
1080fn to_usize(vsf_type: &VsfType) -> Option<usize> {
1081    match vsf_type {
1082        VsfType::u(v, _) => Some(*v),        // usize → usize (no conversion)
1083        VsfType::u0(b) => Some(*b as usize), // bool → usize
1084        VsfType::u3(v) => Some(*v as usize), // u8 → usize (widening)
1085        VsfType::u4(v) => Some(*v as usize), // u16 → usize (widening)
1086        VsfType::u5(v) => Some(*v as usize), // u32 → usize (safe on 64-bit)
1087        VsfType::u6(v) => Some(*v as usize), // u64 → usize (safe on 64-bit)
1088        VsfType::u7(v) => Some(*v as usize), // u128 → usize (truncates!)
1089        _ => None,
1090    }
1091}
1092
1093/// Parse a VSF RAW image file
1094///
1095/// Extracts the image BitPackedTensor and all metadata fields from a VSF RAW file.
1096///
1097/// # Arguments
1098/// * `data` - The complete VSF file bytes
1099///
1100/// # Returns
1101/// ParsedRawImage containing the image and optional metadata, or an error
1102pub fn parse_raw_image(data: &[u8]) -> Result<ParsedRawImage, String> {
1103    use crate::crypto_algorithms::{HASH_BLAKE3, HASH_SHA256, HASH_SHA512};
1104    use crate::file_format::{VsfHeader, VsfSection};
1105
1106    // Parse header using library function
1107    let (header, _) = VsfHeader::decode(data)?;
1108
1109    // Find the "raw" section
1110    let raw_field = header
1111        .fields
1112        .iter()
1113        .find(|f| f.name == "raw")
1114        .ok_or("Required 'raw' section not found")?;
1115
1116    // Parse section at offset
1117    let mut ptr = raw_field.offset_bytes;
1118    let section = VsfSection::parse(data, &mut ptr)?;
1119
1120    // Helper to get first value from a field
1121    fn get_first_value<'a>(section: &'a VsfSection, name: &str) -> Option<&'a VsfType> {
1122        section.get_field(name)?.values.first()
1123    }
1124
1125    // Extract image (required)
1126    let image = match get_first_value(&section, "image") {
1127        Some(VsfType::p(tensor)) => tensor.clone(),
1128        _ => return Err("Missing required 'image' field".to_string()),
1129    };
1130
1131    // Initialize metadata fields
1132    let mut cfa_pattern: Option<Vec<u8>> = None;
1133    let mut black_level: Option<f32> = None;
1134    let mut white_level: Option<f32> = None;
1135    let mut dark_frame_hash: Option<(u8, Vec<u8>)> = None;
1136    let mut flat_field_hash: Option<(u8, Vec<u8>)> = None;
1137    let mut bias_frame_hash: Option<(u8, Vec<u8>)> = None;
1138    let mut vignette_correction_hash: Option<(u8, Vec<u8>)> = None;
1139    let mut distortion_correction_hash: Option<(u8, Vec<u8>)> = None;
1140    let mut magic_9: Option<Vec<f32>> = None;
1141
1142    let mut camera_make: Option<String> = None;
1143    let mut camera_model: Option<String> = None;
1144    let mut camera_serial: Option<String> = None;
1145    let mut iso_speed: Option<f32> = None;
1146    let mut shutter_time_s: Option<f32> = None;
1147    let mut aperture_f_number: Option<f32> = None;
1148    let mut focal_length_m: Option<f32> = None;
1149    let mut exposure_compensation: Option<f32> = None;
1150    let mut focus_distance_m: Option<f32> = None;
1151    let mut flash_fired: Option<bool> = None;
1152    let mut metering_mode: Option<String> = None;
1153
1154    let mut lens_make: Option<String> = None;
1155    let mut lens_model: Option<String> = None;
1156    let mut lens_serial: Option<String> = None;
1157    let mut lens_min_focal_m: Option<f32> = None;
1158    let mut lens_max_focal_m: Option<f32> = None;
1159    let mut lens_min_aperture: Option<f32> = None;
1160    let mut lens_max_aperture: Option<f32> = None;
1161
1162    // Helper to parse hash fields
1163    fn parse_hash(value: &VsfType) -> Option<(u8, Vec<u8>)> {
1164        match value {
1165            VsfType::hb(v) => Some((HASH_BLAKE3, v.clone())),
1166            VsfType::hs(v) => {
1167                let algo = if v.len() == 32 {
1168                    HASH_SHA256
1169                } else {
1170                    HASH_SHA512
1171                };
1172                Some((algo, v.clone()))
1173            }
1174            _ => None,
1175        }
1176    }
1177
1178    // Extract optional fields from section
1179    for field in &section.fields {
1180        let value = match field.values.first() {
1181            Some(v) => v,
1182            None => continue,
1183        };
1184
1185        match field.name.as_str() {
1186            // Raw metadata
1187            "cfa_pattern" => {
1188                if let VsfType::t_u3(tensor) = value {
1189                    cfa_pattern = Some(tensor.data.clone());
1190                }
1191            }
1192            "black_level" => {
1193                if let VsfType::f5(v) = value {
1194                    black_level = Some(*v);
1195                }
1196            }
1197            "white_level" => {
1198                if let VsfType::f5(v) = value {
1199                    white_level = Some(*v);
1200                }
1201            }
1202            "dark_frame_hash" => dark_frame_hash = parse_hash(value),
1203            "flat_field_hash" => flat_field_hash = parse_hash(value),
1204            "bias_frame_hash" => bias_frame_hash = parse_hash(value),
1205            "vignette_correction_hash" => vignette_correction_hash = parse_hash(value),
1206            "distortion_correction_hash" => distortion_correction_hash = parse_hash(value),
1207            "magic_9" => {
1208                if let VsfType::t_f5(tensor) = value {
1209                    magic_9 = Some(tensor.data.clone());
1210                }
1211            }
1212            // Camera settings
1213            "camera_make" => {
1214                if let VsfType::x(v) = value {
1215                    camera_make = Some(v.clone());
1216                }
1217            }
1218            "camera_model" => {
1219                if let VsfType::x(v) = value {
1220                    camera_model = Some(v.clone());
1221                }
1222            }
1223            "camera_serial" => {
1224                if let VsfType::x(v) = value {
1225                    camera_serial = Some(v.clone());
1226                }
1227            }
1228            "iso_speed" => {
1229                if let VsfType::f5(v) = value {
1230                    iso_speed = Some(*v);
1231                }
1232            }
1233            "shutter_time_s" => {
1234                if let VsfType::f5(v) = value {
1235                    shutter_time_s = Some(*v);
1236                }
1237            }
1238            "aperture_f_number" => {
1239                if let VsfType::f5(v) = value {
1240                    aperture_f_number = Some(*v);
1241                }
1242            }
1243            "focal_length_m" => {
1244                if let VsfType::f5(v) = value {
1245                    focal_length_m = Some(*v);
1246                }
1247            }
1248            "exposure_compensation" => {
1249                if let VsfType::f5(v) = value {
1250                    exposure_compensation = Some(*v);
1251                }
1252            }
1253            "focus_distance_m" => {
1254                if let VsfType::f5(v) = value {
1255                    focus_distance_m = Some(*v);
1256                }
1257            }
1258            "flash_fired" => flash_fired = to_usize(value).map(|v| v != 0),
1259            "metering_mode" => {
1260                if let VsfType::x(v) = value {
1261                    metering_mode = Some(v.clone());
1262                }
1263            }
1264            // Lens info
1265            "lens_make" => {
1266                if let VsfType::x(v) = value {
1267                    lens_make = Some(v.clone());
1268                }
1269            }
1270            "lens_model" => {
1271                if let VsfType::x(v) = value {
1272                    lens_model = Some(v.clone());
1273                }
1274            }
1275            "lens_serial" => {
1276                if let VsfType::x(v) = value {
1277                    lens_serial = Some(v.clone());
1278                }
1279            }
1280            "lens_min_focal_m" => {
1281                if let VsfType::f5(v) = value {
1282                    lens_min_focal_m = Some(*v);
1283                }
1284            }
1285            "lens_max_focal_m" => {
1286                if let VsfType::f5(v) = value {
1287                    lens_max_focal_m = Some(*v);
1288                }
1289            }
1290            "lens_min_aperture" => {
1291                if let VsfType::f5(v) = value {
1292                    lens_min_aperture = Some(*v);
1293                }
1294            }
1295            "lens_max_aperture" => {
1296                if let VsfType::f5(v) = value {
1297                    lens_max_aperture = Some(*v);
1298                }
1299            }
1300            _ => {} // Unknown field, skip
1301        }
1302    }
1303
1304    // Build metadata structs from parsed fields (converting to newtypes)
1305    let raw_metadata = if cfa_pattern.is_some()
1306        || black_level.is_some()
1307        || white_level.is_some()
1308        || dark_frame_hash.is_some()
1309        || flat_field_hash.is_some()
1310        || bias_frame_hash.is_some()
1311        || vignette_correction_hash.is_some()
1312        || distortion_correction_hash.is_some()
1313        || magic_9.is_some()
1314    {
1315        Some(RawMetadata {
1316            cfa_pattern: cfa_pattern.map(CfaPattern::new).transpose()?,
1317            black_level: black_level.map(BlackLevel::new).transpose()?,
1318            white_level: white_level.map(WhiteLevel::new).transpose()?,
1319            dark_frame_hash: dark_frame_hash
1320                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
1321                .transpose()?,
1322            flat_field_hash: flat_field_hash
1323                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
1324                .transpose()?,
1325            bias_frame_hash: bias_frame_hash
1326                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
1327                .transpose()?,
1328            vignette_correction_hash: vignette_correction_hash
1329                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
1330                .transpose()?,
1331            distortion_correction_hash: distortion_correction_hash
1332                .map(|(alg, hash)| CalibrationHash::new(alg, hash))
1333                .transpose()?,
1334            magic_9: magic_9.map(Magic9::new).transpose()?,
1335        })
1336    } else {
1337        None
1338    };
1339
1340    let camera_settings = if camera_make.is_some()
1341        || camera_model.is_some()
1342        || camera_serial.is_some()
1343        || iso_speed.is_some()
1344        || shutter_time_s.is_some()
1345        || aperture_f_number.is_some()
1346        || focal_length_m.is_some()
1347        || exposure_compensation.is_some()
1348        || focus_distance_m.is_some()
1349        || flash_fired.is_some()
1350        || metering_mode.is_some()
1351    {
1352        Some(CameraSettings {
1353            make: camera_make.map(Manufacturer::new).transpose()?,
1354            model: camera_model.map(ModelName::new).transpose()?,
1355            serial_number: camera_serial.map(SerialNumber::new).transpose()?,
1356            iso_speed: iso_speed.map(IsoSpeed::new).transpose()?,
1357            shutter_time_s: shutter_time_s.map(ShutterTime::new).transpose()?,
1358            aperture_f_number: aperture_f_number.map(Aperture::new).transpose()?,
1359            focal_length_m: focal_length_m.map(FocalLength::new).transpose()?,
1360            exposure_compensation: exposure_compensation
1361                .map(ExposureCompensation::new)
1362                .transpose()?,
1363            focus_distance_m: focus_distance_m.map(FocusDistance::new).transpose()?,
1364            flash_fired: flash_fired.map(FlashFired::new).transpose()?,
1365            metering_mode: metering_mode.map(MeteringMode::new).transpose()?,
1366        })
1367    } else {
1368        None
1369    };
1370
1371    let lens_info = if lens_make.is_some()
1372        || lens_model.is_some()
1373        || lens_serial.is_some()
1374        || lens_min_focal_m.is_some()
1375        || lens_max_focal_m.is_some()
1376        || lens_min_aperture.is_some()
1377        || lens_max_aperture.is_some()
1378    {
1379        Some(LensInfo {
1380            make: lens_make.map(Manufacturer::new).transpose()?,
1381            model: lens_model.map(ModelName::new).transpose()?,
1382            serial_number: lens_serial.map(SerialNumber::new).transpose()?,
1383            min_focal_length_m: lens_min_focal_m.map(FocalLength::new).transpose()?,
1384            max_focal_length_m: lens_max_focal_m.map(FocalLength::new).transpose()?,
1385            min_aperture_f: lens_min_aperture.map(Aperture::new).transpose()?,
1386            max_aperture_f: lens_max_aperture.map(Aperture::new).transpose()?,
1387        })
1388    } else {
1389        None
1390    };
1391
1392    Ok(ParsedRawImage {
1393        image,
1394        metadata: raw_metadata,
1395        camera: camera_settings,
1396        lens: lens_info,
1397    })
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402    use super::*;
1403    use crate::crypto_algorithms::HASH_BLAKE3;
1404
1405    #[test]
1406    fn test_text_document() {
1407        let doc = VsfType::x("Hello, VSF!".to_string());
1408        if let VsfType::x(s) = doc {
1409            assert_eq!(s, "Hello, VSF!");
1410        } else {
1411            panic!("Expected string");
1412        }
1413    }
1414
1415    #[test]
1416    fn test_raw_image_12bit() {
1417        let samples = vec![2048u64; 100 * 50]; // 100×50 mid-gray
1418        let img = raw_image(12, 100, 50, samples);
1419
1420        if let VsfType::p(tensor) = img {
1421            assert_eq!(tensor.bit_depth, 12);
1422            assert_eq!(tensor.shape, vec![100, 50]);
1423            assert_eq!(tensor.len(), 100 * 50);
1424        } else {
1425            panic!("Expected bitpacked tensor");
1426        }
1427    }
1428
1429    #[test]
1430    fn test_grayscale_image() {
1431        let data = vec![128u8; 64 * 48];
1432        let tensor = Tensor::new(vec![64, 48], data);
1433        let img = VsfType::t_u3(tensor);
1434
1435        if let VsfType::t_u3(tensor) = img {
1436            assert_eq!(tensor.shape, vec![64, 48]);
1437            assert_eq!(tensor.data.len(), 64 * 48);
1438        } else {
1439            panic!("Expected u8 tensor");
1440        }
1441    }
1442
1443    #[test]
1444    fn test_rgb_image() {
1445        let data = vec![255u8; 64 * 48 * 3];
1446        let tensor = Tensor::new(vec![64, 48, 3], data);
1447        let img = VsfType::t_u3(tensor);
1448
1449        if let VsfType::t_u3(tensor) = img {
1450            assert_eq!(tensor.shape, vec![64, 48, 3]);
1451            assert_eq!(tensor.data.len(), 64 * 48 * 3);
1452        } else {
1453            panic!("Expected u8 tensor");
1454        }
1455    }
1456
1457    #[test]
1458    fn test_gps_track() {
1459        let track = gps_track(vec![
1460            (40.7128, -74.0060), // NYC
1461            (51.5074, -0.1278),  // London
1462        ]);
1463
1464        assert_eq!(track.len(), 2);
1465    }
1466
1467    #[test]
1468    fn test_gps_waypoint() {
1469        // Use simple coordinates (equator, prime meridian)
1470        let point = gps_waypoint(0.0, 0.0);
1471        let (lat, lon) = point.to_lat_lon();
1472
1473        // Check reasonable precision
1474        assert!(lat.abs() < 10.0, "Lat error: {}", lat.abs());
1475        assert!(lon.abs() < 10.0, "Lon error: {}", lon.abs());
1476    }
1477
1478    #[test]
1479    fn test_geotagged_photo() {
1480        let rgb_data = vec![0u8; 100 * 100 * 3];
1481        // Use simple coordinates
1482        let (img, loc) = geotagged_photo(100, 100, rgb_data, 0.0, 0.0);
1483
1484        if let VsfType::t_u3(tensor) = img {
1485            assert_eq!(tensor.shape, vec![100, 100, 3]);
1486        } else {
1487            panic!("Expected RGB tensor");
1488        }
1489
1490        let (lat, lon) = loc.to_lat_lon();
1491        assert!(lat.abs() < 10.0);
1492        assert!(lon.abs() < 10.0);
1493    }
1494
1495    #[test]
1496    fn test_complete_raw_image_minimal() {
1497        // Minimal RAW: just the image, no metadata
1498        let samples: Vec<u64> = vec![255; 64]; // 8x8, all white
1499        let image = BitPackedTensor::pack(8, vec![8, 8], &samples);
1500
1501        let result = build_raw_image(image, None, None, None);
1502
1503        assert!(result.is_ok());
1504        let bytes = result.unwrap();
1505
1506        // Verify magic number (RÅ is 3 bytes in UTF-8)
1507        assert_eq!(&bytes[0..3], "RÅ".as_bytes());
1508        assert_eq!(bytes[3], b'<');
1509
1510        // Verify file is structured correctly Should have header + one "raw" section with p type
1511        assert!(bytes.len() > 50); // Minimal file should be small
1512    }
1513
1514    #[cfg(feature = "text-encode")]
1515    #[test]
1516    fn test_complete_raw_image_with_metadata() {
1517        let samples: Vec<u64> = vec![255; 64]; // 8x8
1518        let image = BitPackedTensor::pack(8, vec![8, 8], &samples);
1519
1520        let result = build_raw_image(
1521            image,
1522            Some(RawMetadata {
1523                cfa_pattern: Some(CfaPattern::new(vec![b'R', b'G', b'G', b'B']).unwrap()),
1524                black_level: Some(BlackLevel::new(64.0).unwrap()),
1525                white_level: Some(WhiteLevel::new(255.0).unwrap()),
1526                dark_frame_hash: Some(CalibrationHash::new(HASH_BLAKE3, vec![0xAB; 32]).unwrap()),
1527                flat_field_hash: None,
1528                bias_frame_hash: None,
1529                vignette_correction_hash: None,
1530                distortion_correction_hash: None,
1531                magic_9: Some(
1532                    Magic9::new(vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).unwrap(),
1533                ),
1534            }),
1535            Some(CameraSettings {
1536                make: None,
1537                model: None,
1538                serial_number: None,
1539                iso_speed: Some(IsoSpeed::new(800.0).unwrap()),
1540                shutter_time_s: Some(ShutterTime::new(1. / 60.).unwrap()), // 1/60 second
1541                aperture_f_number: Some(Aperture::new(2.8).unwrap()),
1542                focal_length_m: Some(FocalLength::new(0.024).unwrap()), // 24mm = 0.024m
1543                exposure_compensation: None,
1544                focus_distance_m: None,
1545                flash_fired: Some(FlashFired::new(false).unwrap()),
1546                metering_mode: Some(MeteringMode::new("matrix".to_string()).unwrap()),
1547            }),
1548            None,
1549        );
1550
1551        assert!(result.is_ok());
1552        let bytes = result.unwrap();
1553
1554        // Verify magic number (RÅ is 3 bytes in UTF-8)
1555        assert_eq!(&bytes[0..3], "RÅ".as_bytes());
1556
1557        // Should contain version markers
1558        assert!(bytes.contains(&b'z'));
1559        assert!(bytes.contains(&b'y'));
1560
1561        // Should contain section brackets
1562        assert!(bytes.contains(&b'['));
1563        assert!(bytes.contains(&b']'));
1564    }
1565
1566    #[test]
1567    fn test_lumis_raw_capture() {
1568        // Lumis 12-bit: 4096x3072 = 12,582,912 pixels Samples are u64 values (0-4095), will be bitpacked by the function
1569        let pixel_count = 4096 * 3072;
1570        let samples: Vec<u64> = vec![2048; pixel_count]; // Mid-gray
1571
1572        let result = lumis_raw_capture(
1573            samples,
1574            800.0,
1575            1. / 60., // 1/60 second shutter
1576        );
1577
1578        assert!(result.is_ok());
1579        let bytes = result.unwrap();
1580
1581        // Verify magic number (RÅ is 3 bytes in UTF-8)
1582        assert_eq!(&bytes[0..3], "RÅ".as_bytes());
1583
1584        // File should be large (header + metadata + ~18.9MB bitpacked pixels) 12-bit × 12.6M pixels = 18.9MB
1585        assert!(
1586            bytes.len() > 18_000_000,
1587            "File should be > 18MB with bitpacked pixels"
1588        );
1589    }
1590
1591    #[test]
1592    fn test_roundtrip_minimal_raw() {
1593        // Create minimal RAW image
1594        let samples: Vec<u64> = (0..16).collect(); // 0-15
1595        let original_image = BitPackedTensor::pack(8, vec![4, 4], &samples);
1596
1597        // Build VSF file
1598        let raw_bytes = build_raw_image(original_image.clone(), None, None, None).unwrap();
1599
1600        // Parse it back
1601        let parsed = parse_raw_image(&raw_bytes).unwrap();
1602
1603        // Verify the image matches
1604        assert_eq!(parsed.image.bit_depth, 8);
1605        assert_eq!(parsed.image.shape, vec![4, 4]);
1606
1607        // Unpack and compare pixels
1608        let original_samples = original_image.unpack().into_u64();
1609        let parsed_samples = parsed.image.unpack().into_u64();
1610        assert_eq!(parsed_samples, original_samples);
1611        assert_eq!(parsed_samples, samples);
1612
1613        // Verify no metadata was present
1614        assert!(parsed.metadata.is_none());
1615        assert!(parsed.camera.is_none());
1616        assert!(parsed.lens.is_none());
1617    }
1618
1619    #[cfg(feature = "text-encode")]
1620    #[test]
1621    fn test_roundtrip_full_metadata() {
1622        // Create image with full metadata
1623        let samples: Vec<u64> = vec![200; 64]; // 8x8
1624        let original_image = BitPackedTensor::pack(8, vec![8, 8], &samples);
1625
1626        let original_metadata = RawMetadata {
1627            cfa_pattern: Some(CfaPattern::new(vec![b'R', b'G', b'G', b'B']).unwrap()), // RGGB Bayer pattern
1628            black_level: Some(BlackLevel::new(64.0).unwrap()),
1629            white_level: Some(WhiteLevel::new(255.0).unwrap()),
1630            dark_frame_hash: Some(CalibrationHash::new(HASH_BLAKE3, vec![0xAB; 32]).unwrap()),
1631            flat_field_hash: Some(CalibrationHash::new(HASH_BLAKE3, vec![0xCD; 32]).unwrap()),
1632            bias_frame_hash: None,
1633            vignette_correction_hash: None,
1634            distortion_correction_hash: None,
1635            magic_9: Some(Magic9::new(vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]).unwrap()),
1636        };
1637
1638        let original_camera = CameraSettings {
1639            make: Some(Manufacturer::new("TestCam".to_string()).unwrap()),
1640            model: Some(ModelName::new("Model X".to_string()).unwrap()),
1641            serial_number: Some(SerialNumber::new("CAM123456".to_string()).unwrap()),
1642            iso_speed: Some(IsoSpeed::new(800.0).unwrap()),
1643            shutter_time_s: Some(ShutterTime::new(1. / 60.).unwrap()), // 1/60 sec
1644            aperture_f_number: Some(Aperture::new(2.8).unwrap()),
1645            focal_length_m: Some(FocalLength::new(0.050).unwrap()), // 50mm = 0.050m
1646            exposure_compensation: Some(ExposureCompensation::new(-0.5).unwrap()),
1647            focus_distance_m: Some(FocusDistance::new(3.5).unwrap()),
1648            flash_fired: Some(FlashFired::new(false).unwrap()),
1649            metering_mode: Some(MeteringMode::new("matrix".to_string()).unwrap()),
1650        };
1651
1652        // Build VSF file
1653        let raw_bytes = build_raw_image(
1654            original_image.clone(),
1655            Some(original_metadata.clone()),
1656            Some(original_camera.clone()),
1657            None, // No lens
1658        )
1659        .unwrap();
1660
1661        // Parse it back
1662        let parsed = parse_raw_image(&raw_bytes).unwrap();
1663
1664        // Verify image
1665        assert_eq!(parsed.image.bit_depth, 8);
1666        assert_eq!(parsed.image.shape, vec![8, 8]);
1667        let parsed_samples = parsed.image.unpack().into_u64();
1668        assert_eq!(parsed_samples, samples);
1669
1670        // Verify metadata round-tripped successfully
1671        assert!(parsed.metadata.is_some());
1672        let _meta = parsed.metadata.unwrap();
1673        // Note: Can't use assert_eq on newtypes (no PartialEq), but successful parsing validates data
1674
1675        // Verify camera settings round-tripped successfully
1676        assert!(parsed.camera.is_some());
1677        let _cam = parsed.camera.unwrap();
1678        // Note: Can't use assert_eq on newtypes (no PartialEq), but successful parsing validates data
1679
1680        // Verify no lens
1681        assert!(parsed.lens.is_none());
1682    }
1683
1684    #[test]
1685    fn test_preamble_structure() {
1686        // Create a simple RAW image
1687        let samples: Vec<u64> = vec![100; 16]; // 4x4
1688        let image = BitPackedTensor::pack(8, vec![4, 4], &samples);
1689
1690        let raw_bytes = build_raw_image(
1691            image,
1692            Some(RawMetadata {
1693                cfa_pattern: Some(CfaPattern::new(vec![b'R', b'G', b'G', b'B']).unwrap()),
1694                black_level: Some(BlackLevel::new(64.0).unwrap()),
1695                white_level: Some(WhiteLevel::new(255.0).unwrap()),
1696                dark_frame_hash: None,
1697                flat_field_hash: None,
1698                bias_frame_hash: None,
1699                vignette_correction_hash: None,
1700                distortion_correction_hash: None,
1701                magic_9: None,
1702            }),
1703            None,
1704            None,
1705        )
1706        .unwrap();
1707
1708        // Verify file structure
1709        assert_eq!(&raw_bytes[0..3], "RÅ".as_bytes()); // Magic
1710        assert_eq!(raw_bytes[3], b'<'); // Header start
1711
1712        // Find the first section (after header)
1713        let header_end = raw_bytes.iter().position(|&b| b == b'>').unwrap();
1714
1715        // In v4 wire format, sections start after header '>' There may be no preamble, or there may be additional metadata Just verify we can find a section marker '['
1716        let section_start = raw_bytes[header_end..]
1717            .iter()
1718            .position(|&b| b == b'[')
1719            .expect("Expected to find section start '['");
1720        assert!(
1721            section_start < 200,
1722            "Section should start soon after header"
1723        );
1724    }
1725
1726    #[test]
1727    fn test_builder_pattern_minimal() {
1728        // Test minimal builder with just image
1729        let samples: Vec<u64> = (0..16).collect();
1730        let image = BitPackedTensor::pack(8, vec![4, 4], &samples);
1731
1732        let raw = RawImageBuilder::new(image);
1733        let result = raw.build();
1734
1735        assert!(result.is_ok());
1736        let bytes = result.unwrap();
1737
1738        // Verify magic number
1739        assert_eq!(&bytes[0..3], "RÅ".as_bytes());
1740
1741        // Parse and verify
1742        let parsed = parse_raw_image(&bytes).unwrap();
1743        assert_eq!(parsed.image.bit_depth, 8);
1744        assert_eq!(parsed.image.shape, vec![4, 4]);
1745    }
1746
1747    #[cfg(feature = "text-encode")]
1748    #[test]
1749    fn test_builder_pattern_camera_settings() {
1750        // Test builder with camera settings
1751        let samples: Vec<u64> = vec![100; 64];
1752        let image = BitPackedTensor::pack(8, vec![8, 8], &samples);
1753
1754        let mut raw = RawImageBuilder::new(image);
1755        raw.camera.iso_speed = Some(800.0);
1756        raw.camera.shutter_time_s = Some(1.0 / 60.0);
1757        raw.camera.aperture_f_number = Some(2.8);
1758        raw.camera.flash_fired = Some(false);
1759        raw.camera.metering_mode = Some("matrix".to_string());
1760
1761        let result = raw.build();
1762        assert!(result.is_ok());
1763        let bytes = result.unwrap();
1764
1765        // Parse and verify camera settings round-tripped successfully
1766        let parsed = parse_raw_image(&bytes).unwrap();
1767        assert!(parsed.camera.is_some());
1768        // Note: Can't use assert_eq on newtypes (no PartialEq), but successful parsing validates data
1769    }
1770
1771    #[test]
1772    fn test_builder_pattern_raw_metadata() {
1773        // Test builder with raw metadata
1774        let samples: Vec<u64> = vec![100; 64];
1775        let image = BitPackedTensor::pack(8, vec![8, 8], &samples);
1776
1777        let mut raw = RawImageBuilder::new(image);
1778        raw.raw.cfa_pattern = Some(vec![b'R', b'G', b'G', b'B']);
1779        raw.raw.black_level = Some(64.0);
1780        raw.raw.white_level = Some(4095.0);
1781        raw.raw.dark_frame_hash = Some((HASH_BLAKE3, vec![0xAB; 32]));
1782
1783        let result = raw.build();
1784        assert!(result.is_ok());
1785        let bytes = result.unwrap();
1786
1787        // Parse and verify metadata round-tripped successfully
1788        let parsed = parse_raw_image(&bytes).unwrap();
1789        assert!(parsed.metadata.is_some());
1790        // Note: Can't use assert_eq on newtypes (no PartialEq), but successful parsing validates data
1791    }
1792
1793    #[cfg(feature = "text-encode")]
1794    #[test]
1795    fn test_builder_pattern_lens_info() {
1796        // Test builder with lens info
1797        let samples: Vec<u64> = vec![100; 64];
1798        let image = BitPackedTensor::pack(8, vec![8, 8], &samples);
1799
1800        let mut raw = RawImageBuilder::new(image);
1801        raw.lens.make = Some("Sony".to_string());
1802        raw.lens.model = Some("FE 24-70mm F2.8 GM II".to_string());
1803        raw.lens.serial_number = Some("ABC123456".to_string());
1804        raw.lens.min_focal_length_m = Some(0.024); // 24mm
1805        raw.lens.max_focal_length_m = Some(0.070); // 70mm
1806        raw.lens.min_aperture_f = Some(22.0);
1807        raw.lens.max_aperture_f = Some(2.8);
1808
1809        let result = raw.build();
1810        assert!(result.is_ok());
1811        let bytes = result.unwrap();
1812
1813        // Parse and verify lens info round-tripped successfully
1814        let parsed = parse_raw_image(&bytes).unwrap();
1815        assert!(parsed.lens.is_some());
1816        // Note: Can't use assert_eq on newtypes (no PartialEq), but successful parsing validates data
1817    }
1818
1819    #[cfg(feature = "text-encode")]
1820    #[test]
1821    fn test_builder_pattern_full() {
1822        // Test builder with all fields populated
1823        let samples: Vec<u64> = vec![2048; 64];
1824        let image = BitPackedTensor::pack(12, vec![8, 8], &samples);
1825
1826        let mut raw = RawImageBuilder::new(image);
1827
1828        // Raw metadata
1829        raw.raw.cfa_pattern = Some(vec![b'R', b'G', b'G', b'B']);
1830        raw.raw.black_level = Some(64.0);
1831        raw.raw.white_level = Some(4095.0);
1832        raw.raw.magic_9 = Some(vec![1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
1833
1834        // Camera settings
1835        raw.camera.iso_speed = Some(800.0);
1836        raw.camera.shutter_time_s = Some(1.0 / 125.0);
1837        raw.camera.aperture_f_number = Some(2.8);
1838        raw.camera.focal_length_m = Some(0.050); // 50mm
1839        raw.camera.exposure_compensation = Some(-0.5);
1840        raw.camera.focus_distance_m = Some(3.5);
1841        raw.camera.flash_fired = Some(false);
1842        raw.camera.metering_mode = Some("spot".to_string());
1843
1844        // Lens info
1845        raw.lens.make = Some("Sony".to_string());
1846        raw.lens.model = Some("FE 50mm F1.2 GM".to_string());
1847
1848        let result = raw.build();
1849        assert!(result.is_ok());
1850        let bytes = result.unwrap();
1851
1852        // Parse and verify everything
1853        let parsed = parse_raw_image(&bytes).unwrap();
1854
1855        // Verify image
1856        assert_eq!(parsed.image.bit_depth, 12);
1857        assert_eq!(parsed.image.shape, vec![8, 8]);
1858
1859        // Verify all sections round-tripped successfully
1860        assert!(parsed.metadata.is_some());
1861        assert!(parsed.camera.is_some());
1862        assert!(parsed.lens.is_some());
1863        // Note: Can't use assert_eq on newtypes (no PartialEq), but successful parsing validates data
1864    }
1865
1866    #[test]
1867    fn test_cfa_pattern_validation() {
1868        let samples: Vec<u64> = vec![100; 16];
1869        let image = BitPackedTensor::pack(8, vec![4, 4], &samples);
1870
1871        // Valid patterns should work
1872        let valid_patterns = vec![
1873            vec![b'R', b'G', b'G', b'B'],                               // RGGB Bayer
1874            vec![b'G', b'R', b'B', b'G'],                               // GRBG Bayer
1875            vec![b'B', b'G', b'G', b'R'],                               // BGGR Bayer
1876            vec![b'C', b'Y', b'Y', b'G'],                               // CYYG
1877            vec![b'R', b'G', b'B', b'E', b'W', b'C', b'Y', b'R', b'G'], // 3×3 custom
1878        ];
1879
1880        for cfa in valid_patterns {
1881            let result = build_raw_image(
1882                image.clone(),
1883                Some(RawMetadata {
1884                    cfa_pattern: Some(CfaPattern::new(cfa.clone()).unwrap()),
1885                    black_level: None,
1886                    white_level: None,
1887                    dark_frame_hash: None,
1888                    flat_field_hash: None,
1889                    bias_frame_hash: None,
1890                    vignette_correction_hash: None,
1891                    distortion_correction_hash: None,
1892                    magic_9: None,
1893                }),
1894                None,
1895                None,
1896            );
1897            assert!(
1898                result.is_ok(),
1899                "Valid CFA pattern {:?} should be accepted",
1900                cfa
1901            );
1902        }
1903
1904        // Invalid patterns should fail
1905        let invalid_patterns = vec![
1906            vec![b'R', b'G', b'X', b'B'], // X is not valid
1907            vec![0, 1, 1, 2],             // Numeric values not allowed
1908            vec![b'r', b'g', b'g', b'b'], // Lowercase not valid
1909        ];
1910
1911        for cfa in invalid_patterns {
1912            // Invalid patterns should fail at CfaPattern::new()
1913            let cfa_result = CfaPattern::new(cfa.clone());
1914            assert!(
1915                cfa_result.is_err(),
1916                "Invalid CFA pattern {:?} should be rejected",
1917                cfa
1918            );
1919        }
1920    }
1921}