quercus-core 0.7.0

Rust value types mirroring quercuscore (Rational, TimeRange, format enums) (internal; do not depend directly)
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
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
// Quercus Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Quercus Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

//! OpenColorIO utility queries, mirroring `src/common/src/ocioutils.h`
//! and `include/common/ocioutils.h`. Also the canonical home of
//! [`PixelFormat`], which other modules reuse (mirrors
//! `olive::core::PixelFormat`). The object is stateless; the handle only
//! satisfies the C API lifetime contract.
//!
//! Real OCIO access — config loading, color-space/role enumeration,
//! display/view resolution and RGBA CPU transforms — is provided by
//! [`OcioConfig`] / [`OcioProcessor`], thin wrappers over the crates.io
//! `ocio-rs` bindings. The bit-depth mapping in
//! [`OCIOUtils::get_ocio_bit_depth_from_pixel_format`] reads from the real
//! `ocio_rs::BitDepth` enum rather than hard-coded constants.

use crate::error::{Error, Result};
use ocio_rs::TransformDirection;

/// Native pixel format codes, mirroring `olive::core::PixelFormat`. Values
/// are load-bearing (they cross the C ABI as ints) and must stay in sync
/// with `olive/core/render/pixelformat.h`.
#[repr(i32)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PixelFormat {
	/// Invalid/unknown format.
	Invalid = -1,
	/// 8-bit unsigned integer.
	U8 = 0,
	/// 10-bit unsigned integer.
	U10 = 1,
	/// 16-bit unsigned integer.
	U16 = 2,
	/// 16-bit float (half).
	F16 = 3,
	/// 32-bit float.
	F32 = 4,
	/// Sentinel, not a valid format.
	Count = 5,
}

impl PixelFormat {
	/// One of [`PixelFormat`] for an integer code (invalid/unknown codes map
	/// to `Invalid`).
	pub fn from_code(code: i32) -> PixelFormat {
		match code {
			-1 => PixelFormat::Invalid,
			0 => PixelFormat::U8,
			1 => PixelFormat::U10,
			2 => PixelFormat::U16,
			3 => PixelFormat::F16,
			4 => PixelFormat::F32,
			5 => PixelFormat::Count,
			_ => PixelFormat::Invalid,
		}
	}

	/// This format's integer code as declared in the enum.
	///
	/// The values (`Invalid = -1`, `U8 = 0`, `U10 = 1`, `U16 = 2`, `F16 = 3`,
	/// `F32 = 4`, `Count = 5`) cross the C ABI and are load-bearing; use
	/// [`PixelFormat::from_code`] for the reverse mapping.
	pub fn code(self) -> i32 {
		self as i32
	}
}

/// The OCIO utils family (stateless).
pub struct OCIOUtils;

impl OCIOUtils {
	/// Creates the OCIOUtils object.
	pub fn new() -> Self {
		Self
	}

	/// Map a native pixel format to an OCIO bit depth code (`0` unknown,
	/// `1` uint8, `2` uint10, `3` uint12, `4` uint14, `5` uint16,
	/// `6` uint32, `7` f16, `8` f32).
	///
	/// The return type is `Result` to match the C ABI surface, but the
	/// mapping is total (never fails); invalid/out-of-range formats yield
	/// `BIT_DEPTH_UNKNOWN = 0`.
	pub fn get_ocio_bit_depth_from_pixel_format(&self, pixel_format: PixelFormat) -> Result<i32> {
		// The codes are the real `ocio_rs::BitDepth` discriminants
		// (`#[repr(i32)]`): `Uint8=1`, `Uint10=2`, `Uint16=5`, `F16=7`,
		// `F32=8`, `Unknown=0`. CPP-PARITY: matches
		// `OCIOUtils::get_ocio_bit_depth_from_pixel_format` in
		// src/common/src/ocioutils.cpp, where these are the OCIO `BitDepth`
		// constants `BIT_DEPTH_UINT8` … `BIT_DEPTH_F32`. `u10` maps to uint10
		// (not uint12) and `u16` maps to uint16 (not uint14/uint32), exactly
		// as C++.
		let depth = match pixel_format {
			PixelFormat::U8 => ocio_rs::BitDepth::Uint8,
			PixelFormat::U10 => ocio_rs::BitDepth::Uint10,
			PixelFormat::U16 => ocio_rs::BitDepth::Uint16,
			PixelFormat::F16 => ocio_rs::BitDepth::F16,
			PixelFormat::F32 => ocio_rs::BitDepth::F32,
			PixelFormat::Invalid | PixelFormat::Count => ocio_rs::BitDepth::Unknown,
		};
		Ok(depth as i32)
	}
}

/// An OCIO color-config file loaded from disk (see [`OcioConfig::from_file`]) or
/// the built-in raw config (see [`OcioConfig::raw`]).
pub struct OcioConfig {
	inner: ocio_rs::Config,
}

/// An OCIO CPU processor that transforms a single RGBA pixel.
pub struct OcioProcessor {
	inner: ocio_rs::CPUProcessor,
}

