Skip to main content

SpectrumFile

Enum SpectrumFile 

Source
pub enum SpectrumFile {
    Single {
        schema_version: String,
        spectrum: Box<SpectrumRecord>,
    },
    Batch {
        schema_version: String,
        batch_metadata: Option<Box<BatchMetadata>>,
        spectra: Vec<SpectrumRecord>,
    },
}
Expand description

The top-level structure of a spectrum JSON file. Tagged by file_type: either "single" or "batch".

Variants§

§

Single

Fields

§schema_version: String
§

Batch

Fields

§schema_version: String
§batch_metadata: Option<Box<BatchMetadata>>

Implementations§

Source§

impl SpectrumFile

Source

pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self>

Load and fully validate a UV-Vis JSON file from a file path. Runs structural schema validation then cross-field checks.

Source

pub fn from_json_str(json: &str) -> Result<Self>

Load and fully validate a UV-Vis JSON file from a JSON string.

Source

pub fn from_str_unchecked(json: &str) -> Result<Self>

Deserialise without any validation. Useful when you fully trust the source.

Source

pub fn spectra(&self) -> Vec<&SpectrumRecord>

Returns all spectra in the file (works for both single and batch).

Examples found in repository?
examples/cie_csv_to_json.rs (line 878)
779fn main() {
780    let args: Vec<String> = env::args().collect();
781    let mut input_dir = PathBuf::from("data/cie-raw");
782    let mut output_dir = PathBuf::from("data/spectral-io/cie");
783
784    let mut i = 1usize;
785    while i < args.len() {
786        match args[i].as_str() {
787            "--input" => {
788                i += 1;
789                if i < args.len() {
790                    input_dir = PathBuf::from(&args[i]);
791                }
792            }
793            "--output" => {
794                i += 1;
795                if i < args.len() {
796                    output_dir = PathBuf::from(&args[i]);
797                }
798            }
799            arg => {
800                eprintln!("Unknown argument: {arg}");
801                process::exit(1);
802            }
803        }
804        i += 1;
805    }
806
807    let datasets = datasets();
808    let mut ok = 0usize;
809    let mut skipped = 0usize;
810    let mut failed = 0usize;
811
812    for ds in &datasets {
813        let csv_path = input_dir.join(ds.csv_file);
814        if !csv_path.exists() {
815            eprintln!("  SKIP  {} (file not found)", ds.csv_file);
816            skipped += 1;
817            continue;
818        }
819
820        let raw_owned = match fs::read_to_string(&csv_path) {
821            Ok(s) => s,
822            Err(e) => {
823                eprintln!("  ERROR reading {}: {e}", ds.csv_file);
824                failed += 1;
825                continue;
826            }
827        };
828        // Strip a UTF-8 BOM that some CIE files carry; if left in place it
829        // ends up in the middle of the synthetic header + raw concatenation
830        // where csv_parse's own BOM strip (which only fires at position 0)
831        // cannot reach it.
832        let raw = raw_owned.strip_prefix('\u{FEFF}').unwrap_or(&raw_owned);
833
834        let n_cols = count_data_cols(raw);
835        if n_cols == 0 {
836            eprintln!("  ERROR {}: no data rows found", ds.csv_file);
837            failed += 1;
838            continue;
839        }
840
841        let synthetic = format!("{}{}", build_header(ds, n_cols), raw);
842
843        let mut file = match SpectrumFile::from_csv_str(&synthetic) {
844            Ok(f) => f,
845            Err(e) => {
846                eprintln!("  ERROR parsing {}: {e}", ds.csv_file);
847                failed += 1;
848                continue;
849            }
850        };
851
852        strip_nan_entries(&mut file);
853        set_provenance(&mut file, ds.csv_file);
854
855        let out_dir = output_dir.join(ds.subdir);
856        if let Err(e) = fs::create_dir_all(&out_dir) {
857            eprintln!("  ERROR creating {}: {e}", out_dir.display());
858            failed += 1;
859            continue;
860        }
861
862        let json = match serde_json::to_string_pretty(&file) {
863            Ok(j) => j,
864            Err(e) => {
865                eprintln!("  ERROR serialising {}: {e}", ds.json_file);
866                failed += 1;
867                continue;
868            }
869        };
870
871        let out_path = out_dir.join(ds.json_file);
872        if let Err(e) = fs::write(&out_path, &json) {
873            eprintln!("  ERROR writing {}: {e}", out_path.display());
874            failed += 1;
875            continue;
876        }
877
878        let n_spectra = file.spectra().len();
879        eprintln!(
880            "  OK    {} → {} ({} {})",
881            ds.csv_file,
882            out_path.display(),
883            n_spectra,
884            if n_spectra == 1 {
885                "spectrum"
886            } else {
887                "spectra"
888            }
889        );
890        ok += 1;
891    }
892
893    // Alpha-opic handled separately (per-column wavelength ranges)
894    if convert_alpha_opic(&input_dir, &output_dir) {
895        ok += 1;
896    } else if !input_dir.join("CIE_a-opic_action_spectra.csv").exists() {
897        skipped += 1;
898    } else {
899        failed += 1;
900    }
901
902    eprintln!();
903    eprintln!("{ok} converted, {skipped} skipped, {failed} failed");
904    if failed > 0 {
905        process::exit(1);
906    }
907}
Source

