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§
Implementations§
Source§impl SpectrumFile
impl SpectrumFile
Sourcepub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self>
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.
Sourcepub fn from_json_str(json: &str) -> Result<Self>
pub fn from_json_str(json: &str) -> Result<Self>
Load and fully validate a UV-Vis JSON file from a JSON string.
Sourcepub fn from_str_unchecked(json: &str) -> Result<Self>
pub fn from_str_unchecked(json: &str) -> Result<Self>
Deserialise without any validation. Useful when you fully trust the source.
Sourcepub fn spectra(&self) -> Vec<&SpectrumRecord>
pub fn spectra(&self) -> Vec<&SpectrumRecord>
Returns all spectra in the file (works for both single and batch).
Examples found in repository?
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}Sourcepub fn schema_version(&self) -> &str
pub fn schema_version(&self) -> &str
The schema version declared in the file.
Sourcepub fn batch_metadata(&self) -> Option<&BatchMetadata>
pub fn batch_metadata(&self) -> Option<&BatchMetadata>
Batch metadata, if this is a batch file.
Source§impl SpectrumFile
impl SpectrumFile
Sourcepub fn from_spectrashop_path<P: AsRef<Path>>(path: P) -> Result<Self>
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?
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}Sourcepub fn from_spectrashop_str(input: &str) -> Result<Self>
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
impl SpectrumFile
Sourcepub fn from_csv_path<P: AsRef<Path>>(path: P) -> Result<Self>
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.
Sourcepub fn from_csv_str(input: &str) -> Result<Self>
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?
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}Sourcepub fn to_tsv(&self) -> String
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.
Sourcepub fn to_csv(&self) -> String
pub fn to_csv(&self) -> String
Serialise to a comma-separated string.
See SpectrumFile::to_tsv for format details.
Trait Implementations§
Source§impl Clone for SpectrumFile
impl Clone for SpectrumFile
Source§fn clone(&self) -> SpectrumFile
fn clone(&self) -> SpectrumFile
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more