// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/gfx/icc_profile.h" #include #include #include "base/command_line.h" #include "base/containers/lru_cache.h" #include "base/lazy_instance.h" #include "base/logging.h" #include "base/synchronization/lock.h" #include "third_party/skia/include/core/SkColorSpace.h" #include "third_party/skia/modules/skcms/skcms.h" #include "ui/gfx/skia_color_space_util.h" namespace gfx { namespace { static const size_t kMaxCachedICCProfiles = 16; // An LRU cache mapping data to ICCProfile objects, to avoid re-parsing // profiles every time they are read. using DataToProfileCacheBase = base::LRUCache, ICCProfile>; class DataToProfileCache : public DataToProfileCacheBase { public: DataToProfileCache() : DataToProfileCacheBase(kMaxCachedICCProfiles) {} }; base::LazyInstance::Leaky g_data_to_profile_cache = LAZY_INSTANCE_INITIALIZER; // Lock that must be held to access |g_data_to_profile_cache|. base::LazyInstance::Leaky g_icc_profile_lock = LAZY_INSTANCE_INITIALIZER; } // namespace void ICCProfile::Internals::Initialize() { // Start out with no parametric data. if (data_.empty()) return; // Parse the profile. skcms_ICCProfile profile; if (!skcms_Parse(data_.data(), data_.size(), &profile)) { DLOG(ERROR) << "Failed to parse ICC profile."; return; } // We have seen many users with profiles that don't have a D50 white point. // Windows appears to detect these profiles, and not use them for OS drawing. // It still returns them when we query the system for the installed profile. // For consistency (and to match old behavior) we reject these profiles on // all platforms. // https://crbug.com/847024 const skcms_Matrix3x3& m(profile.toXYZD50); float wX = m.vals[0][0] + m.vals[0][1] + m.vals[0][2]; float wY = m.vals[1][0] + m.vals[1][1] + m.vals[1][2]; float wZ = m.vals[2][0] + m.vals[2][1] + m.vals[2][2]; static const float kD50_WhitePoint[3] = { 0.96420f, 1.00000f, 0.82491f }; if (fabsf(wX - kD50_WhitePoint[0]) > 0.04f || fabsf(wY - kD50_WhitePoint[1]) > 0.04f || fabsf(wZ - kD50_WhitePoint[2]) > 0.04f) { return; } // At this point, the profile is considered valid. We still need to determine // if it's representable with a parametric transfer function. is_valid_ = true; // Extract the primary matrix, and assume that transfer function is sRGB until // we get something more precise. to_XYZD50_ = profile.toXYZD50; transfer_fn_ = SkNamedTransferFn::kSRGB; // Coerce it into a rasterization destination (if possible). If the profile // can't be approximated accurately, then use an sRGB transfer function and // return failure. We will continue to use the gamut from this profile. if (!skcms_MakeUsableAsDestinationWithSingleCurve(&profile)) { DLOG(ERROR) << "Parsed ICC profile but can't make usable as destination, " "using sRGB gamma"; return; } // If SkColorSpace will treat the gamma as that of sRGB, then use the named // constants. sk_sp sk_color_space = SkColorSpace::Make(profile); if (!sk_color_space) { DLOG(ERROR) << "Parsed ICC profile but cannot create SkColorSpace from it, " "using sRGB gamma."; return; } // We were able to get a parametric representation of the transfer function. is_parametric_ = true; if (sk_color_space->gammaCloseToSRGB()) return; // We assume that if we accurately approximated the profile, then the // single-curve version (which may have higher error) is also okay. If we // want to maintain the distinction between accurate and inaccurate profiles, // we could check to see if the single-curve version is/ approximately equal // to the original (or to the multi-channel approximation). transfer_fn_ = profile.trc[0].parametric; } ICCProfile::ICCProfile() = default; ICCProfile::ICCProfile(ICCProfile&& other) = default; ICCProfile::ICCProfile(const ICCProfile& other) = default; ICCProfile& ICCProfile::operator=(ICCProfile&& other) = default; ICCProfile& ICCProfile::operator=(const ICCProfile& other) = default; ICCProfile::~ICCProfile() = default; bool ICCProfile::operator==(const ICCProfile& other) const { if (!internals_ && !other.internals_) return true; if (internals_ && other.internals_) { return internals_->data_ == other.internals_->data_; } return false; } bool ICCProfile::operator!=(const ICCProfile& other) const { return !(*this == other); } bool ICCProfile::IsValid() const { return internals_ ? internals_->is_valid_ : false; } std::vector ICCProfile::GetData() const { return internals_ ? internals_->data_ : std::vector(); } // static ICCProfile ICCProfile::FromData(const void* data_as_void, size_t size) { const char* data_as_byte = reinterpret_cast(data_as_void); std::vector data(data_as_byte, data_as_byte + size); base::AutoLock lock(g_icc_profile_lock.Get()); // See if there is already an entry with the same data. If so, return that // entry. If not, parse the data. ICCProfile icc_profile; auto found_by_data = g_data_to_profile_cache.Get().Get(data); if (found_by_data != g_data_to_profile_cache.Get().end()) { icc_profile = found_by_data->second; } else { icc_profile.internals_ = base::MakeRefCounted(std::move(data)); } // Insert the profile into all caches. g_data_to_profile_cache.Get().Put(icc_profile.internals_->data_, icc_profile); return icc_profile; } ColorSpace ICCProfile::GetColorSpace() const { if (!internals_ || !internals_->is_valid_) return ColorSpace(); return ColorSpace(ColorSpace::PrimaryID::CUSTOM, ColorSpace::TransferID::CUSTOM, ColorSpace::MatrixID::RGB, ColorSpace::RangeID::FULL, &internals_->to_XYZD50_, &internals_->transfer_fn_); } ColorSpace ICCProfile::GetPrimariesOnlyColorSpace() const { if (!internals_ || !internals_->is_valid_) return ColorSpace(); return ColorSpace(ColorSpace::PrimaryID::CUSTOM, ColorSpace::TransferID::SRGB, ColorSpace::MatrixID::RGB, ColorSpace::RangeID::FULL, &internals_->to_XYZD50_, nullptr); } bool ICCProfile::IsColorSpaceAccurate() const { if (!internals_) return false; if (!internals_->is_valid_) return false; return internals_->is_parametric_; } // static ICCProfile ICCProfile::FromColorSpace(const ColorSpace& color_space) { if (!color_space.IsValid()) { return ICCProfile(); } if (color_space.GetMatrixID() != ColorSpace::MatrixID::RGB) { DLOG(ERROR) << "Not creating non-RGB ICCProfile"; return ICCProfile(); } if (color_space.GetRangeID() != ColorSpace::RangeID::FULL) { DLOG(ERROR) << "Not creating non-full-range ICCProfile"; return ICCProfile(); } skcms_Matrix3x3 to_XYZD50_matrix; color_space.GetPrimaryMatrix(&to_XYZD50_matrix); skcms_TransferFunction fn; if (!color_space.GetTransferFunction(&fn)) { DLOG(ERROR) << "Failed to get ColorSpace transfer function for ICCProfile."; return ICCProfile(); } sk_sp data = SkWriteICCProfile(fn, to_XYZD50_matrix); if (!data) { DLOG(ERROR) << "Failed to create SkICC."; return ICCProfile(); } return FromData(data->data(), data->size()); } ICCProfile::Internals::Internals(std::vector data) : data_(std::move(data)) { // Parse the ICC profile Initialize(); } ICCProfile::Internals::~Internals() {} } // namespace gfx