// The wrapped `ocio-rs` objects own OCIO rcptrs that are immutable for the
// lifetime of the objects; OCIO documents its config/processor objects as
// safe to share for read-only use, and `ocio-rs` keeps all mutable error
// state in thread-local storage. `ocio-rs` itself marks `Processor` and
// `CPUProcessor` `Send`; `Config` holds a raw handle, so we claim `Send` and
// `Sync` here for both wrapper types.
unsafe impl Send for OcioConfig {}
unsafe impl Sync for OcioConfig {}
unsafe impl Send for OcioProcessor {}
unsafe impl Sync for OcioProcessor {}

impl OcioConfig {
	/// Loads an OCIO config from a `.ocio` file on disk.
	///
	/// `path` is a UTF-8 filesystem path; parsing (including relative includes
	/// and environment-variable expansion) is done by OCIO itself. Use
	/// [`Self::raw`] instead when no config file is available.
	///
	/// # Errors
	///
	/// Propagates the load error when the file cannot be read or is not a valid
	/// OCIO config.
	pub fn from_file(path: &str) -> Result<Self> {
		let inner = ocio_rs::Config::from_file(path)?;
		Ok(OcioConfig { inner })
	}

	/// The built-in raw (identity) config — the OCIO 2.x replacement for the
	/// removed `CreateDefault`.
	pub fn raw() -> Result<Self> {
		let inner = ocio_rs::Config::raw()?;
		Ok(OcioConfig { inner })
	}

	/// Number of color spaces registered in the config.
	pub fn colorspace_count(&self) -> Result<i32> {
		Ok(self.inner.num_color_spaces())
	}

	/// The name of the color space at `index`.
	///
	/// `index` is 0-based and must be below [`Self::colorspace_count`]; the
	/// enumeration order is the config's own, and [`Self::colorspaces`] builds
	/// its list through this function so the indices stay consistent.
	///
	/// # Errors
	///
	/// Returns an error when `index` is negative or past the last color space.
	pub fn colorspace_name(&self, index: i32) -> Result<String> {
		self.inner.color_space_name_by_index(index).ok_or_else(|| {
			Error::new(format!(
				"OcioConfig::colorspace_name: no color space at index {index}"
			))
		})
	}

	/// Names of all color spaces in config order.
	pub fn colorspaces(&self) -> Result<Vec<String>> {
		let count = self.colorspace_count()?;
		let mut out = Vec::with_capacity(count as usize);
		for i in 0..count {
			out.push(self.colorspace_name(i)?);
		}
		Ok(out)
	}

	/// Number of roles registered in the config.
	pub fn role_count(&self) -> Result<i32> {
		Ok(self.inner.num_roles())
	}

	/// The name of the role at `index`.
	///
	/// `index` is 0-based and must be below [`Self::role_count`]; roles are
	/// enumerated in the config's own order, which is also the order
	/// [`Self::roles`] returns.
	///
	/// # Errors
	///
	/// Returns an error when `index` is negative or past the last role.
	pub fn role_name(&self, index: i32) -> Result<String> {
		self.inner
			.role_name(index)
			.ok_or_else(|| Error::new(format!("OcioConfig::role_name: no role at index {index}")))
	}

	/// Names of all roles in config order.
	pub fn roles(&self) -> Result<Vec<String>> {
		let count = self.role_count()?;
		let mut out = Vec::with_capacity(count as usize);
		for i in 0..count {
			out.push(self.role_name(i)?);
		}
		Ok(out)
	}

	/// Reports whether the config defines a role named `role`.
	///
	/// `role` is a role name such as `"default"` or `"scene_linear"`; the
	/// built-in raw config always defines `"default"`. No name canonicalization
	/// is applied, so an alias is not matched here.
	///
	/// # Errors
	///
	/// The query cannot fail; the `Result` only mirrors the C-ABI signature used
	/// by the other `OcioConfig` queries.
	pub fn has_role(&self, role: &str) -> Result<bool> {
		Ok(self.inner.has_role(role))
	}

	/// Canonical name for a color space or role. Unknown names come back
	/// unchanged (OCIO's documented behavior).
	pub fn canonical_name(&self, name: &str) -> Result<String> {
		Ok(self
			.inner
			.canonical_name(name)
			.unwrap_or_else(|| name.to_string()))
	}

	/// The config's default display name.
	pub fn default_display(&self) -> Result<String> {
		self.inner
			.default_display()
			.ok_or_else(|| Error::new("OcioConfig::default_display: config defines no displays"))
	}

	/// The name of the config's default view for a display.
	///
	/// `display` is the display name as registered in the config (the name
	/// [`Self::default_display`] returns for the config default); OCIO resolves
	/// it case-sensitively.
	///
	/// # Errors
	///
	/// Returns an error when the config has no display named `display`, or that
	/// display declares no default view.
	pub fn default_view(&self, display: &str) -> Result<String> {
		self.inner.default_view(display).ok_or_else(|| {
			Error::new(format!(
				"OcioConfig::default_view: no default view for display '{display}'"
			))
		})
	}