pub fn schema_version(&self) -> &str

The schema version declared in the file.

Source

pub fn batch_metadata(&self) -> Option<&BatchMetadata>

Batch metadata, if this is a batch file.

Source§

impl SpectrumFile

Source

pub fn from_spectrashop_path<P: AsRef<Path>>(path: P) -> Result<Self>

Load a SpectraShop text-export file from a path.

Parses the SpectraShop tab-separated text format (.txt) and converts each data record into a SpectrumRecord. File-level metadata (illuminant, observer, geometry, etc.) is applied to every record. Returns SpectrumFile::Single for one record or SpectrumFile::Batch for multiple records.

Non-UTF-8 bytes (e.g. Latin-1 encoded files) are replaced with U+FFFD.

Examples found in repository?
examples/spectrashop_to_json.rs (line 58)
24fn main() {
25    let args: Vec<String> = env::args().collect();
26    let prog = &args[0];
27
28    let mut copyright: Option<String> = None;
29    let mut positional: Vec<String> = Vec::new();
30
31    let mut i = 1;
32    while i < args.len() {
33        match args[i].as_str() {
34            "-c" => {
35                i += 1;
36                if i >= args.len() {
37                    eprintln!("error: -c requires an argument");
38                    usage(prog);
39                }
40                copyright = Some(args[i].clone());
41            }
42            arg if arg.starts_with('-') => {
43                eprintln!("error: unknown option '{arg}'");
44                usage(prog);
45            }
46            _ => positional.push(args[i].clone()),
47        }
48        i += 1;
49    }
50
51    if positional.is_empty() {
52        usage(prog);
53    }
54
55    let input = PathBuf::from(&positional[0]);
56    let output: Option<PathBuf> = positional.get(1).map(PathBuf::from);
57
58    let mut file = SpectrumFile::from_spectrashop_path(&input).unwrap_or_else(|e| {
59        eprintln!("error: {e}");
60        process::exit(1);
61    });
62
63    if let Some(ref cr) = copyright {
64        match &mut file {
65            SpectrumFile::Single { spectrum, .. } => {
66                spectrum.metadata.copyright = Some(cr.clone());
67            }
68            SpectrumFile::Batch { spectra, .. } => {
69                for sp in spectra.iter_mut() {
70                    sp.metadata.copyright = Some(cr.clone());
71                }
72            }
73        }
74    }
75
76    let json = serde_json::to_string_pretty(&file).unwrap_or_else(|e| {
77        eprintln!("error serialising to JSON: {e}");
78        process::exit(1);
79    });
80
81    match output {
82        Some(ref path) => {
83            fs::write(path, &json).unwrap_or_else(|e| {
84                eprintln!("error writing to {}: {e}", path.display());
85                process::exit(1);
86            });
87            eprintln!("Written to {}", path.display());
88        }
89        None => print!("{json}"),
90    }
91}
Source

pub fn from_spectrashop_str(input: &str) -> Result<Self>

Parse a SpectraShop text-export string.

See SpectrumFile::from_spectrashop_path for format details.

Source§

impl SpectrumFile

Source

pub fn from_csv_path<P: AsRef<Path>>(path: P) -> Result<Self>

Load a CSV or TSV spectral data file from a path.

The delimiter (tab or comma) is auto-detected. An optional header block of KEY: VALUE lines precedes the data. The first row whose first cell parses as a number starts the data block; the immediately preceding non-blank line (if non-numeric) is treated as the column-header row. First data column = wavelength in nm; each subsequent column becomes one SpectrumRecord. Returns SpectrumFile::Single for one data column or SpectrumFile::Batch for multiple.

Source

pub fn from_csv_str(input: &str) -> Result<Self>

Parse a CSV or TSV spectral data string.

See SpectrumFile::from_csv_path for format details.

