Treat kWEBP encode with quality=100 as lossless

In SkEncodeImage and friends, treat quality of 100 as a lossless encode
when using kWEBP. This seems a good fit for the intent - which is
presumably to save the highest quality image. This also matches
Chromium's blink::ImageEncoder::ComputeWebpOptions, which treats a
quality of 1 (on a float scale from 0 to 1) as a lossless encode.

FWIW, Chromium has had this behavior since
https://codereview.chromium.org/1937433002, in response to
crbug.com/523098. The goal is to "maintain sharpness to
match the JPEG encoder behavior (use WEBP lossless encoding)".

Add a test to verify the new behavior. This requires making tests
depend on libwebp to use WebPGetFeatures, since the Skia API does not
provide a way to determine whether an encoded webp file was encoded
lossless-ly or lossily.

Bug: skia:8586
Change-Id: Ie9e09c2f7414ab701d696c4ad9edf405868a716f
Reviewed-on: https://skia-review.googlesource.com/c/175823
Commit-Queue: Leon Scroggins <scroggo@google.com>
Reviewed-by: Derek Sollenberger <djsollen@google.com>
Reviewed-by: Mike Reed <reed@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 5b6f7e2..9d3c5c0 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1560,6 +1560,7 @@
       "modules/skottie:tests",
       "modules/sksg:tests",
       "//third_party/libpng",
+      "//third_party/libwebp",
       "//third_party/zlib",
     ]
     public_deps = [
diff --git a/include/core/SkImageEncoder.h b/include/core/SkImageEncoder.h
index a6d7cb8..b01771a 100644
--- a/include/core/SkImageEncoder.h
+++ b/include/core/SkImageEncoder.h
@@ -27,7 +27,8 @@
  * Will always return false if Skia is compiled without image
  * encoders.
  *
- * Note that webp encodes will use webp lossy compression.
+ * For SkEncodedImageFormat::kWEBP, if quality is 100, it will use lossless compression. Otherwise
+ * it will use lossy.
  *
  * For examples of encoding an image to a file or to a block of memory,
  * see tools/sk_tool_utils.h.
@@ -56,7 +57,8 @@
  * Will always return nullptr if Skia is compiled without image
  * encoders.
  *
- * Note that webp encodes will use webp lossy compression.
+ * For SkEncodedImageFormat::kWEBP, if quality is 100, it will use lossless compression. Otherwise
+ * it will use lossy.
  */
 SK_API sk_sp<SkData> SkEncodePixmap(const SkPixmap& src, SkEncodedImageFormat format, int quality);
 
diff --git a/src/images/SkImageEncoder.cpp b/src/images/SkImageEncoder.cpp
index 946a67f..d5cf2ec 100644
--- a/src/images/SkImageEncoder.cpp
+++ b/src/images/SkImageEncoder.cpp
@@ -48,8 +48,25 @@
             }
             case SkEncodedImageFormat::kWEBP: {
                 SkWebpEncoder::Options opts;
-                opts.fCompression = SkWebpEncoder::Compression::kLossy;
-                opts.fQuality = quality;
+                if (quality == 100) {
+                    opts.fCompression = SkWebpEncoder::Compression::kLossless;
+                    // Note: SkEncodeImage treats 0 quality as the lowest quality
+                    // (greatest compression) and 100 as the highest quality (least
+                    // compression). For kLossy, this matches libwebp's
+                    // interpretation, so it is passed directly to libwebp. But
+                    // with kLossless, libwebp always creates the highest quality
+                    // image. In this case, fQuality is reinterpreted as how much
+                    // effort (time) to put into making a smaller file. This API
+                    // does not provide a way to specify this value (though it can
+                    // be specified by using SkWebpEncoder::Encode) so we have to
+                    // pick one arbitrarily. This value matches that chosen by
+                    // blink::ImageEncoder::ComputeWebpOptions as well
+                    // WebPConfigInit.
+                    opts.fQuality = 75;
+                } else {
+                    opts.fCompression = SkWebpEncoder::Compression::kLossy;
+                    opts.fQuality = quality;
+                }
                 return SkWebpEncoder::Encode(dst, src, opts);
             }
             default:
diff --git a/tests/EncodeTest.cpp b/tests/EncodeTest.cpp
index 30b5302..5dab124 100644
--- a/tests/EncodeTest.cpp
+++ b/tests/EncodeTest.cpp
@@ -23,6 +23,8 @@
 #include <string>
 #include <vector>
 
+#include "webp/decode.h"
+
 static bool encode(SkEncodedImageFormat format, SkWStream* dst, const SkPixmap& src) {
     switch (format) {
         case SkEncodedImageFormat::kJPEG:
@@ -286,6 +288,52 @@
     REPORTER_ASSERT(r, almost_equals(bm0, bm2, 0));
 }
 
+DEF_TEST(Encode_WebpQuality, r) {
+    SkBitmap bm;
+    bm.allocN32Pixels(100, 100);
+    bm.eraseColor(SK_ColorBLUE);
+
+    auto dataLossy    = SkEncodeBitmap(bm, SkEncodedImageFormat::kWEBP, 99);
+    auto dataLossLess = SkEncodeBitmap(bm, SkEncodedImageFormat::kWEBP, 100);
+
+    enum Format {
+        kMixed    = 0,
+        kLossy    = 1,
+        kLossless = 2,
+    };
+
+    auto test = [&r](const sk_sp<SkData>& data, Format expected) {
+        auto printFormat = [](int f) {
+            switch (f) {
+                case kMixed:    return "mixed";
+                case kLossy:    return "lossy";
+                case kLossless: return "lossless";
+                default:        return "unknown";
+            }
+        };
+
+        if (!data) {
+            ERRORF(r, "Failed to encode. Expected %s", printFormat(expected));
+            return;
+        }
+
+        WebPBitstreamFeatures features;
+        auto status = WebPGetFeatures(data->bytes(), data->size(), &features);
+        if (status != VP8_STATUS_OK) {
+            ERRORF(r, "Encode had an error %i. Expected %s", status, printFormat(expected));
+            return;
+        }
+
+        if (expected != features.format) {
+            ERRORF(r, "Expected %s encode, but got format %s", printFormat(expected),
+                                                               printFormat(features.format));
+        }
+    };
+
+    test(dataLossy,    kLossy);
+    test(dataLossLess, kLossless);
+}
+
 DEF_TEST(Encode_WebpOptions, r) {
     SkBitmap bitmap;
     bool success = GetResourceAsBitmap("images/google_chrome.ico", &bitmap);