Implement SkHighContrastFilter

This is a color filter to apply several contrast adjustments for users
with low vision, including inverting the colors (in either RGB or HSL
space), applying gamma correction, converting to grayscale, and increasing
the contrast.

BUG=skia:6235

CQ_INCLUDE_TRYBOTS=skia.primary:Test-Ubuntu-GCC-GCE-CPU-AVX2-x86_64-Release-SKNX_NO_SIMD

Change-Id: Icb8f3e290932d8bcd9387fb1f39dd20767e15cf6
Reviewed-on: https://skia-review.googlesource.com/7460
Commit-Queue: Mike Klein <mtklein@chromium.org>
Reviewed-by: Mike Reed <reed@google.com>
Reviewed-by: Mike Klein <mtklein@chromium.org>
diff --git a/gm/highcontrastfilter.cpp b/gm/highcontrastfilter.cpp
new file mode 100644
index 0000000..68a5449
--- /dev/null
+++ b/gm/highcontrastfilter.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm.h"
+#include "SkCanvas.h"
+#include "SkGradientShader.h"
+#include "SkHighContrastFilter.h"
+
+using InvertStyle = SkHighContrastConfig::InvertStyle;
+
+static SkScalar kSize   = 200;
+static SkColor  kColor1 = SkColorSetARGB(0xff, 0xff, 0xff, 0);
+static SkColor  kColor2 = SkColorSetARGB(0xff, 0x82, 0xff, 0);
+
+static void draw_label(SkCanvas* canvas, const SkHighContrastConfig& config) {
+    char labelBuffer[256];
+    const char* invertStr =
+        (config.fInvertStyle == InvertStyle::kInvertBrightness ?
+             "InvBrightness" :
+            (config.fInvertStyle == InvertStyle::kInvertLightness ?
+                 "InvLightness" : "NoInvert"));
+
+    snprintf(labelBuffer, sizeof(labelBuffer), "%s%s contrast=%.1f",
+             config.fGrayscale ? "Gray " : "",
+             invertStr,
+             config.fContrast);
+
+    SkPaint paint;
+    sk_tool_utils::set_portable_typeface(&paint);
+    paint.setTextSize(0.05f);
+    size_t len = strlen(labelBuffer);
+
+    SkScalar width = paint.measureText(labelBuffer, len);
+    canvas->drawText(labelBuffer, len, 0.5f - width / 2, 0.16f, paint);
+}
+
+static void draw_scene(SkCanvas* canvas, const SkHighContrastConfig& config) {
+    SkRect bounds = SkRect::MakeLTRB(0.0f, 0.0f, 1.0f, 1.0f);
+    SkPaint xferPaint;
+    xferPaint.setColorFilter(SkHighContrastFilter::Make(config));
+    canvas->saveLayer(&bounds, &xferPaint);
+
+    SkPaint paint;
+    bounds = SkRect::MakeLTRB(0.1f, 0.2f, 0.9f, 0.4f);
+    paint.setARGB(0xff, 0x66, 0x11, 0x11);
+    canvas->drawRect(bounds, paint);
+
+    paint.setARGB(0xff, 0xbb, 0x77, 0x77);
+    paint.setTextSize(0.15f);
+    canvas->drawText("A", 1, 0.15f, 0.35f, paint);
+
+    bounds = SkRect::MakeLTRB(0.1f, 0.8f, 0.9f, 1.0f);
+    paint.setARGB(0xff, 0xcc, 0xcc, 0xff);
+    canvas->drawRect(bounds, paint);
+
+    paint.setARGB(0xff, 0x88, 0x88, 0xbb);
+    paint.setTextSize(0.15f);
+    canvas->drawText("Z", 1, 0.75f, 0.95f, paint);
+
+    bounds = SkRect::MakeLTRB(0.1f, 0.4f, 0.9f, 0.6f);
+    SkPoint     pts[] = { { 0, 0 }, { 1, 0 } };
+    SkColor     colors[] = { SK_ColorWHITE, SK_ColorBLACK };
+    SkScalar    pos[] = { 0.2f, 0.8f };
+    paint.setShader(SkGradientShader::MakeLinear(
+        pts, colors, pos,
+        SK_ARRAY_COUNT(colors), SkShader::kClamp_TileMode));
+    canvas->drawRect(bounds, paint);
+
+    bounds = SkRect::MakeLTRB(0.1f, 0.6f, 0.9f, 0.8f);
+    SkColor colors2[] = { SK_ColorGREEN, SK_ColorWHITE };
+    paint.setShader(SkGradientShader::MakeLinear(
+        pts, colors2, pos,
+        SK_ARRAY_COUNT(colors2), SkShader::kClamp_TileMode));
+    canvas->drawRect(bounds, paint);
+
+    canvas->restore();
+}
+
+class HighContrastFilterGM : public skiagm::GM {
+public:
+    HighContrastFilterGM() {
+        SkColor  g1Colors[] = { kColor1, SkColorSetA(kColor1, 0x20) };
+        SkColor  g2Colors[] = { kColor2, SkColorSetA(kColor2, 0x20) };
+        SkPoint  g1Points[] = { { 0, 0 }, { 0,     100 } };
+        SkPoint  g2Points[] = { { 0, 0 }, { kSize, 0   } };
+        SkScalar pos[] = { 0.2f, 1.0f };
+
+        SkHighContrastConfig fConfig;
+        fFilter = SkHighContrastFilter::Make(fConfig);
+        fGr1 = SkGradientShader::MakeLinear(
+            g1Points, g1Colors, pos, SK_ARRAY_COUNT(g1Colors),
+            SkShader::kClamp_TileMode);
+        fGr2 = SkGradientShader::MakeLinear(
+            g2Points, g2Colors, pos, SK_ARRAY_COUNT(g2Colors),
+            SkShader::kClamp_TileMode);
+    }
+
+protected:
+
+    SkString onShortName() override {
+        return SkString("highcontrastfilter");
+    }
+
+    SkISize onISize() override {
+        return SkISize::Make(600, 420);
+    }
+
+    void onDraw(SkCanvas* canvas) override {
+        SkHighContrastConfig configs[] = {
+            { false, InvertStyle::kNoInvert, 0.0f },
+            { false, InvertStyle::kInvertBrightness, 0.0f },
+            { false, InvertStyle::kInvertLightness, 0.0f },
+            { false, InvertStyle::kInvertLightness, 0.2f },
+            { true, InvertStyle::kNoInvert, 0.0f },
+            { true, InvertStyle::kInvertBrightness, 0.0f },
+            { true, InvertStyle::kInvertLightness, 0.0f },
+            { true, InvertStyle::kInvertLightness, 0.2f },
+        };
+
+        for (size_t i = 0; i < SK_ARRAY_COUNT(configs); ++i) {
+            SkScalar x = kSize * (i % 4);
+            SkScalar y = kSize * (i / 4);
+            canvas->save();
+            canvas->translate(x, y);
+            canvas->scale(kSize, kSize);
+            draw_scene(canvas, configs[i]);
+            draw_label(canvas, configs[i]);
+            canvas->restore();
+        }
+    }
+
+private:
+    sk_sp<SkColorFilter>    fFilter;
+    sk_sp<SkShader>         fGr1, fGr2;
+
+    typedef skiagm::GM INHERITED;
+};
+
+DEF_GM(return new HighContrastFilterGM;)
diff --git a/gn/effects.gni b/gn/effects.gni
index d79481f..3ae4ebe 100644
--- a/gn/effects.gni
+++ b/gn/effects.gni
@@ -40,6 +40,7 @@
   "$_src/effects/SkEmbossMaskFilter.cpp",
   "$_src/effects/SkImageSource.cpp",
   "$_src/effects/SkGaussianEdgeShader.cpp",
