use crate::error::Result;
use crate::transforms::{Lut1DTransform, Lut3DTransform, MatrixTransform};
use crate::types::{BitDepth, Interpolation};
pub const MAX_1D_LUT_LENGTH: usize = 300_000;
pub const MAX_3D_LUT_LENGTH: usize = 129;
pub fn is_valid_lut1d_interpolation(interp: Interpolation) -> bool {
matches!(
interp,
Interpolation::Best
| Interpolation::Default
| Interpolation::Linear
| Interpolation::Nearest
)
}
pub fn is_valid_lut3d_interpolation(interp: Interpolation) -> bool {
matches!(
interp,
Interpolation::Best
| Interpolation::Tetrahedral
| Interpolation::Default
| Interpolation::Linear
| Interpolation::Nearest
)
}
pub fn lut1d_concrete_interpolation(_interp: Interpolation) -> Interpolation {
Interpolation::Linear
}
pub fn lut3d_concrete_interpolation(interp: Interpolation) -> Interpolation {
match interp {
Interpolation::Best | Interpolation::Tetrahedral => Interpolation::Tetrahedral,
_ => Interpolation::Linear,
}
}
pub fn handle_lut1d(lut: &mut Lut1DTransform, file_interp: Interpolation) -> bool {
let valid = is_valid_lut1d_interpolation(file_interp);
let interp = if valid {
file_interp
} else {
Interpolation::Default
};
if lut1d_concrete_interpolation(lut.interpolation) != lut1d_concrete_interpolation(interp) {
lut.interpolation = interp;
}
valid
}
pub fn handle_lut3d(lut: &mut Lut3DTransform, file_interp: Interpolation) -> bool {
let valid = is_valid_lut3d_interpolation(file_interp);
let interp = if valid {
file_interp
} else {
Interpolation::Default
};
if lut3d_concrete_interpolation(lut.interpolation) != lut3d_concrete_interpolation(interp) {
lut.interpolation = interp;
}
valid
}
pub fn new_lut1d(
length: usize,
half_domain: bool,
interp: Interpolation,
file_bd: BitDepth,
) -> Lut1DTransform {
let mut lut = Lut1DTransform::new(length, half_domain);
if is_valid_lut1d_interpolation(interp) {
lut.interpolation = interp;
}
lut.file_output_bit_depth = file_bd;
lut
}
pub fn new_lut3d(grid_size: usize, interp: Interpolation, file_bd: BitDepth) -> Lut3DTransform {
let mut lut = Lut3DTransform::new(grid_size);
if is_valid_lut3d_interpolation(interp) {
lut.interpolation = interp;
}
lut.file_output_bit_depth = file_bd;
lut
}
pub fn set_lut3d_from_red_fastest(lut: &mut Lut3DTransform, raw: &[f32]) -> Result<()> {
let n = lut.grid_size;
if raw.len() != n * n * n * 3 {
crate::bail!(
"Lut3DOpData length does not match the vector size: {} != {}.",
n * n * n * 3,
raw.len()
);
}
for b in 0..n {
for g in 0..n {
for r in 0..n {
let src = 3 * ((b * n + g) * n + r);
let dst = 3 * ((r * n + g) * n + b);
lut.values[dst..dst + 3].copy_from_slice(&raw[src..src + 3]);
}
}
}
Ok(())
}
pub fn get_3d_lut_edge_len_from_num_pixels(num_pixels: i64) -> Result<usize> {
let dim = (num_pixels as f32).powf(1.0 / 3.0).round() as i64;
if dim.checked_mul(dim).and_then(|d| d.checked_mul(dim)) != Some(num_pixels) {
crate::bail!(
"Cannot infer 3D LUT size. {} element(s) does not correspond to a unform cube edge length. (nearest edge length is {}).",
num_pixels,
dim
);
}
Ok(dim.max(0) as usize)
}
pub fn lut3d_index_blue_fast(r: usize, g: usize, b: usize, size_g: usize, size_b: usize) -> usize {
3 * (b + size_b * (g + size_g * r))
}
pub fn min_max_matrix(from_min: [f64; 3], from_max: [f64; 3]) -> Result<Option<MatrixTransform>> {
let mut scale4 = [1.0f64; 4];
let mut offset4 = [0.0f64; 4];
let mut something_to_do = false;
for i in 0..3 {
let range = from_max[i] - from_min[i];
if range == 0.0 {
crate::bail!("CreateMinMaxOp: from_min and from_max must not be equal.");
}
scale4[i] = 1.0 / range;
offset4[i] = -from_min[i] * scale4[i];
something_to_do |= scale4[i] != 1.0 || offset4[i] != 0.0;
}
if !something_to_do {
return Ok(None);
}
let (m, _) = MatrixTransform::scale(&scale4);
Ok(Some(MatrixTransform::new(m, offset4)))
}
pub fn min_max_matrix_f32(from_min: f32, from_max: f32) -> Result<Option<MatrixTransform>> {
let mn = from_min as f64;
let mx = from_max as f64;
min_max_matrix([mn; 3], [mx; 3])
}
pub fn bit_depth_max_value(bd: BitDepth) -> Result<f64> {
match bd {
BitDepth::UInt8 => Ok(255.0),
BitDepth::UInt10 => Ok(1023.0),
BitDepth::UInt12 => Ok(4095.0),
BitDepth::UInt16 => Ok(65535.0),
BitDepth::F16 | BitDepth::F32 => Ok(1.0),
_ => crate::bail!("Bit depth is not supported: {}.", bd.as_str()),
}
}
pub fn bitdepth_from_max_value(max_value: u32) -> BitDepth {
if max_value < 128 {
BitDepth::F32
} else if max_value < 639 {
BitDepth::UInt8
} else if max_value < 2559 {
BitDepth::UInt10
} else if max_value < 34815 {
BitDepth::UInt12
} else {
BitDepth::UInt16
}
}
pub fn is_space(c: u8) -> bool {
c <= b' '
}
fn is_c_space(c: u8) -> bool {
matches!(c, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c)
}
fn float_prefix_len(s: &[u8]) -> usize {
let mut i = 0;
if i < s.len() && (s[i] == b'+' || s[i] == b'-') {
i += 1;
}
let rest = &s[i..];
let lower: Vec<u8> = rest
.iter()
.take(8)
.map(|c| c.to_ascii_lowercase())
.collect();
if lower.starts_with(b"infinity") {
return i + 8;
}
if lower.starts_with(b"inf") {
return i + 3;
}
if lower.starts_with(b"nan") {
let mut j = i + 3;
if j < s.len() && s[j] == b'(' {
let mut k = j + 1;
while k < s.len() && (s[k].is_ascii_alphanumeric() || s[k] == b'_') {
k += 1;
}
if k < s.len() && s[k] == b')' {
j = k + 1;
}
}
return j;
}
if rest.len() > 2 && rest[0] == b'0' && (rest[1] == b'x' || rest[1] == b'X') {
let mut j = i + 2;
let mut digits = 0;
while j < s.len() && s[j].is_ascii_hexdigit() {
j += 1;
digits += 1;
}
if j < s.len() && s[j] == b'.' {
j += 1;
while j < s.len() && s[j].is_ascii_hexdigit() {
j += 1;
digits += 1;
}
}
if digits == 0 {
return i + 1;
}
if j < s.len() && (s[j] == b'p' || s[j] == b'P') {
let mut k = j + 1;
if k < s.len() && (s[k] == b'+' || s[k] == b'-') {
k += 1;
}
let start = k;
while k < s.len() && s[k].is_ascii_digit() {
k += 1;
}
if k > start {
j = k;
}
}
return j;
}
let mut j = i;
let mut digits = 0;
while j < s.len() && s[j].is_ascii_digit() {
j += 1;
digits += 1;
}
if j < s.len() && s[j] == b'.' {
j += 1;
while j < s.len() && s[j].is_ascii_digit() {
j += 1;
digits += 1;
}
}
if digits == 0 {
return 0;
}
if j < s.len() && (s[j] == b'e' || s[j] == b'E') {
let mut k = j + 1;
if k < s.len() && (s[k] == b'+' || s[k] == b'-') {
k += 1;
}
let start = k;
while k < s.len() && s[k].is_ascii_digit() {
k += 1;
}
if k > start {
j = k;
}
}
j
}
fn parse_hex_float(s: &str) -> Option<f64> {
let body = &s[2..];
let (mant, exp) = match body.find(['p', 'P']) {
Some(p) => (&body[..p], body[p + 1..].parse::<i32>().ok()?),
None => (body, 0),
};
let mut value = 0.0f64;
let mut frac_digits = 0i32;
let mut after_dot = false;
for c in mant.chars() {
if c == '.' {
after_dot = true;
continue;
}
let d = c.to_digit(16)? as f64;
value = value * 16.0 + d;
if after_dot {
frac_digits += 1;
}
}
Some(value * 2f64.powi(exp - 4 * frac_digits))
}
fn parse_float_text(s: &str) -> Option<f64> {
let (neg, body) = match s.as_bytes().first() {
Some(b'-') => (true, &s[1..]),
Some(b'+') => (false, &s[1..]),
_ => (false, s),
};
let lower = body.to_ascii_lowercase();
let v = if lower.starts_with("nan") {
f64::NAN
} else if lower.starts_with("inf") {
f64::INFINITY
} else if lower.starts_with("0x") {
parse_hex_float(&lower)?
} else {
body.parse::<f64>().ok()?
};
Some(if neg { -v } else { v })
}
fn has_nonzero_digit(s: &str) -> bool {
let mant = match s.find(['e', 'E']) {
Some(p) if !s.to_ascii_lowercase().contains("0x") => &s[..p],
_ => s,
};
mant.bytes().any(|c| c.is_ascii_digit() && c != b'0')
}
pub fn from_chars_f32(s: &str) -> Option<f32> {
strtof_prefix(skip_prefix(s)?).map(|(v, _)| v)
}
fn skip_prefix(s: &str) -> Option<&str> {
let t = s.trim_start_matches(|c: char| c.is_ascii() && is_c_space(c as u8));
match t.strip_prefix('+') {
Some(r) if r.starts_with(['+', '-']) => None,
Some(r) => Some(r),
None => Some(t),
}
}
fn strtof_prefix(s: &str) -> Option<(f32, usize)> {
let len = float_prefix_len(s.as_bytes());
if len == 0 {
return None;
}
let text = &s[..len];
let v64 = parse_float_text(text)?;
let v = v64 as f32;
let lower = text.to_ascii_lowercase();
if v.is_infinite() && !lower.contains("inf") {
return None;
}
if v == 0.0 && has_nonzero_digit(text) {
return None;
}
Some((v, len))
}
pub fn from_chars_f64(s: &str) -> Option<f64> {
let t = skip_prefix(s)?;
let len = float_prefix_len(t.as_bytes());
if len == 0 {
return None;
}
let text = &t[..len];
let v = parse_float_text(text)?;
if v.is_infinite() && !text.to_ascii_lowercase().contains("inf") {
return None;
}
Some(v)
}
pub fn from_chars_i64(s: &str) -> Option<i64> {
let t = skip_prefix(s)?;
let b = t.as_bytes();
if b.len() > 2 && b[0] == b'0' && (b[1] == b'x' || b[1] == b'X') {
let digits: String = t[2..]
.chars()
.take_while(|c| c.is_ascii_hexdigit())
.collect();
return i64::from_str_radix(&digits, 16).ok();
}
let mut end = 0;
if end < b.len() && b[end] == b'-' {
end += 1;
}
let start = end;
while end < b.len() && b[end].is_ascii_digit() {
end += 1;
}
if end == start {
return None;
}
t[..end].parse::<i64>().ok()
}
pub fn string_to_float(s: &str) -> Option<f32> {
from_chars_f32(s)
}
pub fn string_to_int(s: &str, fail_if_leftover_chars: bool) -> Option<i32> {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() && is_c_space(b[i]) {
i += 1;
}
let start = i;
if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
i += 1;
}
let digits_start = i;
while i < b.len() && b[i].is_ascii_digit() {
i += 1;
}
if i == digits_start {
return None;
}
let v = s[start..i].parse::<i32>().ok()?;
if fail_if_leftover_chars && i < b.len() {
return None;
}
Some(v)
}
pub fn string_vec_to_float_vec(parts: &[String]) -> Option<Vec<f32>> {
parts.iter().map(|p| from_chars_f32(p)).collect()
}
pub fn string_vec_to_int_vec(parts: &[String]) -> Option<Vec<i32>> {
parts.iter().map(|p| string_to_int(p, true)).collect()
}
pub fn split_by_white_spaces(s: &str) -> Vec<String> {
s.split(|c: char| c.is_ascii() && is_c_space(c as u8))
.filter(|p| !p.is_empty())
.map(|p| p.to_string())
.collect()
}
pub fn trim(s: &str) -> &str {
s.trim_matches(|c: char| c.is_ascii() && is_space(c as u8))
}
pub fn left_trim(s: &str) -> &str {
s.trim_start_matches(|c: char| c.is_ascii() && is_space(c as u8))
}
pub fn is_empty_or_white_space(s: &str) -> bool {
s.bytes().all(is_space)
}
pub fn equal_with_rel_error(x1: f32, x2: f32, e: f32) -> bool {
let diff = if x1 > x2 { x1 - x2 } else { x2 - x1 };
diff <= e * if x1 > 0.0 { x1 } else { -x1 }
}
pub fn vecs_equal_with_rel_error(v1: &[f32], v2: &[f32], e: f32) -> bool {
v1.len() == v2.len()
&& v1
.iter()
.zip(v2)
.all(|(a, b)| equal_with_rel_error(*a, *b, e))
}
pub fn lerpf(a: f32, b: f32, z: f32) -> f32 {
(b - a) * z + a
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScanValue {
Int(i32),
Float(f32),
Str(String),
Char(char),
}
impl ScanValue {
pub fn as_int(&self) -> i32 {
match self {
ScanValue::Int(v) => *v,
_ => 0,
}
}
pub fn as_str(&self) -> &str {
match self {
ScanValue::Str(s) => s,
_ => "",
}
}
pub fn as_float(&self) -> f32 {
match self {
ScanValue::Float(v) => *v,
_ => f32::NAN,
}
}
pub fn as_char(&self) -> char {
match self {
ScanValue::Char(c) => *c,
_ => '\0',
}
}
}
pub fn scanf(input: &str, fmt: &str) -> (i32, Vec<ScanValue>) {
let inp = input.as_bytes();
let f = fmt.as_bytes();
let mut ip = 0usize;
let mut fp = 0usize;
let mut values = Vec::new();
let mut count = 0i32;
let mut converted_any = false;
let input_failure = |count: i32, converted_any: bool| -> i32 {
if count == 0 && !converted_any {
-1
} else {
count
}
};
while fp < f.len() {
let fc = f[fp];
if is_c_space(fc) {
while fp < f.len() && is_c_space(f[fp]) {
fp += 1;
}
while ip < inp.len() && is_c_space(inp[ip]) {
ip += 1;
}
continue;
}
if fc != b'%' {
if ip >= inp.len() {
return (input_failure(count, converted_any), values);
}
if inp[ip] != fc {
return (count, values);
}
ip += 1;
fp += 1;
continue;
}
fp += 1;
if fp < f.len() && f[fp] == b'%' {
while ip < inp.len() && is_c_space(inp[ip]) {
ip += 1;
}
if ip >= inp.len() {
return (input_failure(count, converted_any), values);
}
if inp[ip] != b'%' {
return (count, values);
}
ip += 1;
fp += 1;
continue;
}
let mut suppress = false;
if fp < f.len() && f[fp] == b'*' {
suppress = true;
fp += 1;
}
let mut width = 0usize;
while fp < f.len() && f[fp].is_ascii_digit() {
width = width * 10 + (f[fp] - b'0') as usize;
fp += 1;
}
if fp >= f.len() {
return (count, values);
}
let conv = f[fp];
fp += 1;
if conv != b'c' {
while ip < inp.len() && is_c_space(inp[ip]) {
ip += 1;
}
}
if ip >= inp.len() {
return (input_failure(count, converted_any), values);
}
let limit = if width > 0 {
(ip + width).min(inp.len())
} else {
inp.len()
};
match conv {
b'd' | b'i' | b'u' => {
let mut j = ip;
if j < limit && (inp[j] == b'+' || inp[j] == b'-') {
j += 1;
}
let ds = j;
while j < limit && inp[j].is_ascii_digit() {
j += 1;
}
if j == ds {
return (count, values);
}
let text = &input[ip..j];
let v = text.parse::<i64>().unwrap_or(if text.starts_with('-') {
i64::MIN
} else {
i64::MAX
});
ip = j;
converted_any = true;
if !suppress {
values.push(ScanValue::Int(v as i32));
count += 1;
}
}
b'f' | b'g' | b'e' => {
let sub = &input[ip..limit];
let len = float_prefix_len(sub.as_bytes());
if len == 0 {
return (count, values);
}
let v = parse_float_text(&sub[..len]).unwrap_or(f64::NAN) as f32;
ip += len;
converted_any = true;
if !suppress {
values.push(ScanValue::Float(v));
count += 1;
}
}
b's' => {
let mut j = ip;
while j < limit && !is_c_space(inp[j]) {
j += 1;
}
let text = String::from_utf8_lossy(&inp[ip..j]).into_owned();
ip = j;
converted_any = true;
if !suppress {
values.push(ScanValue::Str(text));
count += 1;
}
}
b'c' => {
let c = inp[ip] as char;
ip += 1;
converted_any = true;
if !suppress {
values.push(ScanValue::Char(c));
count += 1;
}
}
_ => return (count, values),
}
}
(count, values)
}
#[derive(Debug, Clone)]
pub struct IStream<'a> {
data: &'a [u8],
pos: usize,
eof: bool,
fail: bool,
}
impl<'a> IStream<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self {
data,
pos: 0,
eof: false,
fail: false,
}
}
pub fn good(&self) -> bool {
!self.eof && !self.fail
}
pub fn ok(&self) -> bool {
!self.fail
}
pub fn getline(&mut self) -> String {
self.getline_impl(None)
}
pub fn getline_limited(&mut self, size: usize) -> String {
self.getline_impl(Some(size.saturating_sub(1)))
}
fn getline_impl(&mut self, max_chars: Option<usize>) -> String {
if !self.good() {
self.fail = true;
return String::new();
}
let start = self.pos;
let mut extracted = 0usize;
let mut end = start;
loop {
if self.pos >= self.data.len() {
self.eof = true;
break;
}
let c = self.data[self.pos];
if c == b'\n' {
self.pos += 1;
extracted += 1;
break;
}
if let Some(m) = max_chars {
if end - start >= m {
self.fail = true;
break;
}
}
self.pos += 1;
end += 1;
extracted += 1;
}
if extracted == 0 {
self.fail = true;
}
String::from_utf8_lossy(&self.data[start..end]).into_owned()
}
pub fn next_word(&mut self) -> Option<String> {
if !self.good() {
self.fail = true;
return None;
}
while self.pos < self.data.len() && is_c_space(self.data[self.pos]) {
self.pos += 1;
}
if self.pos >= self.data.len() {
self.eof = true;
self.fail = true;
return None;
}
let start = self.pos;
while self.pos < self.data.len() && !is_c_space(self.data[self.pos]) {
self.pos += 1;
}
if self.pos >= self.data.len() {
self.eof = true;
}
Some(String::from_utf8_lossy(&self.data[start..self.pos]).into_owned())
}
pub fn nextline(&mut self) -> Option<String> {
while self.good() {
let mut line = self.getline();
if line.ends_with('\r') {
line.pop();
}
if !is_empty_or_white_space(&line) {
return Some(line);
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct BinaryStream<'a> {
data: &'a [u8],
pos: usize,
fail: bool,
}
impl<'a> BinaryStream<'a> {
pub fn new(data: &'a [u8]) -> Self {
Self {
data,
pos: 0,
fail: false,
}
}
pub fn good(&self) -> bool {
!self.fail
}
pub fn seek(&mut self, pos: usize) {
if !self.fail {
self.pos = pos;
}
}
pub fn read(&mut self, n: usize) -> Option<&'a [u8]> {
if self.fail {
return None;
}
match self.pos.checked_add(n) {
Some(end) if end <= self.data.len() => {
let s = &self.data[self.pos..end];
self.pos = end;
Some(s)
}
_ => {
self.pos = self.data.len();
self.fail = true;
None
}
}
}
pub fn read_u16(&mut self) -> Option<u16> {
self.read(2).map(|b| u16::from_be_bytes([b[0], b[1]]))
}
pub fn read_u32(&mut self) -> Option<u32> {
self.read(4)
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
pub fn read_i32(&mut self) -> Option<i32> {
self.read_u32().map(|v| v as i32)
}
pub fn read_u64(&mut self) -> Option<u64> {
self.read(8)
.map(|b| u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]))
}
}
pub fn format_fixed(v: f32, precision: usize) -> String {
if v.is_nan() {
return if v.is_sign_negative() { "-nan" } else { "nan" }.to_string();
}
if v.is_infinite() {
return if v < 0.0 { "-inf" } else { "inf" }.to_string();
}
format!("{:.*}", precision, f64::from(v))
}
pub fn format_fixed6(v: f32) -> String {
format_fixed(v, 6)
}
pub fn format_fixed6_rgb(rgb: &[f32]) -> String {
rgb.iter()
.take(3)
.map(|&v| format_fixed6(v))
.collect::<Vec<_>>()
.join(" ")
}
pub fn alloc_bake_buffer(num_values: Option<usize>) -> Result<Vec<f32>> {
let too_large = || crate::error::Error::msg("The requested LUT size is too large.");
let n = num_values.ok_or_else(too_large)?;
let mut v: Vec<f32> = Vec::new();
v.try_reserve_exact(n).map_err(|_| too_large())?;
v.resize(n, 0.0);
Ok(v)
}
pub fn bake_identity_lut3d(
edge_len: usize,
order: crate::ops::lut3d::Lut3DOrder,
) -> Result<Vec<f32>> {
let n = edge_len
.checked_mul(edge_len)
.and_then(|v| v.checked_mul(edge_len))
.and_then(|v| v.checked_mul(3));
let mut data = alloc_bake_buffer(n)?;
crate::ops::lut3d::generate_identity_lut3d(&mut data, edge_len, 3, order)?;
Ok(data)
}
pub fn bake_identity_lut1d(size: usize) -> Result<Vec<f32>> {
let mut data = alloc_bake_buffer(size.checked_mul(3))?;
crate::ops::lut1d::generate_identity_lut1d(&mut data, size, 3);
Ok(data)
}
pub fn bake_linear_scale_lut1d(size: usize, start: f32, end: f32) -> Result<Vec<f32>> {
let mut data = alloc_bake_buffer(size.checked_mul(3))?;
crate::ops::lut1d::generate_linear_scale_lut1d(&mut data, size, 3, start, end);
Ok(data)
}
pub fn write_metadata_lines(
out: &mut String,
metadata: &crate::format_metadata::FormatMetadata,
prefix: &str,
) {
for child in &metadata.children {
out.push_str(prefix);
out.push_str(child.element_value());
out.push('\n');
}
}
#[cfg(test)]
pub(crate) mod bake_test_utils {
use crate::baker::{input_to_target_processor, Baker};
use crate::config::Config;
use crate::fileformats::FormatRegistry;
use crate::transforms::Transform;
use crate::types::{Interpolation, OptimizationFlags, TransformDirection};
pub const SHAPER_LOG2_CONFIG: &str = r#"
ocio_profile_version: 1
colorspaces:
- !<ColorSpace>
name : Raw
isdata : false
- !<ColorSpace>
name: Log2
isdata: false
from_reference: !<GroupTransform>
children:
- !<MatrixTransform> {matrix: [5.55556, 0, 0, 0, 0, 5.55556, 0, 0, 0, 0, 5.55556, 0, 0, 0, 0, 1]}
- !<LogTransform> {base: 2}
- !<MatrixTransform> {offset: [6.5, 6.5, 6.5, 0]}
- !<MatrixTransform> {matrix: [0.076923, 0, 0, 0, 0, 0.076923, 0, 0, 0, 0, 0.076923, 0, 0, 0, 0, 1]}
"#;
pub fn config_yaml(spaces: &[(&str, &str)]) -> String {
let mut s = String::from("ocio_profile_version: 2\n\n");
s.push_str("luma: [0.333, 0.333, 0.333]\n\n");
s.push_str(&format!(
"roles:\n reference: {0}\n default: {0}\n\ncolorspaces:\n",
spaces[0].0
));
for (name, extra) in spaces {
s.push_str(&format!(
" - !<ColorSpace>\n name: {name}\n family: {name}\n"
));
for line in extra.lines().filter(|l| !l.trim().is_empty()) {
s.push_str(" ");
s.push_str(line.trim_start());
s.push('\n');
}
s.push('\n');
}
s
}
pub fn baker(yaml: &str, format: &str) -> Baker {
let config = Config::create_from_str(yaml).unwrap();
let mut baker = Baker::new();
baker.set_config(&config);
baker.set_format(format).unwrap();
baker
}
pub fn bake(baker: &Baker) -> String {
String::from_utf8(baker.bake().unwrap()).unwrap()
}
pub fn compare_lines(result: &str, expected: &str, tol: f32, numeric: impl Fn(usize) -> bool) {
let res: Vec<&str> = result.lines().collect();
let exp: Vec<&str> = expected.lines().collect();
assert_eq!(res.len(), exp.len(), "{result}");
for (i, (r, e)) in res.iter().zip(exp.iter()).enumerate() {
if numeric(i) {
let rv: Vec<f32> = r.split_whitespace().map(|s| s.parse().unwrap()).collect();
let ev: Vec<f32> = e.split_whitespace().map(|s| s.parse().unwrap()).collect();
assert_eq!(rv.len(), ev.len(), "line {i}: '{r}' vs '{e}'");
for (a, b) in rv.iter().zip(ev.iter()) {
assert!((a - b).abs() <= tol, "line {i}: '{r}' vs '{e}'");
}
} else {
assert_eq!(r, e, "line {i}");
}
}
}
pub fn check_round_trip(baker: &Baker, samples: &[[f32; 3]], tol: f32) {
let bytes = baker.bake().unwrap();
let fmt = FormatRegistry::instance()
.format_by_name(baker.format())
.unwrap();
let cached = fmt
.read(&bytes, "memory file", Interpolation::Linear)
.unwrap();
let config = baker.config().unwrap();
let lut = config
.get_processor_for_transform(
&Transform::Group(cached.group),
TransformDirection::Forward,
)
.unwrap()
.optimized_cpu_processor(OptimizationFlags::NONE);
let reference = input_to_target_processor(baker).unwrap();
for s in samples {
let mut a = *s;
let mut b = *s;
lut.apply_rgb(&mut a);
reference.apply_rgb(&mut b);
for c in 0..3 {
assert!(
(a[c] - b[c]).abs() <= tol,
"sample {s:?}: LUT {a:?} vs transform {b:?}"
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn baking_fixed_format() {
assert_eq!(format_fixed6(0.0), "0.000000");
assert_eq!(format_fixed6(-0.0), "-0.000000");
assert_eq!(format_fixed6(1.0 / 3.0), "0.333333");
assert_eq!(format_fixed6(16.291878), "16.291878");
assert_eq!(format_fixed6(-2.0), "-2.000000");
assert_eq!(format_fixed6(f32::NAN), "nan");
assert_eq!(format_fixed6(f32::INFINITY), "inf");
assert_eq!(format_fixed6(f32::NEG_INFINITY), "-inf");
assert_eq!(format_fixed(0.5, 0), "0");
assert!(alloc_bake_buffer(None).is_err());
assert!(alloc_bake_buffer(Some(usize::MAX)).is_err());
assert!(bake_identity_lut3d(usize::MAX, crate::ops::lut3d::Lut3DOrder::FastRed).is_err());
let lut = bake_identity_lut3d(2, crate::ops::lut3d::Lut3DOrder::FastRed).unwrap();
assert_eq!(&lut[..6], &[0.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
let ramp = bake_linear_scale_lut1d(3, 1.0, 2.0).unwrap();
assert_eq!(ramp, vec![1.0, 1.0, 1.0, 1.5, 1.5, 1.5, 2.0, 2.0, 2.0]);
assert_eq!(
bake_identity_lut1d(2).unwrap(),
vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
);
}
#[test]
fn number_parsing() {
assert_eq!(from_chars_f32("1.5"), Some(1.5));
assert_eq!(from_chars_f32(" +2e2x"), Some(200.0));
assert_eq!(from_chars_f32("-0.25"), Some(-0.25));
assert!(from_chars_f32("nan").unwrap().is_nan());
assert_eq!(from_chars_f32("-inf"), Some(f32::NEG_INFINITY));
assert_eq!(from_chars_f32("0x1p1"), Some(2.0));
assert_eq!(from_chars_f32("abc"), None);
assert_eq!(from_chars_f32(""), None);
assert_eq!(from_chars_f32("1e50"), None);
assert_eq!(from_chars_f32(".5"), Some(0.5));
assert_eq!(from_chars_f32("5."), Some(5.0));
assert_eq!(from_chars_f32("."), None);
assert_eq!(string_to_int(" 12", false), Some(12));
assert_eq!(string_to_int("12abc", false), Some(12));
assert_eq!(string_to_int("12abc", true), None);
assert_eq!(string_to_int("-3", true), Some(-3));
assert_eq!(string_to_int("x", false), None);
assert_eq!(string_to_int("99999999999", false), None);
assert_eq!(from_chars_i64("\"8\""), None);
assert_eq!(from_chars_i64("8"), Some(8));
}
#[test]
fn sscanf() {
let (n, v) = scanf("Version 1", "Version %d");
assert_eq!(n, 1);
assert_eq!(v[0].as_int(), 1);
let (n, _) = scanf("Version1", "Version %d");
assert_eq!(n, 1);
let (n, _) = scanf("Versionx", "Version %d");
assert_eq!(n, 0);
let (n, _) = scanf("", "%d");
assert_eq!(n, -1);
let (n, v) = scanf("lut_3d_size 2 x", "lut_3d_size %d %c");
assert_eq!(n, 2);
assert_eq!(v[1].as_char(), 'x');
let (n, v) = scanf("LUT: 3 1024 65536f", "%*s %d %d %15s");
assert_eq!(n, 3);
assert_eq!(v[2].as_str(), "65536f");
let (n, v) = scanf("0 1 2 0.5 0.25 1e-1", "%d %d %d %63s %63s %63s");
assert_eq!(n, 6);
assert_eq!(v[5].as_str(), "1e-1");
}
#[test]
fn stream() {
let mut s = IStream::new(b"a\n\nb");
assert_eq!(s.getline(), "a");
assert!(s.good());
assert_eq!(s.getline(), "");
assert!(s.good());
assert_eq!(s.getline(), "b");
assert!(!s.good());
assert!(s.ok());
assert_eq!(s.getline(), "");
assert!(!s.ok());
let mut s = IStream::new(b"x\n\n \r\ny z\n");
assert_eq!(s.nextline().as_deref(), Some("x"));
assert_eq!(s.nextline().as_deref(), Some("y z"));
assert_eq!(s.nextline(), None);
let mut s = IStream::new(b" w1 w2\n");
assert_eq!(s.next_word().as_deref(), Some("w1"));
assert_eq!(s.next_word().as_deref(), Some("w2"));
assert_eq!(s.next_word(), None);
let mut s = IStream::new(b"abcdef\n");
assert_eq!(s.getline_limited(4), "abc");
assert!(!s.ok());
}
#[test]
fn red_fastest() {
let mut lut = Lut3DTransform::new(2);
let raw: Vec<f32> = (0..24).map(|v| v as f32).collect();
set_lut3d_from_red_fastest(&mut lut, &raw).unwrap();
assert_eq!(lut.value(1, 0, 0), [3.0, 4.0, 5.0]);
assert_eq!(lut.value(0, 0, 1), [12.0, 13.0, 14.0]);
assert!(set_lut3d_from_red_fastest(&mut lut, &raw[..3]).is_err());
}
#[test]
fn edge_len() {
assert_eq!(get_3d_lut_edge_len_from_num_pixels(8).unwrap(), 2);
assert_eq!(get_3d_lut_edge_len_from_num_pixels(35937).unwrap(), 33);
assert!(get_3d_lut_edge_len_from_num_pixels(9).is_err());
}
#[test]
fn min_max() {
assert!(min_max_matrix_f32(0.0, 1.0).unwrap().is_none());
let m = min_max_matrix_f32(-1.0, 3.0).unwrap().unwrap();
assert_eq!(m.matrix[0], 0.25);
assert_eq!(m.offset[0], 0.25);
assert_eq!(m.matrix[15], 1.0);
assert!(min_max_matrix_f32(1.0, 1.0).is_err());
}
}