ocio 0.1.0

A pure Rust port of OpenColorIO, the color management framework for visual effects and animation.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
//! Color mixing (color picker) helpers (port of
//! `apphelpers/MixingHelpers.cpp`): [`MixingColorSpaceManager`] and
//! [`MixingSlider`].
//!
//! A user may want to mix colors in different color spaces, typically a
//! scene-linear working space or the display space. Since scene-linear color
//! spaces are not perceptually uniform, UI sliders are compensated using a
//! mapping from linear into an approximately perceptually uniform space.
//! Mixing values may extend outside the typical [0, 1] domain.

use super::color_space_helpers::ColorSpaceInfo;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::processor::Processor;
use crate::transforms::{
    DisplayViewTransform, FixedFunctionTransform, GroupTransform, MatrixTransform, Transform,
};
use crate::types::{FixedFunctionStyle, TransformDirection, ROLE_COLOR_PICKING};
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};

const GAMMA: f32 = 2.0;
const LOGSLOPE: f32 = 0.55;
const BREAKPNT: f32 = 0.18;
const NEGSLOPE: f32 = 0.5;
const INVGAMMA: f32 = 0.5; // 1/GAMMA
const INVBREAKPNT: f32 = 0.42426406871192851; // BREAKPNT^(1/GAMMA)
const LOGOFFSET: f32 = 0.83386419090511021; // BREAKPNT^(1/GAMMA) - log10(BREAKPNT)*LOGSLOPE

fn linear_to_perceptual(linear: f32) -> f32 {
    if linear <= 0.0 {
        linear * NEGSLOPE
    } else if linear > BREAKPNT {
        LOGOFFSET + linear.log10() * LOGSLOPE
    } else {
        linear.powf(INVGAMMA)
    }
}

fn perceptual_to_linear(percept: f32) -> f32 {
    if percept <= 0.0 {
        percept / NEGSLOPE
    } else if percept > INVBREAKPNT {
        10.0f32.powf((percept - LOGOFFSET) / LOGSLOPE)
    } else {
        percept.powf(GAMMA)
    }
}

#[derive(Debug)]
struct SliderShared {
    // (min edge, max edge).
    edges: Mutex<(f32, f32)>,
    // Mirror of `MixingColorSpaceManager::is_perceptually_uniform`.
    perceptually_uniform: AtomicBool,
}

/// A UI slider for color mixing. It is a handle on the slider owned by a
/// [`MixingColorSpaceManager`] (clones share the same state, as the
/// reference returned by OCIO's `getSlider`), and it follows the selected
/// mixing space of the manager.
#[derive(Debug, Clone)]
pub struct MixingSlider {
    shared: Arc<SliderShared>,
}

impl MixingSlider {
    fn new() -> Self {
        Self {
            shared: Arc::new(SliderShared {
                edges: Mutex::new((0.0, 1.0)),
                perceptually_uniform: AtomicBool::new(false),
            }),
        }
    }

    fn edges(&self) -> (f32, f32) {
        self.shared
            .edges
            .lock()
            .map(|e| *e)
            .unwrap_or_else(|e| *e.into_inner())
    }

    fn is_perceptually_uniform(&self) -> bool {
        self.shared.perceptually_uniform.load(Ordering::Relaxed)
    }

    /// Set the minimum edge of the slider (in mixing space units).
    pub fn set_slider_min_edge(&self, v: f32) {
        if let Ok(mut e) = self.shared.edges.lock() {
            e.0 = v;
        }
    }

    /// Minimum edge of the slider for the conversion to mixing space.
    pub fn slider_min_edge(&self) -> f32 {
        let (min, max) = self.edges();
        if !self.is_perceptually_uniform() {
            linear_to_perceptual(min.min(max - 0.01))
        } else {
            min
        }
    }

    /// Set the maximum edge of the slider (in mixing space units).
    pub fn set_slider_max_edge(&self, v: f32) {
        if let Ok(mut e) = self.shared.edges.lock() {
            e.1 = v;
        }
    }

    /// Maximum edge of the slider for the conversion to mixing space.
    pub fn slider_max_edge(&self) -> f32 {
        let (min, max) = self.edges();
        if !self.is_perceptually_uniform() {
            linear_to_perceptual(max.max(min + 0.01))
        } else {
            max
        }
    }

    /// Convert from units in distance along the slider to mixing space units.
    pub fn slider_to_mixing(&self, slider_units: f32) -> f32 {
        let min = self.slider_min_edge();
        let max = self.slider_max_edge();
        let percept = min + slider_units * (max - min);
        if !self.is_perceptually_uniform() {
            perceptual_to_linear(percept)
        } else {
            percept
        }
    }

