blob: 2e76430bcba7dec91f672b4495f8fd8eed0ff274 [file]
// Copyright 2025 Google LLC
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! ICC profile parser FFI bindings.
//!
//! Provides C++ bindings for parsing ICC profiles. All parsing happens in
//! Rust for memory safety, populating data structures for use by skcms / Skia codecs.
mod icc_parse;
#[cxx::bridge(namespace = "rust_icc")]
mod ffi {
extern "C++" {
include!("modules/skcms/skcms.h");
include!("rust/icc/FFI.h");
}
/// 3x3 matrix for color space transforms (matches `skcms_Matrix3x3`).
#[derive(Clone, Copy)]
struct Matrix3x3 {
vals: [[f32; 3]; 3],
}
/// Transfer function parameters (matches `skcms_TransferFunction`).
#[derive(Clone, Copy, Default)]
struct TransferFunction {
g: f32,
a: f32,
b: f32,
c: f32,
d: f32,
e: f32,
f: f32,
}
/// CICP color metadata (matches `skcms_CICP` and `CICP_Layout`).
#[derive(Clone, Copy)]
struct Cicp {
color_primaries: u8,
transfer_characteristics: u8,
matrix_coefficients: u8,
video_full_range_flag: u8,
}
/// Table data byte width / element format (matches `table_8` vs `table_16`
/// and `grid_8` vs `grid_16`).
enum TableFormat {
U8,
U16,
}
/// Curve for LUT transforms - parametric or table-based (matches `skcms_Curve`).
#[derive(Clone)]
struct Curve {
table_entries: u32, // 0 = parametric, >0 = table
parametric: TransferFunction,
table_data: Vec<u8>, // raw table bytes
table_format: TableFormat,
}
/// Multi-stage device <-> PCS transform (matches `skcms_A2B` and `skcms_B2A`).
struct A2BOrB2ATransform {
input_curves: Vec<Curve>,
input_channels: u32,
grid_points: [u8; 4],
grid_data: Vec<u8>,
grid_format: TableFormat,
matrix_curves: Vec<Curve>,
matrix: Matrix3x3,
matrix_bias: [f32; 3],
matrix_channels: u32,
output_curves: Vec<Curve>,
output_channels: u32,
}
/// Parsed ICC profile data (matches `skcms_ICCProfile`).
struct IccProfile {
data_color_space: u32,
connection_space: u32,
to_xyzd50: Matrix3x3,
has_to_xyzd50: bool,
/// Transfer curves for R, G, B channels (or gray replicated x3).
trc: [Curve; 3],
has_trc: bool,
cicp: Cicp,
has_cicp: bool,
hagc: Vec<u8>,
has_hagc: bool,
a2b: A2BOrB2ATransform,
has_a2b: bool,
b2a: A2BOrB2ATransform,
has_b2a: bool,
}
extern "Rust" {
/// Parses ICC profile from `data`. If successful, returns `true`
/// and writes result to `out`. If failure, returns `false`.
/// (Matches `skcms_Parse`).
fn parse_icc_profile(data: &[u8], out: &mut IccProfile) -> bool;
/// Parses ICC profile from `data` with caller-specified A2B / B2A priority.
/// (Matches `skcms_ParseWithA2BPriority`).
fn parse_icc_profile_with_a2b_priority(
data: &[u8],
priority: &[i32],
out: &mut IccProfile,
) -> bool;
}
}
impl ffi::TableFormat {
#[inline]
pub fn byte_width(self) -> usize {
match self {
ffi::TableFormat::U8 => 1,
ffi::TableFormat::U16 => 2,
_ => 1,
}
}
}
// =============================================================================
// FFI Interface Implementation
// =============================================================================
/// Parses ICC profile using default priority [0, 1] (Perceptual, then Relative Colorimetric).
/// (Matches `skcms_Parse`).
pub fn parse_icc_profile(data: &[u8], out: &mut ffi::IccProfile) -> bool {
const DEFAULT_PRIORITY: [i32; 2] = [0, 1];
parse_icc_profile_with_a2b_priority(data, &DEFAULT_PRIORITY, out)
}
/// Parses ICC profile from `data` with caller-specified A2B / B2A priority.
/// `priority` contains indices in 0..=2:
/// 0 = Perceptual (A2B0 / B2A0)
/// 1 = Relative Colorimetric (A2B1 / B2A1)
/// 2 = Saturation (A2B2 / B2A2)
/// (Matches `skcms_ParseWithA2BPriority`).
pub fn parse_icc_profile_with_a2b_priority(
data: &[u8],
priority: &[i32],
out: &mut ffi::IccProfile,
) -> bool {
icc_parse::parse_icc_profile(data, priority, out)
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_curve() -> ffi::Curve {
ffi::Curve {
table_entries: 0,
parametric: ffi::TransferFunction::default(),
table_data: Vec::new(),
table_format: ffi::TableFormat::U8,
}
}
fn empty_lut() -> ffi::A2BOrB2ATransform {
ffi::A2BOrB2ATransform {
input_curves: Vec::new(),
input_channels: 0,
grid_points: [0; 4],
grid_data: Vec::new(),
grid_format: ffi::TableFormat::U8,
matrix_curves: Vec::new(),
matrix: ffi::Matrix3x3 {
vals: [[0.0; 3]; 3],
},
matrix_bias: [0.0; 3],
matrix_channels: 0,
output_curves: Vec::new(),
output_channels: 0,
}
}
fn empty_icc_profile() -> ffi::IccProfile {
ffi::IccProfile {
data_color_space: u32::from_be_bytes(*b"RGB "),
connection_space: u32::from_be_bytes(*b"XYZ "),
to_xyzd50: ffi::Matrix3x3 {
vals: [[0.0; 3]; 3],
},
has_to_xyzd50: false,
trc: [empty_curve(), empty_curve(), empty_curve()],
has_trc: false,
cicp: ffi::Cicp {
color_primaries: 0,
transfer_characteristics: 0,
matrix_coefficients: 0,
video_full_range_flag: 0,
},
has_cicp: false,
hagc: Vec::new(),
has_hagc: false,
a2b: empty_lut(),
has_a2b: false,
b2a: empty_lut(),
has_b2a: false,
}
}
#[test]
fn test_empty_profile() {
let mut out = empty_icc_profile();
let result = parse_icc_profile(&[], &mut out);
assert!(!result);
}
#[test]
fn test_too_short() {
let mut out = empty_icc_profile();
let result = parse_icc_profile(&[0; 39], &mut out);
assert!(!result);
}
#[test]
fn test_invalid_signature() {
let mut data = vec![0u8; 132];
data[36..40].copy_from_slice(b"badd");
let mut out = empty_icc_profile();
let result = parse_icc_profile(&data, &mut out);
assert!(!result);
}
#[test]
fn test_valid_signature_invalid_profile() {
let mut data = vec![0u8; 132];
data[36..40].copy_from_slice(b"acsp");
let mut out = empty_icc_profile();
let result = parse_icc_profile(&data, &mut out);
assert!(!result);
}
}