0

[Color Enhancement] Adds color vision deficiency filters in CROS

Adds a drop-down setting and severity slider to Chrome OS settings
for common color deficiencies protanomaly, deuteranomaly and
tritanomaly.

Adds a setting to turn on/off all color filtering options for common
color deficiency types as well as the other experimental color
filtering settings.

Settings are only added behind a flag:
--enable-features=ExperimentalAccessibilityColorEnhancementSettings

Screenshot: https://screenshot.googleplex.com/9Lk7oWEUGBtwAMC.png
Note: This does not match the UX mocks and the mocks haven't yet gone
for UX approval. Further UX changes will be made before it can be
removed from behind the flag. This change focuses on functionality.

See more at go/cros-color-filtering.

Bug: b:259372272
Test: New, and manual comparison to other OSes
Change-Id: I8a92d95b96f5e598647e356aec24c80c54e1662b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4304804
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Reviewed-by: Josiah Krutz <josiahk@google.com>
Commit-Queue: Katie Dektar <katie@chromium.org>
Reviewed-by: Wes Okuhara <wesokuhara@google.com>
Cr-Commit-Position: refs/heads/main@{#1114098}
This commit is contained in:
Katie Dektar
2023-03-07 19:17:08 +00:00
committed by Chromium LUCI CQ
parent 340853f288
commit d8567dc8fc
19 changed files with 518 additions and 92 deletions

@ -1183,6 +1183,9 @@ void AccessibilityControllerImpl::RegisterProfilePrefs(
if (::features::
AreExperimentalAccessibilityColorEnhancementSettingsEnabled()) {
registry->RegisterBooleanPref(
prefs::kAccessibilityColorFiltering, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
registry->RegisterIntegerPref(
prefs::kAccessibilityGreyscaleAmount, 0,
user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
@ -1195,6 +1198,13 @@ void AccessibilityControllerImpl::RegisterProfilePrefs(
registry->RegisterIntegerPref(
prefs::kAccessibilityHueRotationAmount, 0,
user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
registry->RegisterIntegerPref(
prefs::kAccessibilityColorVisionCorrectionAmount, 100,
user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
registry->RegisterIntegerPref(
prefs::kAccessibilityColorVisionDeficiencyType,
ColorVisionDeficiencyType::kDeuteranomaly,
user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
}
}
@ -1999,25 +2009,40 @@ void AccessibilityControllerImpl::ObservePrefs(PrefService* prefs) {
&AccessibilityControllerImpl::UpdateCursorColorFromPrefs,
base::Unretained(this)));
if (color_enhancement_feature_enabled) {
pref_change_registrar_->Add(
prefs::kAccessibilityColorFiltering,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilityGreyscaleAmount,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateFilterGreyscaleFromPrefs,
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilitySaturationAmount,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateFilterSaturationFromPrefs,
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilitySepiaAmount,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateFilterSepiaFromPrefs,
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilityHueRotationAmount,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateFilterHueRotationFromPrefs,
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilityColorVisionCorrectionAmount,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
pref_change_registrar_->Add(
prefs::kAccessibilityColorVisionDeficiencyType,
base::BindRepeating(
&AccessibilityControllerImpl::UpdateColorFilteringFromPrefs,
base::Unretained(this)));
}
@ -2038,10 +2063,7 @@ void AccessibilityControllerImpl::ObservePrefs(PrefService* prefs) {
UpdateShortcutsEnabledFromPref();
UpdateTabletModeShelfNavigationButtonsFromPref();
if (color_enhancement_feature_enabled) {
UpdateFilterGreyscaleFromPrefs();
UpdateFilterSaturationFromPrefs();
UpdateFilterSepiaFromPrefs();
UpdateFilterHueRotationFromPrefs();
UpdateColorFilteringFromPrefs();
}
}
@ -2214,42 +2236,47 @@ void AccessibilityControllerImpl::UpdateCursorColorFromPrefs() {
shell->UpdateCursorCompositingEnabled();
}
void AccessibilityControllerImpl::UpdateFilterGreyscaleFromPrefs() {
void AccessibilityControllerImpl::UpdateColorFilteringFromPrefs() {
DCHECK(active_user_prefs_);
const float amount =
auto* color_enhancement_controller =
Shell::Get()->color_enhancement_controller();
if (!active_user_prefs_->GetBoolean(prefs::kAccessibilityColorFiltering)) {
color_enhancement_controller->SetColorFilteringEnabledAndUpdateDisplays(
false);
return;
}
const float greyscale_amount =
active_user_prefs_->GetInteger(prefs::kAccessibilityGreyscaleAmount) /
100.f;
color_enhancement_controller->SetGreyscaleAmount(greyscale_amount);
Shell::Get()->color_enhancement_controller()->SetGreyscaleAmount(amount);
}
void AccessibilityControllerImpl::UpdateFilterSaturationFromPrefs() {
DCHECK(active_user_prefs_);
const float amount =
const float saturation_amount =
active_user_prefs_->GetInteger(prefs::kAccessibilitySaturationAmount) /
100.f;
color_enhancement_controller->SetSaturationAmount(saturation_amount);
Shell::Get()->color_enhancement_controller()->SetSaturationAmount(amount);
}
void AccessibilityControllerImpl::UpdateFilterSepiaFromPrefs() {
DCHECK(active_user_prefs_);
const float amount =
const float sepia_amount =
active_user_prefs_->GetInteger(prefs::kAccessibilitySepiaAmount) / 100.f;
color_enhancement_controller->SetSepiaAmount(sepia_amount);
Shell::Get()->color_enhancement_controller()->SetSepiaAmount(amount);
}
void AccessibilityControllerImpl::UpdateFilterHueRotationFromPrefs() {
DCHECK(active_user_prefs_);
const int amount =
const int hue_rotation_amount =
active_user_prefs_->GetInteger(prefs::kAccessibilityHueRotationAmount);
color_enhancement_controller->SetHueRotationAmount(hue_rotation_amount);
Shell::Get()->color_enhancement_controller()->SetHueRotationAmount(amount);
const float cvd_correction_amount =
active_user_prefs_->GetInteger(
prefs::kAccessibilityColorVisionCorrectionAmount) /
100.0f;
ColorVisionDeficiencyType type =
static_cast<ColorVisionDeficiencyType>(active_user_prefs_->GetInteger(
prefs::kAccessibilityColorVisionDeficiencyType));
color_enhancement_controller->SetColorVisionCorrectionFilter(
type, cvd_correction_amount);
// Ensure displays get updated.
color_enhancement_controller->SetColorFilteringEnabledAndUpdateDisplays(true);
}
void AccessibilityControllerImpl::UpdateAccessibilityHighlightingFromPrefs() {

@ -514,10 +514,7 @@ class ASH_EXPORT AccessibilityControllerImpl : public AccessibilityController,
void UpdateLargeCursorFromPref();
void UpdateLiveCaptionFromPref();
void UpdateCursorColorFromPrefs();
void UpdateFilterGreyscaleFromPrefs();
void UpdateFilterSaturationFromPrefs();
void UpdateFilterSepiaFromPrefs();
void UpdateFilterHueRotationFromPrefs();
void UpdateColorFilteringFromPrefs();
void UpdateSwitchAccessKeyCodesFromPref(SwitchAccessCommand command);
void UpdateSwitchAccessAutoScanEnabledFromPref();
void UpdateSwitchAccessAutoScanSpeedFromPref();

@ -3,10 +3,13 @@
// found in the LICENSE file.
#include "ash/color_enhancement/color_enhancement_controller.h"
#include <memory>
#include "ash/shell.h"
#include "cc/paint/filter_operation.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/matrix3_f.h"
namespace ash {
@ -18,6 +21,151 @@ namespace {
// larger.
const float kMinSepiaPerceptableDifference = 0.3f;
//
// Parameters for simulating color vision deficiency.
// Copied from the Javascript ColorEnhancer extension:
// ui/accessibility/extensions/colorenhancer/src/cvd.js
// Initial source:
// http://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/CVD_Simulation.html
// Original Research Paper:
// http://www.inf.ufrgs.br/~oliveira/pubs_files/CVD_Simulation/Machado_Oliveira_Fernandes_CVD_Vis2009_final.pdf
//
// The first index is ColorVisionDeficiencyType enum, so this must be kept in
// that order.
const float kSimulationParams[3][9][3] = {
// ColorVisionDeficiencyType::kProtanomaly:
{{0.4720, -1.2946, 0.9857},
{-0.6128, 1.6326, 0.0187},
{0.1407, -0.3380, -0.0044},
{-0.1420, 0.2488, 0.0044},
{0.1872, -0.3908, 0.9942},
{-0.0451, 0.1420, 0.0013},
{0.0222, -0.0253, -0.0004},
{-0.0290, -0.0201, 0.0006},
{0.0068, 0.0454, 0.9990}},
// ColorVisionDeficiencyType::kDeuteranomaly:
{{0.5442, -1.1454, 0.9818},
{-0.7091, 1.5287, 0.0238},
{0.1650, -0.3833, -0.0055},
{-0.1664, 0.4368, 0.0056},
{0.2178, -0.5327, 0.9927},
{-0.0514, 0.0958, 0.0017},
{0.0180, -0.0288, -0.0006},
{-0.0232, -0.0649, 0.0007},
{0.0052, 0.0360, 0.9998}},
// ColorVisionDeficiencyType::kTritanomaly:
{{0.4275, -0.0181, 0.9307},
{-0.2454, 0.0013, 0.0827},
{-0.1821, 0.0168, -0.0134},
{-0.1280, 0.0047, 0.0202},
{0.0233, -0.0398, 0.9728},
{0.1048, 0.0352, 0.0070},
{-0.0156, 0.0061, 0.0071},
{0.3841, 0.2947, 0.0151},
{-0.3685, -0.3008, 0.9778}}};
// Returns a 3x3 matrix for simulating the given type of CVD with the given
// severity.
// Calculation from CVD.getCvdSimulationMatrix_ in
// ui/accessibility/extensions/colorenhancer/src/cvd.js.
gfx::Matrix3F GetCvdSimulationMatrix(ColorVisionDeficiencyType type,
float severity) {
float severity_squared = severity * severity;
gfx::Matrix3F result = gfx::Matrix3F::Zeros();
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
int param_row = i * 3 + j;
result.set(i, j,
kSimulationParams[type][param_row][0] * severity_squared +
kSimulationParams[type][param_row][1] * severity +
kSimulationParams[type][param_row][2]);
}
}
return result;
}
// Computes a 3x3 matrix that can be applied to any three-color-channel image
// to shift original colors to be more visible for someone with the given `type`
// and `severity` of color vision deficiency.
gfx::Matrix3F ComputeColorVisionFilterMatrix(ColorVisionDeficiencyType type,
float severity) {
// Compute the matrix that could be used to simulate the color vision
// deficiency.
gfx::Matrix3F simulation_matrix = GetCvdSimulationMatrix(type, severity);
// Now use the simulation to calculate a correction matrix. This process is
// called Daltonizing.
// "Daltonizing" an image consists of calculating the error, which is the
// original image minus the simulation and represents the information lost to
// the user, and linearly transforming that error to a color space the user
// can see, then adding it back onto the original image (Fidaner, Lin and
// Ozguven, 2006). The correction matrix is used to map the error between the
// initial image and the simulated image into a color space that can be seen
// by the user based on their type of color deficiency. So for example someone
// with Protanopia can see less of the red channel, so the correction matrix
// could be:
// [0.0, 0.0, 0.0,
// 0.7, 1.0, 0.0,
// 0.7, 0.0, 1.0]
// Multiplying this correction matrix by the error and adding it back to the
// original will have the effect of shifting more of image information lost to
// protanopes back into the image in a part of the spectrum they can see.
// Similarly, for Deuteranopia we correct on the green axis, and for
// Tritanopia we correct on the blue axis.
gfx::Matrix3F correction_matrix = gfx::Matrix3F::Zeros();
switch (type) {
case ColorVisionDeficiencyType::kProtanomaly:
// Correct on red axis: Shift colors in the red channel to the other
// channels.
correction_matrix.set(0.0, 0.0, 0.0, 0.7, 1.0, 0.0, 0.7, 0.0, 1.0);
break;
case ColorVisionDeficiencyType::kDeuteranomaly:
// Correct on green axis: Shift colors in the green channel to the other
// channels.
correction_matrix.set(1.0, 0.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7, 1.0);
break;
case ColorVisionDeficiencyType::kTritanomaly:
// Correct on blue axis: Shift colors in the blue channel into the other
// channels.
correction_matrix.set(1.0, 0.0, 0.7, 0.0, 1.0, 0.7, 0.0, 0.0, 0.0);
break;
}
// For Daltonization of an image `original_img`, we would calculate the
// Daltonized version based on the `simulated_img` image and the
// `correction_matrix` as:
//
// result_img = original_img + correction_matrix x (original_img -
// simulated_img)
//
// We know that simulation_matrix x original_img = simulated_img, and can
// substitute:
//
// result_image = original_img + correction_matrix x (original_img -
// simulation_matrix x original_img)
//
// We can factor out `original_img` because matrix multiplication distributes:
//
// result_image = (Identity + correction_matrix x
// (Identity - simulation_matrix)) x original_img
//
// This method should return the matrix that multiplies by `original_img` to
// get the result, i.e.
//
// result_matrix = Identity + correction_matrix x (Identity -
// simulation_matrix)
//
// Distributing the `correction_matrix` gives us:
//
// result_matrix = Identity + correction_matrix - correction_matrix x
// simulation_matrix
//
// Compute this and return it.
return gfx::Matrix3F::Identity() + correction_matrix -
MatrixProduct(correction_matrix, simulation_matrix);
}
} // namespace
ColorEnhancementController::ColorEnhancementController() {
@ -38,13 +186,18 @@ void ColorEnhancementController::SetHighContrastEnabled(bool enabled) {
UpdateAllDisplays();
}
void ColorEnhancementController::SetColorFilteringEnabledAndUpdateDisplays(
bool enabled) {
color_filtering_enabled_ = enabled;
UpdateAllDisplays();
}
void ColorEnhancementController::SetGreyscaleAmount(float amount) {
if (greyscale_amount_ == amount || amount < 0 || amount > 1)
return;
greyscale_amount_ = amount;
// Note: No need to do cursor compositing since cursors are greyscale already.
UpdateAllDisplays();
}
void ColorEnhancementController::SetSaturationAmount(float amount) {
@ -54,7 +207,6 @@ void ColorEnhancementController::SetSaturationAmount(float amount) {
saturation_amount_ = amount;
// Note: No need to do cursor compositing since cursors are greyscale and not
// impacted by saturation.
UpdateAllDisplays();
}
void ColorEnhancementController::SetSepiaAmount(float amount) {
@ -64,7 +216,6 @@ void ColorEnhancementController::SetSepiaAmount(float amount) {
sepia_amount_ = amount;
// The cursor should be tinted sepia as well. Update cursor compositing.
Shell::Get()->UpdateCursorCompositingEnabled();
UpdateAllDisplays();
}
void ColorEnhancementController::SetHueRotationAmount(int amount) {
@ -74,7 +225,33 @@ void ColorEnhancementController::SetHueRotationAmount(int amount) {
hue_rotation_amount_ = amount;
// Note: No need to do cursor compositing since cursors are greyscale and not
// impacted by hue rotation.
UpdateAllDisplays();
}
void ColorEnhancementController::SetColorVisionCorrectionFilter(
ColorVisionDeficiencyType type,
float amount) {
if ((amount <= 0 || amount > 1) && cvd_correction_matrix_) {
cvd_correction_matrix_.reset();
return;
}
gfx::Matrix3F filter_matrix = ComputeColorVisionFilterMatrix(type, amount);
// The color matrix used by ui::Layer is a 4 x 5 matrix.
// Convert the 3x3 result into the shape needed for the layer.
cvd_correction_matrix_ = std::make_unique<cc::FilterOperation::Matrix>();
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 5; col++) {
int index = row * 5 + col;
if (row < 3 && col < 3) {
(*cvd_correction_matrix_)[index] = filter_matrix.get(row, col);
} else if (index != 18) {
(*cvd_correction_matrix_)[index] = 0;
} else {
(*cvd_correction_matrix_)[index] = 1;
}
}
}
}
bool ColorEnhancementController::ShouldEnableCursorCompositingForSepia() const {
@ -107,12 +284,25 @@ void ColorEnhancementController::UpdateDisplay(aura::Window* root_window) {
return;
}
if (!color_filtering_enabled_) {
// Reset layer state to defaults.
layer->SetLayerGrayscale(0.0);
layer->SetLayerSaturation(1.0);
layer->SetLayerSepia(0);
layer->SetLayerHueRotation(0);
layer->ClearLayerCustomColorMatrix();
return;
}
layer->SetLayerGrayscale(greyscale_amount_);
layer->SetLayerSaturation(saturation_amount_);
layer->SetLayerSepia(sepia_amount_);
layer->SetLayerHueRotation(hue_rotation_amount_);
// TODO(crbug.com/1031959): Use SetLayerCustomColorMatrix to create color
// filters for common color blindness types.
if (cvd_correction_matrix_) {
layer->SetLayerCustomColorMatrix(*cvd_correction_matrix_);
} else {
layer->ClearLayerCustomColorMatrix();
}
}
} // namespace ash

@ -7,6 +7,7 @@
#include "ash/ash_export.h"
#include "ash/shell_observer.h"
#include "cc/paint/filter_operation.h"
namespace aura {
class Window;
@ -14,6 +15,12 @@ class Window;
namespace ash {
enum ColorVisionDeficiencyType {
kProtanomaly = 0,
kDeuteranomaly = 1,
kTritanomaly = 2,
};
// Controls the color enhancement options on all displays. These options
// are applied globally.
class ASH_EXPORT ColorEnhancementController : public ShellObserver {
@ -26,21 +33,31 @@ class ASH_EXPORT ColorEnhancementController : public ShellObserver {
~ColorEnhancementController() override;
// Sets high contrast mode and updates all available displays.
// Sets high contrast mode (which inverts colors) and updates all available
// displays.
void SetHighContrastEnabled(bool enabled);
// Sets greyscale amount and updates all available displays.
// Sets whether color filtering options are enabled and updates all available
// displays.
void SetColorFilteringEnabledAndUpdateDisplays(bool enabled);
// Sets greyscale amount.
void SetGreyscaleAmount(float amount);
// Sets saturation amount and updates all available displays.
// Sets saturation amount.
void SetSaturationAmount(float amount);
// Sets sepia amount and updates all available displays.
// Sets sepia amount.
void SetSepiaAmount(float amount);
// Sets hue rotation amount and updates all available displays.
// Sets hue rotation amount.
void SetHueRotationAmount(int amount);
// Sets the color vision correction filter type and severity.
// `severity` should be between 0 and 1.0, inclusive.
void SetColorVisionCorrectionFilter(ColorVisionDeficiencyType type,
float severity);
bool ShouldEnableCursorCompositingForSepia() const;
// ShellObserver:
@ -57,6 +74,9 @@ class ASH_EXPORT ColorEnhancementController : public ShellObserver {
// Indicates if the high contrast mode is enabled or disabled.
bool high_contrast_enabled_ = false;
// Indicates if the color filtering options are enabled or disabled.
bool color_filtering_enabled_ = false;
// Amount of hue rotation, on the scale of 0 to 359.
int hue_rotation_amount_ = 0;
@ -69,6 +89,9 @@ class ASH_EXPORT ColorEnhancementController : public ShellObserver {
// Amount of saturation where 1 is normal. Values may range from
// 0 to max float.
float saturation_amount_ = 1;
// Color correction matrix.
std::unique_ptr<cc::FilterOperation::Matrix> cvd_correction_matrix_;
};
} // namespace ash

@ -68,6 +68,7 @@ TEST_F(ColorEnhancementControllerTest, HighContrast) {
TEST_F(ColorEnhancementControllerTest, Greyscale) {
PrefService* prefs = GetPrefs();
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, true);
prefs->SetInteger(prefs::kAccessibilityGreyscaleAmount, 0);
EXPECT_FALSE(IsCursorCompositingEnabled());
for (auto* root_window : Shell::GetAllRootWindows()) {
@ -99,6 +100,7 @@ TEST_F(ColorEnhancementControllerTest, Greyscale) {
TEST_F(ColorEnhancementControllerTest, Saturation) {
PrefService* prefs = GetPrefs();
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, true);
prefs->SetInteger(prefs::kAccessibilitySaturationAmount, 50);
for (auto* root_window : Shell::GetAllRootWindows()) {
EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_saturation());
@ -119,6 +121,7 @@ TEST_F(ColorEnhancementControllerTest, Saturation) {
TEST_F(ColorEnhancementControllerTest, HueRotation) {
PrefService* prefs = GetPrefs();
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, true);
prefs->SetInteger(prefs::kAccessibilityHueRotationAmount, 42);
EXPECT_FALSE(IsCursorCompositingEnabled());
for (auto* root_window : Shell::GetAllRootWindows()) {
@ -143,6 +146,7 @@ TEST_F(ColorEnhancementControllerTest, HueRotation) {
TEST_F(ColorEnhancementControllerTest, Sepia) {
PrefService* prefs = GetPrefs();
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, true);
prefs->SetInteger(prefs::kAccessibilitySepiaAmount, 10);
EXPECT_FALSE(IsCursorCompositingEnabled());
for (auto* root_window : Shell::GetAllRootWindows()) {
@ -187,4 +191,81 @@ TEST_F(ColorEnhancementControllerTest, Sepia) {
}
}
TEST_F(ColorEnhancementControllerTest, ColorVisionDeficiencyFilters) {
PrefService* prefs = GetPrefs();
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, true);
// Try for each of the color deficiency types.
for (int i = 0; i < 3; i++) {
prefs->SetInteger(prefs::kAccessibilityColorVisionDeficiencyType, i);
// With severity at 0, no matrix should be applied.
prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 0);
for (auto* root_window : Shell::GetAllRootWindows()) {
EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
}
// With a non-zero severity, a matrix should be applied.
prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 50);
for (auto* root_window : Shell::GetAllRootWindows()) {
EXPECT_TRUE(root_window->layer()->LayerHasCustomColorMatrix());
}
prefs->SetInteger(prefs::kAccessibilityColorVisionCorrectionAmount, 100);
for (auto* root_window : Shell::GetAllRootWindows()) {
const cc::FilterOperation::Matrix* matrix =
root_window->layer()->GetLayerCustomColorMatrix();
EXPECT_TRUE(matrix);
// For protanopes (i == 1), the first row in the resulting matrix should
// have a 1 for red and a zero for the other colors. Similarly with
// deuteranopes (i == 2) and tritanopes (i == 3). This ensures we are
// correcting around the right axis.
for (int j = 0; j < 3; j++) {
if (i == j) {
EXPECT_EQ(1, matrix->at(i * 5 + j));
} else {
EXPECT_EQ(0, matrix->at(i * 5 + j));
}
}
}
}
}
TEST_F(ColorEnhancementControllerTest, ColorFiltersBehindColorFilteringOption) {
PrefService* prefs = GetPrefs();
// Color filtering off.
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, false);
prefs->SetInteger(prefs::kAccessibilitySepiaAmount, 10);
prefs->SetInteger(prefs::kAccessibilityGreyscaleAmount, 50);
prefs->SetInteger(prefs::kAccessibilitySaturationAmount, 50);
prefs->SetInteger(prefs::kAccessibilityHueRotationAmount, 42);
// Default values.
for (auto* root_window : Shell::GetAllRootWindows()) {
EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_sepia());
EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_grayscale());
EXPECT_FLOAT_EQ(1.0f, root_window->layer()->layer_saturation());
EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_hue_rotation());
EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
}
// Turn on color filtering, values should now be from prefs.
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, true);
for (auto* root_window : Shell::GetAllRootWindows()) {
EXPECT_FLOAT_EQ(0.1f, root_window->layer()->layer_sepia());
EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_grayscale());
EXPECT_FLOAT_EQ(0.5f, root_window->layer()->layer_saturation());
EXPECT_FLOAT_EQ(42.f, root_window->layer()->layer_hue_rotation());
EXPECT_TRUE(root_window->layer()->LayerHasCustomColorMatrix());
}
// Turn it off again, expect defaults to be restored.
prefs->SetBoolean(prefs::kAccessibilityColorFiltering, false);
for (auto* root_window : Shell::GetAllRootWindows()) {
EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_sepia());
EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_grayscale());
EXPECT_FLOAT_EQ(1.0f, root_window->layer()->layer_saturation());
EXPECT_FLOAT_EQ(0.0f, root_window->layer()->layer_hue_rotation());
EXPECT_FALSE(root_window->layer()->LayerHasCustomColorMatrix());
}
}
} // namespace ash

@ -415,15 +415,27 @@ const char kAccessibilityAutoclickMovementThreshold[] =
// The Autoclick menu position on the screen, an AutoclickMenuPosition.
const char kAccessibilityAutoclickMenuPosition[] =
"settings.a11y.autoclick_menu_position";
// Whether to enable color filtering settings.
const char kAccessibilityColorFiltering[] =
"settings.a11y.color_filtering.enabled";
// How much to greyscale the display.
const char kAccessibilityGreyscaleAmount[] = "settings.a11y.greyscale_amount";
const char kAccessibilityGreyscaleAmount[] =
"settings.a11y.color_filtering.greyscale_amount";
// How much to saturate the display.
const char kAccessibilitySaturationAmount[] = "settings.a11y.saturation_amount";
const char kAccessibilitySaturationAmount[] =
"settings.a11y.color_filtering.saturation_amount";
// How much sepia the display.
const char kAccessibilitySepiaAmount[] = "settings.a11y.sepia_amount";
const char kAccessibilitySepiaAmount[] =
"settings.a11y.color_filtering.sepia_amount";
// How much to rotate the hue on the display.
const char kAccessibilityHueRotationAmount[] =
"settings.a11y.hue_rotation_amount";
"settings.a11y.color_filtering.hue_rotation_amount";
// The amount of a color vision correction filter to apply.
const char kAccessibilityColorVisionCorrectionAmount[] =
"settings.a11y.color_filtering.color_vision_correction_amount";
// The type of color vision correction to apply.
const char kAccessibilityColorVisionDeficiencyType[] =
"settings.a11y.color_filtering.color_vision_deficiency_type";
// A boolean pref which determines whether caret highlighting is enabled.
const char kAccessibilityCaretHighlightEnabled[] =
"settings.a11y.caret_highlight";

@ -202,6 +202,8 @@ extern const char kAccessibilityAutoclickMovementThreshold[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityAutoclickMenuPosition[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityColorFiltering[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityGreyscaleAmount[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilitySaturationAmount[];
@ -210,6 +212,10 @@ extern const char kAccessibilitySepiaAmount[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityHueRotationAmount[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityColorVisionCorrectionAmount[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityColorVisionDeficiencyType[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityCaretHighlightEnabled[];
COMPONENT_EXPORT(ASH_CONSTANTS)
extern const char kAccessibilityCursorHighlightEnabled[];

@ -965,6 +965,12 @@
<message name="IDS_SETTINGS_HIGH_CONTRAST_DESCRIPTION" desc="Label for checkbox which enables high-contrast UI.">
Turn light screens dark, and dark screens light. Press Search + Ctrl + H to turn color inversion on and off.
</message>
<message name="IDS_SETTINGS_COLOR_FILTERING_LABEL" desc="Label for a checkbox which enables color filtering settings.">
Color filters
</message>
<message name="IDS_SETTINGS_COLOR_FILTERING_DESCRIPTION" desc="Description for a checkbox which enables color filtering settings.">
Adjust how colors appear on your screen
</message>
<message name="IDS_SETTINGS_GREYSCALE_LABEL" desc="Label for slider which controls greyscale UI filter.">
Greyscale
</message>
@ -977,6 +983,21 @@
<message name="IDS_SETTINGS_HUE_ROTATION_LABEL" desc="Label for slider which controls hue rotation filter UI.">
Hue rotation
</message>
<message name="IDS_SETTINGS_PROTANOMALY_FILTER" desc="Label for a setting drop-down menu option for a color filter that helps people with protanomaly.">
Red-green filter (red weak, protanomaly)
</message>
<message name="IDS_SETTINGS_TRITANOMALY_FILTER" desc="Label for a setting drop-down menu option for a color filter that helps people with tritanomaly.">
Blue-yellow filter (tritanomaly)
</message>
<message name="IDS_SETTINGS_DEUTERANOMALY_FILTER" desc="Label for a setting drop-down menu option for a color filter that helps people with deuteranomaly.">
Red-green filter (green weak, deuteranomaly)
</message>
<message name="IDS_SETTINGS_COLOR_VISION_DEFICIENCY_TYPE_LABEL" desc="Label for a drop-down menu option that provides color vision correction filters">
Filter type
</message>
<message name="IDS_SETTINGS_COLOR_VISION_FILTER_INTENSITY_LABEL" desc="Label for a slider that allows people to control the intensity of the color vision correction filter">
Intensity
</message>
<message name="IDS_SETTINGS_COLOR_FILTER_MINIMUM_LABEL" desc="Label for the side of the color filter slider control that causes the least visual change.">
Less
</message>

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -0,0 +1 @@
34d073691a44b689b3a080dd4057b6b5083948c5

@ -533,6 +533,8 @@ const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() {
settings_api::PrefType::PREF_TYPE_BOOLEAN;
(*s_allowlist)[ash::prefs::kAccessibilityAutoclickMovementThreshold] =
settings_api::PrefType::PREF_TYPE_NUMBER;
(*s_allowlist)[ash::prefs::kAccessibilityColorFiltering] =
settings_api::PrefType::PREF_TYPE_BOOLEAN;
(*s_allowlist)[ash::prefs::kAccessibilityGreyscaleAmount] =
settings_api::PrefType::PREF_TYPE_NUMBER;
(*s_allowlist)[ash::prefs::kAccessibilitySaturationAmount] =
@ -541,6 +543,10 @@ const PrefsUtil::TypedPrefMap& PrefsUtil::GetAllowlistedKeys() {
settings_api::PrefType::PREF_TYPE_NUMBER;
(*s_allowlist)[ash::prefs::kAccessibilityHueRotationAmount] =
settings_api::PrefType::PREF_TYPE_NUMBER;
(*s_allowlist)[ash::prefs::kAccessibilityColorVisionCorrectionAmount] =
settings_api::PrefType::PREF_TYPE_NUMBER;
(*s_allowlist)[ash::prefs::kAccessibilityColorVisionDeficiencyType] =
settings_api::PrefType::PREF_TYPE_NUMBER;
(*s_allowlist)[ash::prefs::kShouldAlwaysShowAccessibilityMenu] =
settings_api::PrefType::PREF_TYPE_BOOLEAN;
(*s_allowlist)[ash::prefs::kAccessibilityDictationLocale] =

@ -117,54 +117,86 @@
</div>
</template>
<template is="dom-if" if="[[experimentalColorEnhancementSettingsEnabled_]]">
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{greyscaleLabel}
<settings-toggle-button
class="hr"
pref="{{prefs.settings.a11y.color_filtering.enabled}}"
label="$i18n{colorFilteringLabel}"
sub-label="$i18n{colorFilteringDescription}">
</settings-toggle-button>
<template is="dom-if"
if="[[prefs.settings.a11y.color_filtering.enabled.value]]">
<div class="settings-box settings-box-row">
<div class="start settings-box-text">
<div class="label" aria-hidden="true">
$i18n{colorVisionDeficiencyTypeLabel}
</div>
</div>
<settings-dropdown-menu label="$i18n{colorVisionDeficiencyTypeLabel}"
pref="{{prefs.settings.a11y.color_filtering.color_vision_deficiency_type}}"
menu-options="[[colorVisionDeficiencyTypeOptions_]]">
</settings-dropdown-menu>
</div>
<settings-slider
pref="{{prefs.settings.a11y.greyscale_amount}}"
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{colorVisionFilterIntensityLabel}
</div>
<settings-slider
pref="{{prefs.settings.a11y.color_filtering.color_vision_correction_amount}}"
min="0" max="100"
label-aria="$i18n{greyscaleLabel}"
label-aria="$i18n{colorVisionFilterIntensityLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{saturationLabel}
</settings-slider>
</div>
<settings-slider
pref="{{prefs.settings.a11y.saturation_amount}}"
min="100" max="1000"
label-aria="$i18n{saturationLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{sepiaLabel}
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{greyscaleLabel}
</div>
<settings-slider
pref="{{prefs.settings.a11y.color_filtering.greyscale_amount}}"
min="0" max="100"
label-aria="$i18n{greyscaleLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<settings-slider
pref="{{prefs.settings.a11y.sepia_amount}}"
min="0" max="100"
label-aria="$i18n{sepiaLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{hueRotationLabel}
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{saturationLabel}
</div>
<settings-slider
pref="{{prefs.settings.a11y.color_filtering.saturation_amount}}"
min="100" max="1000"
label-aria="$i18n{saturationLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<settings-slider
pref="{{prefs.settings.a11y.hue_rotation_amount}}"
min="0" max="359"
label-aria="$i18n{hueRotationLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{sepiaLabel}
</div>
<settings-slider
pref="{{prefs.settings.a11y.color_filtering.sepia_amount}}"
min="0" max="100"
label-aria="$i18n{sepiaLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
<div class="settings-box settings-box-row">
<div class="start settings-box-text" aria-hidden="true">
$i18n{hueRotationLabel}
</div>
<settings-slider
pref="{{prefs.settings.a11y.color_filtering.hue_rotation_amount}}"
min="0" max="359"
label-aria="$i18n{hueRotationLabel}"
label-min="$i18n{colorFilterMinLabel}"
label-max="$i18n{colorFilterMaxLabel}">
</settings-slider>
</div>
</template>
</template>
<template is="dom-if" if="[[!isKioskModeActive_]]">
<cr-link-row id="displaySubpageButton" class="hr"

@ -91,6 +91,21 @@ class SettingsDisplayAndMagnificationElement extends
},
},
colorVisionDeficiencyTypeOptions_: {
readOnly: true,
type: Array,
value() {
// These values correspond to ColorVisionDeficiencyType enums in
// ash/color_enhancement/color_enhancement_controller.cc.
// CVD types are ordered here by how common they are.
return [
{value: 1, name: loadTimeData.getString('deuteranomalyFilter')},
{value: 0, name: loadTimeData.getString('protanomalyFilter')},
{value: 2, name: loadTimeData.getString('tritanomalyFilter')},
];
},
},
experimentalColorEnhancementSettingsEnabled_: {
type: Boolean,
value() {

@ -680,6 +680,15 @@ void AccessibilitySection::AddLoadTimeData(
{"highContrastDescription", IDS_SETTINGS_HIGH_CONTRAST_DESCRIPTION},
{"highContrastLabel", IDS_SETTINGS_HIGH_CONTRAST_LABEL},
{"hueRotationLabel", IDS_SETTINGS_HUE_ROTATION_LABEL},
{"protanomalyFilter", IDS_SETTINGS_PROTANOMALY_FILTER},
{"tritanomalyFilter", IDS_SETTINGS_TRITANOMALY_FILTER},
{"deuteranomalyFilter", IDS_SETTINGS_DEUTERANOMALY_FILTER},
{"colorFilteringLabel", IDS_SETTINGS_COLOR_FILTERING_LABEL},
{"colorFilteringDescription", IDS_SETTINGS_COLOR_FILTERING_DESCRIPTION},
{"colorVisionDeficiencyTypeLabel",
IDS_SETTINGS_COLOR_VISION_DEFICIENCY_TYPE_LABEL},
{"colorVisionFilterIntensityLabel",
IDS_SETTINGS_COLOR_VISION_FILTER_INTENSITY_LABEL},
{"keyboardAndTextInputHeading",
IDS_SETTINGS_ACCESSIBILITY_KEYBOARD_AND_TEXT_INPUT_HEADING},
{"keyboardAndTextInputLinkDescription",