Skip to main content

ocio/config/
colorspace.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright Contributors to the OpenColorIO Project.
3// Copyright (c) 2026 Philippe Llerena.
4
5//! Color spaces and sets of color spaces (port of `ColorSpace.cpp` and
6//! `ColorSpaceSet.cpp`).
7
8use super::tokens::TokensManager;
9use super::transform_display::format_transform;
10use super::utils::{compare, contain, fmt_f32, join, lower, remove};
11use crate::error::{Error, Result};
12use crate::transforms::Transform;
13use crate::types::{Allocation, BitDepth, ColorSpaceDirection, ReferenceSpaceType};
14use std::collections::BTreeMap;
15use std::fmt;
16
17const KNOWN_INTERCHANGE_NAMES: [&str; 2] = ["amf_transform_ids", "icc_profile_name"];
18
19/// Look up a known interchange attribute name (case insensitive).
20pub(crate) fn find_interchange_key(known: &[&'static str], name: &str) -> Result<&'static str> {
21    known
22        .iter()
23        .find(|k| compare(k, name))
24        .copied()
25        .ok_or_else(|| Error::msg(format!("Unknown attribute name '{name}'.")))
26}
27
28/// A color space: a named encoding of colors plus the transforms to and from
29/// the reference space.
30#[derive(Debug, Clone, PartialEq)]
31pub struct ColorSpace {
32    name: String,
33    family: String,
34    equality_group: String,
35    description: String,
36    encoding: String,
37    interop_id: String,
38    aliases: Vec<String>,
39    interchange: BTreeMap<String, String>,
40    bit_depth: BitDepth,
41    is_data: bool,
42    reference_space: ReferenceSpaceType,
43    allocation: Allocation,
44    allocation_vars: Vec<f32>,
45    to_reference: Option<Transform>,
46    from_reference: Option<Transform>,
47    categories: TokensManager,
48}
49
50impl Default for ColorSpace {
51    fn default() -> Self {
52        Self::new(ReferenceSpaceType::Scene)
53    }
54}
55
56impl ColorSpace {
57    /// An empty color space using the given reference space.
58    pub fn new(reference_space: ReferenceSpaceType) -> Self {
59        Self {
60            name: String::new(),
61            family: String::new(),
62            equality_group: String::new(),
63            description: String::new(),
64            encoding: String::new(),
65            interop_id: String::new(),
66            aliases: Vec::new(),
67            interchange: BTreeMap::new(),
68            bit_depth: BitDepth::Unknown,
69            is_data: false,
70            reference_space,
71            allocation: Allocation::Uniform,
72            allocation_vars: Vec::new(),
73            to_reference: None,
74            from_reference: None,
75            categories: TokensManager::new(),
76        }
77    }
78
79    /// A color space with a name, using the scene reference space.
80    pub fn with_name(name: &str) -> Self {
81        let mut cs = Self::new(ReferenceSpaceType::Scene);
82        cs.set_name(name);
83        cs
84    }
85
86    /// The name.
87    pub fn name(&self) -> &str {
88        &self.name
89    }
90
91    /// Set the name (an alias with the same name is removed).
92    pub fn set_name(&mut self, name: &str) {
93        self.name = name.to_string();
94        let n = self.name.clone();
95        remove(&mut self.aliases, &n);
96    }
97
98    /// Number of aliases.
99    pub fn num_aliases(&self) -> usize {
100        self.aliases.len()
101    }
102
103    /// Alias at `idx` (`""` if out of range).
104    pub fn alias(&self, idx: usize) -> &str {
105        self.aliases.get(idx).map(|s| s.as_str()).unwrap_or("")
106    }
107
108    /// All the aliases.
109    pub fn aliases(&self) -> &[String] {
110        &self.aliases
111    }
112
113    /// True if the alias exists (case insensitive).
114    pub fn has_alias(&self, alias: &str) -> bool {
115        self.aliases.iter().any(|a| compare(a, alias))
116    }
117
118    /// Add an alias (ignored if empty, equal to the name or already present).
119    pub fn add_alias(&mut self, alias: &str) {
120        if !alias.is_empty() && !compare(alias, &self.name) && !contain(&self.aliases, alias) {
121            self.aliases.push(alias.to_string());
122        }
123    }
124
125    /// Remove an alias (case insensitive).
126    pub fn remove_alias(&mut self, alias: &str) {
127        if !alias.is_empty() {
128            remove(&mut self.aliases, alias);
129        }
130    }
131
132    /// Remove all the aliases.
133    pub fn clear_aliases(&mut self) {
134        self.aliases.clear();
135    }
136
137    /// The family.
138    pub fn family(&self) -> &str {
139        &self.family
140    }
141    /// Set the family.
142    pub fn set_family(&mut self, family: &str) {
143        self.family = family.to_string();
144    }
145    /// The equality group.
146    pub fn equality_group(&self) -> &str {
147        &self.equality_group
148    }
149    /// Set the equality group.
150    pub fn set_equality_group(&mut self, group: &str) {
151        self.equality_group = group.to_string();
152    }
153    /// The description.
154    pub fn description(&self) -> &str {
155        &self.description
156    }
157    /// Set the description.
158    pub fn set_description(&mut self, description: &str) {
159        self.description = description.to_string();
160    }
161
162    /// The interop id.
163    pub fn interop_id(&self) -> &str {
164        &self.interop_id
165    }
166
167    /// Set the interop id, validating its syntax.
168    pub fn set_interop_id(&mut self, id: &str) -> Result<()> {
169        if !id.is_empty() {
170            let allowed = |c: char| {
171                c.is_ascii_digit()
172                    || c.is_ascii_lowercase()
173                    || matches!(
174                        c,
175                        '.' | '-'
176                            | '_'
177                            | '~'
178                            | '/'
179                            | '*'
180                            | '#'
181                            | '%'
182                            | '^'
183                            | '+'
184                            | '('
185                            | ')'
186                            | '['
187                            | ']'
188                            | '|'
189                            | ':'
190                    )
191            };
192            if !id.chars().all(allowed) {
193                return Err(Error::msg(format!(
194                    "InteropID '{id}' contains invalid characters. Only lowercase a-z, 0-9 and . - _ ~ / * # % ^ + ( ) [ ] | are allowed.\n"
195                )));
196            }
197            if let Some(pos) = id.find(':') {
198                let ns = &id[..pos];
199                let cs = &id[pos + 1..];
200                if ns.is_empty() || cs.is_empty() {
201                    return Err(Error::msg(format!(
202                        "InteropID '{id}' is not valid. If ':' is used, both the namespace and the color space parts must be non-empty.\n"
203                    )));
204                }
205                if cs.contains(':') {
206                    return Err(Error::msg(format!(
207                        "ERROR: InteropID '{id}' is not valid. Only one ':' is allowed to separate the namespace and the color space.\n"
208                    )));
209                }
210            }
211        }
212        self.interop_id = id.to_string();
213        Ok(())
214    }
215
216    /// Value of a known interchange attribute (`amf_transform_ids`,
217    /// `icc_profile_name`).
218    pub fn interchange_attribute(&self, name: &str) -> Result<&str> {
219        let key = find_interchange_key(&KNOWN_INTERCHANGE_NAMES, name)?;
220        Ok(self.interchange.get(key).map(|s| s.as_str()).unwrap_or(""))
221    }
222
223    /// Set (or remove if `value` is empty) a known interchange attribute.
224    pub fn set_interchange_attribute(&mut self, name: &str, value: &str) -> Result<()> {
225        let key = find_interchange_key(&KNOWN_INTERCHANGE_NAMES, name)?;
226        if value.is_empty() {
227            self.interchange.remove(key);
228        } else {
229            self.interchange.insert(key.to_string(), value.to_string());
230        }
231        Ok(())
232    }
233
234    /// All the interchange attributes.
235    pub fn interchange_attributes(&self) -> &BTreeMap<String, String> {
236        &self.interchange
237    }
238
239    /// The bit depth.
240    pub fn bit_depth(&self) -> BitDepth {
241        self.bit_depth
242    }
243    /// Set the bit depth.
244    pub fn set_bit_depth(&mut self, bd: BitDepth) {
245        self.bit_depth = bd;
246    }
247    /// The reference space (scene or display) this color space relates to.
248    pub fn reference_space_type(&self) -> ReferenceSpaceType {
249        self.reference_space
250    }
251
252    /// True if the category exists (case insensitive).
253    pub fn has_category(&self, category: &str) -> bool {
254        self.categories.has_token(category)
255    }
256    /// Add a category (trimmed, ignored if empty or already present).
257    pub fn add_category(&mut self, category: &str) {
258        self.categories.add_token(category);
259    }
260    /// Remove a category.
261    pub fn remove_category(&mut self, category: &str) {
262        self.categories.remove_token(category);
263    }
264    /// Number of categories.
265    pub fn num_categories(&self) -> usize {
266        self.categories.num_tokens()
267    }
268    /// Category at `index` (`None` if out of range).
269    pub fn category(&self, index: usize) -> Option<&str> {
270        self.categories.token(index)
271    }
272    /// All the categories.
273    pub fn categories(&self) -> &[String] {
274        self.categories.tokens()
275    }
276    /// Remove all the categories.
277    pub fn clear_categories(&mut self) {
278        self.categories.clear_tokens();
279    }
280
281    /// The encoding.
282    pub fn encoding(&self) -> &str {
283        &self.encoding
284    }
285    /// Set the encoding.
286    pub fn set_encoding(&mut self, encoding: &str) {
287        self.encoding = encoding.to_string();
288    }
289    /// True if the color space holds non-color data.
290    pub fn is_data(&self) -> bool {
291        self.is_data
292    }
293    /// Set whether the color space holds non-color data.
294    pub fn set_is_data(&mut self, is_data: bool) {
295        self.is_data = is_data;
296    }
297    /// The allocation.
298    pub fn allocation(&self) -> Allocation {
299        self.allocation
300    }
301    /// Set the allocation.
302    pub fn set_allocation(&mut self, allocation: Allocation) {
303        self.allocation = allocation;
304    }
305    /// Number of allocation variables.
306    pub fn allocation_num_vars(&self) -> usize {
307        self.allocation_vars.len()
308    }
309    /// The allocation variables.
310    pub fn allocation_vars(&self) -> &[f32] {
311        &self.allocation_vars
312    }
313    /// Set the allocation variables.
314    pub fn set_allocation_vars(&mut self, vars: &[f32]) {
315        self.allocation_vars = vars.to_vec();
316    }
317
318    /// The transform in the given direction, if any.
319    pub fn transform(&self, dir: ColorSpaceDirection) -> Option<&Transform> {
320        match dir {
321            ColorSpaceDirection::ToReference => self.to_reference.as_ref(),
322            ColorSpaceDirection::FromReference => self.from_reference.as_ref(),
323        }
324    }
325
326    /// Set (or remove with `None`) the transform in the given direction.
327    pub fn set_transform(&mut self, t: Option<Transform>, dir: ColorSpaceDirection) {
328        match dir {
329            ColorSpaceDirection::ToReference => self.to_reference = t,
330            ColorSpaceDirection::FromReference => self.from_reference = t,
331        }
332    }
333}
334
335impl fmt::Display for ColorSpace {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        write!(f, "<ColorSpace referenceSpaceType=")?;
338        match self.reference_space {
339            ReferenceSpaceType::Scene => write!(f, "scene, ")?,
340            ReferenceSpaceType::Display => write!(f, "display, ")?,
341        }
342        write!(f, "name={}, ", self.name)?;
343        if self.aliases.len() == 1 {
344            write!(f, "alias= {}, ", self.aliases[0])?;
345        } else if self.aliases.len() > 1 {
346            write!(f, "aliases=[{}], ", self.aliases.join(", "))?;
347        }
348        if !self.interop_id.is_empty() {
349            write!(f, "interop_id={}, ", self.interop_id)?;
350        }
351        if !self.family.is_empty() {
352            write!(f, "family={}, ", self.family)?;
353        }
354        if !self.equality_group.is_empty() {
355            write!(f, "equalityGroup={}, ", self.equality_group)?;
356        }
357        if self.bit_depth != BitDepth::Unknown {
358            write!(f, "bitDepth={}, ", self.bit_depth.as_str())?;
359        }
360        write!(f, "isData={}", crate::types::bool_to_string(self.is_data))?;
361        if !self.allocation_vars.is_empty() {
362            write!(f, ", allocation={}, ", self.allocation.as_str())?;
363            let vars: Vec<String> = self.allocation_vars.iter().map(|v| fmt_f32(*v)).collect();
364            write!(f, "vars={}", vars.join(" "))?;
365        }
366        if self.num_categories() > 0 {
367            write!(f, ", categories={}", join(self.categories.tokens(), ','))?;
368        }
369        if !self.encoding.is_empty() {
370            write!(f, ", encoding={}", self.encoding)?;
371        }
372        if !self.description.is_empty() {
373            write!(f, ", description={}", self.description)?;
374        }
375        for (k, v) in &self.interchange {
376            write!(f, ", {k}={v}")?;
377        }
378        if let Some(t) = &self.to_reference {
379            write!(f, ",\n    {} --> Reference", self.name)?;
380            write!(f, "\n        {}", format_transform(t))?;
381        }
382        if let Some(t) = &self.from_reference {
383            write!(f, ",\n    Reference --> {}", self.name)?;
384            write!(f, "\n        {}", format_transform(t))?;
385        }
386        write!(f, ">")
387    }
388}
389
390/// An ordered set of color spaces (names and aliases are unique, case
391/// insensitive).
392#[derive(Debug, Clone, Default)]
393pub struct ColorSpaceSet {
394    color_spaces: Vec<ColorSpace>,
395}
396
397impl PartialEq for ColorSpaceSet {
398    /// Only the names are compared.
399    fn eq(&self, other: &Self) -> bool {
400        self.color_spaces.len() == other.color_spaces.len()
401            && self
402                .color_spaces
403                .iter()
404                .all(|cs| other.has_color_space(cs.name()))
405    }
406}
407
408impl ColorSpaceSet {
409    /// An empty set.
410    pub fn new() -> Self {
411        Self::default()
412    }
413
414    /// Number of color spaces.
415    pub fn num_color_spaces(&self) -> usize {
416        self.color_spaces.len()
417    }
418
419    /// Name of the color space at `index`.
420    pub fn color_space_name_by_index(&self, index: usize) -> Option<&str> {
421        self.color_spaces.get(index).map(|c| c.name())
422    }
423
424    /// Color space at `index`.
425    pub fn color_space_by_index(&self, index: usize) -> Option<&ColorSpace> {
426        self.color_spaces.get(index)
427    }
428
429    /// Color space by name or alias.
430    pub fn color_space(&self, name: &str) -> Option<&ColorSpace> {
431        self.color_space_index(name)
432            .and_then(|i| self.color_spaces.get(i))
433    }
434
435    /// Index of the color space by name or alias.
436    pub fn color_space_index(&self, name: &str) -> Option<usize> {
437        if name.is_empty() {
438            return None;
439        }
440        let s = lower(name);
441        self.color_spaces
442            .iter()
443            .position(|cs| compare(cs.name(), &s) || cs.aliases.iter().any(|a| compare(a, &s)))
444    }
445
446    /// True if a color space has this name or alias.
447    pub fn has_color_space(&self, name: &str) -> bool {
448        self.color_space_index(name).is_some()
449    }
450
451    /// Add a copy of the color space, replacing a color space with the same
452    /// name.
453    pub fn add_color_space(&mut self, cs: &ColorSpace) -> Result<()> {
454        let cs_name = cs.name();
455        if cs_name.is_empty() {
456            return Err(Error::msg("Cannot add a color space with an empty name."));
457        }
458        let mut replace = None;
459        if let Some(idx) = self.color_space_index(cs_name) {
460            if !compare(self.color_spaces[idx].name(), cs_name) {
461                return Err(Error::msg(format!(
462                    "Cannot add '{}' color space, existing color space, '{}' is using this name as an alias.",
463                    cs_name,
464                    self.color_spaces[idx].name()
465                )));
466            }
467            replace = Some(idx);
468        }
469        for alias in &cs.aliases {
470            if let Some(idx) = self.color_space_index(alias) {
471                if Some(idx) != replace {
472                    return Err(Error::msg(format!(
473                        "Cannot add '{}' color space, it has '{}' alias and existing color space, '{}' is using the same alias.",
474                        cs_name,
475                        alias,
476                        self.color_spaces[idx].name()
477                    )));
478                }
479            }
480        }
481        match replace {
482            Some(i) => self.color_spaces[i] = cs.clone(),
483            None => self.color_spaces.push(cs.clone()),
484        }
485        Ok(())
486    }
487
488    /// Add all the color spaces of another set.
489    pub fn add_color_spaces(&mut self, other: &ColorSpaceSet) -> Result<()> {
490        for cs in &other.color_spaces {
491            self.add_color_space(cs)?;
492        }
493        Ok(())
494    }
495
496    /// Remove a color space by name (aliases are not considered).
497    pub fn remove_color_space(&mut self, name: &str) {
498        if name.is_empty() {
499            return;
500        }
501        let n = lower(name);
502        if let Some(pos) = self
503            .color_spaces
504            .iter()
505            .position(|cs| lower(cs.name()) == n)
506        {
507            self.color_spaces.remove(pos);
508        }
509    }
510
511    /// Remove all the color spaces of another set.
512    pub fn remove_color_spaces(&mut self, other: &ColorSpaceSet) {
513        for cs in &other.color_spaces {
514            self.remove_color_space(cs.name());
515        }
516    }
517
518    /// Remove all the color spaces.
519    pub fn clear_color_spaces(&mut self) {
520        self.color_spaces.clear();
521    }
522
523    /// Iterate over the color spaces.
524    pub fn iter(&self) -> impl Iterator<Item = &ColorSpace> {
525        self.color_spaces.iter()
526    }
527
528    /// Union (`operator||`).
529    pub fn union(&self, other: &ColorSpaceSet) -> Result<ColorSpaceSet> {
530        let mut s = self.clone();
531        s.add_color_spaces(other)?;
532        Ok(s)
533    }
534
535    /// Intersection (`operator&&`): the color spaces of `other` present in `self`.
536    pub fn intersection(&self, other: &ColorSpaceSet) -> Result<ColorSpaceSet> {
537        let mut s = ColorSpaceSet::new();
538        for cs in &other.color_spaces {
539            if self.has_color_space(cs.name()) {
540                s.add_color_space(cs)?;
541            }
542        }
543        Ok(s)
544    }
545
546    /// Difference (`operator-`): the color spaces of `self` not in `other`.
547    pub fn difference(&self, other: &ColorSpaceSet) -> Result<ColorSpaceSet> {
548        let mut s = ColorSpaceSet::new();
549        for cs in &self.color_spaces {
550            if !other.has_color_space(cs.name()) {
551                s.add_color_space(cs)?;
552            }
553        }
554        Ok(s)
555    }
556}