chromium/ui/gfx/mac/color_space_util.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/mac/color_space_util.h"

#include <CoreMedia/CoreMedia.h>
#include <CoreVideo/CoreVideo.h>
#include <optional>

#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/memory/scoped_policy.h"
#include "base/no_destructor.h"
#include "third_party/skia/modules/skcms/skcms.h"
#include "ui/gfx/color_space.h"

namespace gfx {

namespace {

// Read the value for the key in |key| to CFString and convert it to IdType.
// Use the list of pairs in |cfstr_id_pairs| to do the conversion (by doing a
// linear lookup).
template <typename IdType, typename StringIdPair>
bool GetImageBufferProperty(CFTypeRef value_untyped,
                            const std::vector<StringIdPair>& cfstr_id_pairs,
                            IdType* value_as_id) {
  CFStringRef value_as_string = base::apple::CFCast<CFStringRef>(value_untyped);
  if (!value_as_string) {
    return false;
  }

  for (const auto& p : cfstr_id_pairs) {
    if (p.cfstr_cm) {
      DCHECK(!CFStringCompare(p.cfstr_cv, p.cfstr_cm, 0));
    }
    if (!CFStringCompare(value_as_string, p.cfstr_cv, 0)) {
      *value_as_id = p.id;
      return true;
    }
  }

  return false;
}

struct CVImagePrimary {
  const CFStringRef cfstr_cv;
  const CFStringRef cfstr_cm;
  const gfx::ColorSpace::PrimaryID id;
};
const std::vector<CVImagePrimary>& GetSupportedImagePrimaries() {
  static const base::NoDestructor<std::vector<CVImagePrimary>>
      kSupportedPrimaries([] {
        std::vector<CVImagePrimary> supported_primaries;
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_ITU_R_709_2,
             kCMFormatDescriptionColorPrimaries_ITU_R_709_2,
             gfx::ColorSpace::PrimaryID::BT709});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_EBU_3213,
             kCMFormatDescriptionColorPrimaries_EBU_3213,
             gfx::ColorSpace::PrimaryID::BT470BG});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_EBU_3213,
             kCMFormatDescriptionColorPrimaries_EBU_3213,
             gfx::ColorSpace::PrimaryID::EBU_3213_E});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_SMPTE_C,
             kCMFormatDescriptionColorPrimaries_SMPTE_C,
             gfx::ColorSpace::PrimaryID::SMPTE170M});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_SMPTE_C,
             kCMFormatDescriptionColorPrimaries_SMPTE_C,
             gfx::ColorSpace::PrimaryID::SMPTE240M});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_ITU_R_2020,
             kCMFormatDescriptionColorPrimaries_ITU_R_2020,
             gfx::ColorSpace::PrimaryID::BT2020});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_DCI_P3,
             kCMFormatDescriptionColorPrimaries_DCI_P3,
             gfx::ColorSpace::PrimaryID::SMPTEST431_2});
        supported_primaries.push_back(
            {kCVImageBufferColorPrimaries_P3_D65,
             kCMFormatDescriptionColorPrimaries_P3_D65,
             gfx::ColorSpace::PrimaryID::P3});
        return supported_primaries;
      }());
  return *kSupportedPrimaries;
}

gfx::ColorSpace::PrimaryID GetCoreVideoPrimary(CFTypeRef primaries_untyped) {
  auto primary_id = gfx::ColorSpace::PrimaryID::INVALID;
  if (!GetImageBufferProperty(primaries_untyped, GetSupportedImagePrimaries(),
                              &primary_id)) {
    DLOG(ERROR) << "Failed to find CVImageBufferRef primaries: "
                << primaries_untyped;
  }
  return primary_id;
}

