Skip to main content

oximedia_graph/filters/video/
log.rs

1//! Log/Linear conversion filter for color grading workflows.
2//!
3//! This filter provides professional log encoding and decoding for various
4//! camera systems and color grading workflows. Supports:
5//!
6//! - **Log Formats:**
7//!   - Cineon Log
8//!   - ARRI LogC (v3, v4)
9//!   - Sony S-Log3
10//!   - Panasonic V-Log
11//!   - RED Log3G10
12//!   - DJI D-Log
13//!   - Canon C-Log
14//!   - Blackmagic Film Gen 5
15//!
16//! - **ACES Transforms:**
17//!   - ACES2065-1 (AP0)
18//!   - ACEScg (AP1)
19//!   - ACEScct (log working space)
20//!   - ACES Proxy
21//!
22//! - **Display Transforms:**
23//!   - sRGB
24//!   - Rec.709
25//!   - DCI-P3
26//!   - Rec.2020
27//!
28//! # Example
29//!
30//! ```ignore
31//! use oximedia_graph::filters::video::{LogLinearFilter, LogFormat, LogDirection};
32//! use oximedia_graph::node::NodeId;
33//!
34//! // Convert linear to ARRI LogC
35//! let filter = LogLinearFilter::new(
36//!     NodeId(0),
37//!     "to_logc",
38//!     LogFormat::ArriLogC3,
39//!     LogDirection::LinearToLog,
40//! );
41//! ```
42
43#![forbid(unsafe_code)]
44#![allow(clippy::cast_lossless)]
45#![allow(clippy::cast_precision_loss)]
46#![allow(clippy::cast_possible_truncation)]
47#![allow(clippy::cast_sign_loss)]
48#![allow(clippy::similar_names)]
49#![allow(clippy::many_single_char_names)]
50#![allow(clippy::too_many_lines)]
51#![allow(clippy::excessive_precision)]
52#![allow(clippy::module_name_repetitions)]
53#![allow(dead_code)]
54
55use crate::error::{GraphError, GraphResult};
56use crate::frame::FilterFrame;
57use crate::node::{Node, NodeId, NodeState, NodeType};
58use crate::port::{InputPort, OutputPort, PortId, PortType};
59use oximedia_codec::VideoFrame;
60use oximedia_core::PixelFormat;
61
62// ============================================================================
63// Log Format Definitions
64// ============================================================================
65
66/// Log encoding format.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum LogFormat {
69    /// Cineon Log (used in film scanning).
70    Cineon,
71    /// ARRI LogC version 3 (ALEXA, AMIRA).
72    ArriLogC3,
73    /// ARRI LogC version 4 (ALEXA 35).
74    ArriLogC4,
75    /// Sony S-Log3 (Venice, FX9, FX6, A7S series).
76    SonySLog3,
77    /// Panasonic V-Log (Varicam, GH5, S1H).
78    PanasonicVLog,
79    /// RED Log3G10 (RED cameras).
80    RedLog3G10,
81    /// DJI D-Log (drones and gimbals).
82    DjiDLog,
83    /// Canon C-Log (Cinema EOS).
84    CanonCLog,
85    /// Blackmagic Film Gen 5.
86    BlackmagicFilm5,
87    /// ACEScct (ACES logarithmic working space).
88    AcesCct,
89    /// ACES Proxy (10-bit, 12-bit).
90    AcesProxy10,
91}
92
93/// Direction of log conversion.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum LogDirection {
96    /// Convert from linear to log.
97    LinearToLog,
98    /// Convert from log to linear.
99    LogToLinear,
100}
101
102// ============================================================================
103// Cineon Log
104// ============================================================================
105
106/// Cineon Log encoding parameters.
107///
108/// Cineon is the original film scanning log format, widely used in
109/// film-to-digital workflows.
110#[derive(Clone, Copy, Debug)]
111pub struct CineonLog {
112    /// Black point (typically 95/1023 for 10-bit).
113    pub black: f64,
114    /// White point (typically 685/1023 for 10-bit).
115    pub white: f64,
116    /// Gamma (typically 0.6).
117    pub gamma: f64,
118}
119
120impl Default for CineonLog {
121    fn default() -> Self {
122        Self {
123            black: 95.0 / 1023.0,
124            white: 685.0 / 1023.0,
125            gamma: 0.6,
126        }
127    }
128}
129
130impl CineonLog {
131    /// Convert linear to Cineon log.
132    #[must_use]
133    pub fn linear_to_log(self, linear: f64) -> f64 {
134        if linear <= 0.0 {
135            return self.black;
136        }
137
138        let log_val = self.black
139            + (self.white - self.black) * (linear.log10() * self.gamma + (1.0 - self.black));
140
141        log_val.clamp(0.0, 1.0)
142    }
143
144    /// Convert Cineon log to linear.
145    #[must_use]
146    pub fn log_to_linear(self, log: f64) -> f64 {
147        if log <= self.black {
148            return 0.0;
149        }
150
151        let normalized = (log - self.black) / (self.white - self.black);
152        let linear = 10_f64.powf((normalized - (1.0 - self.black)) / self.gamma);
153
154        linear.max(0.0)
155    }
156}
157
158// ============================================================================
159// ARRI LogC
160// ============================================================================
161
162/// ARRI LogC v3 encoding.
163///
164/// Used in ALEXA, ALEXA Mini, AMIRA cameras.
165/// Provides 14+ stops of dynamic range.
166#[derive(Clone, Copy, Debug)]
167pub struct ArriLogC3 {
168    /// Cut point between linear and log sections.
169    pub cut: f64,
170    /// Slope in linear section.
171    pub a: f64,
172    /// Offset in linear section.
173    pub b: f64,
174    /// Slope in log section.
175    pub c: f64,
176    /// Offset in log section.
177    pub d: f64,
178    /// Log base multiplier.
179    pub e: f64,
180    /// Log offset.
181    pub f: f64,
182}
183
184impl Default for ArriLogC3 {
185    fn default() -> Self {
186        // LogC3 parameters for EI 800
187        Self {
188            cut: 0.010591,
189            a: 5.555556,
190            b: 0.052272,
191            c: 0.247190,
192            d: 0.385537,
193            e: 5.555556,
194            f: 0.092809,
195        }
196    }
197}
198
199impl ArriLogC3 {
200    /// Convert linear to LogC3.
201    #[must_use]
202    pub fn linear_to_log(self, linear: f64) -> f64 {
203        if linear > self.cut {
204            self.c * (self.a * linear + self.b).log10() + self.d
205        } else {
206            self.e * linear + self.f
207        }
208    }
209
210    /// Convert LogC3 to linear.
211    #[must_use]
212    pub fn log_to_linear(self, log: f64) -> f64 {
213        let cut_log = self.c * (self.a * self.cut + self.b).log10() + self.d;
214
215        if log > cut_log {
216            (10_f64.powf((log - self.d) / self.c) - self.b) / self.a
217        } else {
218            (log - self.f) / self.e
219        }
220    }
221}
222
223/// ARRI LogC v4 encoding.
224///
225/// Used in ALEXA 35. Improved encoding with better shadow detail.
226#[derive(Clone, Copy, Debug)]
227pub struct ArriLogC4 {
228    /// Curve parameter a
229    pub a: f64,
230    /// Curve parameter b
231    pub b: f64,
232    /// Curve parameter c
233    pub c: f64,
234    /// Curve parameter d
235    pub d: f64,
236    /// Curve parameter e
237    pub e: f64,
238    /// Curve parameter f
239    pub f: f64,
240}
241
242impl Default for ArriLogC4 {
243    fn default() -> Self {
244        Self {
245            a: 2048.0,
246            b: 0.0,
247            c: 0.184904,
248            d: 0.385537,
249            e: 5.555556,
250            f: 0.092809,
251        }
252    }
253}
254
255impl ArriLogC4 {
256    /// Convert linear to LogC4.
257    #[must_use]
258    pub fn linear_to_log(self, linear: f64) -> f64 {
259        if linear <= 0.0 {
260            return self.b;
261        }
262
263        (((linear + self.e) / (1.0 + self.e)).ln() / self.c.ln() + self.d + self.b) / self.a
264    }
265
266    /// Convert LogC4 to linear.
267    #[must_use]
268    pub fn log_to_linear(self, log: f64) -> f64 {
269        let x = log * self.a - self.d - self.b;
270        self.c.powf(x) * (1.0 + self.e) - self.e
271    }
272}
273
274// ============================================================================
275// Sony S-Log3
276// ============================================================================
277
278/// Sony S-Log3 encoding.
279///
280/// Used in Venice, FX9, FX6, A7S series cameras.
281/// Provides 14+ stops of dynamic range with improved shadow detail.
282#[derive(Clone, Copy, Debug, Default)]
283pub struct SonySLog3;
284
285impl SonySLog3 {
286    /// Convert linear to S-Log3.
287    #[must_use]
288    pub fn linear_to_log(self, linear: f64) -> f64 {
289        if linear >= 0.01125000 {
290            (420.0 + (((linear + 0.01) / (0.18 + 0.01)).log10() * 261.5)) / 1023.0
291        } else {
292            (linear * (171.2102946929 - 95.0) / 0.01125000 + 95.0) / 1023.0
293        }
294    }
295
296    /// Convert S-Log3 to linear.
297    #[must_use]
298    pub fn log_to_linear(self, log: f64) -> f64 {
299        let log_1023 = log * 1023.0;
300
301        if log_1023 >= 171.2102946929 {
302            10_f64.powf((log_1023 - 420.0) / 261.5) * (0.18 + 0.01) - 0.01
303        } else {
304            (log_1023 - 95.0) * 0.01125000 / (171.2102946929 - 95.0)
305        }
306    }
307}
308
309// ============================================================================
310// Panasonic V-Log
311// ============================================================================
312
313/// Panasonic V-Log encoding.
314///
315/// Used in Varicam, GH5, GH5S, GH6, S1H cameras.
316/// Provides 12+ stops of dynamic range.
317#[derive(Clone, Copy, Debug)]
318pub struct PanasonicVLog {
319    /// Cut point.
320    pub cut: f64,
321    /// Linear coefficient.
322    pub b: f64,
323    /// Log coefficient.
324    pub c: f64,
325    /// Log offset.
326    pub d: f64,
327}
328
329impl Default for PanasonicVLog {
330    fn default() -> Self {
331        Self {
332            cut: 0.01,
333            b: 0.00873,
334            c: 0.241514,
335            d: 0.598206,
336        }
337    }
338}
339
340impl PanasonicVLog {
341    /// Convert linear to V-Log.
342    #[must_use]
343    pub fn linear_to_log(self, linear: f64) -> f64 {
344        if linear < self.cut {
345            5.6 * linear + 0.125
346        } else {
347            self.c * (linear + self.b).log10() + self.d
348        }
349    }
350
351    /// Convert V-Log to linear.
352    #[must_use]
353    pub fn log_to_linear(self, log: f64) -> f64 {
354        let cut_log = self.c * (self.cut + self.b).log10() + self.d;
355
356        if log < cut_log {
357            (log - 0.125) / 5.6
358        } else {
359            10_f64.powf((log - self.d) / self.c) - self.b
360        }
361    }
362}
363
364// ============================================================================
365// RED Log3G10
366// ============================================================================
367
368/// RED Log3G10 encoding.
369///
370/// Used in RED cameras (DSMC2, DSMC3, Komodo).
371/// Log3 with 10-bit gamma encoding.
372#[derive(Clone, Copy, Debug, Default)]
373pub struct RedLog3G10;
374
375impl RedLog3G10 {
376    /// Convert linear to Log3G10.
377    #[must_use]
378    pub fn linear_to_log(self, linear: f64) -> f64 {
379        if linear < 0.0 {
380            return 0.0;
381        }
382
383        let a = 0.224282;
384        let b = 155.975327;
385        let c = 0.01;
386
387        a * (linear * b + c).log10() + 0.5
388    }
389
390    /// Convert Log3G10 to linear.
391    #[must_use]
392    pub fn log_to_linear(self, log: f64) -> f64 {
393        let a = 0.224282;
394        let b = 155.975327;
395        let c = 0.01;
396
397        (10_f64.powf((log - 0.5) / a) - c) / b
398    }
399}
400
401// ============================================================================
402// DJI D-Log
403// ============================================================================
404
405/// DJI D-Log encoding.
406///
407/// Used in DJI drones and gimbals (Inspire, Phantom, Mavic).
408#[derive(Clone, Copy, Debug, Default)]
409pub struct DjiDLog;
410
411impl DjiDLog {
412    /// Convert linear to D-Log.
413    #[must_use]
414    pub fn linear_to_log(self, linear: f64) -> f64 {
415        if linear <= 0.0078 {
416            6.025 * linear + 0.0929
417        } else {
418            ((linear + 0.0078) / (1.0 + 0.0078)).ln() / 0.9892 / 6.025 + 0.584
419        }
420    }
421
422    /// Convert D-Log to linear.
423    #[must_use]
424    pub fn log_to_linear(self, log: f64) -> f64 {
425        if log <= 0.14 {
426            (log - 0.0929) / 6.025
427        } else {
428            (0.9892_f64.powf(6.025 * (log - 0.584))) * (1.0 + 0.0078) - 0.0078
429        }
430    }
431}
432
433// ============================================================================
434// Canon C-Log
435// ============================================================================
436
437/// Canon C-Log encoding.
438///
439/// Used in Canon Cinema EOS cameras (C300, C500, C70, C200).
440#[derive(Clone, Copy, Debug, Default)]
441pub struct CanonCLog;
442
443impl CanonCLog {
444    /// Convert linear to C-Log.
445    #[must_use]
446    pub fn linear_to_log(self, linear: f64) -> f64 {
447        if linear < 0.0 {
448            return 0.0;
449        }
450
451        let a = 0.529136;
452        let b = 0.0047622;
453        let c = 0.312689;
454        let d = 0.092864;
455
456        a * (a * linear + b).ln() + c * linear + d
457    }
458
459    /// Convert C-Log to linear.
460    #[must_use]
461    pub fn log_to_linear(self, log: f64) -> f64 {
462        // Iterative solver for inverse (simplified approximation)
463        let a = 0.529136;
464        let c = 0.312689;
465        let d = 0.092864;
466
467        if log <= d {
468            return 0.0;
469        }
470
471        // Newton-Raphson approximation
472        let mut x = (log - d) / c;
473        for _ in 0..5 {
474            let fx = a * (a * x + 0.0047622).ln() + c * x + d - log;
475            let dfx = a * a / (a * x + 0.0047622) + c;
476            x -= fx / dfx;
477        }
478
479        x.max(0.0)
480    }
481}
482
483// ============================================================================
484// Blackmagic Film Gen 5
485// ============================================================================
486
487/// Blackmagic Film Gen 5 encoding.
488///
489/// Used in Blackmagic Cinema Camera, Pocket Cinema Camera, URSA.
490#[derive(Clone, Copy, Debug, Default)]
491pub struct BlackmagicFilm5;
492
493impl BlackmagicFilm5 {
494    /// Convert linear to Blackmagic Film.
495    #[must_use]
496    pub fn linear_to_log(self, linear: f64) -> f64 {
497        if linear < 0.005 {
498            return linear * 8.283605932402494;
499        }
500
501        0.2 * (linear + 0.01).ln() + 0.40975773852480107
502    }
503
504    /// Convert Blackmagic Film to linear.
505    #[must_use]
506    pub fn log_to_linear(self, log: f64) -> f64 {
507        if log < 0.04426550899923 {
508            return log / 8.283605932402494;
509        }
510
511        ((log - 0.40975773852480107) / 0.2).exp() - 0.01
512    }
513}
514
515// ============================================================================
516// ACES Color Spaces
517// ============================================================================
518
519/// ACEScct (ACES Color Correction Transform).
520///
521/// Logarithmic working space for color grading in ACES workflows.
522#[derive(Clone, Copy, Debug, Default)]
523pub struct AcesCct;
524
525impl AcesCct {
526    /// Convert linear AP1 to ACEScct.
527    #[must_use]
528    pub fn linear_to_log(self, linear: f64) -> f64 {
529        if linear <= 0.0078125 {
530            10.5402377416545 * linear + 0.0729055341958355
531        } else {
532            ((linear + 0.0000152587890625).max(1e-10).log2() + 9.72) / 17.52
533        }
534    }
535
536    /// Convert ACEScct to linear AP1.
537    #[must_use]
538    pub fn log_to_linear(self, log: f64) -> f64 {
539        if log <= 0.155251141552511 {
540            (log - 0.0729055341958355) / 10.5402377416545
541        } else {
542            2_f64.powf(log * 17.52 - 9.72) - 0.0000152587890625
543        }
544    }
545}
546
547/// ACES Proxy 10-bit encoding.
548///
549/// Compact log encoding for proxy workflows.
550#[derive(Clone, Copy, Debug, Default)]
551pub struct AcesProxy10;
552
553impl AcesProxy10 {
554    /// Convert linear AP1 to ACES Proxy 10.
555    #[must_use]
556    pub fn linear_to_log(self, linear: f64) -> f64 {
557        if linear <= 0.0 {
558            return 0.0;
559        }
560
561        let log2_val = linear.max(1e-10).log2();
562        ((log2_val + 2.5) / 10.0 * 1023.0 + 64.0) / 1023.0
563    }
564
565    /// Convert ACES Proxy 10 to linear AP1.
566    #[must_use]
567    pub fn log_to_linear(self, log: f64) -> f64 {
568        let cv = log * 1023.0;
569        let log2_val = (cv - 64.0) / 1023.0 * 10.0 - 2.5;
570
571        2_f64.powf(log2_val)
572    }
573}
574
575// ============================================================================
576// Log Converter
577// ============================================================================
578
579/// Unified log converter that handles all formats.
580#[derive(Clone, Copy, Debug)]
581pub struct LogConverter {
582    format: LogFormat,
583}
584
585impl LogConverter {
586    /// Create a new log converter for the specified format.
587    #[must_use]
588    pub const fn new(format: LogFormat) -> Self {
589        Self { format }
590    }
591
592    /// Convert linear to log.
593    #[must_use]
594    pub fn linear_to_log(self, linear: f64) -> f64 {
595        match self.format {
596            LogFormat::Cineon => CineonLog::default().linear_to_log(linear),
597            LogFormat::ArriLogC3 => ArriLogC3::default().linear_to_log(linear),
598            LogFormat::ArriLogC4 => ArriLogC4::default().linear_to_log(linear),
599            LogFormat::SonySLog3 => SonySLog3.linear_to_log(linear),
600            LogFormat::PanasonicVLog => PanasonicVLog::default().linear_to_log(linear),
601            LogFormat::RedLog3G10 => RedLog3G10.linear_to_log(linear),
602            LogFormat::DjiDLog => DjiDLog.linear_to_log(linear),
603            LogFormat::CanonCLog => CanonCLog.linear_to_log(linear),
604            LogFormat::BlackmagicFilm5 => BlackmagicFilm5.linear_to_log(linear),
605            LogFormat::AcesCct => AcesCct.linear_to_log(linear),
606            LogFormat::AcesProxy10 => AcesProxy10.linear_to_log(linear),
607        }
608    }
609
610    /// Convert log to linear.
611    #[must_use]
612    pub fn log_to_linear(self, log: f64) -> f64 {
613        match self.format {
614            LogFormat::Cineon => CineonLog::default().log_to_linear(log),
615            LogFormat::ArriLogC3 => ArriLogC3::default().log_to_linear(log),
616            LogFormat::ArriLogC4 => ArriLogC4::default().log_to_linear(log),
617            LogFormat::SonySLog3 => SonySLog3.log_to_linear(log),
618            LogFormat::PanasonicVLog => PanasonicVLog::default().log_to_linear(log),
619            LogFormat::RedLog3G10 => RedLog3G10.log_to_linear(log),
620            LogFormat::DjiDLog => DjiDLog.log_to_linear(log),
621            LogFormat::CanonCLog => CanonCLog.log_to_linear(log),
622            LogFormat::BlackmagicFilm5 => BlackmagicFilm5.log_to_linear(log),
623            LogFormat::AcesCct => AcesCct.log_to_linear(log),
624            LogFormat::AcesProxy10 => AcesProxy10.log_to_linear(log),
625        }
626    }
627
628    /// Convert RGB color.
629    #[must_use]
630    pub fn convert_rgb(self, r: f64, g: f64, b: f64, direction: LogDirection) -> (f64, f64, f64) {
631        match direction {
632            LogDirection::LinearToLog => (
633                self.linear_to_log(r),
634                self.linear_to_log(g),
635                self.linear_to_log(b),
636            ),
637            LogDirection::LogToLinear => (
638                self.log_to_linear(r),
639                self.log_to_linear(g),
640                self.log_to_linear(b),
641            ),
642        }
643    }
644}
645
646// ============================================================================
647// Log/Linear Filter
648// ============================================================================
649
650/// Log/Linear conversion filter.
651pub struct LogLinearFilter {
652    id: NodeId,
653    name: String,
654    state: NodeState,
655    input: InputPort,
656    output: OutputPort,
657    converter: LogConverter,
658    direction: LogDirection,
659}
660
661impl LogLinearFilter {
662    /// Create a new log/linear filter.
663    #[must_use]
664    pub fn new(id: NodeId, name: &str, format: LogFormat, direction: LogDirection) -> Self {
665        Self {
666            id,
667            name: name.to_string(),
668            state: NodeState::Idle,
669            input: InputPort::new(PortId(0), "input", PortType::Video),
670            output: OutputPort::new(PortId(1), "output", PortType::Video),
671            converter: LogConverter::new(format),
672            direction,
673        }
674    }
675
676    /// Process a video frame.
677    fn process_frame(&self, frame: VideoFrame) -> GraphResult<VideoFrame> {
678        // Only process RGB formats
679        match frame.format {
680            PixelFormat::Rgb24 | PixelFormat::Rgba32 => self.process_rgb_frame(frame),
681            _ => Ok(frame),
682        }
683    }
684
685    /// Process an RGB frame.
686    fn process_rgb_frame(&self, mut frame: VideoFrame) -> GraphResult<VideoFrame> {
687        let width = frame.width;
688        let height = frame.height;
689        let planes = &mut frame.planes;
690
691        if planes.is_empty() {
692            return Ok(frame);
693        }
694
695        let plane = &mut planes[0];
696        let stride = plane.stride;
697        let data = plane.data.as_mut_slice();
698
699        let bytes_per_pixel = match frame.format {
700            PixelFormat::Rgb24 => 3,
701            PixelFormat::Rgba32 => 4,
702            _ => return Ok(frame),
703        };
704
705        for y in 0..height as usize {
706            for x in 0..width as usize {
707                let offset = y * stride + x * bytes_per_pixel;
708
709                // Read color (normalized to 0-1)
710                let r = data[offset] as f64 / 255.0;
711                let g = data[offset + 1] as f64 / 255.0;
712                let b = data[offset + 2] as f64 / 255.0;
713
714                // Convert
715                let (r_out, g_out, b_out) = self.converter.convert_rgb(r, g, b, self.direction);
716
717                // Write back (clamped to 0-1)
718                data[offset] = (r_out.clamp(0.0, 1.0) * 255.0) as u8;
719                data[offset + 1] = (g_out.clamp(0.0, 1.0) * 255.0) as u8;
720                data[offset + 2] = (b_out.clamp(0.0, 1.0) * 255.0) as u8;
721            }
722        }
723
724        Ok(frame)
725    }
726}
727
728impl Node for LogLinearFilter {
729    fn id(&self) -> NodeId {
730        self.id
731    }
732
733    fn name(&self) -> &str {
734        &self.name
735    }
736
737    fn node_type(&self) -> NodeType {
738        NodeType::Filter
739    }
740
741    fn state(&self) -> NodeState {
742        self.state
743    }
744
745    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
746        self.state = state;
747        Ok(())
748    }
749
750    fn inputs(&self) -> &[InputPort] {
751        std::slice::from_ref(&self.input)
752    }
753
754    fn outputs(&self) -> &[OutputPort] {
755        std::slice::from_ref(&self.output)
756    }
757
758    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
759        match input {
760            Some(FilterFrame::Video(video_frame)) => {
761                let processed = self.process_frame(video_frame)?;
762                Ok(Some(FilterFrame::Video(processed)))
763            }
764            Some(_) => Err(GraphError::ProcessingError {
765                node: self.id,
766                message: "Log/Linear filter expects video input".to_string(),
767            }),
768            None => Ok(None),
769        }
770    }
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn test_cineon_roundtrip() {
779        let cineon = CineonLog::default();
780        let linear = 0.18; // 18% gray
781        let log = cineon.linear_to_log(linear);
782        let back = cineon.log_to_linear(log);
783        assert!((linear - back).abs() < 0.01);
784    }
785
786    #[test]
787    fn test_arri_logc3_roundtrip() {
788        let logc = ArriLogC3::default();
789        let linear = 0.18;
790        let log = logc.linear_to_log(linear);
791        let back = logc.log_to_linear(log);
792        assert!((linear - back).abs() < 0.01);
793    }
794
795    #[test]
796    fn test_sony_slog3_roundtrip() {
797        let slog = SonySLog3;
798        let linear = 0.18;
799        let log = slog.linear_to_log(linear);
800        let back = slog.log_to_linear(log);
801        assert!((linear - back).abs() < 0.01);
802    }
803
804    #[test]
805    fn test_panasonic_vlog_roundtrip() {
806        let vlog = PanasonicVLog::default();
807        let linear = 0.18;
808        let log = vlog.linear_to_log(linear);
809        let back = vlog.log_to_linear(log);
810        assert!((linear - back).abs() < 0.01);
811    }
812
813    #[test]
814    fn test_red_log3g10_roundtrip() {
815        let red = RedLog3G10;
816        let linear = 0.18;
817        let log = red.linear_to_log(linear);
818        let back = red.log_to_linear(log);
819        assert!((linear - back).abs() < 0.01);
820    }
821
822    #[test]
823    fn test_aces_cct_roundtrip() {
824        let aces = AcesCct;
825        let linear = 0.18;
826        let log = aces.linear_to_log(linear);
827        let back = aces.log_to_linear(log);
828        assert!((linear - back).abs() < 0.01);
829    }
830
831    #[test]
832    fn test_log_converter() {
833        let converter = LogConverter::new(LogFormat::ArriLogC3);
834        let linear = 0.18;
835        let log = converter.linear_to_log(linear);
836        let back = converter.log_to_linear(log);
837        assert!((linear - back).abs() < 0.01);
838    }
839
840    #[test]
841    fn test_log_converter_rgb() {
842        let converter = LogConverter::new(LogFormat::SonySLog3);
843        let (r, g, b) = (0.2, 0.5, 0.8);
844        let (log_r, log_g, log_b) = converter.convert_rgb(r, g, b, LogDirection::LinearToLog);
845        let (back_r, back_g, back_b) =
846            converter.convert_rgb(log_r, log_g, log_b, LogDirection::LogToLinear);
847
848        assert!((r - back_r).abs() < 0.01);
849        assert!((g - back_g).abs() < 0.01);
850        assert!((b - back_b).abs() < 0.01);
851    }
852}