summaryrefslogtreecommitdiff
path: root/chromium/ui/gfx/icc_profile.cc
blob: 0a9517ac7bd9855317d97e24e28a1b7dbbbb62b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
// 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/color_space_switches.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;

// A MRU cache of ICC profiles. The cache key is a uin64_t which a
// gfx::ColorSpace may use to refer back to an ICC profile in the cache. The
// data cached for each profile is the gfx::ICCProfile structure (which includes
// the associated gfx::ColorSpace approximations and SkColorSpace structures)
// and whether or not the ICC profile has been histogrammed.
class ICCProfileCache {
 public:
  // Allow keeping around a maximum of 16 cached ICC profiles. Beware that
  // we will do a linear search thorugh currently-cached ICC profiles,
  // when creating a new ICC profile.
  static const size_t kMaxCachedICCProfiles = 16;

  ICCProfileCache() : id_to_icc_profile_mru_(kMaxCachedICCProfiles) {}
  ~ICCProfileCache() {}

  // Add |icc_profile| to the cache. If |icc_profile| does not have an id set
  // yet, assign an id to it.
  void InsertAndSetIdIfNeeded(ICCProfile* icc_profile) {
    base::AutoLock lock(lock_);

    if (FindByIdUnderLock(icc_profile->id_, icc_profile))
      return;

    if (FindByDataUnderLock(icc_profile->data_.data(),
                            icc_profile->data_.size(), icc_profile)) {
      return;
    }

    if (!icc_profile->id_)
      icc_profile->id_ = next_unused_id_++;

    // Ensure that GetColorSpace() point back to this ICCProfile.
    gfx::ColorSpace& color_space = icc_profile->color_space_;
    color_space.icc_profile_id_ = icc_profile->id_;

    // Ensure that the GetParametricColorSpace() point back to this ICCProfile
    // only if the parametric version is accurate.
    if (color_space.primaries_ != ColorSpace::PrimaryID::ICC_BASED &&
        color_space.transfer_ != ColorSpace::TransferID::ICC_BASED) {
      icc_profile->parametric_color_space_.icc_profile_id_ = icc_profile->id_;
    }

    Entry entry;
    entry.icc_profile = *icc_profile;
    id_to_icc_profile_mru_.Put(icc_profile->id_, entry);
  }

  // We maintain UMA histograms of display ICC profiles. Only histogram a
  // display once for each |display_id| (because we will re-read the same
  // ICC profile repeatedly when reading other display profiles, which will
  // skew samples). Return true if we need to histogram this profile for
  // |display_id|, and ensure that all future calls will return false for
  // |display_id|.
  bool GetAndSetNeedsHistogram(uint64_t display_id,
                               const ICCProfile& icc_profile) {
    base::AutoLock lock(lock_);

    // If we don't find the profile in the cache, don't histogram it.
    auto found = id_to_icc_profile_mru_.Get(icc_profile.id_);
    if (found == id_to_icc_profile_mru_.end())
      return false;

    // If we have already histogrammed this display, don't histogram it.
    std::set<int64_t>& histogrammed_display_ids =
        found->second.histogrammed_display_ids;
    if (histogrammed_display_ids.count(display_id))
      return false;

    // Histogram this display, and mark that we have done so.
    histogrammed_display_ids.insert(display_id);
    return true;
  }

  // Move this ICC profile to the most recently used end of the cache,
  // re-inserting if needed.
  void TouchEntry(const ICCProfile& icc_profile) {
    base::AutoLock lock(lock_);

    if (!icc_profile.id_)
      return;

    // Look up the profile by id to move it to the front of the MRU.
    auto found = id_to_icc_profile_mru_.Get(icc_profile.id_);
    if (found != id_to_icc_profile_mru_.end())
      return;

    // Look up the profile by its data. If there is a new entry for the same
    // data, don't add a duplicate.
    if (FindByDataUnderLock(icc_profile.data_.data(), icc_profile.data_.size(),
                            nullptr)) {
      return;
    }

    // If the entry was not found, insert it.
    Entry entry;
    entry.icc_profile = icc_profile;
    id_to_icc_profile_mru_.Put(icc_profile.id_, entry);
  }

  // Look up an ICC profile in the cache by its data (to ensure that the same
  // data gets the same id every time). On success, return true and populate
  // |icc_profile| with the associated profile.
  bool FindByData(const void* data, size_t size, ICCProfile* icc_profile) {
    base::AutoLock lock(lock_);
    return FindByDataUnderLock(data, size, icc_profile);
  }