	/// Builds a processor transforming `src` to `dst` (either may be a role
	/// name, an alias, or a color space name).
	pub fn processor(&self, src: &str, dst: &str) -> Result<OcioProcessor> {
		let processor = self.inner.processor(src, dst)?;
		Ok(OcioProcessor {
			inner: processor.default_cpu_processor()?,
		})
	}

	/// Builds a processor applying `src` through the display transform for
	/// `display`/`view` in the forward direction.
	pub fn display_processor(&self, src: &str, display: &str, view: &str) -> Result<OcioProcessor> {
		let processor =
			self.inner
				.processor_display(src, display, view, TransformDirection::Forward)?;
		Ok(OcioProcessor {
			inner: processor.default_cpu_processor()?,
		})
	}
}

impl OcioProcessor {
	/// Applies the transform to one RGBA pixel in place (0..=1 floats).
	///
	/// OCIO applies the curve to all four channels; the alpha channel is not
	/// preserved verbatim, so callers must not assume `px[3]` is unchanged.
	pub fn apply_rgba(&self, pixel: &mut [f32; 4]) -> Result<()> {
		self.inner.try_apply_rgba(pixel)?;
		Ok(())
	}
}

impl std::fmt::Debug for OcioConfig {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		f.debug_struct("OcioConfig").finish_non_exhaustive()
	}
}

impl std::fmt::Debug for OcioProcessor {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		f.debug_struct("OcioProcessor").finish_non_exhaustive()
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn pixel_format_from_code_maps_known_codes() {
		assert_eq!(PixelFormat::from_code(-1), PixelFormat::Invalid);
		assert_eq!(PixelFormat::from_code(0), PixelFormat::U8);
		assert_eq!(PixelFormat::from_code(1), PixelFormat::U10);
		assert_eq!(PixelFormat::from_code(2), PixelFormat::U16);
		assert_eq!(PixelFormat::from_code(3), PixelFormat::F16);
		assert_eq!(PixelFormat::from_code(4), PixelFormat::F32);
		assert_eq!(PixelFormat::from_code(5), PixelFormat::Count);
	}

	#[test]
	fn pixel_format_from_code_maps_unknown_codes_to_invalid() {
		assert_eq!(PixelFormat::from_code(-2), PixelFormat::Invalid);
		assert_eq!(PixelFormat::from_code(6), PixelFormat::Invalid);
		assert_eq!(PixelFormat::from_code(100), PixelFormat::Invalid);
		assert_eq!(PixelFormat::from_code(i32::MIN), PixelFormat::Invalid);
		assert_eq!(PixelFormat::from_code(i32::MAX), PixelFormat::Invalid);
	}

	#[test]
	fn pixel_format_code_round_trips() {
		for fmt in [
			PixelFormat::Invalid,
			PixelFormat::U8,
			PixelFormat::U10,
			PixelFormat::U16,
			PixelFormat::F16,
			PixelFormat::F32,
			PixelFormat::Count,
		] {
			assert_eq!(PixelFormat::from_code(fmt.code()), fmt);
		}
	}

	#[test]
	fn pixel_format_codes_have_load_bearing_values() {
		assert_eq!(PixelFormat::Invalid.code(), -1);
		assert_eq!(PixelFormat::U8.code(), 0);
		assert_eq!(PixelFormat::U10.code(), 1);
		assert_eq!(PixelFormat::U16.code(), 2);
		assert_eq!(PixelFormat::F16.code(), 3);
		assert_eq!(PixelFormat::F32.code(), 4);
		assert_eq!(PixelFormat::Count.code(), 5);
	}

	#[test]
	fn new_returns_a_value() {
		let utils = OCIOUtils::new();
		// Stateless; just confirm construction works and stays usable.
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::U8)
				.unwrap(),
			1
		);
	}

	#[test]
	fn ocio_bit_depth_maps_supported_formats() {
		let utils = OCIOUtils::new();
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::U8)
				.unwrap(),
			1
		);
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::U10)
				.unwrap(),
			2
		);
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::U16)
				.unwrap(),
			5
		);
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::F16)
				.unwrap(),
			7
		);
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::F32)
				.unwrap(),
			8
		);
	}

	#[test]
	fn ocio_bit_depth_maps_invalid_and_count_to_unknown() {
		let utils = OCIOUtils::new();
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::Invalid)
				.unwrap(),
			0
		);
		assert_eq!(
			utils
				.get_ocio_bit_depth_from_pixel_format(PixelFormat::Count)
				.unwrap(),
			0
		);
	}

	#[test]
	fn ocio_config_raw_round_trips() {
		// A real OCIO library call through the ocio-sys bridge that needs no
		// config file and no OCIO env var: the built-in raw (identity) config
		// always defines at least the "default" role, whose canonical color
		// space is named "raw".
		let config = OcioConfig::raw().expect("raw config should load");
		assert!(config.colorspace_count().expect("count should work") > 0);
		assert!(
			config.has_role("default").expect("has_role should work"),
			"raw config should define the default role"
		);
		let canonical = config
			.canonical_name("default")
			.expect("canonical should work");
		assert_eq!(canonical, "raw");
	}
}