Examples found in repository?
examples/cie_csv_to_json.rs (line 843)
779fn main() {
780    let args: Vec<String> = env::args().collect();
781    let mut input_dir = PathBuf::from("data/cie-raw");
782    let mut output_dir = PathBuf::from("data/spectral-io/cie");
783
784    let mut i = 1usize;
785    while i < args.len() {
786        match args[i].as_str() {
787            "--input" => {
788                i += 1;
789                if i < args.len() {
790                    input_dir = PathBuf::from(&args[i]);
791                }
792            }
793            "--output" => {
794                i += 1;
795                if i < args.len() {
796                    output_dir = PathBuf::from(&args[i]);
797                }
798            }
799            arg => {
800                eprintln!("Unknown argument: {arg}");
801                process::exit(1);
802            }
803        }
804        i += 1;
805    }
806
807    let datasets = datasets();
808    let mut ok = 0usize;
809    let mut skipped = 0usize;
810    let mut failed = 0usize;
811
812    for ds in &datasets {
813        let csv_path = input_dir.join(ds.csv_file);
814        if !csv_path.exists() {
815            eprintln!("  SKIP  {} (file not found)", ds.csv_file);
816            skipped += 1;
817            continue;
818        }
819
820        let raw_owned = match fs::read_to_string(&csv_path) {
821            Ok(s) => s,
822            Err(e) => {
823                eprintln!("  ERROR reading {}: {e}", ds.csv_file);
824                failed += 1;
825                continue;
826            }
827        };
828        // Strip a UTF-8 BOM that some CIE files carry; if left in place it
829        // ends up in the middle of the synthetic header + raw concatenation
830        // where csv_parse's own BOM strip (which only fires at position 0)
831        // cannot reach it.
832        let raw = raw_owned.strip_prefix('\u{FEFF}').unwrap_or(&raw_owned);
833
834        let n_cols = count_data_cols(raw);
835        if n_cols == 0 {
836            eprintln!("  ERROR {}: no data rows found", ds.csv_file);
837            failed += 1;
838            continue;
839        }
840
841        let synthetic = format!("{}{}", build_header(ds, n_cols), raw);
842
843        let mut file = match SpectrumFile::from_csv_str(&synthetic) {
844            Ok(f) => f,
845            Err(e) => {
846                eprintln!("  ERROR parsing {}: {e}", ds.csv_file);
847                failed += 1;
848                continue;
849            }
850        };
851
852        strip_nan_entries(&mut file);
853        set_provenance(&mut file, ds.csv_file);
854
855        let out_dir = output_dir.join(ds.subdir);
856        if let Err(e) = fs::create_dir_all(&out_dir) {
857            eprintln!("  ERROR creating {}: {e}", out_dir.display());
858            failed += 1;
859            continue;
860        }
861
862        let json = match serde_json::to_string_pretty(&file) {
863            Ok(j) => j,
864            Err(e) => {
865                eprintln!("  ERROR serialising {}: {e}", ds.json_file);
866                failed += 1;
867                continue;
868            }
869        };
870
871        let out_path = out_dir.join(ds.json_file);
872        if let Err(e) = fs::write(&out_path, &json) {
873            eprintln!("  ERROR writing {}: {e}", out_path.display());
874            failed += 1;
875            continue;
876        }
877
878        let n_spectra = file.spectra().len();
879        eprintln!(
880            "  OK    {} → {} ({} {})",
881            ds.csv_file,
882            out_path.display(),
883            n_spectra,
884            if n_spectra == 1 {
885                "spectrum"
886            } else {
887                "spectra"
888            }
889        );
890        ok += 1;
891    }
892
893    // Alpha-opic handled separately (per-column wavelength ranges)
894    if convert_alpha_opic(&input_dir, &output_dir) {
895        ok += 1;
896    } else if !input_dir.join("CIE_a-opic_action_spectra.csv").exists() {
897        skipped += 1;
898    } else {
899        failed += 1;
900    }
901
902    eprintln!();
903    eprintln!("{ok} converted, {skipped} skipped, {failed} failed");
904    if failed > 0 {
905        process::exit(1);
906    }
907}
Source

pub fn to_tsv(&self) -> String

Serialise to a tab-separated string.

Writes a KEY: VALUE metadata header derived from the first spectrum, followed by a column-header row and one data row per wavelength point. For a batch file all spectra are written as parallel columns sharing the wavelength axis of the first spectrum.

Source

pub fn to_csv(&self) -> String

Serialise to a comma-separated string.

See SpectrumFile::to_tsv for format details.

Source

pub fn write_tsv<P: AsRef<Path>>(&self, path: P) -> Result<()>

Write a tab-separated file to the given path.

Source

pub fn write_csv<P: AsRef<Path>>(&self, path: P) -> Result<()>

Write a comma-separated file to the given path.

Trait Implementations§

Source§

impl Clone for SpectrumFile

Source§

fn clone(&self) -> SpectrumFile

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for SpectrumFile

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for SpectrumFile

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl FromStr for SpectrumFile

Source§

type Err = SpectrumFileError

The associated error which can be returned from parsing.
Source§

fn from_str(s: &str) -> Result<Self>

Parses a string s to return a value of this type. Read more
Source§

impl Serialize for SpectrumFile

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,