use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use crate::PixelFormat;
use crate::error::{Error, Result};
use crate::texture::Frame;
pub struct ColorProcessor {
inner: Option<ocio_rs::Processor>,
cpu: Option<ocio_rs::CPUProcessor>,
}
fn rb_swap_matrix() -> Option<ocio_rs::transform::MatrixTransform> {
let m = ocio_rs::transform::MatrixTransform::create().ok()?;
m.set_matrix(&[
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
])
.ok()?;
Some(m)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
Normal,
Inverse,
}
impl Direction {
fn to_ocio(self) -> ocio_rs::TransformDirection {
match self {
Direction::Normal => ocio_rs::TransformDirection::Forward,
Direction::Inverse => ocio_rs::TransformDirection::Inverse,
}
}
}
unsafe impl Send for ColorProcessor {}
unsafe impl Sync for ColorProcessor {}
impl ColorProcessor {
pub fn create(src_space: &str, dst_transform: &str, dir: Direction) -> Option<Self> {
let config = default_config()?;
let src = if config.has_role(src_space) {
config
.canonical_name(src_space)
.unwrap_or_else(|| src_space.to_string())
} else {
src_space.to_string()
};
let processor = match dir {
Direction::Normal => config.processor(&src, dst_transform),
Direction::Inverse => config.processor(dst_transform, &src),
}
.ok();
let cpu = processor
.as_ref()
.and_then(|p| p.default_cpu_processor().ok());
Some(Self {
inner: processor,
cpu,
})
}
pub fn create_lut(path: &str, dir: Direction) -> Option<Self> {
let config = default_config()?;
let transform = ocio_rs::transform::FileTransform::create().ok()?;
transform.set_src(path).ok()?;
transform.set_interpolation(ocio_rs::Interpolation::Linear);
transform.set_direction(dir.to_ocio());
let processor = config
.processor_from_transform(&transform, dir.to_ocio())
.ok();
let cpu = processor
.as_ref()
.and_then(|p| p.default_cpu_processor().ok());
Some(Self {
inner: processor,
cpu,
})
}
pub fn create_lut_on(config: &ocio_rs::Config, path: &str, dir: Direction) -> Option<Self> {
let transform = ocio_rs::transform::FileTransform::create().ok()?;
transform.set_src(path).ok()?;
transform.set_interpolation(ocio_rs::Interpolation::Linear);
transform.set_direction(dir.to_ocio());
let processor = config
.processor_from_transform(&transform, dir.to_ocio())
.ok();
let cpu = processor
.as_ref()
.and_then(|p| p.default_cpu_processor().ok());
Some(Self {
inner: processor,
cpu,
})
}
pub fn create_grading_primary(style: GradingStyle) -> Option<Self> {
let config = default_config()?;
let transform =
ocio_rs::transform::GradingPrimaryTransform::create(style.to_ocio()).ok()?;
transform.make_dynamic();
transform.set_direction(ocio_rs::TransformDirection::Forward);
let processor = config
.processor_from_transform(&transform, ocio_rs::TransformDirection::Forward)
.ok();
let cpu = processor
.as_ref()
.and_then(|p| p.default_cpu_processor().ok());
Some(Self {
inner: processor,
cpu,
})
}
pub fn create_display_icc(src_space: &str, icc_path: &str) -> Option<Self> {
Self::create_display_icc_impl(src_space, icc_path, false)
}
pub fn create_display_icc_bgra8(src_space: &str, icc_path: &str) -> Option<Self> {
Self::create_display_icc_impl(src_space, icc_path, true)
}
pub fn create_display_icc_xyz(icc_path: &str) -> Option<Self> {
Self::create_display_icc_xyz_impl(icc_path, false)
}
pub fn create_display_icc_xyz_bgra8(icc_path: &str) -> Option<Self> {
Self::create_display_icc_xyz_impl(icc_path, true)
}
fn create_display_icc_xyz_impl(icc_path: &str, bgra: bool) -> Option<Self> {
let p = Self::create_display_icc_impl("cie_xyz_d65_interchange", icc_path, bgra)?;
p.is_valid().then_some(p)
}
fn create_display_icc_impl(src_space: &str, icc_path: &str, bgra: bool) -> Option<Self> {
let config = default_config()?;
let to_lin709 = ocio_rs::transform::ColorSpaceTransform::create().ok()?;
to_lin709.set_src(src_space).ok()?;
to_lin709.set_dst("Linear Rec.709 (sRGB)").ok()?;
let to_xyz = ocio_rs::transform::MatrixTransform::create().ok()?;
to_xyz
.set_matrix(&[
0.4123908, 0.3575843, 0.1804808, 0.0, 0.2126390, 0.7151687, 0.0721923, 0.0, 0.0193308, 0.1191948, 0.9505322, 0.0, 0.0, 0.0, 0.0, 1.0,
])
.ok()?;
let icc = ocio_rs::transform::FileTransform::create().ok()?;
icc.set_src(icc_path).ok()?;
icc.set_interpolation(ocio_rs::Interpolation::Linear);
icc.set_direction(ocio_rs::TransformDirection::Forward);
let group = ocio_rs::transform::GroupTransform::create().ok()?;
if bgra {
group.append_transform(&rb_swap_matrix()?).ok()?;
}
group.append_transform(&to_lin709).ok()?;
group.append_transform(&to_xyz).ok()?;
group.append_transform(&icc).ok()?;
if bgra {
group.append_transform(&rb_swap_matrix()?).ok()?;
}
let processor = config
.processor_from_transform(&group, ocio_rs::TransformDirection::Forward)
.ok();
let cpu = processor
.as_ref()
.and_then(|p| p.default_cpu_processor().ok());
Some(Self {
inner: processor,
cpu,
})
}
pub fn from_processor(processor: ocio_rs::Processor) -> Self {
let cpu = processor.default_cpu_processor().ok();
Self {
inner: Some(processor),
cpu,
}
}
pub fn pass_through() -> Self {
Self {
inner: None,
cpu: None,
}
}
pub fn processor(&self) -> Option<&ocio_rs::Processor> {
self.inner.as_ref()
}
pub fn convert_color(&self, rgba: [f64; 4]) -> [f64; 4] {
match &self.cpu {
Some(cpu) => {
let mut c = [
rgba[0] as f32,
rgba[1] as f32,
rgba[2] as f32,
rgba[3] as f32,
];
cpu.apply_rgba(&mut c);
[c[0] as f64, c[1] as f64, c[2] as f64, c[3] as f64]
}
None => rgba,
}
}
pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> {
let Some(cpu) = &self.cpu else {
return Ok(()); };
if frame.format != PixelFormat::F32 {
return Err(Error::Invalid);
}
let pixels = frame.pixel_count() as i64;
let buf: &mut [f32] = bytemuck_f32_slice(&mut frame.data)
.ok_or_else(|| Error::Failed("pixel buffer not f32 aligned".to_string()))?;
cpu.apply_rgba_pixels(buf, pixels, 4);
Ok(())
}
pub fn is_valid(&self) -> bool {
self.inner.is_some()
}
pub fn cache_id(&self) -> String {
match &self.inner {
Some(p) => p.cache_id().unwrap_or_default(),
None => String::new(),
}
}
pub fn convert_bgra8(&self, data: &mut [u8], pixels: i64) -> Result<()> {
let Some(cpu) = &self.cpu else {
return Ok(());
};
let count = (pixels.max(0) as usize) * 4;
if data.len() < count {
return Err(Error::Invalid);
}
let mut f32s: Vec<f32> = data[..count].iter().map(|&v| v as f32 / 255.0).collect();
cpu.try_apply_rgba_pixels(&mut f32s, pixels, 4)
.map_err(|e| Error::Failed(format!("OCIO f32 apply (bgra8 detour): {e}")))?;
for (dst, v) in data[..count].iter_mut().zip(f32s) {
*dst = (v.clamp(0.0, 1.0) * 255.0).round() as u8;
}
Ok(())
}
pub fn convert_f32_rgba(&self, samples: &mut [f32], pixels: i64) -> Result<()> {
let Some(cpu) = &self.cpu else {
return Ok(());
};
cpu.try_apply_rgba_pixels(samples, pixels, 4)
.map_err(|e| Error::Failed(format!("OCIO f32 apply: {e}")))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GradingStyle {
Lin,
Log,
}
impl GradingStyle {
fn to_ocio(self) -> ocio_rs::GradingStyle {
match self {
GradingStyle::Lin => ocio_rs::GradingStyle::Lin,
GradingStyle::Log => ocio_rs::GradingStyle::Log,
}
}
}
fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> {
if !data.len().is_multiple_of(4) {
return None;
}
Some(unsafe { std::slice::from_raw_parts_mut(data.as_mut_ptr() as *mut f32, data.len() / 4) })
}
static PIPELINE_COLOR: LazyLock<
Mutex<(
crate::colormath::WorkingColorSpace,
crate::colormath::OutputColorSpec,
)>,
> = LazyLock::new(|| {
Mutex::new((
crate::colormath::WorkingColorSpace::default(),
crate::colormath::OutputColorSpec::default(),
))
});
pub fn set_pipeline_color_settings(
working: crate::colormath::WorkingColorSpace,
output: crate::colormath::OutputColorSpec,
) {
*PIPELINE_COLOR.lock().unwrap_or_else(|e| e.into_inner()) = (working, output);
}
pub fn pipeline_working_space() -> crate::colormath::WorkingColorSpace {
PIPELINE_COLOR.lock().unwrap_or_else(|e| e.into_inner()).0
}
pub fn pipeline_output_spec() -> crate::colormath::OutputColorSpec {
PIPELINE_COLOR.lock().unwrap_or_else(|e| e.into_inner()).1
}
pub fn pipeline_working_ofx_name() -> &'static str {
match pipeline_working_space() {
crate::colormath::WorkingColorSpace::AcesCg => "ACEScg",
crate::colormath::WorkingColorSpace::SrgbLegacy => "sRGB",
}
}
pub struct SafeConfig(ocio_rs::Config);
unsafe impl Send for SafeConfig {}
unsafe impl Sync for SafeConfig {}
impl std::ops::Deref for SafeConfig {
type Target = ocio_rs::Config;
fn deref(&self) -> &ocio_rs::Config {
&self.0
}
}
static DEFAULT_CONFIG: LazyLock<Mutex<Option<std::sync::Arc<SafeConfig>>>> =
LazyLock::new(|| Mutex::new(None));
pub fn default_config() -> Option<std::sync::Arc<SafeConfig>> {
DEFAULT_CONFIG
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
pub fn set_up_default_config() -> Result<()> {
let env = std::env::var("OCIO").ok().filter(|p| !p.is_empty());
set_up_default_config_from(env.as_deref())
}
pub fn set_up_default_config_from(path: Option<&str>) -> Result<()> {
let config = match path {
Some(path) => ocio_rs::Config::from_file(path)
.map_err(|e| Error::Failed(format!("load OCIO config \"{path}\": {e}")))?,
None => ocio_rs::Config::create_from_builtin_config("default")
.or_else(|_| ocio_rs::Config::create_from_builtin_config("ocio-2.2-default"))
.map_err(|e| Error::Failed(format!("load bundled config: {e}")))?,
};
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) =
Some(std::sync::Arc::new(SafeConfig(config)));
Ok(())
}
pub fn display_transform_result(
display: &str,
view: &str,
) -> std::result::Result<Option<String>, Error> {
let config = default_config().ok_or(Error::State)?;
let mut display_found = false;
for i in 0..config.num_displays_all() {
if config.display_all(i).as_deref() == Some(display) {
display_found = true;
break;
}
}
if !display_found {
return Ok(None);
}
let mut view_found = false;
let views =
config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, display);
for i in 0..views {
if config
.view_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, display, i)
.as_deref()
== Some(view)
{
view_found = true;
break;
}
}
if !view_found {
return Ok(None);
}
let src = config
.color_space("reference")
.and_then(|cs| cs.name())
.filter(|s| !s.is_empty())
.or_else(|| {
config
.role_color_space("reference")
.filter(|s| !s.is_empty())
})
.or_else(|| {
config
.role_color_space("aces_interchange")
.filter(|s| !s.is_empty())
})
.or_else(|| {
config
.role_color_space("scene_linear")
.filter(|s| !s.is_empty())
})
.ok_or(Error::State)?;
let processor = config
.processor_display(src, display, view, ocio_rs::TransformDirection::Forward)
.map_err(|_| Error::NotFound)?;
Ok(processor.cache_id())
}
pub fn display_transform(display: &str, view: &str) -> Option<String> {
display_transform_result(display, view).ok().flatten()
}
pub fn config_path() -> Option<String> {
if let Ok(path) = std::env::var("OCIO") {
if !path.is_empty() {
return Some(path);
}
}
default_config()?;
Some(format!(
"{}/ocioconf/config.ocio",
crate::commonutil::configuration_location()
))
}
static OCIO_STUB_CACHE: LazyLock<Mutex<HashMap<String, Option<String>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn ocio_function_shader(fn_name: &str, from_space: &str, to_space: &str) -> Option<String> {
let config = default_config()?;
let cache_key = format!(
"{}:{}:{}:{}",
fn_name,
from_space,
to_space,
config.cache_id().unwrap_or_default()
);
if let Some(hit) = OCIO_STUB_CACHE
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&cache_key)
{
return hit.clone();
}
let stub = build_ocio_function_shader(&config, fn_name, from_space, to_space);
OCIO_STUB_CACHE
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(cache_key, stub.clone());
stub
}
fn build_ocio_function_shader(
config: &SafeConfig,
fn_name: &str,
from_space: &str,
to_space: &str,
) -> Option<String> {
let processor = config.processor(from_space, to_space).ok()?;
let gpu = processor.default_gpu_processor().ok()?;
let mut desc = ocio_rs::GpuShaderDesc::create().ok()?;
desc.set_language(ocio_rs::GpuLanguage::GlslEs3_0).ok()?;
desc.set_function_name(fn_name).ok()?;
desc.set_resource_prefix("ocio_").ok()?;
gpu.try_extract_shader_info(&mut desc).ok()?;
if desc.num_textures() > 0 || desc.num_3d_textures() > 0 {
return None;
}
desc.shader_text()
}
static OCIO_GRADING_CACHE: LazyLock<Mutex<HashMap<String, Option<String>>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn grading_primary_function_shader(style: GradingStyle) -> Option<String> {
let config = default_config()?;
let style_id = match style {
GradingStyle::Lin => "lin",
GradingStyle::Log => "log",
};
let cache_key = format!("{style_id}:{}", config.cache_id().unwrap_or_default());
if let Some(hit) = OCIO_GRADING_CACHE
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&cache_key)
{
return hit.clone();
}
let stub = build_ocio_grading_shader(&config, style);
OCIO_GRADING_CACHE
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(cache_key, stub.clone());
stub
}
fn build_ocio_grading_shader(config: &SafeConfig, style: GradingStyle) -> Option<String> {
let transform = ocio_rs::transform::GradingPrimaryTransform::create(style.to_ocio()).ok()?;
transform.make_dynamic();
transform.set_direction(ocio_rs::TransformDirection::Forward);
let processor = config
.processor_from_transform(&transform, ocio_rs::TransformDirection::Forward)
.ok()?;
let gpu = processor.default_gpu_processor().ok()?;
let mut desc = ocio_rs::GpuShaderDesc::create().ok()?;
desc.set_language(ocio_rs::GpuLanguage::GlslEs3_0).ok()?;
desc.set_function_name("ove_grading_primary").ok()?;
desc.set_resource_prefix("ocio_").ok()?;
gpu.try_extract_shader_info(&mut desc).ok()?;
if desc.num_textures() > 0 || desc.num_3d_textures() > 0 {
return None;
}
desc.shader_text()
}
pub const SUPPORTED_LUT_EXTENSIONS: [&str; 9] = [
"cube", "3dl", "spi1d", "spi3d", "spimtx", "csp", "clf", "ctf", "cub",
];
pub fn is_supported_lut_extension(extension: &str) -> bool {
let ext = extension.strip_prefix('.').unwrap_or(extension);
SUPPORTED_LUT_EXTENSIONS
.iter()
.any(|e| e.eq_ignore_ascii_case(ext))
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
static CONFIG_TEST_LOCK: Mutex<()> = Mutex::new(());
fn config_lock() -> MutexGuard<'static, ()> {
CONFIG_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn system_icc() -> Option<&'static str> {
[
"/System/Library/ColorSync/Profiles/sRGB Profile.icc",
"/System/Library/ColorSync/Profiles/Display P3.icc",
"/usr/share/color/icc/colord/sRGB.icc",
"/usr/share/color/icc/ghostscript/srgb.icc",
"/usr/share/color/icc/sRGB.icc",
"/usr/local/share/color/icc/colord/sRGB.icc",
]
.into_iter()
.find(|p| std::path::Path::new(p).exists())
}
#[test]
fn lut_extensions_case_insensitive_and_dot_optional() {
assert!(is_supported_lut_extension("cube"));
assert!(is_supported_lut_extension(".CUBE"));
assert!(is_supported_lut_extension("clf"));
assert!(!is_supported_lut_extension("exr"));
assert!(!is_supported_lut_extension(""));
assert_eq!(SUPPORTED_LUT_EXTENSIONS.len(), 9);
}
#[test]
fn pass_through_processor_identity() {
let p = ColorProcessor::pass_through();
assert!(!p.is_valid());
assert_eq!(
p.convert_color([0.25, 0.5, 0.75, 1.0]),
[0.25, 0.5, 0.75, 1.0]
);
let mut f = Frame::dummy();
assert!(p.convert_frame(&mut f).is_ok());
}
#[test]
fn convert_frame_rejects_non_f32() {
let p = ColorProcessor::pass_through();
let mut f = Frame::new();
f.format = PixelFormat::U8;
assert!(
p.convert_frame(&mut f).is_ok(),
"pass-through converts nothing"
);
if set_up_default_config().is_err() {
return;
}
let valid = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
.and_then(|p| p.is_valid().then_some(p));
if let Some(valid) = valid {
let mut f = Frame::new();
f.format = PixelFormat::U8;
assert_eq!(
valid.convert_frame(&mut f).unwrap_err().code(),
crate::error::QUERCUSCORE_E_INVALID
);
}
}
#[test]
fn create_without_config_yields_none() {
let _lock = config_lock();
let saved = default_config();
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) = None;
assert!(ColorProcessor::create("scene_linear", "sdr-video", Direction::Normal).is_none());
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) = saved;
}
#[test]
fn default_config_setup_and_role_resolution() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let config = default_config().expect("config set up");
assert!(config.has_role("scene_linear") || config.has_role("aces_interchange"));
}
#[test]
fn processor_from_builtin_converts() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
} let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal);
let p = p.expect("processor handle always returned");
assert!(
p.is_valid(),
"ACEScg→sRGB Encoded must be a valid processor"
);
let out = p.convert_color([0.18, 0.18, 0.18, 1.0]);
assert!((out[3] - 1.0).abs() < 1e-6, "alpha passes through");
assert!(out[0].is_finite() && out[1].is_finite() && out[2].is_finite());
assert!(
(out[0] - 0.18).abs() > 1e-3,
"the transform must change the value"
);
}
#[test]
fn unknown_colorspaces_yield_invalid_processor() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let p = ColorProcessor::create("not_a_real_colorspace", "also_not_real", Direction::Normal)
.unwrap();
assert!(!p.is_valid(), "lookup failure is non-fatal (pass-through)");
assert_eq!(
p.convert_color([1.0, 0.5, 0.25, 0.0]),
[1.0, 0.5, 0.25, 0.0]
);
}
#[test]
fn display_transform_queries() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let config = default_config().unwrap();
if config.num_displays_all() <= 0 {
return;
}
let display = config.display_all(0).unwrap();
let n =
config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display);
assert!(n >= 0);
if n > 0 {
let view = config
.view_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display, 0)
.unwrap();
let id = display_transform(&display, &view);
assert!(id.is_some());
assert!(!id.unwrap().is_empty());
let last = config
.view_by_reference_space(
ocio_rs::SearchReferenceSpaceType::Scene,
&display,
n - 1,
)
.unwrap();
assert!(display_transform_result(&display, &last).is_ok());
assert_eq!(display_transform_result(&display, "no-such-view").unwrap(), None);
}
assert!(display_transform("no-such-display", "x").is_none());
}
#[test]
fn convert_frame_with_valid_processor_changes_pixels() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
.expect("handle always returned");
if !p.is_valid() {
return;
}
let mut f = Frame::new();
let mut pod = crate::frame::VideoParamsPod::default();
pod.width = 4;
pod.height = 2;
f.set_video_params(pod);
f.allocate();
let f32s: &mut [f32] = unsafe {
std::slice::from_raw_parts_mut(f.data.as_mut_ptr() as *mut f32, f.pixel_count() * 4)
};
for px in f32s.as_chunks_mut::<4>().0 {
px[0] = 0.18;
px[1] = 0.18;
px[2] = 0.18;
px[3] = 1.0;
}
p.convert_frame(&mut f).unwrap();
let out: &[f32] = unsafe {
std::slice::from_raw_parts(f.data.as_ptr() as *const f32, f.pixel_count() * 4)
};
assert!(
(out[0] - 0.18).abs() > 1e-4,
"scene_linear→sdr-video must change 0.18 grey (got {})",
out[0]
);
assert!((out[3] - 1.0).abs() < 1e-5, "alpha preserved");
}
#[test]
fn display_icc_processor_applies_srgb_profile() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let Some(icc) = system_icc() else {
eprintln!("no system ICC profile; skipping");
return;
};
let p = ColorProcessor::create_display_icc("scene_linear", icc)
.expect("handle always returned");
assert!(p.is_valid(), "ICC processor builds from {icc}");
let out = p.convert_color([0.18, 0.18, 0.18, 1.0]);
assert!(
(out[0] - 0.5).abs() < 0.08,
"0.18 linear grey should land near 0.5 sRGB (got {})",
out[0]
);
assert!(
(out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3,
"grey stays grey: {out:?}"
);
assert!((out[3] - 1.0).abs() < 1e-5, "alpha preserved");
}
#[test]
fn display_icc_xyz_accepts_interchange_role() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let Some(icc) = system_icc() else {
eprintln!("no system ICC profile; skipping");
return;
};
let p = ColorProcessor::create_display_icc_xyz(icc)
.expect("handle always returned or explicit None");
assert!(
p.is_valid(),
"the cie_xyz_d65_interchange role must build the XYZ→ICC chain from {icc}"
);
let spec = crate::colormath::OutputColorSpec::default();
let encoded = [0.5f32, 0.5, 0.5, 1.0];
let mut xyz_in = encoded;
crate::colormath::output_spec_to_xyz_d65(&mut xyz_in, spec);
let mut via_xyz = xyz_in;
let _ = p.convert_f32_rgba(&mut via_xyz, 1);
let srgb = ColorProcessor::create_display_icc("sRGB Encoded Rec.709 (sRGB)", icc)
.expect("handle always returned");
let mut via_srgb = encoded;
let _ = srgb.convert_f32_rgba(&mut via_srgb, 1);
for c in 0..3 {
assert!(
(via_xyz[c] - via_srgb[c]).abs() < 0.02,
"channel {c}: XYZ chain {} vs sRGB chain {} (round trip must be identity)",
via_xyz[c],
via_srgb[c]
);
}
assert!(
(via_xyz[0] - via_xyz[1]).abs() < 1e-3 && (via_xyz[1] - via_xyz[2]).abs() < 1e-3,
"grey stays grey: {via_xyz:?}"
);
assert!((via_xyz[3] - 1.0).abs() < 1e-5, "alpha preserved");
}
#[test]
fn display_icc_bgra8_never_outputs_black() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let env_icc = std::env::var("QUERCUS_DISPLAY_ICC").unwrap_or_default();
let icc = if env_icc.is_empty() {
let Some(system) = system_icc() else {
eprintln!("QUERCUS_DISPLAY_ICC unset and no system ICC profile; skipping");
return;
};
system.to_string()
} else {
env_icc
};
let p = ColorProcessor::create_display_icc_bgra8("sRGB Encoded Rec.709 (sRGB)", &icc)
.expect("handle always returned");
assert!(p.is_valid(), "BGRA8 ICC processor builds from {icc}");
let mut data: Vec<u8> = vec![
128, 128, 128, 255, 0, 0, 191, 255, 0, 64, 0, 255, 153, 0, 0, 255, ];
let bgra_result = p.convert_bgra8(&mut data, 4);
eprintln!("bgra8 convert: {bgra_result:?} -> {data:?}");
let p32 = ColorProcessor::create_display_icc("sRGB Encoded Rec.709 (sRGB)", &icc)
.expect("handle always returned");
assert!(p32.is_valid(), "F32 ICC processor builds from {icc}");
let mut f32s: Vec<f32> = vec![
0.5, 0.5, 0.5, 1.0, 0.75, 0.0, 0.0, 1.0, 0.0, 0.25, 0.0, 1.0, 0.0, 0.0, 0.6, 1.0, ];
let f32_result = p32.convert_f32_rgba(&mut f32s, 4);
eprintln!("f32 convert: {f32_result:?} -> {f32s:?}");
bgra_result.expect("convert");
for (i, px) in data.as_chunks::<4>().0.iter().enumerate() {
assert_eq!(px[3], 255, "pixel {i}: alpha preserved");
let rgb: u32 = px[0] as u32 + px[1] as u32 + px[2] as u32;
assert!(rgb > 0, "pixel {i} must not be crushed to black: {px:?}");
}
let (b, g, r) = (data[0] as i32, data[1] as i32, data[2] as i32);
assert!(
(b - g).abs() < 24 && (g - r).abs() < 24,
"grey stays grey through the display ICC: {b} {g} {r}"
);
f32_result.expect("convert f32");
for (i, px) in f32s.as_chunks::<4>().0.iter().enumerate() {
assert!((px[3] - 1.0).abs() < 1e-3, "pixel {i}: alpha preserved");
let rgb = px[0] + px[1] + px[2];
assert!(rgb > 0.0, "pixel {i} must not be crushed to black: {px:?}");
}
}
#[test]
fn inverse_direction_reverses() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let fwd =
ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
.expect("handle");
let inv =
ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Inverse)
.expect("handle");
if !fwd.is_valid() || !inv.is_valid() {
return;
}
let x = fwd.convert_color([0.18, 0.18, 0.18, 1.0]);
let back = inv.convert_color(x);
assert!(
(back[0] - 0.18).abs() < 0.05,
"inverse undoes forward ({} -> {})",
x[0],
back[0]
);
}
#[test]
fn create_lut_from_real_file() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let lut = "TITLE quercusrender test\nLUT_1D_SIZE 2\n0.0 0.0 0.0\n1.0 1.0 1.0\n";
let path = std::env::temp_dir().join("quercusrender-test-lut.cube");
std::fs::write(&path, lut).unwrap();
let p = ColorProcessor::create_lut(path.to_string_lossy().as_ref(), Direction::Normal)
.expect("processor handle always returned");
assert!(
p.is_valid(),
"a readable .cube must produce a valid processor"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn grading_primary_and_from_processor() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping grading-primary/from-processor assertions");
return;
}
let p = ColorProcessor::create_grading_primary(GradingStyle::Log)
.expect("grading-primary processor on a live config");
assert!(p.is_valid(), "grading-primary processor must be valid");
let out = p.convert_color([0.18, 0.5, 0.7, 1.0]);
assert!(out.iter().all(|v| v.is_finite()));
let config = default_config().expect("config set up");
let proc = config
.processor("ACEScg", "sRGB Encoded Rec.709 (sRGB)")
.expect("ACEScg→sRGB Encoded processor on the default config");
let p = ColorProcessor::from_processor(proc);
assert!(p.is_valid());
assert!(!p.cache_id().is_empty(), "OCIO cache id present");
}
#[test]
fn grading_primary_shader_generates_analytic_glsl() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return; }
for style in [GradingStyle::Lin, GradingStyle::Log] {
let stub = grading_primary_function_shader(style)
.expect("default config generates an analytic grading shader");
assert!(
stub.contains("ove_grading_primary"),
"function name present"
);
assert!(
!stub.contains("sampler"),
"no LUT upload expected for grading"
);
let again = grading_primary_function_shader(style).unwrap();
assert_eq!(stub, again);
}
}
#[test]
fn create_lut_missing_file_is_invalid() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let p = ColorProcessor::create_lut("/nonexistent/never.cube", Direction::Normal).unwrap();
assert!(!p.is_valid(), "unreadable LUT → pass-through processor");
}
#[test]
fn ocio_function_shader_generates_glsl_for_chromakey() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return; }
let stub = ocio_function_shader(
"SceneLinearToCIEXYZ_d65",
"scene_linear",
"cie_xyz_d65_interchange",
)
.expect("default config generates an analytic shader");
assert!(
stub.contains("SceneLinearToCIEXYZ_d65"),
"function name present"
);
assert!(
!stub.contains("sampler"),
"no LUT upload expected in the default config"
);
let again = ocio_function_shader(
"SceneLinearToCIEXYZ_d65",
"scene_linear",
"cie_xyz_d65_interchange",
)
.unwrap();
assert_eq!(stub, again);
}
#[test]
fn ocio_function_shader_rejects_lut_processors() {
let _lock = config_lock();
let Ok(cfg) =
ocio_rs::Config::create_from_builtin_config("studio-config-v2.1.0_aces-v1.3_ocio-v2.3")
else {
return;
};
let cfg = SafeConfig(cfg);
assert!(
build_ocio_function_shader(&cfg, "probe", "ACEScg", "Rec.709 - Display").is_none(),
"LUT-based processor must be refused"
);
assert!(
build_ocio_function_shader(&cfg, "probe", "ACEScg", "ACES2065-1").is_some(),
"analytic processor still generates"
);
}
#[test]
fn rb_swap_matrix_builds_from_ocio() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping");
return;
}
assert!(rb_swap_matrix().is_some(), "matrix transform available");
}
#[test]
fn pass_through_processor_accessors_and_conversions() {
let p = ColorProcessor::pass_through();
assert!(p.processor().is_none());
assert_eq!(p.cache_id(), "", "pass-through has no cache id");
let mut data = [1u8, 2, 3, 4];
p.convert_bgra8(&mut data, 1).unwrap();
assert_eq!(data, [1, 2, 3, 4]);
let mut samples = [0.25f32, 0.5, 0.75, 1.0];
p.convert_f32_rgba(&mut samples, 1).unwrap();
assert_eq!(samples, [0.25, 0.5, 0.75, 1.0]);
p.convert_bgra8(&mut data, -3).unwrap();
}
#[test]
fn create_lut_on_config_and_inverse_direction() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping");
return;
}
let config = default_config().expect("config set up");
let lut = "TITLE quercus create_lut_on test\nLUT_1D_SIZE 2\n0.0 0.0 0.0\n1.0 1.0 1.0\n";
let path = std::env::temp_dir().join(format!("quercus-color-lut-on-{}.cube", std::process::id()));
std::fs::write(&path, lut).unwrap();
let path = path.to_string_lossy().into_owned();
let fwd = ColorProcessor::create_lut_on(&config, &path, Direction::Normal)
.expect("processor handle always returned");
assert!(fwd.is_valid(), "readable LUT on a specific config");
let inv = ColorProcessor::create_lut_on(&config, &path, Direction::Inverse)
.expect("processor handle always returned");
assert!(inv.is_valid(), "inverse LUT processor");
let inv_default = ColorProcessor::create_lut(&path, Direction::Inverse)
.expect("processor handle always returned");
assert!(inv_default.is_valid());
let missing = ColorProcessor::create_lut_on(&config, "/nonexistent/quercus.cube", Direction::Normal)
.expect("processor handle always returned");
assert!(!missing.is_valid(), "unreadable LUT → pass-through");
let _ = std::fs::remove_file(&path);
}
#[test]
fn valid_processor_accessor_and_cache_id() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping processor accessor assertions");
return;
}
let config = default_config().expect("config set up");
let proc = config
.processor("ACEScg", "sRGB Encoded Rec.709 (sRGB)")
.expect("ACEScg→sRGB Encoded processor on the default config");
let p = ColorProcessor::from_processor(proc);
assert!(p.processor().is_some(), "valid processor exposes its handle");
assert!(!p.cache_id().is_empty());
}
#[test]
fn convert_frame_rejects_non_f32_and_unaligned_buffers() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping");
return;
}
let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
.expect("handle always returned");
if !p.is_valid() {
eprintln!("processor unavailable in this config; skipping");
return;
}
let mut f = Frame::new();
f.format = PixelFormat::U8;
assert_eq!(
p.convert_frame(&mut f).unwrap_err().code(),
crate::error::QUERCUSCORE_E_INVALID
);
let mut f = Frame::new();
f.format = PixelFormat::F32;
f.data = vec![0u8; 3];
assert!(p.convert_frame(&mut f).is_err(), "unaligned buffer rejected");
let mut f = Frame::new();
f.width = 1;
f.height = 1;
f.channels = 4;
f.format = PixelFormat::F32;
f.data = vec![0u8; 16];
p.convert_frame(&mut f).unwrap();
}
#[test]
fn display_icc_bgra8_and_xyz_bgra8_chains() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping");
return;
}
let Some(icc) = system_icc() else {
eprintln!("no system ICC profile; skipping");
return;
};
let p = ColorProcessor::create_display_icc_bgra8("sRGB Encoded Rec.709 (sRGB)", icc)
.expect("handle always returned");
assert!(p.is_valid(), "BGRA8 ICC chain builds from {icc}");
let mut data: Vec<u8> = vec![128, 128, 128, 255, 0, 0, 191, 255];
assert!(p.convert_bgra8(&mut data, 2).is_ok());
assert_eq!(data[3], 255, "alpha preserved");
assert!(data[..3].iter().any(|&b| b != 0), "grey survives");
let mut short = vec![0u8; 3];
assert_eq!(
p.convert_bgra8(&mut short, 2).unwrap_err().code(),
crate::error::QUERCUSCORE_E_INVALID
);
let mut unchanged = vec![7u8; 4];
let _ = p.convert_bgra8(&mut unchanged, 0);
let _ = p.convert_bgra8(&mut unchanged, -3);
let mut samples: Vec<f32> = vec![0.5, 0.5, 0.5, 1.0];
assert!(p.convert_f32_rgba(&mut samples, 1).is_ok());
assert!((samples[3] - 1.0).abs() < 1e-3, "alpha preserved");
let xyz = ColorProcessor::create_display_icc_xyz_bgra8(icc).expect("handle");
assert!(xyz.is_valid(), "XYZ BGRA8 ICC chain builds from {icc}");
}
struct PipelineColorGuard {
working: crate::colormath::WorkingColorSpace,
output: crate::colormath::OutputColorSpec,
}
impl PipelineColorGuard {
fn capture() -> Self {
Self {
working: pipeline_working_space(),
output: pipeline_output_spec(),
}
}
fn restore(&mut self) {
set_pipeline_color_settings(self.working, self.output);
}
}
impl Drop for PipelineColorGuard {
fn drop(&mut self) {
self.restore();
}
}
#[test]
fn pipeline_working_ofx_name_legacy_mode() {
use crate::colormath::WorkingColorSpace;
let _lock = config_lock();
let mut restore = PipelineColorGuard::capture();
set_pipeline_color_settings(WorkingColorSpace::SrgbLegacy, restore.output);
assert_eq!(pipeline_working_ofx_name(), "sRGB");
restore.restore();
let expected = match restore.working {
WorkingColorSpace::AcesCg => "ACEScg",
WorkingColorSpace::SrgbLegacy => "sRGB",
};
assert_eq!(pipeline_working_ofx_name(), expected, "settings restored");
}
#[test]
fn config_path_env_and_bundled_fallback() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping");
return;
}
let saved = std::env::var_os("OCIO");
struct EnvGuard(Option<std::ffi::OsString>);
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.0.take() {
Some(v) => std::env::set_var("OCIO", v),
None => std::env::remove_var("OCIO"),
}
}
}
let _env_guard = EnvGuard(saved);
std::env::set_var("OCIO", "/tmp/quercus-config-path-test.ocio");
assert_eq!(
config_path().as_deref(),
Some("/tmp/quercus-config-path-test.ocio"),
"a non-empty $OCIO wins"
);
std::env::set_var("OCIO", "");
let fallback = config_path().expect("bundled fallback when a config exists");
assert!(fallback.ends_with("ocioconf/config.ocio"), "got {fallback}");
}
#[test]
fn set_up_default_config_from_bad_path_errors_and_keeps_config() {
let _lock = config_lock();
if set_up_default_config().is_err() {
return;
}
let before = default_config();
let err = set_up_default_config_from(Some("/nonexistent/quercus-config.ocio"));
assert!(err.is_err(), "a bad config path is a load error");
assert!(
default_config().is_some(),
"the previous default config survives a failed load"
);
assert_eq!(
default_config().map(|c| c.cache_id()),
before.map(|c| c.cache_id())
);
}
#[test]
fn display_transform_scene_linear_role_fallback() {
let _lock = config_lock();
let cfg_text = r#"
ocio_profile_version: 2
name: quercus-fallback-test
search_path: ""
roles:
scene_linear: Raw
displays:
quercusdisplay:
- !<View> {name: quercusview, colorspace: Raw}
colorspaces:
- !<ColorSpace>
name: Raw
family: ""
isdata: false
allocation: uniform
"#;
let Ok(cfg) = ocio_rs::Config::from_stream(cfg_text) else {
eprintln!("OCIO config-from-stream unavailable; skipping");
return;
};
assert!(cfg
.color_space("reference")
.and_then(|cs| cs.name())
.filter(|s| !s.is_empty())
.is_none());
assert!(cfg
.role_color_space("reference")
.filter(|s| !s.is_empty())
.is_none());
assert!(cfg
.role_color_space("aces_interchange")
.filter(|s| !s.is_empty())
.is_none());
assert_eq!(cfg.role_color_space("scene_linear").as_deref(), Some("Raw"));
let saved = default_config();
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) =
Some(std::sync::Arc::new(SafeConfig(cfg)));
let found = display_transform_result("quercusdisplay", "quercusview");
let unknown_view = display_transform_result("quercusdisplay", "nope");
let unknown_display = display_transform_result("nope", "quercusview");
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) = saved;
assert!(
found.is_ok(),
"scene_linear fallback resolves the reference space: {found:?}"
);
assert_eq!(unknown_view.unwrap(), None);
assert_eq!(unknown_display.unwrap(), None);
}
struct DefaultConfigGuard(Option<std::sync::Arc<SafeConfig>>);
impl DefaultConfigGuard {
fn clear() -> Self {
let saved = default_config();
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) = None;
Self(saved)
}
}
impl Drop for DefaultConfigGuard {
fn drop(&mut self) {
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) = self.0.take();
}
}
struct OcioEnvGuard(Option<std::ffi::OsString>);
impl OcioEnvGuard {
fn set(value: &str) -> Self {
let saved = std::env::var_os("OCIO");
std::env::set_var("OCIO", value);
Self(saved)
}
}
impl Drop for OcioEnvGuard {
fn drop(&mut self) {
match self.0.take() {
Some(v) => std::env::set_var("OCIO", v),
None => std::env::remove_var("OCIO"),
}
}
}
#[test]
fn factories_without_a_default_config_return_none() {
let _lock = config_lock();
let _env = OcioEnvGuard::set("");
let _guard = DefaultConfigGuard::clear();
assert!(ColorProcessor::create("scene_linear", "sdr-video", Direction::Normal).is_none());
assert!(ColorProcessor::create_lut("/tmp/quercus-none.cube", Direction::Normal).is_none());
assert!(ColorProcessor::create_grading_primary(GradingStyle::Lin).is_none());
assert!(ColorProcessor::create_grading_primary(GradingStyle::Log).is_none());
assert!(ColorProcessor::create_display_icc("scene_linear", "/tmp/quercus-none.icc").is_none());
assert!(
ColorProcessor::create_display_icc_bgra8("scene_linear", "/tmp/quercus-none.icc").is_none()
);
assert!(ColorProcessor::create_display_icc_xyz("/tmp/quercus-none.icc").is_none());
assert!(ColorProcessor::create_display_icc_xyz_bgra8("/tmp/quercus-none.icc").is_none());
assert!(ocio_function_shader("quercus_fn", "a", "b").is_none());
assert!(grading_primary_function_shader(GradingStyle::Lin).is_none());
assert!(grading_primary_function_shader(GradingStyle::Log).is_none());
assert_eq!(
display_transform_result("display", "view")
.unwrap_err()
.code(),
crate::error::QUERCUSCORE_E_STATE
);
assert!(display_transform("display", "view").is_none());
assert!(config_path().is_none(), "no config and empty $OCIO");
}
#[test]
fn set_up_default_config_from_explicit_file_replaces_default() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no bundled OCIO config; skipping");
return;
}
let path =
std::env::temp_dir().join(format!("quercus-color-explicit-{}.ocio", std::process::id()));
let cfg_text = r#"
ocio_profile_version: 2
name: quercus-explicit-test
search_path: ""
roles:
scene_linear: Raw
default: Raw
displays:
quercusdisplay:
- !<View> {name: quercusview, colorspace: Raw}
colorspaces:
- !<ColorSpace>
name: Raw
family: ""
isdata: false
allocation: uniform
"#;
std::fs::write(&path, cfg_text).unwrap();
let result = set_up_default_config_from(Some(path.to_string_lossy().as_ref()));
let installed_name = default_config().and_then(|c| c.name());
let _ = std::fs::remove_file(&path);
let restored = set_up_default_config_from(None);
assert!(result.is_ok(), "an explicit valid config loads: {result:?}");
assert_eq!(installed_name.as_deref(), Some("quercus-explicit-test"));
assert!(
restored.is_ok(),
"the bundled config restores: {restored:?}"
);
}
#[test]
fn display_icc_xyz_returns_none_when_chain_is_invalid() {
let _lock = config_lock();
let cfg_text = r#"
ocio_profile_version: 2
name: quercus-xyz-test
search_path: ""
roles:
scene_linear: Raw
cie_xyz_d65_interchange: Raw
displays:
quercusdisplay:
- !<View> {name: quercusview, colorspace: Raw}
colorspaces:
- !<ColorSpace>
name: Raw
family: ""
isdata: false
allocation: uniform
- !<ColorSpace>
name: Linear Rec.709 (sRGB)
family: ""
isdata: false
allocation: uniform
"#;
let Ok(cfg) = ocio_rs::Config::from_stream(cfg_text) else {
eprintln!("OCIO config-from-stream unavailable; skipping");
return;
};
let icc = std::env::temp_dir().join(format!("quercus-xyz-junk-{}.icc", std::process::id()));
std::fs::write(&icc, b"this is not an ICC profile").unwrap();
let icc = icc.to_string_lossy().into_owned();
let _guard = DefaultConfigGuard::clear();
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) =
Some(std::sync::Arc::new(SafeConfig(cfg)));
let classic =
ColorProcessor::create_display_icc("cie_xyz_d65_interchange", &icc).expect("handle");
let classic_bgra8 =
ColorProcessor::create_display_icc_bgra8("cie_xyz_d65_interchange", &icc)
.expect("handle");
let xyz = ColorProcessor::create_display_icc_xyz(&icc);
let xyz_bgra8 = ColorProcessor::create_display_icc_xyz_bgra8(&icc);
assert!(
!classic.is_valid(),
"the ICC leg fails against a junk profile (non-fatal pass-through)"
);
assert!(!classic_bgra8.is_valid());
assert!(
xyz.is_none(),
"an invalid chain must surface as None for the XYZ wrapper"
);
assert!(xyz_bgra8.is_none());
let _ = std::fs::remove_file(&icc);
}
#[test]
fn display_transform_result_errors_when_no_reference_role_resolves() {
let _lock = config_lock();
let cfg_text = r#"
ocio_profile_version: 2
name: quercus-no-roles-test
search_path: ""
displays:
quercusdisplay:
- !<View> {name: quercusview, colorspace: Raw}
colorspaces:
- !<ColorSpace>
name: Raw
family: ""
isdata: false
allocation: uniform
"#;
let Ok(cfg) = ocio_rs::Config::from_stream(cfg_text) else {
eprintln!("OCIO config-from-stream unavailable; skipping");
return;
};
let _guard = DefaultConfigGuard::clear();
*DEFAULT_CONFIG.lock().unwrap_or_else(|e| e.into_inner()) =
Some(std::sync::Arc::new(SafeConfig(cfg)));
let err = display_transform_result("quercusdisplay", "quercusview")
.expect_err("no reference role resolves to a source space");
assert_eq!(err.code(), crate::error::QUERCUSCORE_E_STATE);
assert!(display_transform("quercusdisplay", "quercusview").is_none());
}
#[test]
fn set_up_default_config_honours_the_ocio_environment() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no bundled OCIO config; skipping");
return;
}
let path = std::env::temp_dir().join(format!("quercus-color-env-{}.ocio", std::process::id()));
let cfg_text = r#"
ocio_profile_version: 2
name: quercus-env-test
search_path: ""
roles:
scene_linear: Raw
default: Raw
displays:
quercusdisplay:
- !<View> {name: quercusview, colorspace: Raw}
colorspaces:
- !<ColorSpace>
name: Raw
family: ""
isdata: false
allocation: uniform
"#;
std::fs::write(&path, cfg_text).unwrap();
let path_str = path.to_string_lossy().into_owned();
let loaded = {
let _env = OcioEnvGuard::set(&path_str);
set_up_default_config()
};
let installed_name = default_config().and_then(|c| c.name());
let bundled = {
let _env = OcioEnvGuard::set("");
set_up_default_config()
};
let _ = std::fs::remove_file(&path);
assert!(loaded.is_ok(), "a non-empty $OCIO loads: {loaded:?}");
assert_eq!(installed_name.as_deref(), Some("quercus-env-test"));
assert!(bundled.is_ok(), "an empty $OCIO falls back: {bundled:?}");
}
#[test]
fn conversion_error_arms_from_buffer_size_mismatches() {
let _lock = config_lock();
if set_up_default_config().is_err() {
eprintln!("no default OCIO config; skipping");
return;
}
let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal)
.expect("handle always returned");
if !p.is_valid() {
eprintln!("processor unavailable in this config; skipping");
return;
}
let mut short = [0f32; 4];
let err = p.convert_f32_rgba(&mut short, 2).unwrap_err();
assert!(
err.to_string().contains("OCIO f32 apply"),
"the apply error is named: {err}"
);
let mut bytes = [9u8; 4];
assert_eq!(
p.convert_bgra8(&mut bytes, 2).unwrap_err().code(),
crate::error::QUERCUSCORE_E_INVALID
);
}
}