struct CVImageTransferFn {
  const CFStringRef cfstr_cv;
  const CFStringRef cfstr_cm;
  const gfx::ColorSpace::TransferID id;
};
const std::vector<CVImageTransferFn>& GetSupportedImageTransferFn() {
  static const base::NoDestructor<std::vector<CVImageTransferFn>>
      kSupportedTransferFuncs([] {
        std::vector<CVImageTransferFn> supported_transfer_funcs;
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_ITU_R_709_2,
             kCMFormatDescriptionTransferFunction_ITU_R_709_2,
             gfx::ColorSpace::TransferID::BT709_APPLE});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_ITU_R_709_2,
             kCMFormatDescriptionTransferFunction_ITU_R_709_2,
             gfx::ColorSpace::TransferID::BT709});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_ITU_R_709_2,
             kCMFormatDescriptionTransferFunction_ITU_R_709_2,
             gfx::ColorSpace::TransferID::SMPTE170M});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_SMPTE_240M_1995,
             kCMFormatDescriptionTransferFunction_SMPTE_240M_1995,
             gfx::ColorSpace::TransferID::SMPTE240M});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_UseGamma,
             kCMFormatDescriptionTransferFunction_UseGamma,
             gfx::ColorSpace::TransferID::CUSTOM});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_ITU_R_2020,
             kCMFormatDescriptionTransferFunction_ITU_R_2020,
             gfx::ColorSpace::TransferID::BT2020_10});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_ITU_R_2020,
             kCMFormatDescriptionTransferFunction_ITU_R_2020,
             gfx::ColorSpace::TransferID::BT2020_12});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_SMPTE_ST_428_1,
             kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1,
             gfx::ColorSpace::TransferID::SMPTEST428_1});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ,
             kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ,
             gfx::ColorSpace::TransferID::PQ});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_ITU_R_2100_HLG,
             kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG,
             gfx::ColorSpace::TransferID::HLG});
        supported_transfer_funcs.push_back({kCVImageBufferTransferFunction_sRGB,
                                            nullptr,
                                            gfx::ColorSpace::TransferID::SRGB});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_Linear,
             kCMFormatDescriptionTransferFunction_Linear,
             gfx::ColorSpace::TransferID::LINEAR});
        supported_transfer_funcs.push_back(
            {kCVImageBufferTransferFunction_sRGB,
             kCMFormatDescriptionTransferFunction_sRGB,
             gfx::ColorSpace::TransferID::SRGB});

        return supported_transfer_funcs;
      }());
  return *kSupportedTransferFuncs;
}

gfx::ColorSpace::TransferID GetCoreVideoTransferFn(CFTypeRef transfer_untyped,
                                                   CFTypeRef gamma_untyped,
                                                   double* gamma) {
  // The named transfer function.
  auto transfer_id = gfx::ColorSpace::TransferID::INVALID;
  if (!GetImageBufferProperty(transfer_untyped, GetSupportedImageTransferFn(),
                              &transfer_id)) {
    DLOG(ERROR) << "Failed to find CVImageBufferRef transfer: "
                << transfer_untyped;
  }

  if (transfer_id != gfx::ColorSpace::TransferID::CUSTOM) {
    return transfer_id;
  }

  CFNumberRef gamma_number = base::apple::CFCast<CFNumberRef>(gamma_untyped);
  if (!gamma_number) {
    DLOG(ERROR) << "Failed to get gamma level.";
    return gfx::ColorSpace::TransferID::INVALID;
  }

  // CGFloat is a double on 64-bit systems.
  CGFloat gamma_double = 0;
  if (!CFNumberGetValue(gamma_number, kCFNumberCGFloatType, &gamma_double)) {
    DLOG(ERROR) << "Failed to get CVImageBufferRef gamma level as float.";
    return gfx::ColorSpace::TransferID::INVALID;
  }

  if (gamma_double == 2.2) {
    return gfx::ColorSpace::TransferID::GAMMA22;
  }
  if (gamma_double == 2.8) {
    return gfx::ColorSpace::TransferID::GAMMA28;
  }

  *gamma = gamma_double;
  return transfer_id;
}

struct CVImageMatrix {
  const CFStringRef cfstr_cv;
  const CFStringRef cfstr_cm;
  gfx::ColorSpace::MatrixID id;
};
const std::vector<CVImageMatrix>& GetSupportedImageMatrix() {
  static const base::NoDestructor<std::vector<CVImageMatrix>>
      kSupportedMatrices([] {
        std::vector<CVImageMatrix> supported_matrices;
        supported_matrices.push_back(
            {kCVImageBufferYCbCrMatrix_ITU_R_709_2,
             kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2,
             gfx::ColorSpace::MatrixID::BT709});
        supported_matrices.push_back(
            {kCVImageBufferYCbCrMatrix_ITU_R_601_4,
             kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4,
             gfx::ColorSpace::MatrixID::SMPTE170M});
        supported_matrices.push_back(
            {kCVImageBufferYCbCrMatrix_ITU_R_601_4,
             kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4,
             gfx::ColorSpace::MatrixID::BT470BG});
        supported_matrices.push_back(
            {kCVImageBufferYCbCrMatrix_SMPTE_240M_1995,
             kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995,
             gfx::ColorSpace::MatrixID::SMPTE240M});
        supported_matrices.push_back(
            {kCVImageBufferYCbCrMatrix_ITU_R_2020,
             kCMFormatDescriptionYCbCrMatrix_ITU_R_2020,
             gfx::ColorSpace::MatrixID::BT2020_NCL});
        return supported_matrices;
      }());
  return *kSupportedMatrices;
}