+  "$_src/effects/SkHighContrastFilter.cpp",
   "$_src/effects/SkLayerDrawLooper.cpp",
   "$_src/effects/SkLayerRasterizer.cpp",
   "$_src/effects/SkLightingImageFilter.cpp",
diff --git a/gn/gm.gni b/gn/gm.gni
index 8fd946b..d9c2565 100644
--- a/gn/gm.gni
+++ b/gn/gm.gni
@@ -147,6 +147,7 @@
   "$_gm/hairlines.cpp",
   "$_gm/hairmodes.cpp",
   "$_gm/hardstop_gradients.cpp",
+  "$_gm/highcontrastfilter.cpp",
   "$_gm/hittestpath.cpp",
   "$_gm/image.cpp",
   "$_gm/image_pict.cpp",
diff --git a/gn/tests.gni b/gn/tests.gni
index 3a25760..6646f3d 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -97,6 +97,7 @@
   "$_tests/GrTextureStripAtlasTest.cpp",
   "$_tests/GrTRecorderTest.cpp",
   "$_tests/HashTest.cpp",
+  "$_tests/HighContrastFilterTest.cpp",
   "$_tests/HSVRoundTripTest.cpp",
   "$_tests/image-bitmap.cpp",
   "$_tests/ICCTest.cpp",
