colorbox/formats/
cube_iridas.rs

1//! IRIDAS .cube LUT format.
2//!
3//! This format can contain either a 1D LUT or a 3D LUT, but not both,
4//! and there is no indication in the file extension which is which.
5
6use std::io::{BufRead, Write};
7
8use super::filter_non_finite;
9use crate::lut::{Lut1D, Lut3D};
10
11/// Writes a 1D .cube file.
12pub fn write_1d<W: Write>(
13    mut writer: W,
14    ranges: [(f32, f32); 3],
15    tables: [&[f32]; 3],
16) -> std::io::Result<()> {
17    assert!(tables[0].len() == tables[1].len() && tables[1].len() == tables[2].len());
18
19    writer.write_all(b"TITLE \"untitled\"\n")?;
20    writer.write_all(
21        format!(
22            "DOMAIN_MIN {} {} {}\n",
23            filter_non_finite(ranges[0].0),
24            filter_non_finite(ranges[1].0),
25            filter_non_finite(ranges[2].0),
26        )
27        .as_bytes(),
28    )?;
29    writer.write_all(
30        format!(
31            "DOMAIN_MAX {} {} {}\n",
32            filter_non_finite(ranges[0].1),
33            filter_non_finite(ranges[1].1),
34            filter_non_finite(ranges[2].1),
35        )
36        .as_bytes(),
37    )?;
38    writer.write_all(format!("LUT_1D_SIZE {}\n", tables[0].len()).as_bytes())?;
39
40    for ((r, g), b) in tables[0]
41        .iter()
42        .copied()
43        .zip(tables[1].iter().copied())
44        .zip(tables[2].iter().copied())
45    {
46        writer.write_all(
47            format!(
48                "{} {} {}\n",
49                filter_non_finite(r),
50                filter_non_finite(g),
51                filter_non_finite(b),
52            )
53            .as_bytes(),
54        )?;
55    }
56
57    Ok(())
58}
59
60/// Writes a 3D .cube file.
61///
62/// The tables should have a length of `resolution * resolution * resolution`,
63/// and their indices should be ordered the same as the `Lut3D` type.
64pub fn write_3d<W: Write>(
65    mut writer: W,
66    ranges: [(f32, f32); 3],
67    resolution: usize,
68    tables: [&[f32]; 3],
69) -> std::io::Result<()> {
70    assert!(tables[0].len() == (resolution * resolution * resolution));
71    assert!(tables[0].len() == tables[1].len() && tables[1].len() == tables[2].len());
72
73    writer.write_all(b"TITLE \"untitled\"\n")?;
74    writer.write_all(
75        format!(
76            "DOMAIN_MIN {} {} {}\n",
77            filter_non_finite(ranges[0].0),
78            filter_non_finite(ranges[1].0),
79            filter_non_finite(ranges[2].0),
80        )
81        .as_bytes(),
82    )?;
83    writer.write_all(
84        format!(
85            "DOMAIN_MAX {} {} {}\n",
86            filter_non_finite(ranges[0].1),
87            filter_non_finite(ranges[1].1),
88            filter_non_finite(ranges[2].1),
89        )
90        .as_bytes(),
91    )?;
92    writer.write_all(format!("LUT_3D_SIZE {}\n", resolution).as_bytes())?;
93
94    for ((r, g), b) in tables[0]
95        .iter()
96        .copied()
97        .zip(tables[1].iter().copied())
98        .zip(tables[2].iter().copied())
99    {
100        writer.write_all(
101            format!(
102                "{} {} {}\n",
103                filter_non_finite(r),
104                filter_non_finite(g),
105                filter_non_finite(b),
106            )
107            .as_bytes(),
108        )?;
109    }
110
111    Ok(())
112}
113
114/// Reads a 1D .cube file.
115pub fn read_1d<R: BufRead>(reader: R) -> Result<Lut1D, super::ReadError> {
116    // let mut name: Option<String> = None;
117    let mut ranges = [(0.0f32, 1.0f32); 3];
118    let mut length = None;
119    let mut tables = [Vec::new(), Vec::new(), Vec::new()];
120
121    for line in reader.lines() {
122        let line = line?;
123        let parts: Vec<_> = line.split_whitespace().collect();
124
125        if parts.is_empty() || parts[0].starts_with("#") {
126            continue;
127        } else if parts[0] == "TITLE" && parts.len() > 1 {
128            let name_parts: Vec<_> = line.trim().split("\"").collect();
129            if name_parts.len() != 3 || !name_parts[2].is_empty() {
130                return Err(super::ReadError::FormatErr);
131            }
132            // name = Some(name_parts[1].into());
133            continue;
134        } else if parts[0] == "DOMAIN_MIN" && parts.len() == 4 {
135            ranges[0].0 = parts[1].parse::<f32>()?;
136            ranges[1].0 = parts[2].parse::<f32>()?;
137            ranges[2].0 = parts[3].parse::<f32>()?;
138            continue;
139        } else if parts[0] == "DOMAIN_MAX" && parts.len() == 4 {
140            ranges[0].1 = parts[1].parse::<f32>()?;
141            ranges[1].1 = parts[2].parse::<f32>()?;
142            ranges[2].1 = parts[3].parse::<f32>()?;
143            continue;
144        } else if parts[0] == "LUT_1D_SIZE" && parts.len() == 2 {
145            length = Some(parts[1].parse::<usize>()?);
146            continue;
147        } else if parts.len() == 3 {
148            tables[0].push(parts[0].parse::<f32>()?);
149            tables[1].push(parts[1].parse::<f32>()?);
150            tables[2].push(parts[2].parse::<f32>()?);
151            continue;
152        } else {
153            // Line didn't match any acceptable pattern.
154            return Err(super::ReadError::FormatErr);
155        }
156    }
157
158    if !tables.iter().flatten().all(|n| n.is_finite())
159        || !ranges.iter().all(|(a, b)| a.is_finite() && b.is_finite())
160    {
161        // Non-finite values in the file.
162        return Err(super::ReadError::FormatErr);
163    }
164
165    let [table_r, table_g, table_b] = tables;
166    match length {
167        Some(len) if len == table_r.len() => Ok(Lut1D {
168            ranges: vec![ranges[0], ranges[1], ranges[2]],
169            tables: vec![table_r, table_g, table_b],
170        }),
171        _ => Err(super::ReadError::FormatErr),
172    }
173}
174
175/// Reads a 3D .cube file.
176pub fn read_3d<R: BufRead>(reader: R) -> Result<Lut3D, super::ReadError> {
177    // let mut name: Option<String> = None;
178    let mut ranges = [(0.0f32, 1.0f32); 3];
179    let mut resolution = None;
180    let mut tables = [Vec::new(), Vec::new(), Vec::new()];
181
182    for line in reader.lines() {
183        let line = line?;
184        let parts: Vec<_> = line.split_whitespace().collect();
185
186        if parts.is_empty() || parts[0].starts_with("#") {
187            continue;
188        } else if parts[0] == "TITLE" && parts.len() > 1 {
189            let name_parts: Vec<_> = line.trim().split("\"").collect();
190            if name_parts.len() != 3 || !name_parts[2].is_empty() {
191                return Err(super::ReadError::FormatErr);
192            }
193            // name = Some(name_parts[1].into());
194            continue;
195        } else if parts[0] == "DOMAIN_MIN" && parts.len() == 4 {
196            ranges[0].0 = parts[1].parse::<f32>()?;
197            ranges[1].0 = parts[2].parse::<f32>()?;
198            ranges[2].0 = parts[3].parse::<f32>()?;
199            continue;
200        } else if parts[0] == "DOMAIN_MAX" && parts.len() == 4 {
201            ranges[0].1 = parts[1].parse::<f32>()?;
202            ranges[1].1 = parts[2].parse::<f32>()?;
203            ranges[2].1 = parts[3].parse::<f32>()?;
204            continue;
205        } else if parts[0] == "LUT_3D_SIZE" && parts.len() == 2 {
206            resolution = Some(parts[1].parse::<usize>()?);
207            continue;
208        } else if parts.len() == 3 {
209            tables[0].push(parts[0].parse::<f32>()?);
210            tables[1].push(parts[1].parse::<f32>()?);
211            tables[2].push(parts[2].parse::<f32>()?);
212            continue;
213        } else {
214            // Line didn't match any acceptable pattern.
215            return Err(super::ReadError::FormatErr);
216        }
217    }
218
219    if !tables.iter().flatten().all(|n| n.is_finite())
220        || !ranges.iter().all(|(a, b)| a.is_finite() && b.is_finite())
221    {
222        // Non-finite values in the file.
223        return Err(super::ReadError::FormatErr);
224    }
225
226    let [table_r, table_g, table_b] = tables;
227    match resolution {
228        Some(res) if (res * res * res) == table_r.len() => Ok(Lut3D {
229            range: ranges,
230            resolution: [res, res, res],
231            tables: vec![table_r, table_g, table_b],
232        }),
233        _ => Err(super::ReadError::FormatErr),
234    }
235}