use super::category_helpers::{
extract_items, find_color_space_infos, find_color_space_names, Categories, Encodings, Infos,
};
use crate::config::logging::log_warning;
use crate::config::utils::{compare, contain, split, trim};
use crate::config::{ColorSpace, Config, NamedTransform};
use crate::error::{Error, Result};
use crate::transforms::{FileTransform, GroupTransform, Transform};
use crate::types::{
ColorSpaceDirection, ColorSpaceVisibility, SearchReferenceSpaceType, OCIO_DISABLE_ALL_CACHES,
OCIO_USER_CATEGORIES_ENVVAR,
};
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColorSpaceInfo {
name: String,
ui_name: String,
family: String,
description: String,
hierarchy_levels: Vec<String>,
}
impl ColorSpaceInfo {
pub fn new(
config: &Config,
name: &str,
ui_name: &str,
family: &str,
description: &str,
) -> Self {
let ui_name = if ui_name.is_empty() { name } else { ui_name };
let sep = config.family_separator();
let vals = if sep != '\0' && !family.is_empty() {
split(family, sep)
} else {
vec![family.to_string()]
};
let hierarchy_levels = vals
.iter()
.map(|v| trim(v).to_string())
.filter(|v| !v.is_empty())
.collect();
Self {
name: name.to_string(),
ui_name: ui_name.to_string(),
family: family.to_string(),
description: description.to_string(),
hierarchy_levels,
}
}
pub fn from_color_space(config: &Config, cs: &ColorSpace) -> Self {
Self::new(config, cs.name(), "", cs.family(), cs.description())
}
pub fn from_named_transform(config: &Config, nt: &NamedTransform) -> Self {
Self::new(config, nt.name(), "", nt.family(), nt.description())
}
pub fn from_role(config: &Config, role: &str, family: &str) -> Option<Self> {
if !config.has_role(role) {
return None;
}
let cs = config.get_color_space(role)?;
let ui_name = format!("{} ({})", role, cs.name());
Some(Self::new(config, role, &ui_name, family, ""))
}
pub fn from_single_role(config: &Config, role: &str) -> Option<Self> {
if !config.has_role(role) {
return None;
}
let cs = config.get_color_space(role)?;
let ui_name = format!("{} ({})", role, cs.name());
Some(Self::new(config, cs.name(), &ui_name, "", ""))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn ui_name(&self) -> &str {
&self.ui_name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn family(&self) -> &str {
&self.family
}
pub fn num_hierarchy_levels(&self) -> usize {
self.hierarchy_levels.len()
}
pub fn hierarchy_level(&self, i: usize) -> &str {
self.hierarchy_levels
.get(i)
.map(|s| s.as_str())
.unwrap_or("")
}
}
#[derive(Debug, Clone)]
pub struct ColorSpaceMenuParameters {
config: Option<Arc<Config>>,
role: String,
app_categories: String,
user_categories: String,
encodings: String,
include_color_spaces: bool,
include_roles: bool,
include_named_transforms: bool,
treat_no_category_as_any: bool,
color_space_type: SearchReferenceSpaceType,
additional_color_spaces: Vec<String>,
}
impl ColorSpaceMenuParameters {
pub fn new(config: Arc<Config>) -> Self {
Self {
config: Some(config),
role: String::new(),
app_categories: String::new(),
user_categories: String::new(),
encodings: String::new(),
include_color_spaces: true,
include_roles: false,
include_named_transforms: false,
treat_no_category_as_any: true,
color_space_type: SearchReferenceSpaceType::All,
additional_color_spaces: Vec::new(),
}
}
pub fn set_config(&mut self, config: Arc<Config>) {
self.config = Some(config);
}
pub fn config(&self) -> Option<&Arc<Config>> {
self.config.as_ref()
}
pub fn set_role(&mut self, role: &str) {
self.role = role.to_string();
}
pub fn role(&self) -> &str {
&self.role
}
pub fn set_include_color_spaces(&mut self, include: bool) {
self.include_color_spaces = include;
}
pub fn include_color_spaces(&self) -> bool {
self.include_color_spaces
}
pub fn search_reference_space_type(&self) -> SearchReferenceSpaceType {
self.color_space_type
}
pub fn set_search_reference_space_type(&mut self, t: SearchReferenceSpaceType) {
self.color_space_type = t;
}
pub fn set_include_named_transforms(&mut self, include: bool) {
self.include_named_transforms = include;
}
pub fn include_named_transforms(&self) -> bool {
self.include_named_transforms
}
pub fn set_treat_no_category_as_any(&mut self, value: bool) {
self.treat_no_category_as_any = value;
}
pub fn treat_no_category_as_any(&self) -> bool {
self.treat_no_category_as_any
}
pub fn set_app_categories(&mut self, app_categories: &str) {
self.app_categories = app_categories.to_string();
}
pub fn app_categories(&self) -> &str {
&self.app_categories
}
pub fn set_encodings(&mut self, encodings: &str) {
self.encodings = encodings.to_string();
}
pub fn encodings(&self) -> &str {
&self.encodings
}
pub fn set_user_categories(&mut self, user_categories: &str) {
self.user_categories = user_categories.to_string();
}
pub fn user_categories(&self) -> &str {
&self.user_categories
}
pub fn set_include_roles(&mut self, include: bool) {
self.include_roles = include;
}
pub fn include_roles(&self) -> bool {
self.include_roles
}
pub fn add_color_space(&mut self, name: &str) {
if !name.is_empty() && !contain(&self.additional_color_spaces, name) {
self.additional_color_spaces.push(name.to_string());
}
}
pub fn num_added_color_spaces(&self) -> usize {
self.additional_color_spaces.len()
}
pub fn added_color_space(&self, index: usize) -> &str {
self.additional_color_spaces
.get(index)
.map(|s| s.as_str())
.unwrap_or("")
}
pub fn clear_added_color_spaces(&mut self) {
self.additional_color_spaces.clear();
}
}
impl fmt::Display for ColorSpaceMenuParameters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.config {
Some(c) => write!(f, "config: {}", c.cache_id())?,
None => write!(f, "config: missing")?,
}
if !self.role.is_empty() {
write!(f, ", role: {}", self.role)?;
}
if !self.app_categories.is_empty() {
write!(f, ", appCategories: {}", self.app_categories)?;
}
if !self.user_categories.is_empty() {
write!(f, ", userCategories: {}", self.user_categories)?;
}
if !self.encodings.is_empty() {
write!(f, ", encodings: {}", self.encodings)?;
}
let b = |v: bool| if v { "true" } else { "false" };
write!(f, ", includeColorSpaces: {}", b(self.include_color_spaces))?;
write!(f, ", includeRoles: {}", b(self.include_roles))?;
write!(
f,
", includeNamedTransforms: {}",
b(self.include_named_transforms)
)?;
write!(
f,
", treatNoCategoryAsAny: {}",
b(self.treat_no_category_as_any)
)?;
match self.color_space_type {
SearchReferenceSpaceType::Scene => write!(f, ", colorSpaceType: scene")?,
SearchReferenceSpaceType::Display => write!(f, ", colorSpaceType: display")?,
SearchReferenceSpaceType::All => {}
}
let n = self.additional_color_spaces.len();
if n == 1 {
write!(f, ", addedSpaces: {}", self.additional_color_spaces[0])?;
} else if n > 1 {
write!(
f,
", addedSpaces: [{}]",
self.additional_color_spaces.join(", ")
)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ColorSpaceMenuHelper {
parameters: ColorSpaceMenuParameters,
entries: Infos,
}
type MenuCache = Mutex<HashMap<String, Arc<ColorSpaceMenuHelper>>>;
fn menu_cache() -> &'static MenuCache {
static CACHE: OnceLock<MenuCache> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn is_cache_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| std::env::var_os(OCIO_DISABLE_ALL_CACHES).is_none())
}
impl ColorSpaceMenuHelper {
pub fn create(p: &ColorSpaceMenuParameters) -> Result<Arc<ColorSpaceMenuHelper>> {
let config = p
.config
.as_ref()
.ok_or_else(|| Error::msg("ColorSpaceMenuHelper needs parameters with a config."))?;
if let Err(e) = config.validate() {
log_warning(&format!(
"ColorSpaceMenuHelper needs a valid config. Validation warning is: {}",
e.message()
));
}
if !p.include_color_spaces && p.include_roles {
return Err(Error::msg(
"ColorSpaceMenuHelper needs to include color spaces if roles are included.",
));
}
let mut parameters = p.clone();
if let Ok(env) = std::env::var(OCIO_USER_CATEGORIES_ENVVAR) {
let user = trim(&env);
if !user.is_empty() {
parameters.set_user_categories(user);
}
}
if is_cache_enabled() {
let key = parameters.to_string();
let mut cache = menu_cache().lock().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = cache.get(&key) {
return Ok(entry.clone());
}
let entry = Arc::new(ColorSpaceMenuHelper::new(parameters)?);
cache.insert(key, entry.clone());
return Ok(entry);
}
Ok(Arc::new(ColorSpaceMenuHelper::new(parameters)?))
}
fn new(parameters: ColorSpaceMenuParameters) -> Result<Self> {
let mut helper = Self {
parameters,
entries: Vec::new(),
};
helper.refresh()?;
Ok(helper)
}
fn refresh(&mut self) -> Result<()> {
self.entries.clear();
let params = &self.parameters;
let config = match ¶ms.config {
Some(c) => c.clone(),
None => {
return Err(Error::msg(
"ColorSpaceMenuHelper needs parameters with a config.",
))
}
};
if !params.role.is_empty() && config.has_role(¶ms.role) {
if let Some(info) = ColorSpaceInfo::from_single_role(&config, ¶ms.role) {
self.entries.push(info);
}
return Ok(());
}
let app: Categories = extract_items(¶ms.app_categories);
let user: Categories = extract_items(¶ms.user_categories);
let encodings: Encodings = extract_items(¶ms.encodings);
let num_nt = config.num_named_transforms();
let num_cs =
config.num_color_spaces_filtered(params.color_space_type, ColorSpaceVisibility::Active);
if (params.include_color_spaces && num_cs != 0)
|| (params.include_named_transforms && num_nt != 0)
{
self.entries = find_color_space_infos(
&config,
&app,
&user,
params.include_color_spaces,
params.include_named_transforms,
params.treat_no_category_as_any,
&encodings,
params.color_space_type,
);
}
if params.include_roles {
for idx in 0..config.num_roles() {
if let Some(info) =
ColorSpaceInfo::from_role(&config, config.role_name(idx), "Roles")
{
self.entries.push(info);
}
}
}
let mut additional: Infos = Vec::new();
for name in ¶ms.additional_color_spaces {
let already_there = |n: &str, entries: &Infos, added: &Infos| {
entries.iter().any(|e| compare(n, e.name()))
|| added.iter().any(|e| compare(n, e.name()))
};
if let Some(cs) = config.get_color_space(name) {
if !already_there(cs.name(), &self.entries, &additional) {
additional.push(ColorSpaceInfo::from_color_space(&config, cs));
}
} else if let Some(nt) = config.get_named_transform(name) {
if !already_there(nt.name(), &self.entries, &additional) {
additional.push(ColorSpaceInfo::from_named_transform(&config, nt));
}
} else {
return Err(Error::msg(format!(
"Element '{name}' is neither a color space not a named transform."
)));
}
}
self.entries.extend(additional);
Ok(())
}
pub fn parameters(&self) -> &ColorSpaceMenuParameters {
&self.parameters
}
pub fn num_color_spaces(&self) -> usize {
self.entries.len()
}
pub fn name(&self, idx: usize) -> &str {
self.entries.get(idx).map(|e| e.name()).unwrap_or("")
}
pub fn ui_name(&self, idx: usize) -> &str {
self.entries.get(idx).map(|e| e.ui_name()).unwrap_or("")
}
pub fn index_from_name(&self, name: &str) -> Option<usize> {
if name.is_empty() {
return None;
}
self.entries.iter().position(|e| compare(e.name(), name))
}
pub fn index_from_ui_name(&self, name: &str) -> Option<usize> {
if name.is_empty() {
return None;
}
self.entries.iter().position(|e| compare(e.ui_name(), name))
}
pub fn description(&self, idx: usize) -> &str {
self.entries.get(idx).map(|e| e.description()).unwrap_or("")
}
pub fn family(&self, idx: usize) -> &str {
self.entries.get(idx).map(|e| e.family()).unwrap_or("")
}
pub fn num_hierarchy_levels(&self, idx: usize) -> usize {
self.entries
.get(idx)
.map(|e| e.num_hierarchy_levels())
.unwrap_or(0)
}
pub fn hierarchy_level(&self, idx: usize, i: usize) -> &str {
self.entries
.get(idx)
.map(|e| e.hierarchy_level(i))
.unwrap_or("")
}
pub fn name_from_ui_name(&self, ui_name: &str) -> &str {
if ui_name.is_empty() {
return "";
}
self.entries
.iter()
.find(|e| compare(ui_name, e.ui_name()))
.map(|e| e.name())
.unwrap_or("")
}
pub fn ui_name_from_name(&self, name: &str) -> &str {
if name.is_empty() {
return "";
}
self.entries
.iter()
.find(|e| compare(name, e.name()))
.map(|e| e.ui_name())
.unwrap_or("")
}
}
impl fmt::Display for ColorSpaceMenuHelper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.parameters)?;
let names: Vec<&str> = self.entries.iter().map(|e| e.name()).collect();
write!(f, ", color spaces = [{}]", names.join(", "))
}
}
fn add_color_space_impl(
config: &mut Config,
color_space: &mut ColorSpace,
user_transform: FileTransform,
connection_color_space_name: &str,
) -> Result<()> {
if connection_color_space_name.is_empty() {
return Err(Error::msg("Invalid connection color space name."));
}
if config.get_color_space(color_space.name()).is_some() {
return Err(Error::msg(format!(
"Color space name '{}' already exists.",
color_space.name()
)));
}
let mut grp = GroupTransform::new();
grp.append(user_transform);
let connection_cs = config
.get_color_space(connection_color_space_name)
.ok_or_else(|| {
Error::msg(format!(
"Connection color space name '{connection_color_space_name}' does not exist."
))
})?;
if let Some(tr) = connection_cs.transform(ColorSpaceDirection::ToReference) {
grp.append(tr.clone());
} else if let Some(tr) = connection_cs.transform(ColorSpaceDirection::FromReference) {
grp.append(tr.inverted());
}
let grp = Transform::Group(grp);
grp.validate()?;
color_space.set_transform(Some(grp), ColorSpaceDirection::ToReference);
config.add_color_space(color_space)
}
pub fn add_color_space(
config: &mut Config,
name: &str,
transform_file_path: &str,
categories: &str,
connection_color_space_name: &str,
) -> Result<()> {
let info = ColorSpaceInfo::new(config, name, "", "", "");
let mut color_space = ColorSpace::default();
color_space.set_name(info.name());
color_space.set_family(info.family());
color_space.set_description(info.description());
if !categories.is_empty() {
let cats = extract_items(categories);
if !find_color_space_names(config, &cats).is_empty() {
for cat in &cats {
color_space.add_category(cat);
}
}
}
let file = FileTransform::new(transform_file_path);
add_color_space_impl(config, &mut color_space, file, connection_color_space_name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::apphelpers::tests_data::CATEGORY_TEST_CONFIG;
#[test]
fn color_space_info_read_values() {
let config = Config::create_from_str(CATEGORY_TEST_CONFIG).unwrap();
config.validate().unwrap();
let cs = config.get_color_space("in_1").unwrap();
let info = ColorSpaceInfo::from_color_space(&config, cs);
assert_eq!(cs.name(), "in_1");
assert_eq!(info.name(), "in_1");
assert_eq!(cs.family(), "Input / Camera/Acme");
assert_eq!(info.num_hierarchy_levels(), 3);
assert_eq!(info.hierarchy_level(0), "Input");
assert_eq!(info.hierarchy_level(1), "Camera");
assert_eq!(info.hierarchy_level(2), "Acme");
assert_eq!(info.family(), cs.family());
assert_eq!(
cs.description(),
"An input color space.\nFor the Acme camera."
);
assert_eq!(info.description(), cs.description());
let cs = config.get_color_space("lin_1").unwrap();
let info = ColorSpaceInfo::from_color_space(&config, cs);
assert_eq!(cs.name(), "lin_1");
assert_eq!(info.name(), "lin_1");
assert_eq!(cs.family(), "");
assert_eq!(info.num_hierarchy_levels(), 0);
assert_eq!(info.family(), "");
assert_eq!(cs.description(), "");
}
#[test]
fn color_space_info_change_values() {
let config = Config::create_raw();
config.validate().unwrap();
let mut cs = config.get_color_space("raw").unwrap().clone();
let info = ColorSpaceInfo::from_color_space(&config, &cs);
assert_eq!(cs.name(), "raw");
assert_eq!(info.name(), "raw");
assert_eq!(cs.family(), "raw");
assert_eq!(info.num_hierarchy_levels(), 1);
assert_eq!(info.hierarchy_level(0), "raw");
assert_eq!(info.family(), cs.family());
assert_eq!(
cs.description(),
"A raw color space. Conversions to and from this space are no-ops."
);
assert_eq!(info.description(), cs.description());
cs.set_family("");
assert_eq!(cs.family(), "");
let info = ColorSpaceInfo::from_color_space(&config, &cs);
assert_eq!(info.num_hierarchy_levels(), 0);
assert_eq!(info.family(), "");
cs.set_family("Acme / Camera");
assert_eq!(cs.family(), "Acme / Camera");
let mut cfg = config.create_editable_copy();
cfg.set_family_separator('\0').unwrap();
let info = ColorSpaceInfo::from_color_space(&cfg, &cs);
assert_eq!(info.num_hierarchy_levels(), 1);
assert_eq!(info.hierarchy_level(0), cs.family());
cfg.set_family_separator('/').unwrap();
let info = ColorSpaceInfo::from_color_space(&cfg, &cs);
assert_eq!(info.num_hierarchy_levels(), 2);
assert_eq!(info.hierarchy_level(0), "Acme");
assert_eq!(info.hierarchy_level(1), "Camera");
assert_eq!(info.family(), cs.family());
cfg.set_family_separator('-').unwrap();
let info = ColorSpaceInfo::from_color_space(&cfg, &cs);
assert_eq!(info.num_hierarchy_levels(), 1);
assert_eq!(info.hierarchy_level(0), cs.family());
cfg.set_family_separator(Config::default_family_separator())
.unwrap();
let info = ColorSpaceInfo::from_color_space(&cfg, &cs);
assert_eq!(info.num_hierarchy_levels(), 2);
assert_eq!(info.hierarchy_level(0), "Acme");
assert_eq!(info.hierarchy_level(1), "Camera");
assert_eq!(info.family(), cs.family());
cs.set_description("desc 1\n\n\n desc 2");
assert_eq!(cs.description(), "desc 1\n\n\n desc 2");
let info = ColorSpaceInfo::from_color_space(&cfg, &cs);
assert_eq!(info.description(), cs.description());
}
}