chromium/ash/public/cpp/image_util_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/public/cpp/image_util.h"

#include <string>

#include "ash/public/cpp/image_util.h"
#include "ash/public/cpp/test/in_process_data_decoder.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/location.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "services/data_decoder/public/mojom/image_decoder.mojom-shared.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkRect.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/codec/jpeg_codec.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/skia_util.h"

namespace ash {
namespace {

// How long to wait for a DecodeImageCallback to be run before a test times
// out and fails. Prevents the unit tests from hanging indefinitely (or for a
// very long time) if there's a bug in the code where the callback is not being
// run.
constexpr base::TimeDelta kDecodeImageTimeout = base::Seconds(3);

using EncodingFunction =
    base::RepeatingCallback<bool(const SkBitmap&, std::vector<unsigned char>*)>;

void EncodeImage(const gfx::ImageSkia& image,
                 std::string* output,
                 EncodingFunction encoding_fn) {
  std::vector<unsigned char> encoded_data;
  ASSERT_TRUE(encoding_fn.Run(*image.bitmap(), &encoded_data));
  output->assign(reinterpret_cast<const char*>(encoded_data.data()),
                 encoded_data.size());
}

std::string EncodeToString(const gfx::ImageSkia& image,
                           EncodingFunction encoding_fn) {
  std::string output;
  EncodeImage(image, &output, encoding_fn);
  return output;
}

std::string EncodeAsJpeg(const gfx::ImageSkia& image) {
  return EncodeToString(
      image, base::BindRepeating([](const SkBitmap& bitmap,
                                    std::vector<unsigned char>* encoded_data) {
        return gfx::JPEGCodec::Encode(bitmap, /*quality=*/90, encoded_data);
      }));
}

std::string EncodeAsPng(const gfx::ImageSkia& image) {
  return EncodeToString(
      image, base::BindRepeating([](const SkBitmap& bitmap,
                                    std::vector<unsigned char>* encoded_data) {
        return gfx::PNGCodec::EncodeBGRASkBitmap(
            bitmap, /*discard_transparency=*/false, encoded_data);
      }));
}

// `BorderedImageSource` can be used to generate a test image in conjunction
// with `gfx::CanvasImageSource::MakeImageSkia`. Images will be of the given
// size, with one color in the center and borders in a different color, as
// indicated by `borders`.
class BorderedImageSource : public gfx::CanvasImageSource {
 public:
  const SkColor kCenterColor = SK_ColorWHITE;
  const SkColor kBorderColor = SK_ColorBLACK;

  BorderedImageSource(const gfx::Size& size, const gfx::Insets& borders)
      : gfx::CanvasImageSource(size), borders_(borders) {}

  void Draw(gfx::Canvas* canvas) override {
    canvas->DrawColor(kBorderColor);
    gfx::Rect center(size());
    center.Inset(borders_);
    canvas->FillRect(center, kCenterColor);
  }

 private:
  const gfx::Insets borders_;
};

}  // namespace

class ImageUtilTest : public ::testing::Test {
 protected:
  void SetUp() override { ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir()); }

  base::FilePath CreateFilePath(base::FilePath::StringPieceType file_name) {
    return scoped_temp_dir_.GetPath().Append(file_name);
  }

  image_util::DecodeImageCallback CreateDecodeImageCallback(
      base::RunLoop& run_loop,
      gfx::ImageSkia& image_out) {
    return base::BindLambdaForTesting(
        [&image_out, &run_loop](const gfx::ImageSkia& result) {
          image_out = result;
          run_loop.Quit();
        });
  }

  gfx::ImageSkia DecodeImageFile(
      base::FilePath::StringPieceType file_name,
      data_decoder::mojom::ImageCodec codec,
      scoped_refptr<base::SequencedTaskRunner> file_task_runner = nullptr) {
    base::test::ScopedRunLoopTimeout timeout(FROM_HERE, kDecodeImageTimeout);
    base::RunLoop run_loop;
    gfx::ImageSkia image_out;
    image_util::DecodeImageFile(CreateDecodeImageCallback(run_loop, image_out),
                                CreateFilePath(file_name), codec,
                                std::move(file_task_runner));
    run_loop.Run();
    return image_out;
  }

  gfx::ImageSkia DecodeImageData(data_decoder::mojom::ImageCodec codec,
                                 const std::string& data) {
    base::test::ScopedRunLoopTimeout timeout(FROM_HERE, kDecodeImageTimeout);
    base::RunLoop run_loop;
    gfx::ImageSkia image_out;
    image_util::DecodeImageData(CreateDecodeImageCallback(run_loop, image_out),
                                codec, data);
    run_loop.Run();
    return image_out;
  }

  base::test::TaskEnvironment task_environment_;
  InProcessDataDecoder decoder_;
  base::ScopedTempDir scoped_temp_dir_;
};

TEST_F(ImageUtilTest, DecodeImageDataDefaultCodec) {
  gfx::ImageSkia original_image =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  gfx::ImageSkia decoded_image = DecodeImageData(
      data_decoder::mojom::ImageCodec::kDefault, EncodeAsJpeg(original_image));
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));
}

TEST_F(ImageUtilTest, DecodeImageDataPng) {
  gfx::ImageSkia original_image =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  gfx::ImageSkia decoded_image = DecodeImageData(
      data_decoder::mojom::ImageCodec::kPng, EncodeAsPng(original_image));
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));
}

TEST_F(ImageUtilTest, DecodeImageDataFailsForInvalidData) {
  gfx::ImageSkia decoded_image =
      DecodeImageData(data_decoder::mojom::ImageCodec::kDefault, "gibberish");
  EXPECT_TRUE(decoded_image.isNull());
}