diff --git a/include/effects/SkHighContrastFilter.h b/include/effects/SkHighContrastFilter.h
new file mode 100644
index 0000000..2ac37e7
--- /dev/null
+++ b/include/effects/SkHighContrastFilter.h
@@ -0,0 +1,81 @@
+/*
+* Copyright 2017 Google Inc.
+*
+* Use of this source code is governed by a BSD-style license that can be
+* found in the LICENSE file.
+*/
+
+#ifndef SkHighContrastFilter_DEFINED
+#define SkHighContrastFilter_DEFINED
+
+#include "SkColorFilter.h"
+#include "SkPaint.h"
+
+/**
+ *  Configuration struct for SkHighContrastFilter.
+ *
+ *  Provides transformations to improve contrast for users with low vision.
+ */
+struct SkHighContrastConfig {
+    enum class InvertStyle {
+        kNoInvert,
+        kInvertBrightness,
+        kInvertLightness,
+    };
+
+    SkHighContrastConfig() {
+        fGrayscale = false;
+        fInvertStyle = InvertStyle::kNoInvert;
+        fContrast = 0.0f;
+    }
+
+    SkHighContrastConfig(bool grayscale,
+                         InvertStyle invertStyle,
+                         SkScalar contrast)
+        : fGrayscale(grayscale),
+          fInvertStyle(invertStyle),
+          fContrast(contrast) {}
+
+    // Returns true if all of the fields are set within the valid range.
+    bool isValid() const {
+        return fInvertStyle >= InvertStyle::kNoInvert &&
+            fInvertStyle <= InvertStyle::kInvertLightness &&
+            fContrast >= -1.0 &&
+            fContrast <= 1.0;
+    }
+
+    // If true, the color will be converted to grayscale.
+    bool fGrayscale;
+
+    // Whether to invert brightness, lightness, or neither.
+    InvertStyle fInvertStyle;
+
+    // After grayscale and inverting, the contrast can be adjusted linearly.
+    // The valid range is -1.0 through 1.0, where 0.0 is no adjustment.
+    SkScalar  fContrast;
+};
+
+/**
+ *  Color filter that provides transformations to improve contrast
+ *  for users with low vision.
+ *
+ *  Applies the following transformations in this order. Each of these
+ *  can be configured using SkHighContrastConfig.
+ *
+ *    - Conversion to grayscale
+ *    - Color inversion (either in RGB or HSL space)
+ *    - Increasing the resulting contrast.
+ *
+ * Calling SkHighContrastFilter::Make will return nullptr if the config is
+ * not valid, e.g. if you try to call it with a contrast outside the range of
+ * -1.0 to 1.0.
+ */
+class SK_API SkHighContrastFilter {
+public:
+    // Returns the filter, or nullptr if the config is invalid.
+    static sk_sp<SkColorFilter> Make(const SkHighContrastConfig& config);
+
+    SK_DECLARE_FLATTENABLE_REGISTRAR_GROUP()
+};
+
+#endif
diff --git a/src/core/SkRasterPipeline.h b/src/core/SkRasterPipeline.h
index c5661a2..34b0744 100644
--- a/src/core/SkRasterPipeline.h
+++ b/src/core/SkRasterPipeline.h
@@ -98,7 +98,9 @@
     M(save_xy) M(accumulate)                                     \
     M(linear_gradient_2stops)                                    \
     M(byte_tables)                                               \
