| /* |
| * Copyright 2025 Google LLC. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "rust/icc/FFI.h" |
| #include "rust/icc/FFI.rs.h" // Generated by CXX bridge |
| #include "modules/skcms/skcms.h" |
| |
| #include <algorithm> |
| #include <cstddef> |
| #include <cstring> |
| #include <type_traits> |
| |
| namespace rust_icc { |
| |
| bool ApproximateCurveWrapper(rust::Slice<const uint16_t> table, |
| TransferFunction& out_approx, |
| float& out_max_error) { |
| if (table.empty()) { |
| return false; |
| } |
| |
| // Construct skcms_Curve for the table |
| skcms_Curve curve; |
| memset(&curve, 0, sizeof(skcms_Curve)); |
| curve.table_entries = static_cast<uint32_t>(table.size()); |
| curve.table_8 = nullptr; |
| curve.table_16 = reinterpret_cast<const uint8_t*>(table.data()); |
| |
| // Call skcms_ApproximateCurve (signature verified at compile time) |
| skcms_TransferFunction skcms_approx; |
| float max_error = 0.0f; |
| bool success = skcms_ApproximateCurve(&curve, &skcms_approx, &max_error); |
| |
| if (!success) { |
| return false; |
| } |
| |
| // Copy result field-by-field to avoid memcpy of non-trivial type |
| out_approx.g = skcms_approx.g; |
| out_approx.a = skcms_approx.a; |
| out_approx.b = skcms_approx.b; |
| out_approx.c = skcms_approx.c; |
| out_approx.d = skcms_approx.d; |
| out_approx.e = skcms_approx.e; |
| out_approx.f = skcms_approx.f; |
| out_max_error = max_error; |
| |
| return true; |
| } |
| |
| void ToSkcmsMatrix3x3(const Matrix3x3& rust_matrix, skcms_Matrix3x3* out_skcms) { |
| // Note: std::is_layout_compatible_v (C++20) is not yet implemented in LLVM (P0466R5). |
| // Use sizeof/alignof + standard_layout + trivially_copyable as the next best alternative. |
| static_assert(sizeof(Matrix3x3) == sizeof(skcms_Matrix3x3), |
| "Matrix3x3 must have same size as skcms_Matrix3x3 for memcpy"); |
| static_assert(alignof(Matrix3x3) == alignof(skcms_Matrix3x3), |
| "Matrix3x3 must have same alignment as skcms_Matrix3x3 for memcpy"); |
| static_assert(std::is_standard_layout_v<Matrix3x3>, |
| "Matrix3x3 must have standard layout for safe memcpy"); |
| static_assert(std::is_trivially_copyable_v<Matrix3x3>, |
| "Matrix3x3 must be trivially copyable for memcpy"); |
| memcpy(out_skcms, &rust_matrix, sizeof(skcms_Matrix3x3)); |
| } |
| |
| void ToSkcmsTransferFunction(const TransferFunction& rust_tf, |
| skcms_TransferFunction* out_skcms) { |
| static_assert(sizeof(TransferFunction) == sizeof(skcms_TransferFunction), |
| "TransferFunction must have same size as skcms_TransferFunction for memcpy"); |
| static_assert(alignof(TransferFunction) == alignof(skcms_TransferFunction), |
| "TransferFunction must have same alignment as skcms_TransferFunction for memcpy"); |
| static_assert(std::is_standard_layout_v<TransferFunction>, |
| "TransferFunction must have standard layout for safe memcpy"); |
| static_assert(std::is_trivially_copyable_v<TransferFunction>, |
| "TransferFunction must be trivially copyable for memcpy"); |
| // Spot-check field offsets in skcms_TransferFunction to verify the unusual ordering (g, a, b, c, d, e, f) |
| static_assert(offsetof(skcms_TransferFunction, g) == 0, |
| "skcms_TransferFunction::g must be at offset 0"); |
| static_assert(offsetof(skcms_TransferFunction, a) == sizeof(float), |
| "skcms_TransferFunction::a must be at offset sizeof(float)"); |
| memcpy(out_skcms, &rust_tf, sizeof(skcms_TransferFunction)); |
| } |
| |
| static void ToSkcmsCurve(const rust_icc::Curve& rust_curve, skcms_Curve* out_skcms) { |
| // Note: Curve table data (table_16) is borrowed from Rust Vec via .data(). |
| // See ToSkcmsIccProfile documentation about lifetime requirements. |
| if (rust_curve.table_entries == 0) { |
| out_skcms->table_entries = 0; |
| ToSkcmsTransferFunction(rust_curve.parametric, &out_skcms->parametric); |
| } else { |
| out_skcms->table_entries = rust_curve.table_entries; |
| out_skcms->table_8 = nullptr; |
| out_skcms->table_16 = rust_curve.table_data.data(); |
| } |
| } |
| |
| static bool ToSkcmsA2B(const rust_icc::A2B& rust_a2b, skcms_A2B* out_skcms) { |
| memset(out_skcms, 0, sizeof(skcms_A2B)); |
| |
| // Input curves: If input_channels is non-zero, ensure we have enough curves |
| out_skcms->input_channels = rust_a2b.input_channels; |
| if (rust_a2b.input_channels > 0) { |
| if (rust_a2b.input_channels < 1 || rust_a2b.input_channels > 4) { |
| return false; |
| } |
| // Only validate curve count if input_channels is specified |
| if (!rust_a2b.input_curves.empty() && rust_a2b.input_channels > rust_a2b.input_curves.size()) { |
| return false; |
| } |
| for (size_t i = 0; i < rust_a2b.input_curves.size() && i < 4; i++) { |
| ToSkcmsCurve(rust_a2b.input_curves[i], &out_skcms->input_curves[i]); |
| } |
| } |
| |
| // Grid data |
| static_assert(std::is_same_v<decltype(out_skcms->grid_points), uint8_t[4]>, |
| "skcms_A2B::grid_points must be uint8_t[4]"); |
| static_assert(std::is_trivially_copyable_v<uint8_t>, |
| "uint8_t must be trivially copyable for memcpy"); |
| if (rust_a2b.grid_points.size() != 4) { |
| return false; |
| } |
| memcpy(out_skcms->grid_points, rust_a2b.grid_points.data(), 4); |
| if (rust_a2b.is_16bit_grid) { |
| out_skcms->grid_16 = rust_a2b.grid_data.data(); |
| } else { |
| out_skcms->grid_8 = rust_a2b.grid_data.data(); |
| } |
| |
| // Matrix curves and matrix |
| // If matrix_channels is explicitly set, use it. Otherwise, infer from matrix_curves presence. |
| uint32_t effective_matrix_channels = rust_a2b.matrix_channels; |
| if (effective_matrix_channels == 0 && !rust_a2b.matrix_curves.empty()) { |
| // Infer matrix_channels from the number of matrix curves provided |
| effective_matrix_channels = std::min(static_cast<uint32_t>(rust_a2b.matrix_curves.size()), 3u); |
| } |
| |
| if (effective_matrix_channels > rust_a2b.matrix_curves.size()) { |
| return false; |
| } |
| if (effective_matrix_channels > 3) { |
| return false; |
| } |
| out_skcms->matrix_channels = effective_matrix_channels; |
| for (size_t i = 0; i < rust_a2b.matrix_curves.size() && i < 3; i++) { |
| ToSkcmsCurve(rust_a2b.matrix_curves[i], &out_skcms->matrix_curves[i]); |
| } |
| |
| // Copy 3x3 matrix portion (only if we have matrix curves) |
| if (effective_matrix_channels > 0) { |
| skcms_Matrix3x3 temp_matrix; |
| ToSkcmsMatrix3x3(rust_a2b.matrix, &temp_matrix); |
| for (int row = 0; row < 3; row++) { |
| for (int col = 0; col < 3; col++) { |
| out_skcms->matrix.vals[row][col] = temp_matrix.vals[row][col]; |
| } |
| // 4th column is the bias/offset |
| out_skcms->matrix.vals[row][3] = rust_a2b.matrix_bias[row]; |
| } |
| } |
| |
| // Output curves: If output_channels is non-zero, ensure we have enough curves |
| out_skcms->output_channels = rust_a2b.output_channels; |
| if (rust_a2b.output_channels > 0) { |
| if (rust_a2b.output_channels > 4) { |
| return false; |
| } |
| // Only validate curve count if output_channels is specified |
| if (!rust_a2b.output_curves.empty() && rust_a2b.output_channels > rust_a2b.output_curves.size()) { |
| return false; |
| } |
| for (size_t i = 0; i < rust_a2b.output_curves.size() && i < 4; i++) { |
| ToSkcmsCurve(rust_a2b.output_curves[i], &out_skcms->output_curves[i]); |
| } |
| } |
| return true; |
| } |
| |
| // Helper to populate skcms_B2A from Rust B2A data |
| static bool ToSkcmsB2A(const rust_icc::B2A& rust_b2a, skcms_B2A* out_skcms) { |
| memset(out_skcms, 0, sizeof(skcms_B2A)); |
| |
| // Input curves |
| if (rust_b2a.input_channels != 3) { |
| return false; |
| } |
| if (rust_b2a.input_curves.size() < 3) { |
| return false; |
| } |
| out_skcms->input_channels = rust_b2a.input_channels; |
| for (size_t i = 0; i < 3; i++) { |
| ToSkcmsCurve(rust_b2a.input_curves[i], &out_skcms->input_curves[i]); |
| } |
| |
| // Matrix and matrix curves |
| // If matrix_channels is explicitly set, use it. Otherwise, infer from matrix_curves presence. |
| uint32_t effective_matrix_channels = rust_b2a.matrix_channels; |
| if (effective_matrix_channels == 0 && !rust_b2a.matrix_curves.empty()) { |
| // Infer matrix_channels from the number of matrix curves provided |
| effective_matrix_channels = std::min(static_cast<uint32_t>(rust_b2a.matrix_curves.size()), 3u); |
| } |
| |
| if (effective_matrix_channels > rust_b2a.matrix_curves.size()) { |
| return false; |
| } |
| if (effective_matrix_channels > 3) { |
| return false; |
| } |
| out_skcms->matrix_channels = effective_matrix_channels; |
| |
| // Copy matrix and matrix curves only if matrix stage is active |
| if (effective_matrix_channels > 0) { |
| skcms_Matrix3x3 temp_matrix; |
| ToSkcmsMatrix3x3(rust_b2a.matrix, &temp_matrix); |
| for (int row = 0; row < 3; row++) { |
| for (int col = 0; col < 3; col++) { |
| out_skcms->matrix.vals[row][col] = temp_matrix.vals[row][col]; |
| } |
| out_skcms->matrix.vals[row][3] = rust_b2a.matrix_bias[row]; |
| } |
| |
| for (size_t i = 0; i < rust_b2a.matrix_curves.size() && i < 3; i++) { |
| ToSkcmsCurve(rust_b2a.matrix_curves[i], &out_skcms->matrix_curves[i]); |
| } |
| } |
| |
| // Grid data |
| static_assert(std::is_same_v<decltype(out_skcms->grid_points), uint8_t[4]>, |
| "skcms_B2A::grid_points must be uint8_t[4]"); |
| static_assert(std::is_trivially_copyable_v<uint8_t>, |
| "uint8_t must be trivially copyable for memcpy"); |
| if (rust_b2a.grid_points.size() != 4) { |
| return false; |
| } |
| memcpy(out_skcms->grid_points, rust_b2a.grid_points.data(), 4); |
| if (rust_b2a.is_16bit_grid) { |
| out_skcms->grid_16 = rust_b2a.grid_data.data(); |
| } else { |
| out_skcms->grid_8 = rust_b2a.grid_data.data(); |
| } |
| |
| // Output curves |
| if (rust_b2a.output_channels < 1 || rust_b2a.output_channels > 4) { |
| return false; |
| } |
| if (rust_b2a.output_channels > rust_b2a.output_curves.size()) { |
| return false; |
| } |
| out_skcms->output_channels = rust_b2a.output_channels; |
| for (size_t i = 0; i < rust_b2a.output_channels; i++) { |
| ToSkcmsCurve(rust_b2a.output_curves[i], &out_skcms->output_curves[i]); |
| } |
| return true; |
| } |
| |
| bool ToSkcmsIccProfile(const IccProfile& rust_profile, skcms_ICCProfile* out_skcms) { |
| memset(out_skcms, 0, sizeof(skcms_ICCProfile)); |
| |
| // Copy color space information |
| out_skcms->data_color_space = static_cast<uint32_t>(rust_profile.data_color_space); |
| out_skcms->pcs = static_cast<uint32_t>(rust_profile.connection_space); |
| |
| // Copy toXYZD50 matrix if present |
| out_skcms->has_toXYZD50 = rust_profile.has_to_xyzd50; |
| if (rust_profile.has_to_xyzd50) { |
| ToSkcmsMatrix3x3(rust_profile.to_xyzd50, &out_skcms->toXYZD50); |
| } |
| |
| // Copy transfer curves if present |
| out_skcms->has_trc = rust_profile.has_trc; |
| if (rust_profile.has_trc) { |
| // Convert each channel's transfer function |
| for (int i = 0; i < 3; i++) { |
| // Set up the skcms_Curve as a parametric function (not a LUT) |
| out_skcms->trc[i].table_entries = 0; // 0 = parametric, not table |
| ToSkcmsTransferFunction(rust_profile.trc[i], &out_skcms->trc[i].parametric); |
| } |
| } |
| |
| // Copy CICP data if present |
| out_skcms->has_CICP = rust_profile.has_cicp; |
| if (rust_profile.has_cicp) { |
| out_skcms->CICP.color_primaries = rust_profile.cicp.color_primaries; |
| out_skcms->CICP.transfer_characteristics = rust_profile.cicp.transfer_characteristics; |
| out_skcms->CICP.matrix_coefficients = rust_profile.cicp.matrix_coefficients; |
| out_skcms->CICP.video_full_range_flag = rust_profile.cicp.video_full_range_flag; |
| } |
| |
| // Populate A2B and B2A transforms if present |
| out_skcms->has_A2B = rust_profile.has_a2b; |
| if (rust_profile.has_a2b) { |
| if (!ToSkcmsA2B(rust_profile.a2b, &out_skcms->A2B)) { |
| return false; |
| } |
| } |
| |
| out_skcms->has_B2A = rust_profile.has_b2a; |
| if (rust_profile.has_b2a) { |
| if (!ToSkcmsB2A(rust_profile.b2a, &out_skcms->B2A)) { |
| return false; |
| } |
| } |
| |
| // A profile needs either a matrix, transfer curves, or A2B/B2A to be useful |
| if (!out_skcms->has_toXYZD50 && !out_skcms->has_trc && |
| !out_skcms->has_A2B && !out_skcms->has_B2A) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace rust_icc |