TEST_F(ImageUtilTest, DecodeImageFileDefaultCodec) {
  gfx::ImageSkia original_image =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image.jpg"),
                              EncodeAsJpeg(original_image)));
  gfx::ImageSkia decoded_image = DecodeImageFile(
      "test_image.jpg", data_decoder::mojom::ImageCodec::kDefault);
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));
}

TEST_F(ImageUtilTest, DecodeImageFileCustomTaskRunner) {
  gfx::ImageSkia original_image =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image.jpg"),
                              EncodeAsJpeg(original_image)));
  gfx::ImageSkia decoded_image = DecodeImageFile(
      "test_image.jpg", data_decoder::mojom::ImageCodec::kDefault,
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}));
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));
}

TEST_F(ImageUtilTest, DecodeImageFilePng) {
  gfx::ImageSkia original_image =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image.png"),
                              EncodeAsPng(original_image)));
  gfx::ImageSkia decoded_image =
      DecodeImageFile("test_image.png", data_decoder::mojom::ImageCodec::kPng);
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));
}

TEST_F(ImageUtilTest, DecodeImageFileFailsForInvalidData) {
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image.jpg"), "gibberish"));
  gfx::ImageSkia decoded_image = DecodeImageFile(
      "test_image.jpg", data_decoder::mojom::ImageCodec::kDefault);
}

TEST_F(ImageUtilTest, DecodeImageFileFailsForMissingFile) {
  gfx::ImageSkia decoded_image = DecodeImageFile(
      "test_image.jpg", data_decoder::mojom::ImageCodec::kDefault);
  EXPECT_TRUE(decoded_image.isNull());
}

TEST_F(ImageUtilTest, DecodeImageFileMultipleFiles) {
  gfx::ImageSkia original_image_1 =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  gfx::ImageSkia original_image_2 =
      gfx::test::CreateImageSkia(100, 200, SK_ColorCYAN);
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image_1.jpg"),
                              EncodeAsJpeg(original_image_1)));
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image_2.jpg"),
                              EncodeAsJpeg(original_image_2)));
  gfx::ImageSkia decoded_image_1 = DecodeImageFile(
      "test_image_1.jpg", data_decoder::mojom::ImageCodec::kDefault);
  gfx::ImageSkia decoded_image_2 = DecodeImageFile(
      "test_image_2.jpg", data_decoder::mojom::ImageCodec::kDefault);
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image_1),
                                        gfx::Image(original_image_1)));
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image_2),
                                        gfx::Image(original_image_2)));
}

TEST_F(ImageUtilTest, DecodeImageFileSameFileMultipleTimes) {
  gfx::ImageSkia original_image =
      gfx::test::CreateImageSkia(200, 100, SK_ColorYELLOW);
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image.jpg"),
                              EncodeAsJpeg(original_image)));
  gfx::ImageSkia decoded_image = DecodeImageFile(
      "test_image.jpg", data_decoder::mojom::ImageCodec::kDefault);
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));

  original_image = gfx::test::CreateImageSkia(100, 200, SK_ColorCYAN);
  ASSERT_TRUE(base::WriteFile(CreateFilePath("test_image.jpg"),
                              EncodeAsJpeg(original_image)));
  decoded_image = DecodeImageFile("test_image.jpg",
                                  data_decoder::mojom::ImageCodec::kDefault);
  EXPECT_TRUE(gfx::test::AreImagesEqual(gfx::Image(decoded_image),
                                        gfx::Image(original_image)));
}

struct ResizeAndCropTestCase {
  gfx::Size target_size;
  gfx::Insets expected_borders;
};

using ResizeAndCropTest = testing::TestWithParam<ResizeAndCropTestCase>;

TEST_P(ResizeAndCropTest, ResizeAndCropImage) {
  const auto& test_case = GetParam();

  auto expected_image =
      gfx::CanvasImageSource::MakeImageSkia<BorderedImageSource>(
          test_case.target_size, test_case.expected_borders);

  auto original_image =
      gfx::CanvasImageSource::MakeImageSkia<BorderedImageSource>(
          gfx::Size(100, 100), /*borders=*/gfx::Insets(10));

  // The deviation tolerance for the images has to be relatively high here,
  // because the test resizes images with sharp edges and opposite colors, which
  // can create significant artifacts. This is more than ok, since an incorrect
  // pixel will be an opposite color.
  EXPECT_TRUE(gfx::test::AreImagesClose(
      gfx::Image(image_util::ResizeAndCropImage(original_image,
                                                test_case.target_size)),
      gfx::Image(expected_image), /*max_deviation=*/20));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    ResizeAndCropTest,
    testing::ValuesIn<ResizeAndCropTestCase>({
        // Case: Shrink, no crop.
        {gfx::Size(80, 80), gfx::Insets(8)},
        // Case: Enlarge, no crop.
        {gfx::Size(110, 110), gfx::Insets(11)},
        // Case: No scaling, vertical crop.
        {gfx::Size(100, 80), gfx::Insets::VH(0, 10)},
        // Case: No scaling, horizontal crop.
        {gfx::Size(20, 100), gfx::Insets::VH(10, 0)},
        // Case: No scaling, partial vertical crop.
        {gfx::Size(100, 90), gfx::Insets::VH(5, 10)},
        // Case: Shrink, vertical crop.
        {gfx::Size(90, 70), gfx::Insets::VH(0, 9)},
        // Case: Enlarge, partial horizontal crop, with some border showing.
        {gfx::Size(100, 110), gfx::Insets::VH(11, 6)},
        // Case: No scaling, extreme horizontal crop.
        {gfx::Size(1, 100), gfx::Insets::VH(10, 0)},
    }));

}  // namespace ash