    /// Convert from mixing space units to distance along the slider.
    pub fn mixing_to_slider(&self, mixing_units: f32) -> f32 {
        let percept = if !self.is_perceptually_uniform() {
            linear_to_perceptual(mixing_units)
        } else {
            mixing_units
        };
        // The slider is a window onto the perceptual units. Apply affine based on current
        // left/right or min/max edges of the UI.
        let min = self.slider_min_edge();
        let max = self.slider_max_edge();
        (percept - min) / (max - min)
    }
}

impl fmt::Display for MixingSlider {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "minEdge: {}, maxEdge: {}",
            crate::config::utils::fmt_f32(self.slider_min_edge()),
            crate::config::utils::fmt_f32(self.slider_max_edge())
        )
    }
}

const MIXING_ENCODINGS: [&str; 2] = ["RGB", "HSV"];

/// Used to mix (or pick / choose) colors.
#[derive(Debug)]
pub struct MixingColorSpaceManager {
    config: Arc<Config>,
    slider: MixingSlider,
    mixing_spaces: Vec<String>,
    selected_mixing_space_idx: usize,
    selected_mixing_encoding_idx: usize,
    color_picker: Option<ColorSpaceInfo>,
}

impl MixingColorSpaceManager {
    /// A manager for the config (`MixingColorSpaceManager::Create`).
    pub fn new(config: Arc<Config>) -> Self {
        let mut m = Self {
            config,
            slider: MixingSlider::new(),
            mixing_spaces: Vec::new(),
            selected_mixing_space_idx: 0,
            selected_mixing_encoding_idx: 0,
            color_picker: None,
        };
        m.refresh_impl();
        m
    }

    fn update_slider(&self) {
        self.slider
            .shared
            .perceptually_uniform
            .store(self.is_perceptually_uniform(), Ordering::Relaxed);
    }

    fn refresh_impl(&mut self) {
        // Add the mixing spaces.
        self.selected_mixing_space_idx = 0;
        self.mixing_spaces.clear();
        self.color_picker = None;

        if self.config.has_role(ROLE_COLOR_PICKING) {
            self.color_picker = ColorSpaceInfo::from_single_role(&self.config, ROLE_COLOR_PICKING);
            if let Some(cp) = &self.color_picker {
                self.mixing_spaces.push(cp.ui_name().to_string());
            }
        } else {
            // Upstream OCIO note: Replace the 'Display Space' entry (i.e. the color space of the
            // monitor) by the list of all the display color spaces from the configuration.
            self.mixing_spaces.push("Rendering Space".to_string());
            self.mixing_spaces.push("Display Space".to_string());
        }

        // The mixing encodings.
        self.selected_mixing_encoding_idx = 0;
        self.update_slider();
    }

    /// Refresh the instance (e.g. following a config change).
    pub fn refresh(&mut self, config: Arc<Config>) {
        self.config = config;
        self.refresh_impl();
    }

    /// Number of mixing spaces.
    pub fn num_mixing_spaces(&self) -> usize {
        self.mixing_spaces.len()
    }

    /// UI name of the mixing space at `idx`.
    pub fn mixing_space_ui_name(&self, idx: usize) -> Result<&str> {
        self.mixing_spaces
            .get(idx)
            .map(|s| s.as_str())
            .ok_or_else(|| {
                Error::msg(format!(
                    "Invalid mixing space index {} where size is {}.",
                    idx,
                    self.mixing_spaces.len()
                ))
            })
    }

    /// Index of the selected mixing space.
    pub fn selected_mixing_space_idx(&self) -> usize {
        self.selected_mixing_space_idx
    }

    /// Select the mixing space by index.
    pub fn set_selected_mixing_space_idx(&mut self, idx: usize) -> Result<()> {
        if idx >= self.mixing_spaces.len() {
            return Err(Error::msg(format!(
                "Invalid idx for the mixing space index {} where size is {}.",
                idx,
                self.mixing_spaces.len()
            )));
        }
        self.selected_mixing_space_idx = idx;
        self.update_slider();
        Ok(())
    }

    /// Select the mixing space by UI name.
    pub fn set_selected_mixing_space(&mut self, mixing_space: &str) -> Result<()> {
        match self.mixing_spaces.iter().position(|s| s == mixing_space) {
            Some(idx) => {
                self.selected_mixing_space_idx = idx;
                self.update_slider();
                Ok(())
            }
            None => Err(Error::msg(format!(
                "Invalid mixing space name: '{mixing_space}'."
            ))),
        }
    }