  // Look up an ICC profile in the cache by its id. On success, return true and
  // populate |icc_profile| with the associated profile.
  bool FindById(uint64_t id, ICCProfile* icc_profile) {
    base::AutoLock lock(lock_);
    return FindByIdUnderLock(id, icc_profile);
  }

 private:
  struct Entry {
    ICCProfile icc_profile;

    // The set of display ids which have have caused this ICC profile to be
    // recorded in UMA histograms. Only record an ICC profile once per display
    // id (since the same profile will be re-read repeatedly, e.g, when displays
    // are resized).
    std::set<int64_t> histogrammed_display_ids;
  };

  // Body for FindById, executed when the cache lock is already held.
  bool FindByIdUnderLock(uint64_t id, ICCProfile* icc_profile) {
    lock_.AssertAcquired();
    if (!id)
      return false;

    auto found = id_to_icc_profile_mru_.Get(id);
    if (found == id_to_icc_profile_mru_.end())
      return false;

    *icc_profile = found->second.icc_profile;
    return true;
  }

  // Body for FindByData, executed when the cache lock is already held.
  bool FindByDataUnderLock(const void* data,
                           size_t size,
                           ICCProfile* icc_profile) {
    lock_.AssertAcquired();
    if (size == 0)
      return false;

    for (const auto& id_entry_pair : id_to_icc_profile_mru_) {
      const ICCProfile& cached_profile = id_entry_pair.second.icc_profile;
      const std::vector<char>& iter_data = cached_profile.data_;
      if (iter_data.size() != size || memcmp(data, iter_data.data(), size))
        continue;

      if (icc_profile) {
        *icc_profile = cached_profile;
        id_to_icc_profile_mru_.Get(cached_profile.id_);
      }
      return true;
    }
    return false;
  }

  // Start from-ICC-data IDs at the end of the hard-coded test id list above.
  uint64_t next_unused_id_ = 10;
  base::MRUCache<uint64_t, Entry> id_to_icc_profile_mru_;

  // Lock that must be held to access |id_to_icc_profile_mru_| and
  // |next_unused_id_|.
  base::Lock lock_;
};

namespace {

static base::LazyInstance<ICCProfileCache>::DestructorAtExit g_cache =
    LAZY_INSTANCE_INITIALIZER;

}  // namespace