-    M(shader_adapter)
+    M(shader_adapter)                                            \
+    M(rgb_to_hsl)                                                \
+    M(hsl_to_rgb)
 
 class SkRasterPipeline {
 public:
diff --git a/src/effects/SkHighContrastFilter.cpp b/src/effects/SkHighContrastFilter.cpp
new file mode 100644
index 0000000..267a8ef
--- /dev/null
+++ b/src/effects/SkHighContrastFilter.cpp
@@ -0,0 +1,469 @@
+/*
+* Copyright 2017 Google Inc.
+*
+* Use of this source code is governed by a BSD-style license that can be
+* found in the LICENSE file.
+*/
+
+#include "SkHighContrastFilter.h"
+
+#include "SkArenaAlloc.h"
+#include "SkRasterPipeline.h"
+#include "SkReadBuffer.h"
+#include "SkString.h"
+#include "SkWriteBuffer.h"
+
+#if SK_SUPPORT_GPU
+#include "GrContext.h"
+#include "glsl/GrGLSLFragmentProcessor.h"
+#include "glsl/GrGLSLFragmentShaderBuilder.h"
+#endif
+
+using InvertStyle = SkHighContrastConfig::InvertStyle;
+
+namespace {
+
+SkScalar Hue2RGB(SkScalar p, SkScalar q, SkScalar t) {
+    if (t < 0) {
+        t += 1;
+    } else if (t > 1) {
+        t -= 1;
+    }
+
+    if (t < 1/6.f) {
+        return p + (q - p) * 6 * t;
+    }
+
+    if (t < 1/2.f) {
+        return q;
+    }
+
+    if (t < 2/3.f) {
+        return p + (q - p) * (2/3.f - t) * 6;
+    }
+
+    return p;
+}
+
+uint8_t SkScalarToUint8Clamp(SkScalar f) {
+    if (f <= 0) {
+        return 0;
+    } else if (f >= 1) {
+        return 255;
+    }
+    return static_cast<unsigned char>(255 * f);
+}
+
+SkScalar IncreaseContrast(SkScalar f, SkScalar contrast) {
+    SkScalar m = (1 + contrast) / (1 - contrast);
+    SkScalar b = (-0.5f * m + 0.5f);
+    return m * f + b;
+}
+
+static SkPMColor ApplyHighContrastFilter(const SkHighContrastConfig& config,
+                                         SkPMColor pmColor) {
+    SkColor color = SkUnPreMultiply::PMColorToColor(pmColor);
+    SkScalar rf = SkColorGetR(color) / 255.f;
+    SkScalar gf = SkColorGetG(color) / 255.f;
+    SkScalar bf = SkColorGetB(color) / 255.f;
+
+    // Apply a gamma of 2.0 so that the rest of the calculations
+    // happen roughly in linear space.
+    rf *= rf;
+    gf *= gf;
+    bf *= bf;
+
+    // Convert to grayscale using luminance coefficients.
+    if (config.fGrayscale) {
+        SkScalar lum =
+            rf * SK_LUM_COEFF_R + gf * SK_LUM_COEFF_G + bf * SK_LUM_COEFF_B;
+        rf = lum;
+        gf = lum;
+        bf = lum;
+    }
+
+    // Now invert.
+    if (config.fInvertStyle == InvertStyle::kInvertBrightness) {
+        rf = 1 - rf;
+        gf = 1 - gf;
+        bf = 1 - bf;
+    } else if (config.fInvertStyle == InvertStyle::kInvertLightness) {
+        // Convert to HSL
+        SkScalar max = SkTMax(SkTMax(rf, gf), bf);
+        SkScalar min = SkTMin(SkTMin(rf, gf), bf);
+        SkScalar l = (max + min) / 2;
+        SkScalar h, s;
+
+        if (max == min) {
+            h = 0;
+            s = 0;
+        } else {
+            SkScalar d = max - min;
+            s = l > 0.5f ? d / (2 - max - min) : d / (max + min);
+            if (max == rf) {
+                h = (gf - bf) / d + (gf < bf ? 6 : 0);
+            } else if (max == gf) {
+                h = (bf - rf) / d + 2;
+            } else {
+                h = (rf - gf) / d + 4;
+            }
+            h /= 6;
+        }
+
+        // Invert lightness.
+        l = 1 - l;
+
+        // Now convert back to RGB.
+        if (s == 0) {
+            // Grayscale
+            rf = l;
+            gf = l;
+            bf = l;
+        } else {
+            SkScalar q = l < 0.5f ? l * (1 + s) : l + s - l * s;
+            SkScalar p = 2 * l - q;
+            rf = Hue2RGB(p, q, h + 1/3.f);
+            gf = Hue2RGB(p, q, h);
+            bf = Hue2RGB(p, q, h - 1/3.f);
+        }
+    }
+
+    // Increase contrast.
+    if (config.fContrast != 0.0f) {
+        rf = IncreaseContrast(rf, config.fContrast);
+        gf = IncreaseContrast(gf, config.fContrast);
+        bf = IncreaseContrast(bf, config.fContrast);
+    }
+
+    // Convert back from linear to a color space with a gamma of ~2.0.
+    rf = SkScalarSqrt(rf);
+    gf = SkScalarSqrt(gf);
+    bf = SkScalarSqrt(bf);
+
+    return SkPremultiplyARGBInline(SkColorGetA(color),
+                                   SkScalarToUint8Clamp(rf),
+                                   SkScalarToUint8Clamp(gf),
+                                   SkScalarToUint8Clamp(bf));
+}
+
+}  // namespace
+
+class SkHighContrast_Filter : public SkColorFilter {
+public:
+    SkHighContrast_Filter(const SkHighContrastConfig& config) {
+        fConfig = config;
+        // Clamp contrast to just inside -1 to 1 to avoid division by zero.
+        fConfig.fContrast = SkScalarPin(fConfig.fContrast,
+                                        -1.0f + FLT_EPSILON,
+                                        1.0f - FLT_EPSILON);
+    }
+
+    virtual ~SkHighContrast_Filter() { }
+
+#if SK_SUPPORT_GPU
+    sk_sp<GrFragmentProcessor> asFragmentProcessor(GrContext*, SkColorSpace*) const override;
+ #endif
+
+    void filterSpan(const SkPMColor src[], int count, SkPMColor dst[]) const
+          override;
+    bool onAppendStages(SkRasterPipeline* p,
+                        SkColorSpace* dst,
+                        SkArenaAlloc* scratch,
+                        bool shaderIsOpaque) const override;
+
+    SK_TO_STRING_OVERRIDE()
+
+    SK_DECLARE_PUBLIC_FLATTENABLE_DESERIALIZATION_PROCS(SkHighContrast_Filter)
+
+protected:
+    void flatten(SkWriteBuffer&) const override;
+
+private:
+    SkHighContrastConfig fConfig;
+
+    friend class SkHighContrastFilter;
+
+    typedef SkColorFilter INHERITED;
+};
+
+void SkHighContrast_Filter::filterSpan(const SkPMColor src[], int count,
+                                       SkPMColor dst[]) const {
+    for (int i = 0; i < count; ++i)
+        dst[i] = ApplyHighContrastFilter(fConfig, src[i]);
+}
+
+bool SkHighContrast_Filter::onAppendStages(SkRasterPipeline* p,
+                                           SkColorSpace* dst,
+                                           SkArenaAlloc* scratch,
+                                           bool shaderIsOpaque) const {
+    if (!shaderIsOpaque) {
+        p->append(SkRasterPipeline::unpremul);
+    }
+
+    if (fConfig.fGrayscale) {
+        float r = SK_LUM_COEFF_R;
+        float g = SK_LUM_COEFF_G;
+        float b = SK_LUM_COEFF_B;
+        float* matrix = scratch->makeArray<float>(12);
+        matrix[0] = matrix[1] = matrix[2] = r;
+        matrix[3] = matrix[4] = matrix[5] = g;
+        matrix[6] = matrix[7] = matrix[8] = b;
+        p->append(SkRasterPipeline::matrix_3x4, matrix);
+    }
+
+    if (fConfig.fInvertStyle == InvertStyle::kInvertBrightness) {
+        float* matrix = scratch->makeArray<float>(12);
+        matrix[0] = matrix[4] = matrix[8] = -1;
+        matrix[9] = matrix[10] = matrix[11] = 1;
+        p->append(SkRasterPipeline::matrix_3x4, matrix);
+    } else if (fConfig.fInvertStyle == InvertStyle::kInvertLightness) {
+        p->append(SkRasterPipeline::rgb_to_hsl);
+        float* matrix = scratch->makeArray<float>(12);
+        matrix[0] = matrix[4] = matrix[11] = 1;
+        matrix[8] = -1;
+        p->append(SkRasterPipeline::matrix_3x4, matrix);
+        p->append(SkRasterPipeline::hsl_to_rgb);
+    }
+
+    if (fConfig.fContrast != 0.0) {
+        float* matrix = scratch->makeArray<float>(12);
+        float c = fConfig.fContrast;
+        float m = (1 + c) / (1 - c);
+        float b = (-0.5f * m + 0.5f);
+        matrix[0] = matrix[4] = matrix[8] = m;
+        matrix[9] = matrix[10] = matrix[11] = b;
+        p->append(SkRasterPipeline::matrix_3x4, matrix);
+    }
+
+    p->append(SkRasterPipeline::clamp_0);
+    p->append(SkRasterPipeline::clamp_1);
+
+    if (!shaderIsOpaque) {
+        p->append(SkRasterPipeline::premul);
+    }
+
+    return true;
+}
+
+void SkHighContrast_Filter::flatten(SkWriteBuffer& buffer) const {
+    buffer.writeBool(fConfig.fGrayscale);
+    buffer.writeInt(static_cast<int>(fConfig.fInvertStyle));
+    buffer.writeScalar(fConfig.fContrast);
+}
+
+sk_sp<SkFlattenable> SkHighContrast_Filter::CreateProc(SkReadBuffer& buffer) {
+    SkHighContrastConfig config;
+    config.fGrayscale = buffer.readBool();
+    config.fInvertStyle = static_cast<InvertStyle>(buffer.readInt());
+    config.fContrast = buffer.readScalar();
+    return SkHighContrastFilter::Make(config);
+}
+
+sk_sp<SkColorFilter> SkHighContrastFilter::Make(
+    const SkHighContrastConfig& config) {
+    if (!config.isValid())
+        return nullptr;
+    return sk_make_sp<SkHighContrast_Filter>(config);
+}
+
+#ifndef SK_IGNORE_TO_STRING
+void SkHighContrast_Filter::toString(SkString* str) const {
+    str->append("SkHighContrastColorFilter ");
+}
+#endif
+
+SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_START(SkHighContrastFilter)
+    SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(SkHighContrast_Filter)
+SK_DEFINE_FLATTENABLE_REGISTRAR_GROUP_END
+
+#if SK_SUPPORT_GPU
+class HighContrastFilterEffect : public GrFragmentProcessor {
+public:
+    static sk_sp<GrFragmentProcessor> Make(const SkHighContrastConfig& config) {
+        return sk_sp<GrFragmentProcessor>(new HighContrastFilterEffect(config));
+    }
+
+    const char* name() const override { return "HighContrastFilter"; }
+
+    const SkHighContrastConfig& config() const { return fConfig; }
+
+private:
+    HighContrastFilterEffect(const SkHighContrastConfig& config)
+        : INHERITED(kNone_OptimizationFlags)
+        , fConfig(config) {
+        this->initClassID<HighContrastFilterEffect>();
+    }
+
+    GrGLSLFragmentProcessor* onCreateGLSLInstance() const override;
+
+    virtual void onGetGLSLProcessorKey(const GrShaderCaps& caps,
+                                       GrProcessorKeyBuilder* b) const override;
+
+    bool onIsEqual(const GrFragmentProcessor& other) const override {
+        const HighContrastFilterEffect& that = other.cast<HighContrastFilterEffect>();
+        return fConfig.fGrayscale == that.fConfig.fGrayscale &&
+            fConfig.fInvertStyle == that.fConfig.fInvertStyle &&
+            fConfig.fContrast == that.fConfig.fContrast;
+    }
+
+    SkHighContrastConfig fConfig;
+
+    typedef GrFragmentProcessor INHERITED;
+};
+
+class GLHighContrastFilterEffect : public GrGLSLFragmentProcessor {
+public:
+    static void GenKey(const GrProcessor&, const GrShaderCaps&, GrProcessorKeyBuilder*);
+
+    GLHighContrastFilterEffect(const SkHighContrastConfig& config);
+
+protected:
+    void onSetData(const GrGLSLProgramDataManager&, const GrProcessor&) override;
+    void emitCode(EmitArgs& args) override;
+
+private:
+    UniformHandle fContrastUni;
+    SkHighContrastConfig fConfig;
+
+    typedef GrGLSLFragmentProcessor INHERITED;
+};
+
+GrGLSLFragmentProcessor* HighContrastFilterEffect::onCreateGLSLInstance() const {
+    return new GLHighContrastFilterEffect(fConfig);
+}
+
+void HighContrastFilterEffect::onGetGLSLProcessorKey(const GrShaderCaps& caps,
+                                                     GrProcessorKeyBuilder* b) const {
+    GLHighContrastFilterEffect::GenKey(*this, caps, b);
+}
+
+void GLHighContrastFilterEffect::onSetData(const GrGLSLProgramDataManager& pdm, const GrProcessor& proc) {
+    const HighContrastFilterEffect& hcfe = proc.cast<HighContrastFilterEffect>();
+    pdm.set1f(fContrastUni, hcfe.config().fContrast);
+}
+
+GLHighContrastFilterEffect::GLHighContrastFilterEffect(const SkHighContrastConfig& config)
+    : INHERITED()
+    , fConfig(config) {
+}
+
+void GLHighContrastFilterEffect::GenKey(
+    const GrProcessor& proc, const GrShaderCaps&, GrProcessorKeyBuilder* b) {
+  const HighContrastFilterEffect& hcfe = proc.cast<HighContrastFilterEffect>();
+  b->add32(static_cast<uint32_t>(hcfe.config().fGrayscale));
+  b->add32(static_cast<uint32_t>(hcfe.config().fInvertStyle));
+}
+
+void GLHighContrastFilterEffect::emitCode(EmitArgs& args) {
+    const char* contrast;
+    fContrastUni = args.fUniformHandler->addUniform(kFragment_GrShaderFlag,
+                                                    kFloat_GrSLType, kDefault_GrSLPrecision,
+                                                    "contrast", &contrast);
+
+    if (nullptr == args.fInputColor) {
+        args.fInputColor = "vec4(1)";
+    }
+
+    GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder;
+
+    fragBuilder->codeAppendf("vec4 color = %s;", args.fInputColor);
+
+    // Unpremultiply. The max() is to guard against 0 / 0.
+    fragBuilder->codeAppendf("float nonZeroAlpha = max(color.a, 0.00001);");
+    fragBuilder->codeAppendf("color = vec4(color.rgb / nonZeroAlpha, nonZeroAlpha);");
+
+    // Grayscale.
+    if (fConfig.fGrayscale) {
+        fragBuilder->codeAppendf("float luma = dot(color, vec4(%f, %f, %f, 0));",
+                                 SK_LUM_COEFF_R, SK_LUM_COEFF_G, SK_LUM_COEFF_B);
+        fragBuilder->codeAppendf("color = vec4(luma, luma, luma, 0);");
+    }
+
+    if (fConfig.fInvertStyle == InvertStyle::kInvertBrightness) {
+        fragBuilder->codeAppendf("color = vec4(1, 1, 1, 1) - color;");
+    }
+
+    if (fConfig.fInvertStyle == InvertStyle::kInvertLightness) {
+        // Convert from RGB to HSL.
+        fragBuilder->codeAppendf("float fmax = max(color.r, max(color.g, color.b));");
+        fragBuilder->codeAppendf("float fmin = min(color.r, min(color.g, color.b));");
+        fragBuilder->codeAppendf("float l = (fmax + fmin) / 2;");
+
+        fragBuilder->codeAppendf("float h;");
+        fragBuilder->codeAppendf("float s;");
+
+        fragBuilder->codeAppendf("if (fmax == fmin) {");
+        fragBuilder->codeAppendf("  h = 0;");
+        fragBuilder->codeAppendf("  s = 0;");
+        fragBuilder->codeAppendf("} else {");
+        fragBuilder->codeAppendf("  float d = fmax - fmin;");
+        fragBuilder->codeAppendf("  s = l > 0.5 ?");
+        fragBuilder->codeAppendf("      d / (2 - fmax - fmin) :");
+        fragBuilder->codeAppendf("      d / (fmax + fmin);");
+        fragBuilder->codeAppendf("  if (fmax == color.r) {");
+        fragBuilder->codeAppendf("    h = (color.g - color.b) / d + ");
+        fragBuilder->codeAppendf("        (color.g < color.b ? 6 : 0);");
+        fragBuilder->codeAppendf("  } else if (fmax == color.g) {");
+        fragBuilder->codeAppendf("    h = (color.b - color.r) / d + 2;");
+        fragBuilder->codeAppendf("  } else {");
+        fragBuilder->codeAppendf("    h = (color.r - color.g) / d + 4;");
+        fragBuilder->codeAppendf("  }");
+        fragBuilder->codeAppendf("}");
+        fragBuilder->codeAppendf("h /= 6;");
+        fragBuilder->codeAppendf("l = 1.0 - l;");
+        // Convert back from HSL to RGB.
+        SkString hue2rgbFuncName;
+        static const GrShaderVar gHue2rgbArgs[] = {
+            GrShaderVar("p", kFloat_GrSLType),
+            GrShaderVar("q", kFloat_GrSLType),
+            GrShaderVar("t", kFloat_GrSLType),
+        };
+        fragBuilder->emitFunction(kFloat_GrSLType,
+                                  "hue2rgb",
+                                  SK_ARRAY_COUNT(gHue2rgbArgs),
+                                  gHue2rgbArgs,
+                                  "if (t < 0)"
+                                  "  t += 1;"
+                                  "if (t > 1)"
+                                  "  t -= 1;"
+                                  "if (t < 1/6.)"
+                                  "  return p + (q - p) * 6 * t;"
+                                  "if (t < 1/2.)"
+                                  "  return q;"
+                                  "if (t < 2/3.)"
+                                  "  return p + (q - p) * (2/3. - t) * 6;"
+                                  "return p;",
+                                  &hue2rgbFuncName);
+        fragBuilder->codeAppendf("if (s == 0) {");
+        fragBuilder->codeAppendf("  color = vec4(l, l, l, 0);");
+        fragBuilder->codeAppendf("} else {");
+        fragBuilder->codeAppendf("  float q = l < 0.5 ? l * (1 + s) : l + s - l * s;");
+        fragBuilder->codeAppendf("  float p = 2 * l - q;");
+        fragBuilder->codeAppendf("  color.r = %s(p, q, h + 1/3.);", hue2rgbFuncName.c_str());
+        fragBuilder->codeAppendf("  color.g = %s(p, q, h);", hue2rgbFuncName.c_str());
+        fragBuilder->codeAppendf("  color.b = %s(p, q, h - 1/3.);", hue2rgbFuncName.c_str());
+        fragBuilder->codeAppendf("}");
+    }
+
+    // Contrast.
+    fragBuilder->codeAppendf("if (%s != 0) {", contrast);
+    fragBuilder->codeAppendf("  float m = (1 + %s) / (1 - %s);", contrast, contrast);
+    fragBuilder->codeAppendf("  float off = (-0.5 * m + 0.5);");
+    fragBuilder->codeAppendf("  color = m * color + off;");
+    fragBuilder->codeAppendf("}");
+
+    // Clamp.
+    fragBuilder->codeAppendf("color = clamp(color, 0, 1);");
+
+    // Restore the original alpha and premultiply.
+    fragBuilder->codeAppendf("color.a = %s.a;", args.fInputColor);
+    fragBuilder->codeAppendf("color.rgb *= color.a;");
+
+    // Copy to the output color.
+    fragBuilder->codeAppendf("%s = color;", args.fOutputColor);
+}
+
+sk_sp<GrFragmentProcessor> SkHighContrast_Filter::asFragmentProcessor(GrContext*, SkColorSpace*) const {
+    return HighContrastFilterEffect::Make(fConfig);
+}
+#endif
diff --git a/src/opts/SkRasterPipeline_opts.h b/src/opts/SkRasterPipeline_opts.h
index a815168..fb0271b 100644
--- a/src/opts/SkRasterPipeline_opts.h
+++ b/src/opts/SkRasterPipeline_opts.h
@@ -776,6 +776,48 @@
     r = g = b = 0;
 }
 