    /// True if the selected mixing space is perceptually uniform.
    pub fn is_perceptually_uniform(&self) -> bool {
        // Upstream OCIO note: This response should vary as a function of the mixing space.
        // Only display color spaces are perceptually linear.
        match self.color_picker {
            None => self.selected_mixing_space_idx != 0,
            Some(_) => true,
        }
    }

    /// Number of mixing encodings (RGB and HSV).
    pub fn num_mixing_encodings(&self) -> usize {
        MIXING_ENCODINGS.len()
    }

    /// Name of the mixing encoding at `idx`.
    pub fn mixing_encoding_name(&self, idx: usize) -> Result<&str> {
        MIXING_ENCODINGS.get(idx).copied().ok_or_else(|| {
            Error::msg(format!(
                "Invalid mixing encoding index {} where size is {}.",
                idx,
                MIXING_ENCODINGS.len()
            ))
        })
    }

    /// Index of the selected mixing encoding.
    pub fn selected_mixing_encoding_idx(&self) -> usize {
        self.selected_mixing_encoding_idx
    }

    /// Select the mixing encoding by index.
    pub fn set_selected_mixing_encoding_idx(&mut self, idx: usize) -> Result<()> {
        if idx >= MIXING_ENCODINGS.len() {
            return Err(Error::msg(format!(
                "Invalid idx for the mixing encoding index {} where size is {}.",
                idx,
                MIXING_ENCODINGS.len()
            )));
        }
        self.selected_mixing_encoding_idx = idx;
        Ok(())
    }

    /// Select the mixing encoding by name.
    pub fn set_selected_mixing_encoding(&mut self, mixing_encoding: &str) -> Result<()> {
        match MIXING_ENCODINGS.iter().position(|s| *s == mixing_encoding) {
            Some(idx) => {
                self.selected_mixing_encoding_idx = idx;
                Ok(())
            }
            None => Err(Error::msg(format!(
                "Invalid mixing encoding: '{mixing_encoding}'."
            ))),
        }
    }

    // Processor converting from the working / rendering space to the mixing space (using
    // the RGB encoding rather than HSV).
    fn processor_without_encoding(
        &self,
        working_name: &str,
        display_name: &str,
        view_name: &str,
    ) -> Result<Processor> {
        if let Some(cp) = &self.color_picker {
            // Mix colors in the color_picker role color space.
            self.config.get_processor(working_name, cp.name())
        } else if self.selected_mixing_space_idx > 0 {
            // Mix colors in the selected (display, view) space.
            let dt = DisplayViewTransform::new(working_name, display_name, view_name);
            self.config.get_processor_for_transform(
                &Transform::DisplayView(dt),
                TransformDirection::Forward,
            )
        } else {
            // Mix colors directly in the working / rendering space.
            self.config.get_processor_for_transform(
                &Transform::Matrix(MatrixTransform::default()),
                TransformDirection::Forward,
            )
        }
    }

    /// Processor from the working space to the mixing space (forward) or
    /// back (inverse).
    pub fn get_processor(
        &self,
        working_name: &str,
        display_name: &str,
        view_name: &str,
        direction: TransformDirection,
    ) -> Result<Processor> {
        let mut group = GroupTransform::new();
        let processor = self.processor_without_encoding(working_name, display_name, view_name)?;
        group.append(processor.create_group_transform());

        if self.selected_mixing_encoding_idx == 1 {
            // i.e. HSV
            group.append(FixedFunctionTransform::new(
                FixedFunctionStyle::RgbToHsv,
                &[],
            ));
        }

        self.config
            .get_processor_for_transform(&Transform::Group(group), direction)
    }

    /// The slider (a handle sharing its state with the manager).
    pub fn slider(&self) -> MixingSlider {
        self.slider.clone()
    }

    /// The slider, after setting its edges.
    pub fn slider_with_edges(&self, min_edge: f32, max_edge: f32) -> MixingSlider {
        self.slider.set_slider_min_edge(min_edge);
        self.slider.set_slider_max_edge(max_edge);
        self.slider.clone()
    }
}

impl fmt::Display for MixingColorSpaceManager {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "config: {}", self.config.cache_id())?;
        write!(f, ", slider: [{}]", self.slider)?;
        if !self.mixing_spaces.is_empty() {
            write!(f, ", mixingSpaces: [{}]", self.mixing_spaces.join(", "))?;
        }
        write!(
            f,
            ", selectedMixingSpaceIdx: {}",
            self.selected_mixing_space_idx
        )?;
        write!(
            f,
            ", selectedMixingEncodingIdx: {}",
            self.selected_mixing_encoding_idx
        )?;
        if self.color_picker.is_some() {
            write!(f, ", colorPicking")?;
        }
        Ok(())
    }
}