gfx::ColorSpace::MatrixID GetCoreVideoMatrix(CFTypeRef matrix_untyped) {
  auto matrix_id = gfx::ColorSpace::MatrixID::INVALID;
  if (!GetImageBufferProperty(matrix_untyped, GetSupportedImageMatrix(),
                              &matrix_id)) {
    DLOG(ERROR) << "Failed to find CVImageBufferRef YUV matrix: "
                << matrix_untyped;
  }
  return matrix_id;
}

}  // anonymous namespace

gfx::ColorSpace ColorSpaceFromCVImageBufferKeys(CFTypeRef primaries_untyped,
                                                CFTypeRef transfer_untyped,
                                                CFTypeRef gamma_untyped,
                                                CFTypeRef matrix_untyped) {
  double gamma;
  auto primary_id = GetCoreVideoPrimary(primaries_untyped);
  auto matrix_id = GetCoreVideoMatrix(matrix_untyped);
  auto transfer_id =
      GetCoreVideoTransferFn(transfer_untyped, gamma_untyped, &gamma);

  if (primary_id == gfx::ColorSpace::PrimaryID::INVALID ||
      matrix_id == gfx::ColorSpace::MatrixID::INVALID ||
      transfer_id == gfx::ColorSpace::TransferID::INVALID) {
    return gfx::ColorSpace();
  }

  // It is specified to the decoder to use luma=[16,235] chroma=[16,240] via
  // the kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange.
  //
  // TODO(crbug.com/40139254): We'll probably need support for more than limited
  // range content if we want this to be used for more than video sites.
  auto range_id = gfx::ColorSpace::RangeID::LIMITED;

  if (transfer_id == gfx::ColorSpace::TransferID::CUSTOM) {
    // Transfer functions can also be specified as a gamma value.
    skcms_TransferFunction custom_tr_fn = {2.2f, 1, 0, 1, 0, 0, 0};
    if (transfer_id == gfx::ColorSpace::TransferID::CUSTOM) {
      custom_tr_fn.g = gamma;
    }

    return gfx::ColorSpace(primary_id, gfx::ColorSpace::TransferID::CUSTOM,
                           matrix_id, range_id, nullptr, &custom_tr_fn);
  }

  return gfx::ColorSpace(primary_id, transfer_id, matrix_id, range_id);
}

// Converts a gfx::ColorSpace to individual kCVImageBuffer* keys.
bool ColorSpaceToCVImageBufferKeys(const gfx::ColorSpace& color_space,
                                   bool prefer_srgb_trfn,
                                   CFStringRef* out_primaries,
                                   CFStringRef* out_transfer,
                                   CFStringRef* out_matrix) {
  DCHECK(out_primaries);
  DCHECK(out_transfer);
  DCHECK(out_matrix);

  bool found_primary = false;
  for (const auto& primaries : GetSupportedImagePrimaries()) {
    if (primaries.id == color_space.GetPrimaryID()) {
      *out_primaries = primaries.cfstr_cv;
      found_primary = true;
      break;
    }
  }

  bool found_transfer = false;
  for (const auto& transfer : GetSupportedImageTransferFn()) {
    if (transfer.id == color_space.GetTransferID()) {
      *out_transfer = transfer.cfstr_cv;
      found_transfer = true;
      break;
    }
  }
  if (found_transfer && prefer_srgb_trfn) {
    if (*out_transfer == kCVImageBufferTransferFunction_ITU_R_709_2) {
      *out_transfer = kCVImageBufferTransferFunction_sRGB;
    }
  }

  bool found_matrix = false;
  for (const auto& matrix : GetSupportedImageMatrix()) {
    if (matrix.id == color_space.GetMatrixID()) {
      *out_matrix = matrix.cfstr_cv;
      found_matrix = true;
      break;
    }
  }

  return found_primary && found_transfer && found_matrix;
}

}  // namespace gfx