| // 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 <list> |
| #include <set> |
| |
| #include "base/command_line.h" |
| #include "base/containers/mru_cache.h" |
| #include "base/lazy_instance.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/synchronization/lock.h" |
| #include "third_party/skia/include/core/SkColorSpaceXform.h" |
| #include "third_party/skia/include/core/SkICC.h" |
| #include "ui/gfx/skia_color_space_util.h" |
| |
| namespace gfx { |
| |
| const uint64_t ICCProfile::test_id_adobe_rgb_ = 1; |
| const uint64_t ICCProfile::test_id_color_spin_ = 2; |
| const uint64_t ICCProfile::test_id_generic_rgb_ = 3; |
| const uint64_t ICCProfile::test_id_srgb_ = 4; |
| |
| namespace { |
| |
| const uint64_t kEmptyProfileId = 5; |
| |
| // Start from-ICC-data IDs at the end of the hard-coded test id list above. |
| uint64_t g_next_unused_id = 10; |
| |
| using ProfileCacheBase = base::MRUCache<uint64_t, ICCProfile>; |
| class ProfileCache : public ProfileCacheBase { |
| public: |
| static const size_t kMaxCachedICCProfiles = 16; |
| ProfileCache() : ProfileCacheBase(kMaxCachedICCProfiles) {} |
| }; |
| base::LazyInstance<ProfileCache>::DestructorAtExit g_cache = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| // Lock that must be held to access |g_cache| and |g_next_unused_id|. |
| base::LazyInstance<base::Lock>::DestructorAtExit g_lock = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| } // namespace |
| |
| ICCProfile::Internals::AnalyzeResult ICCProfile::Internals::Initialize() { |
| // Start out with no parametric data. |
| |
| // Parse the profile and attempt to create a SkColorSpaceXform out of it. |
| sk_sp<SkColorSpace> sk_srgb_color_space = SkColorSpace::MakeSRGB(); |
| sk_sp<SkICC> sk_icc = SkICC::Make(data_.data(), data_.size()); |
| if (!sk_icc) { |
| DLOG(ERROR) << "Failed to parse ICC profile to SkICC."; |
| return kICCFailedToParse; |
| } |
| sk_color_space_ = SkColorSpace::MakeICC(data_.data(), data_.size()); |
| if (!sk_color_space_) { |
| DLOG(ERROR) << "Failed to parse ICC profile to SkColorSpace."; |
| return kICCFailedToExtractSkColorSpace; |
| } |
| std::unique_ptr<SkColorSpaceXform> sk_color_space_xform = |
| SkColorSpaceXform::New(sk_srgb_color_space.get(), sk_color_space_.get()); |
| if (!sk_color_space_xform) { |
| DLOG(ERROR) << "Parsed ICC profile but can't create SkColorSpaceXform."; |
| return kICCFailedToCreateXform; |
| } |
| |
| // Because this SkColorSpace can be used to construct a transform, we can use |
| // it to create a LUT based color transform, at the very least. If we fail to |
| // get any better approximation, we'll use sRGB as our approximation. |
| ColorSpace::CreateSRGB().GetPrimaryMatrix(&to_XYZD50_); |
| ColorSpace::CreateSRGB().GetTransferFunction(&transfer_fn_); |
| |
| // If our SkColorSpace representation is sRGB then return that. |
| if (sk_color_space_->isSRGB()) |
| return kICCExtractedSRGBColorSpace; |
| |
| // A primary matrix is required for our parametric representations. Use it if |
| // it exists. |
| SkMatrix44 to_XYZD50_matrix; |
| if (!sk_icc->toXYZD50(&to_XYZD50_matrix)) { |
| DLOG(ERROR) << "Failed to extract ICC profile primary matrix."; |
| return kICCFailedToExtractMatrix; |
| } |
| to_XYZD50_ = to_XYZD50_matrix; |
| |
| // Try to directly extract a numerical transfer function. Use it if it |
| // exists. |
| SkColorSpaceTransferFn exact_tr_fn; |
| if (sk_icc->isNumericalTransferFn(&exact_tr_fn)) { |
| transfer_fn_ = exact_tr_fn; |
| return kICCExtractedMatrixAndAnalyticTrFn; |
| } |
| |
| // Attempt to fit a parametric transfer function to the table data in the |
| // profile. |
| SkColorSpaceTransferFn approx_tr_fn; |
| if (!SkApproximateTransferFn(sk_icc, &transfer_fn_error_, &approx_tr_fn)) { |
| DLOG(ERROR) << "Failed approximate transfer function."; |
| return kICCFailedToConvergeToApproximateTrFn; |
| } |
| |
| // If this converged, but has too high error, use the sRGB transfer function |
| // from above. |
| const float kMaxError = 2.f / 256.f; |
| if (transfer_fn_error_ >= kMaxError) { |
| DLOG(ERROR) << "Failed to accurately approximate transfer function, error: " |
| << 256.f * transfer_fn_error_ << "/256"; |
| return kICCFailedToApproximateTrFnAccurately; |
| }; |
| |
| // If the error is sufficiently low, declare that the approximation is |
| // accurate. |
| transfer_fn_ = approx_tr_fn; |
| return kICCExtractedMatrixAndApproximatedTrFn; |
| } |
| |
| 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; |
| } |
| |
| // static |
| ICCProfile ICCProfile::FromData(const void* data, size_t size) { |
| return FromDataWithId(data, size, 0); |
| } |
| |
| // static |
| ICCProfile ICCProfile::FromDataWithId(const void* data_as_void, |
| size_t size, |
| uint64_t new_profile_id) { |
| const char* data_as_byte = reinterpret_cast<const char*>(data_as_void); |
| std::vector<char> new_profile_data(data_as_byte, data_as_byte + size); |
| |
| base::AutoLock lock(g_lock.Get()); |
| |
| if (new_profile_id) { |
| // If this has specified an id, see if we have an entry for it (and ensure |
| // that that entry have the same data). |
| auto found = g_cache.Get().Get(new_profile_id); |
| if (found != g_cache.Get().end()) { |
| const ICCProfile& cached_profile = found->second; |
| DCHECK(new_profile_data == cached_profile.internals_->data_); |
| return cached_profile; |
| } |
| } |
| |
| // See if there is already an entry with the same data. If so, return that |
| // entry (even if that means ignoring the profile id that we were provided). |
| for (const auto& iter : g_cache.Get()) { |
| const ICCProfile& iter_profile = iter.second; |
| if (new_profile_data == iter_profile.internals_->data_) |
| return iter_profile; |
| } |
| |
| // Create a new id for this data if one was not specified. Always ensure that |
| // the emptry profile have the same id. |
| if (!new_profile_id) { |
| if (size == 0) |
| new_profile_id = kEmptyProfileId; |
| else |
| new_profile_id = g_next_unused_id++; |
| } |
| |
| // Create a new profile for this data. |
| ICCProfile new_profile; |
| new_profile.internals_ = base::MakeRefCounted<Internals>( |
| std::move(new_profile_data), new_profile_id); |
| g_cache.Get().Put(new_profile_id, new_profile); |
| return new_profile; |
| } |
| |
| ColorSpace ICCProfile::GetColorSpace() const { |
| if (!internals_) |
| return ColorSpace(); |
| |
| TouchCacheEntry(); |
| |
| if (!internals_->is_valid_) |
| return ColorSpace(); |
| |
| gfx::ColorSpace color_space; |
| if (internals_->is_parametric_) { |
| color_space = GetParametricColorSpace(); |
| color_space.icc_profile_sk_color_space_ = internals_->sk_color_space_; |
| } else { |
| color_space = ColorSpace::CreateCustom(internals_->to_XYZD50_, |
| ColorSpace::TransferID::ICC_BASED); |
| color_space.icc_profile_id_ = internals_->id_; |
| color_space.icc_profile_sk_color_space_ = internals_->sk_color_space_; |
| } |
| return color_space; |
| } |
| |
| ColorSpace ICCProfile::GetParametricColorSpace() const { |
| if (!internals_) |
| return ColorSpace(); |
| |
| TouchCacheEntry(); |
| |
| if (!internals_->is_valid_) |
| return ColorSpace(); |
| |
| ColorSpace color_space = |
| internals_->sk_color_space_->isSRGB() |
| ? ColorSpace::CreateSRGB() |
| : ColorSpace::CreateCustom(internals_->to_XYZD50_, |
| internals_->transfer_fn_); |
| if (internals_->is_parametric_) |
| color_space.icc_profile_id_ = internals_->id_; |
| return color_space; |
| } |
| |
| // static |
| bool ICCProfile::FromId(uint64_t id, |
| ICCProfile* icc_profile) { |
| base::AutoLock lock(g_lock.Get()); |
| |
| auto found = g_cache.Get().Get(id); |
| if (found != g_cache.Get().end()) { |
| *icc_profile = found->second; |
| return true; |
| } |
| *icc_profile = ICCProfile(); |
| return false; |
| } |
| |
| void ICCProfile::TouchCacheEntry() const { |
| if (!internals_) |
| return; |
| base::AutoLock lock(g_lock.Get()); |
| |
| // Query for an existing cache entry. |
| auto found = g_cache.Get().Get(internals_->id_); |
| if (found != g_cache.Get().end()) |
| return; |
| |
| // Check if there are any cache entries with the same data, and refuse to add |
| // a duplicate entry with the same data but a different id. |
| // TODO(ccameron): This is a bit odd, but this preserves existing behavior. |
| for (const auto& iter : g_cache.Get()) { |
| const ICCProfile& iter_profile = iter.second; |
| if (internals_->data_ == iter_profile.internals_->data_) |
| return; |
| } |
| |
| // Insert a new cache entry if none existed. |
| g_cache.Get().Put(internals_->id_, *this); |
| } |
| |
| ICCProfile::Internals::Internals(std::vector<char> data, uint64_t id) |
| : id_(id), data_(std::move(data)) { |
| // Early out for empty entries. |
| if (data_.empty()) |
| return; |
| |
| // Parse the ICC profile |
| analyze_result_ = Initialize(); |
| switch (analyze_result_) { |
| case kICCExtractedSRGBColorSpace: |
| case kICCExtractedMatrixAndAnalyticTrFn: |
| case kICCExtractedMatrixAndApproximatedTrFn: |
| // Successfully and accurately extracted color space. |
| is_valid_ = true; |
| is_parametric_ = true; |
| break; |
| case kICCFailedToConvergeToApproximateTrFn: |
| case kICCFailedToApproximateTrFnAccurately: |
| // Successfully but extracted a color space, but it isn't accurate enough. |
| is_valid_ = true; |
| is_parametric_ = false; |
| break; |
| case kICCFailedToExtractRawTrFn: |
| case kICCFailedToExtractMatrix: |
| case kICCFailedToParse: |
| case kICCFailedToExtractSkColorSpace: |
| case kICCFailedToCreateXform: |
| // Can't even use this color space as a LUT. |
| is_valid_ = false; |
| is_parametric_ = false; |
| break; |
| } |
| } |
| |
| ICCProfile::Internals::~Internals() {} |
| |
| void ICCProfile::HistogramDisplay(int64_t display_id) const { |
| if (!internals_) |
| return; |
| internals_->HistogramDisplay(display_id); |
| } |
| |
| void ICCProfile::Internals::HistogramDisplay(int64_t display_id) { |
| // Ensure that we histogram this profile only once per display id. |
| if (histogrammed_display_ids_.count(display_id)) |
| return; |
| histogrammed_display_ids_.insert(display_id); |
| |
| UMA_HISTOGRAM_ENUMERATION("Blink.ColorSpace.Destination.ICCResult", |
| analyze_result_, kICCProfileAnalyzeLast); |
| |
| // Add histograms for numerical approximation. |
| bool nonlinear_fit_converged = |
| analyze_result_ == kICCExtractedMatrixAndApproximatedTrFn || |
| analyze_result_ == kICCFailedToApproximateTrFnAccurately; |
| bool nonlinear_fit_did_not_converge = |
| analyze_result_ == kICCFailedToConvergeToApproximateTrFn; |
| if (nonlinear_fit_converged || nonlinear_fit_did_not_converge) { |
| UMA_HISTOGRAM_BOOLEAN("Blink.ColorSpace.Destination.NonlinearFitConverged", |
| nonlinear_fit_converged); |
| } |
| if (nonlinear_fit_converged) { |
| UMA_HISTOGRAM_CUSTOM_COUNTS( |
| "Blink.ColorSpace.Destination.NonlinearFitError", |
| static_cast<int>(transfer_fn_error_ * 255), 0, 127, 16); |
| } |
| } |
| |
| } // namespace gfx |