// static
ICCProfile::AnalyzeResult ICCProfile::ExtractColorSpaces(
    const std::vector<char>& data,
    gfx::ColorSpace* parametric_color_space,
    float* parametric_tr_fn_max_error,
    sk_sp<SkColorSpace>* useable_sk_color_space) {
  // Initialize the output parameters as invalid.
  *parametric_color_space = gfx::ColorSpace();
  *parametric_tr_fn_max_error = 0;
  *useable_sk_color_space = nullptr;

  // 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_sp<SkColorSpace> sk_icc_color_space =
      SkColorSpace::MakeICC(data.data(), data.size());
  if (!sk_icc_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_icc_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, mark it
  // as "useable". Mark the "best approximation" as sRGB to start.
  *useable_sk_color_space = sk_icc_color_space;
  *parametric_color_space = ColorSpace::CreateSRGB();

  // If our SkColorSpace representation is sRGB then return that.
  if (SkColorSpace::Equals(sk_srgb_color_space.get(),
                           sk_icc_color_space.get())) {
    return kICCExtractedSRGBColorSpace;
  }

  // A primary matrix is required for our parametric approximation.
  SkMatrix44 to_XYZD50_matrix;
  if (!sk_icc->toXYZD50(&to_XYZD50_matrix)) {
    DLOG(ERROR) << "Failed to extract ICC profile primary matrix.";
    return kICCFailedToExtractMatrix;
  }

  // Try to directly extract a numerical transfer function.
  SkColorSpaceTransferFn exact_tr_fn;
  if (sk_icc->isNumericalTransferFn(&exact_tr_fn)) {
    *parametric_color_space =
        gfx::ColorSpace::CreateCustom(to_XYZD50_matrix, exact_tr_fn);
    return kICCExtractedMatrixAndAnalyticTrFn;
  }

  // If we fail to get a transfer function, use the sRGB transfer function,
  // and return false to indicate that the gfx::ColorSpace isn't accurate, but
  // we can construct accurate LUT transforms using the underlying
  // SkColorSpace.
  *parametric_color_space = gfx::ColorSpace::CreateCustom(
      to_XYZD50_matrix, ColorSpace::TransferID::IEC61966_2_1);

  // Attempt to fit a parametric transfer function to the table data in the
  // profile.
  SkColorSpaceTransferFn approx_tr_fn;
  if (!SkApproximateTransferFn(sk_icc, parametric_tr_fn_max_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 (*parametric_tr_fn_max_error >= kMaxError) {
    DLOG(ERROR) << "Failed to accurately approximate transfer function, error: "
                << 256.f * (*parametric_tr_fn_max_error) << "/256";
    return kICCFailedToApproximateTrFnAccurately;
  };

  // If the error is sufficiently low, declare that the approximation is
  // accurate.
  *parametric_color_space =
      gfx::ColorSpace::CreateCustom(to_XYZD50_matrix, 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 {
  return data_ == other.data_;
}

bool ICCProfile::operator!=(const ICCProfile& other) const {
  return !(*this == other);
}

bool ICCProfile::IsValid() const {
  switch (analyze_result_) {
    case kICCFailedToParse:
    case kICCFailedToExtractSkColorSpace:
    case kICCFailedToCreateXform:
      return false;
    default:
      break;
  }
  return true;
}

// static
ICCProfile ICCProfile::FromData(const void* data, size_t size) {
  return FromDataWithId(data, size, 0);
}

// static
ICCProfile ICCProfile::FromDataWithId(const void* data,
                                      size_t size,
                                      uint64_t new_profile_id) {
  ICCProfile icc_profile;

  if (!size)
    return icc_profile;

  // Create a new cached id and add it to the cache.
  icc_profile.id_ = new_profile_id;
  const char* data_as_char = reinterpret_cast<const char*>(data);
  icc_profile.data_.insert(icc_profile.data_.begin(), data_as_char,
                           data_as_char + size);
  icc_profile.ComputeColorSpaceAndCache();
  return icc_profile;
}

#if (!defined(OS_WIN) && !defined(USE_X11) && !defined(OS_MACOSX)) || \
    defined(OS_IOS)
// static
ICCProfile ICCProfile::FromBestMonitor() {
  return ICCProfile();
}
#endif

// static
const std::vector<char>& ICCProfile::GetData() const {
  return data_;
}

const ColorSpace& ICCProfile::GetColorSpace() const {
  g_cache.Get().TouchEntry(*this);
  return color_space_;
}

const ColorSpace& ICCProfile::GetParametricColorSpace() const {
  g_cache.Get().TouchEntry(*this);
  return parametric_color_space_;
}

// static
bool ICCProfile::FromId(uint64_t id,
                        ICCProfile* icc_profile) {
  return g_cache.Get().FindById(id, icc_profile);
}

void ICCProfile::ComputeColorSpaceAndCache() {
  // Early out for empty entries.
  if (data_.empty())
    return;

  // If this id already exists in the cache, copy |this| from the cache entry.
  if (g_cache.Get().FindById(id_, this))
    return;

  // If this data already exists in the cache, copy |this| from the cache entry.
  if (g_cache.Get().FindByData(data_.data(), data_.size(), this))
    return;

  // Parse the ICC profile
  sk_sp<SkColorSpace> useable_sk_color_space;
  analyze_result_ =
      ExtractColorSpaces(data_, &parametric_color_space_,
                         &parametric_tr_fn_error_, &useable_sk_color_space);
  switch (analyze_result_) {
    case kICCExtractedSRGBColorSpace:
    case kICCExtractedMatrixAndAnalyticTrFn:
    case kICCExtractedMatrixAndApproximatedTrFn:
      // Successfully and accurately extracted color space.
      color_space_ = parametric_color_space_;
      break;
    case kICCFailedToExtractRawTrFn:
    case kICCFailedToExtractMatrix:
    case kICCFailedToConvergeToApproximateTrFn:
    case kICCFailedToApproximateTrFnAccurately:
      // Successfully but extracted a color space, but it isn't accurate enough.
      color_space_ = ColorSpace(ColorSpace::PrimaryID::ICC_BASED,
                                ColorSpace::TransferID::ICC_BASED);
      color_space_.icc_profile_sk_color_space_ = useable_sk_color_space;
      break;
    case kICCFailedToParse:
    case kICCFailedToExtractSkColorSpace:
    case kICCFailedToCreateXform:
      // Can't even use this color space as a LUT.
      DCHECK(!parametric_color_space_.IsValid());
      color_space_ = parametric_color_space_;
      break;
  }

  // Add to the cache.
  g_cache.Get().InsertAndSetIdIfNeeded(this);
}

void ICCProfile::HistogramDisplay(int64_t display_id) const {
  if (!g_cache.Get().GetAndSetNeedsHistogram(display_id, *this))
    return;

  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>(parametric_tr_fn_error_ * 255), 0, 127, 16);
  }
}

}  // namespace gfx