blob: f28420501aef4b4ba78dc9efaac09edcb6c0c03c [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/platform/graphics/dark_mode_filter.h"
#include <cmath>
#include <optional>
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/lru_cache.h"
#include "base/notreached.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/blink/renderer/platform/graphics/color.h"
#include "third_party/blink/renderer/platform/graphics/dark_mode_color_classifier.h"
#include "third_party/blink/renderer/platform/graphics/dark_mode_color_filter.h"
#include "third_party/blink/renderer/platform/graphics/dark_mode_image_cache.h"
#include "third_party/blink/renderer/platform/graphics/dark_mode_image_classifier.h"
#include "third_party/blink/renderer/platform/graphics/image.h"
#include "third_party/blink/renderer/platform/wtf/hash_functions.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkColorFilter.h"
#include "third_party/skia/include/core/SkPixmap.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "ui/gfx/color_utils.h"
namespace blink {
namespace {
const size_t kMaxCacheSize = 1024u;
constexpr SkColor SK_ColorDark = SkColorSetARGB(0xFF, 0x12, 0x12, 0x12);
bool IsRasterSideDarkModeForImagesEnabled() {
static bool enabled = base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableRasterSideDarkModeForImages);
return enabled;
}
bool ShouldUseRasterSidePath(Image* image) {
DCHECK(image);
// Raster-side path is not enabled.
if (!IsRasterSideDarkModeForImagesEnabled())
return false;
// Raster-side path is only supported for bitmap images.
return image->IsBitmapImage();
}
sk_sp<cc::ColorFilter> GetDarkModeFilterForImageOnMainThread(
DarkModeFilter* filter,
Image* image,
const SkIRect& rounded_src) {
sk_sp<cc::ColorFilter> color_filter;
DarkModeImageCache* cache = image->GetDarkModeImageCache();
DCHECK(cache);
if (cache->Exists(rounded_src)) {
color_filter = cache->Get(rounded_src);
} else {
// Performance warning: Calling AsSkBitmapForCurrentFrame() will
// synchronously decode image.
SkBitmap bitmap =
image->AsSkBitmapForCurrentFrame(kDoNotRespectImageOrientation);
SkPixmap pixmap;
bitmap.peekPixels(&pixmap);
color_filter = filter->GenerateImageFilter(pixmap, rounded_src);
// Using blink side dark mode for images, it is hard to implement
// caching mechanism for partially loaded bitmap image content, as
// content id for the image frame being rendered gets decided during
// rastering only. So caching of dark mode result will be deferred until
// default frame is completely received. This will help get correct
// classification results for incremental content received for the given
// image.
if (!image->IsBitmapImage() || image->CurrentFrameIsComplete())
cache->Add(rounded_src, color_filter);
}
return color_filter;
}
} // namespace
// DarkModeInvertedColorCache - Implements cache for inverted colors.
class DarkModeInvertedColorCache {
public:
DarkModeInvertedColorCache() : cache_(kMaxCacheSize) {}
~DarkModeInvertedColorCache() = default;
SkColor4f GetInvertedColor(DarkModeColorFilter* filter, SkColor4f color) {
SkColor key = color.toSkColor();
auto it = cache_.Get(key);
if (it != cache_.end())
return it->second;
SkColor4f inverted_color = filter->InvertColor(color);
cache_.Put(key, inverted_color);
return inverted_color;
}
void Clear() { cache_.Clear(); }
size_t size() { return cache_.size(); }
private:
base::HashingLRUCache<SkColor, SkColor4f> cache_;
};
DarkModeFilter::DarkModeFilter(const DarkModeSettings& settings)
: immutable_(settings),
inverted_color_cache_(new DarkModeInvertedColorCache()) {}
DarkModeFilter::~DarkModeFilter() {}
DarkModeFilter::ImmutableData::ImmutableData(const DarkModeSettings& settings)
: settings(settings),
foreground_classifier(nullptr),
background_classifier(nullptr),
image_classifier(nullptr),
color_filter(nullptr),
image_filter(nullptr) {
color_filter = DarkModeColorFilter::FromSettings(settings);
if (!color_filter)
return;
image_filter = color_filter->ToColorFilter();
foreground_classifier =
DarkModeColorClassifier::MakeForegroundColorClassifier(settings);
background_classifier =
DarkModeColorClassifier::MakeBackgroundColorClassifier(settings);
image_classifier = std::make_unique<DarkModeImageClassifier>(
settings.image_classifier_policy);
}
DarkModeImagePolicy DarkModeFilter::GetDarkModeImagePolicy() const {
return immutable_.settings.image_policy;
}
// Heuristic to maintain contrast for borders and selections (see:
// crbug.com/1263545,crbug.com/1298969)
SkColor4f DarkModeFilter::AdjustDarkenColor(
const SkColor4f& color,
DarkModeFilter::ElementRole role,
const SkColor4f& contrast_background) {
const SkColor4f& background = [&contrast_background]() {
if (contrast_background == SkColors::kTransparent)
return SkColor4f::FromColor(SK_ColorDark);
else
return contrast_background;
}();
switch (role) {
case ElementRole::kBorder: {
if (color == SkColor4f{0.0f, 0.0f, 0.0f, color.fA})
return color;
if (color_utils::GetContrastRatio(color, background) <
color_utils::kMinimumReadableContrastRatio)
return color;
return AdjustDarkenColor(Color::FromSkColor4f(color).Dark().toSkColor4f(),
role, background);
}
case ElementRole::kSelection: {
if (!immutable_.color_filter)
return color;
return immutable_.color_filter->AdjustColorForHigherConstrast(
color, background, color_utils::kMinimumVisibleContrastRatio);
}
default:
return color;
}
NOTREACHED();
}
SkColor4f DarkModeFilter::InvertColorIfNeeded(
const SkColor4f& color,
ElementRole role,
const SkColor4f& contrast_background) {
return AdjustDarkenColor(
InvertColorIfNeeded(color, role), role,
InvertColorIfNeeded(contrast_background, ElementRole::kBackground));
}
SkColor4f DarkModeFilter::InvertColorIfNeeded(const SkColor4f& color,
ElementRole role) {
if (!immutable_.color_filter)
return color;
if (ShouldApplyToColor(color, role)) {
return inverted_color_cache_->GetInvertedColor(
immutable_.color_filter.get(), color);
}
return color;
}
void DarkModeFilter::ApplyFilterToImage(Image* image,
cc::PaintFlags* flags,
const SkRect& src) {
DCHECK(image);
DCHECK(flags);
DCHECK_NE(GetDarkModeImagePolicy(), DarkModeImagePolicy::kFilterNone);
if (GetDarkModeImagePolicy() == DarkModeImagePolicy::kFilterAll) {
flags->setColorFilter(GetImageFilter());
return;
}
// Raster-side dark mode path - Just set the dark mode on flags and dark
// mode will be applied at compositor side during rasterization.
if (ShouldUseRasterSidePath(image)) {
flags->setUseDarkModeForImage(true);
return;
}
// Blink-side dark mode path - Apply dark mode to images in main thread
// only. If the result is not cached, calling this path is expensive and
// will block main thread.
sk_sp<cc::ColorFilter> color_filter =
GetDarkModeFilterForImageOnMainThread(this, image, src.roundOut());
if (color_filter)
flags->setColorFilter(std::move(color_filter));
}
bool DarkModeFilter::ShouldApplyFilterToImage(ImageType type) const {
DarkModeImagePolicy image_policy = GetDarkModeImagePolicy();
if (image_policy == DarkModeImagePolicy::kFilterNone)
return false;
if (image_policy == DarkModeImagePolicy::kFilterAll)
return true;
// kIcon: Do not consider images being drawn into bigger rect as these
// images are not meant for icons or representing smaller widgets. These
// images are considered as photos which should be untouched.
// kSeparator: Images being drawn from very smaller |src| rect, i.e. one of
// the dimensions is very small, can be used for the border around the content
// or showing separator. Consider these images irrespective of size of the
// rect being drawn to. Classifying them will not be too costly.
return type == ImageType::kIcon || type == ImageType::kSeparator;
}
sk_sp<cc::ColorFilter> DarkModeFilter::GenerateImageFilter(
const SkPixmap& pixmap,
const SkIRect& src) const {
DCHECK(immutable_.settings.image_policy == DarkModeImagePolicy::kFilterSmart);
DCHECK(immutable_.image_filter);
return (immutable_.image_classifier->Classify(pixmap, src) ==
DarkModeResult::kApplyFilter)
? immutable_.image_filter
: nullptr;
}
sk_sp<cc::ColorFilter> DarkModeFilter::GetImageFilter() const {
DCHECK(immutable_.image_filter);
return immutable_.image_filter;
}
std::optional<cc::PaintFlags> DarkModeFilter::ApplyToFlagsIfNeeded(
const cc::PaintFlags& flags,
ElementRole role,
SkColor4f contrast_background) {
if (!immutable_.color_filter || flags.HasShader())
return std::nullopt;
cc::PaintFlags dark_mode_flags = flags;
SkColor4f flags_color = flags.getColor4f();
if (ShouldApplyToColor(flags_color, role)) {
flags_color = inverted_color_cache_->GetInvertedColor(
immutable_.color_filter.get(), flags_color);
}
dark_mode_flags.setColor(AdjustDarkenColor(
flags_color, role,
InvertColorIfNeeded(contrast_background, ElementRole::kBackground)));
return std::make_optional<cc::PaintFlags>(std::move(dark_mode_flags));
}
bool DarkModeFilter::ShouldApplyToColor(const SkColor4f& color,
ElementRole role) {
switch (role) {
case ElementRole::kBorder:
case ElementRole::kSVG:
case ElementRole::kForeground:
case ElementRole::kListSymbol:
DCHECK(immutable_.foreground_classifier);
return immutable_.foreground_classifier->ShouldInvertColor(
color.toSkColor()) == DarkModeResult::kApplyFilter;
case ElementRole::kBackground:
case ElementRole::kSelection:
DCHECK(immutable_.background_classifier);
return immutable_.background_classifier->ShouldInvertColor(
color.toSkColor()) == DarkModeResult::kApplyFilter;
default:
return false;
}
NOTREACHED();
}
size_t DarkModeFilter::GetInvertedColorCacheSizeForTesting() {
return inverted_color_cache_->size();
}
} // namespace blink