Add jpeg encoder alpha handling option
authorMatt Sarett <msarett@google.com>
Tue, 9 May 2017 16:46:50 +0000 (12:46 -0400)
committerSkia Commit-Bot <skia-commit-bot@chromium.org>
Tue, 9 May 2017 18:32:04 +0000 (18:32 +0000)
This instructs us on how to encode jpegs when the src
image has alpha.  The original behavior is to ignore
the alpha channel.  This CL adds the option to blend
the pixels onto opaque black.

Note that kBlendOnBlack and kIgnore are identical
unless the input alpha type is kUnpremul.

Bug: 713862
Bug: skia:1501
Change-Id: I4891c70bb0ccd83f7974c359bd40a2143b5c49ac
Reviewed-on: https://skia-review.googlesource.com/15817
Reviewed-by: Leon Scroggins <scroggo@google.com>
Commit-Queue: Matt Sarett <msarett@google.com>

gm/encode-alpha-jpeg.cpp [new file with mode: 0644]
gn/gm.gni
resources/rainbow-gradient.png [new file with mode: 0644]
src/images/SkImageEncoderFns.h
src/images/SkJpegEncoder.cpp
src/images/SkJpegEncoder.h

diff --git a/gm/encode-alpha-jpeg.cpp b/gm/encode-alpha-jpeg.cpp
new file mode 100644 (file)
index 0000000..6686e78
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * 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 "SkImage.h"
+#include "SkJpegEncoder.h"
+
+#include "Resources.h"
+
+namespace skiagm {
+
+static inline void read_into_pixmap(SkPixmap* dst, SkImageInfo dstInfo, void* dstPixels,
+        sk_sp<SkImage> src) {
+    dst->reset(dstInfo, dstPixels, dstInfo.minRowBytes());
+    src->readPixels(*dst, 0, 0, SkImage::CachingHint::kDisallow_CachingHint);
+}
+
+static inline sk_sp<SkImage> encode_pixmap_and_make_image(const SkPixmap& src,
+        SkJpegEncoder::AlphaOption alphaOption, SkTransferFunctionBehavior blendBehavior) {
+    SkDynamicMemoryWStream dst;
+    SkJpegEncoder::Options options;
+    options.fAlphaOption = alphaOption;
+    options.fBlendBehavior = blendBehavior;
+    SkJpegEncoder::Encode(&dst, src, options);
+    return SkImage::MakeFromEncoded(dst.detachAsData());
+}
+
+class EncodeJpegAlphaOptsGM : public GM {
+public:
+    EncodeJpegAlphaOptsGM() {}
+
+protected:
+    SkString onShortName() override {
+        return SkString("encode-alpha-jpeg");
+    }
+
+    SkISize onISize() override {
+        return SkISize::Make(400, 200);
+    }
+
+    void onDraw(SkCanvas* canvas) override {
+        sk_sp<SkImage> srcImg = GetResourceAsImage("rainbow-gradient.png");
+        fStorage.reset(srcImg->width() * srcImg->height() *
+                SkColorTypeBytesPerPixel(kRGBA_F16_SkColorType));
+
+        SkPixmap src;
+        SkImageInfo info = SkImageInfo::MakeN32Premul(srcImg->width(), srcImg->height(),
+                canvas->imageInfo().colorSpace() ? SkColorSpace::MakeSRGB() : nullptr);
+        read_into_pixmap(&src, info, fStorage.get(), srcImg);
+
+        SkTransferFunctionBehavior behavior = canvas->imageInfo().colorSpace() ?
+                SkTransferFunctionBehavior::kRespect : SkTransferFunctionBehavior::kIgnore;
+
+        // Encode 8888 premul.
+        sk_sp<SkImage> img0 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kIgnore,
+                behavior);
+        sk_sp<SkImage> img1 = encode_pixmap_and_make_image(src,
+                SkJpegEncoder::AlphaOption::kBlendOnBlack, behavior);
+        canvas->drawImage(img0, 0.0f, 0.0f);
+        canvas->drawImage(img1, 0.0f, 100.0f);
+
+        // Encode 8888 unpremul
+        info = info.makeAlphaType(kUnpremul_SkAlphaType);
+        read_into_pixmap(&src, info, fStorage.get(), srcImg);
+        img0 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kIgnore, behavior);
+        img1 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kBlendOnBlack,
+                behavior);
+        canvas->drawImage(img0, 100.0f, 0.0f);
+        canvas->drawImage(img1, 100.0f, 100.0f);
+
+        if (canvas->imageInfo().colorSpace()) {
+            // Encode F16 premul
+            info = SkImageInfo::Make(srcImg->width(), srcImg->height(), kRGBA_F16_SkColorType,
+                    kPremul_SkAlphaType, SkColorSpace::MakeSRGBLinear());
+            read_into_pixmap(&src, info, fStorage.get(), srcImg);
+            img0 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kIgnore, behavior);
+            img1 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kBlendOnBlack,
+                    behavior);
+            canvas->drawImage(img0, 200.0f, 0.0f);
+            canvas->drawImage(img1, 200.0f, 100.0f);
+
+            // Encode F16 unpremul
+            info = info.makeAlphaType(kUnpremul_SkAlphaType);
+            read_into_pixmap(&src, info, fStorage.get(), srcImg);
+            img0 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kIgnore, behavior);
+            img1 = encode_pixmap_and_make_image(src, SkJpegEncoder::AlphaOption::kBlendOnBlack,
+                    behavior);
+            canvas->drawImage(img0, 300.0f, 0.0f);
+            canvas->drawImage(img1, 300.0f, 100.0f);
+        }
+    }
+
+private:
+    SkAutoTMalloc<uint8_t> fStorage;
+
+    typedef GM INHERITED;
+};
+
+DEF_GM( return new EncodeJpegAlphaOptsGM; )
+
+};
index ae30ca8..5cfde5f 100644 (file)
--- a/gn/gm.gni
+++ b/gn/gm.gni
@@ -112,6 +112,7 @@ gm_sources = [
   "$_gm/emboss.cpp",
   "$_gm/emptypath.cpp",
   "$_gm/encode.cpp",
+  "$_gm/encode-alpha-jpeg.cpp",
   "$_gm/encode-platform.cpp",
   "$_gm/encode-srgb.cpp",
   "$_gm/etc1.cpp",
diff --git a/resources/rainbow-gradient.png b/resources/rainbow-gradient.png
new file mode 100644 (file)
index 0000000..4e18a32
Binary files /dev/null and b/resources/rainbow-gradient.png differ
index 5120570..af75ac9 100644 (file)
@@ -16,6 +16,7 @@
 #include "SkColor.h"
 #include "SkColorPriv.h"
 #include "SkICC.h"
+#include "SkOpts.h"
 #include "SkPreConfig.h"
 #include "SkRasterPipeline.h"
 #include "SkUnPreMultiply.h"
@@ -165,6 +166,30 @@ static inline void transform_scanline_unpremultiply_sRGB(void* dst, const void*
 }
 
 /**
+ * Premultiply RGBA to rgbA.
+ */
+static inline void transform_scanline_to_premul_legacy(char* SK_RESTRICT dst,
+                                                       const char* SK_RESTRICT src,
+                                                       int width, int, const SkPMColor*) {
+    SkOpts::RGBA_to_rgbA((uint32_t*)dst, (const uint32_t*)src, width);
+}
+
+/**
+ * Premultiply RGBA to rgbA linearly.
+ */
+static inline void transform_scanline_to_premul_linear(char* SK_RESTRICT dst,
+                                                       const char* SK_RESTRICT src,
+                                                       int width, int, const SkPMColor*) {
+    SkRasterPipeline p;
+    p.append(SkRasterPipeline::load_8888, (const void**) &src);
+    p.append_from_srgb(kUnpremul_SkAlphaType);
+    p.append(SkRasterPipeline::premul);
+    p.append(SkRasterPipeline::to_srgb);
+    p.append(SkRasterPipeline::store_8888, (void**) &dst);
+    p.run(0, width);
+}
+
+/**
  * Transform from kPremul, kRGBA_8888_SkColorType to 4-bytes-per-pixel unpremultiplied RGBA.
  */
 static inline void transform_scanline_srgbA(char* SK_RESTRICT dst, const char* SK_RESTRICT src,
@@ -276,6 +301,19 @@ static inline void transform_scanline_F16_premul_to_8888(char* SK_RESTRICT dst,
     p.run(0, width);
 }
 
+/**
+ * Transform from kUnpremul, kRGBA_F16 to premultiplied rgbA 8888.
+ */
+static inline void transform_scanline_F16_to_premul_8888(char* SK_RESTRICT dst,
+        const char* SK_RESTRICT src, int width, int, const SkPMColor*) {
+    SkRasterPipeline p;
+    p.append(SkRasterPipeline::load_f16, (const void**) &src);
+    p.append(SkRasterPipeline::premul);
+    p.append(SkRasterPipeline::to_srgb);
+    p.append(SkRasterPipeline::store_8888, (void**) &dst);
+    p.run(0, width);
+}
+
 static inline sk_sp<SkData> icc_from_color_space(const SkColorSpace& cs) {
     SkColorSpaceTransferFn fn;
     SkMatrix44 toXYZD50(SkMatrix44::kUninitialized_Constructor);
index fd88a5c..d87fed8 100644 (file)
@@ -35,7 +35,7 @@ public:
         return std::unique_ptr<SkJpegEncoderMgr>(new SkJpegEncoderMgr(stream));
     }
 
-    bool setParams(const SkImageInfo& srcInfo);
+    bool setParams(const SkImageInfo& srcInfo, const SkJpegEncoder::Options& options);
 
     jpeg_compress_struct* cinfo() { return &fCInfo; }
 
@@ -65,15 +65,36 @@ private:
     transform_scanline_proc fProc;
 };
 
-bool SkJpegEncoderMgr::setParams(const SkImageInfo& srcInfo) {
+bool SkJpegEncoderMgr::setParams(const SkImageInfo& srcInfo, const SkJpegEncoder::Options& options)
+{
+    auto chooseProc8888 = [&]() {
+        if (kUnpremul_SkAlphaType != srcInfo.alphaType() ||
+            SkJpegEncoder::AlphaOption::kIgnore == options.fAlphaOption)
+        {
+            return (transform_scanline_proc) nullptr;
+        }
+
+        // Note that kRespect mode is only supported with sRGB or linear transfer functions.
+        // The legacy code path is incidentally correct when the transfer function is linear.
+        const bool isSRGBTransferFn = srcInfo.gammaCloseToSRGB() &&
+                (SkTransferFunctionBehavior::kRespect == options.fBlendBehavior);
+        if (isSRGBTransferFn) {
+            return transform_scanline_to_premul_linear;
+        } else {
+            return transform_scanline_to_premul_legacy;
+        }
+    };
+
     J_COLOR_SPACE jpegColorType = JCS_EXT_RGBA;
     int numComponents = 0;
     switch (srcInfo.colorType()) {
         case kRGBA_8888_SkColorType:
+            fProc = chooseProc8888();
             jpegColorType = JCS_EXT_RGBA;
             numComponents = 4;
             break;
         case kBGRA_8888_SkColorType:
+            fProc = chooseProc8888();
             jpegColorType = JCS_EXT_BGRA;
             numComponents = 4;
             break;
@@ -83,11 +104,19 @@ bool SkJpegEncoderMgr::setParams(const SkImageInfo& srcInfo) {
             numComponents = 3;
             break;
         case kARGB_4444_SkColorType:
+            if (SkJpegEncoder::AlphaOption::kBlendOnBlack == options.fAlphaOption) {
+                return false;
+            }
+
             fProc = transform_scanline_444;
             jpegColorType = JCS_RGB;
             numComponents = 3;
             break;
         case kIndex_8_SkColorType:
+            if (SkJpegEncoder::AlphaOption::kBlendOnBlack == options.fAlphaOption) {
+                return false;
+            }
+
             fProc = transform_scanline_index8_opaque;
             jpegColorType = JCS_RGB;
             numComponents = 3;
@@ -98,11 +127,18 @@ bool SkJpegEncoderMgr::setParams(const SkImageInfo& srcInfo) {
             numComponents = 1;
             break;
         case kRGBA_F16_SkColorType:
-            if (!srcInfo.colorSpace() || !srcInfo.colorSpace()->gammaIsLinear()) {
+            if (!srcInfo.colorSpace() || !srcInfo.colorSpace()->gammaIsLinear() ||
+                    SkTransferFunctionBehavior::kRespect != options.fBlendBehavior) {
                 return false;
             }
 
-            fProc = transform_scanline_F16_to_8888;
+            if (kUnpremul_SkAlphaType != srcInfo.alphaType() ||
+                SkJpegEncoder::AlphaOption::kIgnore == options.fAlphaOption)
+            {
+                fProc = transform_scanline_F16_to_8888;
+            } else {
+                fProc = transform_scanline_F16_to_premul_8888;
+            }
             jpegColorType = JCS_EXT_RGBA;
             numComponents = 4;
             break;
@@ -125,7 +161,7 @@ bool SkJpegEncoderMgr::setParams(const SkImageInfo& srcInfo) {
 
 std::unique_ptr<SkJpegEncoder> SkJpegEncoder::Make(SkWStream* dst, const SkPixmap& src,
                                                    const Options& options) {
-    if (!SkPixmapIsValid(src, SkTransferFunctionBehavior::kIgnore)) {
+    if (!SkPixmapIsValid(src, options.fBlendBehavior)) {
         return nullptr;
     }
 
@@ -134,7 +170,7 @@ std::unique_ptr<SkJpegEncoder> SkJpegEncoder::Make(SkWStream* dst, const SkPixma
         return nullptr;
     }
 
-    if (!encoderMgr->setParams(src.info())) {
+    if (!encoderMgr->setParams(src.info(), options)) {
         return nullptr;
     }
 
index 49b4978..07290ae 100644 (file)
@@ -16,17 +16,27 @@ class SkWStream;
 class SkJpegEncoder : public SkEncoder {
 public:
 
-    // TODO (skbug.com/1501):
-    // Since jpegs are always opaque, this encoder ignores the alpha channel and treats the
-    // pixels as opaque.
-    // Another possible behavior is to blend the pixels onto opaque black.  We'll need to add
-    // an option for this - and an SkTransferFunctionBehavior.
+    enum class AlphaOption {
+        kIgnore,
+        kBlendOnBlack,
+    };
 
     struct Options {
         /**
-         * |fQuality| must be in [0, 100] where 0 corresponds to the lowest quality.
+         *  |fQuality| must be in [0, 100] where 0 corresponds to the lowest quality.
          */
         int fQuality = 100;
+
+        /**
+         *  Jpegs must be opaque.  This instructs the encoder on how to handle input
+         *  images with alpha.
+         *
+         *  The default is to ignore the alpha channel and treat the image as opaque.
+         *  Another option is to blend the pixels onto a black background before encoding.
+         *  In the second case, the encoder supports linear or legacy blending.
+         */
+        AlphaOption fAlphaOption = AlphaOption::kIgnore;
+        SkTransferFunctionBehavior fBlendBehavior = SkTransferFunctionBehavior::kRespect;
     };
 
     /**