+STAGE(rgb_to_hsl) {
+    auto max = SkNf::Max(SkNf::Max(r, g), b);
+    auto min = SkNf::Min(SkNf::Min(r, g), b);
+    auto l = 0.5f * (max + min);
+
+    auto d = max - min;
+    auto d_inv = 1.0f/d;
+    auto s = (max == min).thenElse(0.0f,
+        d/(l > 0.5f).thenElse(2.0f - max - min, max + min));
+    SkNf h = (max != r).thenElse(0.0f,
+        (g - b)*d_inv + (g < b).thenElse(6.0f, 0.0f));
+    h = (max == g).thenElse((b - r)*d_inv + 2.0f, h);
+    h = (max == b).thenElse((r - g)*d_inv + 4.0f, h);
+    h *= (1/6.0f);
+
+    h = (max == min).thenElse(0.0f, h);
+
+    r = h;
+    g = s;
+    b = l;
+}
+
+STAGE(hsl_to_rgb) {
+    auto h = r;
+    auto s = g;
+    auto l = b;
+    auto q = (l < 0.5f).thenElse(l*(1.0f + s), l + s - l*s);
+    auto p = 2.0f*l - q;
+
+    auto hue_to_rgb = [](const SkNf& p, const SkNf& q, const SkNf& t) {
+        auto t2 = (t < 0.0f).thenElse(t + 1.0f, (t > 1.0f).thenElse(t - 1.0f, t));
+        return (t2 < (1/6.0f)).thenElse(
+            p + (q - p)*6.0f*t, (t2 < (3/6.0f)).thenElse(
+                q, (t2 < (4/6.0f)).thenElse(
+                    p + (q - p)*((4/6.0f) - t2)*6.0f, p)));
+    };
+
+    r = (s == 0.f).thenElse(l, hue_to_rgb(p, q, h + (1/3.0f)));
+    g = (s == 0.f).thenElse(l, hue_to_rgb(p, q, h));
+    b = (s == 0.f).thenElse(l, hue_to_rgb(p, q, h - (1/3.0f)));
+}
+
 STAGE_CTX(matrix_2x3, const float*) {
     auto m = ctx;
 
diff --git a/src/ports/SkGlobalInitialization_default.cpp b/src/ports/SkGlobalInitialization_default.cpp
index dd6ef87..60213bd 100644
--- a/src/ports/SkGlobalInitialization_default.cpp
+++ b/src/ports/SkGlobalInitialization_default.cpp
@@ -26,6 +26,7 @@
 #include "SkGaussianEdgeShader.h"
 #include "SkRRectsGaussianEdgeMaskFilter.h"
 #include "SkGradientShader.h"
+#include "SkHighContrastFilter.h"
 #include "SkImageSource.h"
 #include "SkLayerDrawLooper.h"
 #include "SkLayerRasterizer.h"
@@ -86,6 +87,7 @@
     SkArithmeticMode::InitializeFlattenables();
     SkTableColorFilter::InitializeFlattenables();
     SkOverdrawColorFilter::InitializeFlattenables();
+    SkHighContrastFilter::InitializeFlattenables();
 
     // Shader
     SK_DEFINE_FLATTENABLE_REGISTRAR_ENTRY(SkPerlinNoiseShader)
diff --git a/tests/HighContrastFilterTest.cpp b/tests/HighContrastFilterTest.cpp
new file mode 100644
index 0000000..35b0c9f
--- /dev/null
+++ b/tests/HighContrastFilterTest.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkCanvas.h"
+#include "SkHighContrastFilter.h"
+#include "Test.h"
+
+DEF_TEST(HighContrastFilter_FilterImage, reporter) {
+    SkHighContrastConfig config;
+    config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;
+
+    int w = 10, h = 10;
+    SkBitmap filterResult, paintResult;
+
+    filterResult.allocN32Pixels(w, h);
+    SkCanvas canvasFilter(filterResult);
+    canvasFilter.clear(0x00000000);
+
+    paintResult.allocN32Pixels(w, h);
+    SkCanvas canvasPaint(paintResult);
+    canvasPaint.clear(0x00000000);
+
+    SkPaint paint;
+    paint.setColor(SK_ColorBLUE);
+    SkRect r = SkRect::MakeLTRB(SkIntToScalar(2), SkIntToScalar(2),
+                                SkIntToScalar(8), SkIntToScalar(8));
+    canvasPaint.drawRect(r, paint);
+
+    paint.setColorFilter(SkHighContrastFilter::Make(config));
+    canvasFilter.drawRect(r, paint);
+
+    paintResult.lockPixels();
+    filterResult.lockPixels();
+    for (int y = r.top(); y < r.bottom(); ++y) {
+        for (int x = r.left(); x < r.right(); ++x) {
+            SkColor paintColor = paintResult.getColor(x, y);
+            SkColor filterColor = filterResult.getColor(x, y);
+            REPORTER_ASSERT(
+                reporter, filterColor ==
+                paint.getColorFilter()->filterColor(paintColor));
+        }
+    }
+    paintResult.unlockPixels();
+    filterResult.unlockPixels();
+}
+
+DEF_TEST(HighContrastFilter_SanityCheck, reporter) {
+    SkHighContrastConfig config;
+    config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;
+    sk_sp<SkColorFilter> filter = SkHighContrastFilter::Make(config);
+
+    SkColor white_inverted = filter->filterColor(SK_ColorWHITE);
+    REPORTER_ASSERT(reporter, white_inverted == SK_ColorBLACK);
+
+    SkColor black_inverted = filter->filterColor(SK_ColorBLACK);
+    REPORTER_ASSERT(reporter, black_inverted == SK_ColorWHITE);
+}
+
+DEF_TEST(HighContrastFilter_InvalidInputs, reporter) {
+    SkHighContrastConfig config;
+    REPORTER_ASSERT(reporter, config.isValid());
+
+    // Valid invert style
+    config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertBrightness;
+    REPORTER_ASSERT(reporter, config.isValid());
+    config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;
+    REPORTER_ASSERT(reporter, config.isValid());
+    sk_sp<SkColorFilter> filter = SkHighContrastFilter::Make(config);
+    REPORTER_ASSERT(reporter, filter);
+
+    // Invalid invert style
+    config.fInvertStyle = static_cast<SkHighContrastConfig::InvertStyle>(999);
+    REPORTER_ASSERT(reporter, !config.isValid());
+    filter = SkHighContrastFilter::Make(config);
+    REPORTER_ASSERT(reporter, !filter);
+
+    // Valid contrast
+    config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertBrightness;
+    config.fContrast = 0.5f;
+    REPORTER_ASSERT(reporter, config.isValid());
+    filter = SkHighContrastFilter::Make(config);
+    REPORTER_ASSERT(reporter, filter);
+
+    // Invalid contrast
+    config.fContrast = 1.1f;
+    REPORTER_ASSERT(reporter, !config.isValid());
+    filter = SkHighContrastFilter::Make(config);
+    REPORTER_ASSERT(reporter, !filter);
+}