| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/gfx/icon_util.h" |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/path_service.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/icon_util_unittests_resource.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/image/image_family.h" |
| |
| namespace { |
| |
| static const char kSmallIconName[] = "16_X_16_icon.ico"; |
| static const char kLargeIconName[] = "128_X_128_icon.ico"; |
| static const char kTempIconFilename[] = "temp_test_icon.ico"; |
| |
| } // namespace |
| |
| class IconUtilTest : public testing::Test { |
| public: |
| using ScopedHICON = base::win::ScopedHICON; |
| |
| void SetUp() override { |
| ASSERT_TRUE(base::PathService::Get(base::DIR_SOURCE_ROOT, &test_data_dir_)); |
| test_data_dir_ = test_data_dir_.Append(FILE_PATH_LITERAL("ui")) |
| .Append(FILE_PATH_LITERAL("gfx")) |
| .Append(FILE_PATH_LITERAL("test")) |
| .Append(FILE_PATH_LITERAL("data")) |
| .Append(FILE_PATH_LITERAL("icon_util")); |
| ASSERT_TRUE(base::PathExists(test_data_dir_)); |
| |
| ASSERT_TRUE(temp_directory_.CreateUniqueTempDir()); |
| } |
| |
| static const int kSmallIconWidth = 16; |
| static const int kSmallIconHeight = 16; |
| static const int kLargeIconWidth = 128; |
| static const int kLargeIconHeight = 128; |
| |
| // Given a file name for an .ico file and an image dimensions, this |
| // function loads the icon and returns an HICON handle. |
| ScopedHICON LoadIconFromFile(const base::FilePath& filename, |
| int width, |
| int height) { |
| HICON icon = static_cast<HICON>(LoadImage(NULL, |
| filename.value().c_str(), |
| IMAGE_ICON, |
| width, |
| height, |
| LR_LOADTRANSPARENT | LR_LOADFROMFILE)); |
| return ScopedHICON(icon); |
| } |
| |
| SkBitmap CreateBlackSkBitmap(int width, int height) { |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(width, height); |
| // Setting the pixels to transparent-black. |
| memset(bitmap.getPixels(), 0, width * height * 4); |
| return bitmap; |
| } |
| |
| // Loads an .ico file from |icon_filename| and asserts that it contains all of |
| // the expected icon sizes up to and including |max_icon_size|, and no other |
| // icons. If |max_icon_size| >= 256, this tests for a 256x256 PNG icon entry. |
| void CheckAllIconSizes(const base::FilePath& icon_filename, |
| int max_icon_size); |
| |
| protected: |
| // The root directory for test files. This should be treated as read-only. |
| base::FilePath test_data_dir_; |
| |
| // Directory for creating files by this test. |
| base::ScopedTempDir temp_directory_; |
| }; |
| |
| void IconUtilTest::CheckAllIconSizes(const base::FilePath& icon_filename, |
| int max_icon_size) { |
| ASSERT_TRUE(base::PathExists(icon_filename)); |
| |
| // Determine how many icons to expect, based on |max_icon_size|. |
| int expected_num_icons = 0; |
| for (size_t i = 0; i < IconUtil::kNumIconDimensions; ++i) { |
| if (IconUtil::kIconDimensions[i] > max_icon_size) |
| break; |
| ++expected_num_icons; |
| } |
| |
| // First, use the Windows API to load the icon, a basic validity test. |
| EXPECT_TRUE(LoadIconFromFile(icon_filename, kSmallIconWidth, kSmallIconHeight) |
| .is_valid()); |
| |
| // Read the file completely into memory. |
| std::string icon_data; |
| ASSERT_TRUE(base::ReadFileToString(icon_filename, &icon_data)); |
| ASSERT_GE(icon_data.length(), sizeof(IconUtil::ICONDIR)); |
| |
| // Ensure that it has exactly the expected number and sizes of icons, in the |
| // expected order. This matches each entry of the loaded file's icon directory |
| // with the corresponding element of kIconDimensions. |
| // Also extracts the 256x256 entry as png_entry. |
| const IconUtil::ICONDIR* icon_dir = |
| reinterpret_cast<const IconUtil::ICONDIR*>(icon_data.data()); |
| EXPECT_EQ(expected_num_icons, icon_dir->idCount); |
| ASSERT_GE(IconUtil::kNumIconDimensions, icon_dir->idCount); |
| ASSERT_GE(icon_data.length(), |
| sizeof(IconUtil::ICONDIR) + |
| icon_dir->idCount * sizeof(IconUtil::ICONDIRENTRY)); |
| const IconUtil::ICONDIRENTRY* png_entry = NULL; |
| for (size_t i = 0; i < icon_dir->idCount; ++i) { |
| const IconUtil::ICONDIRENTRY* entry = &icon_dir->idEntries[i]; |
| // Mod 256 because as a special case in ICONDIRENTRY, the value 0 represents |
| // a width or height of 256. |
| int expected_size = IconUtil::kIconDimensions[i] % 256; |
| EXPECT_EQ(expected_size, static_cast<int>(entry->bWidth)); |
| EXPECT_EQ(expected_size, static_cast<int>(entry->bHeight)); |
| if (entry->bWidth == 0 && entry->bHeight == 0) { |
| EXPECT_EQ(NULL, png_entry); |
| png_entry = entry; |
| } |
| } |
| |
| if (max_icon_size >= 256) { |
| ASSERT_TRUE(png_entry); |
| |
| // Convert the PNG entry data back to a SkBitmap to ensure it's valid. |
| ASSERT_GE(icon_data.length(), |
| png_entry->dwImageOffset + png_entry->dwBytesInRes); |
| const unsigned char* png_bytes = reinterpret_cast<const unsigned char*>( |
| icon_data.data() + png_entry->dwImageOffset); |
| gfx::Image image = gfx::Image::CreateFrom1xPNGBytes( |
| png_bytes, png_entry->dwBytesInRes); |
| SkBitmap bitmap = image.AsBitmap(); |
| EXPECT_EQ(256, bitmap.width()); |
| EXPECT_EQ(256, bitmap.height()); |
| } |
| } |
| |
| // The following test case makes sure IconUtil::SkBitmapFromHICON fails |
| // gracefully when called with invalid input parameters. |
| TEST_F(IconUtilTest, TestIconToBitmapInvalidParameters) { |
| base::FilePath icon_filename = test_data_dir_.AppendASCII(kSmallIconName); |
| gfx::Size icon_size(kSmallIconWidth, kSmallIconHeight); |
| ScopedHICON icon( |
| LoadIconFromFile(icon_filename, icon_size.width(), icon_size.height())); |
| ASSERT_TRUE(icon.is_valid()); |
| |
| // Invalid size parameter. |
| gfx::Size invalid_icon_size(kSmallIconHeight, 0); |
| EXPECT_TRUE(IconUtil::CreateSkBitmapFromHICON(icon.get(), invalid_icon_size) |
| .isNull()); |
| |
| // Invalid icon. |
| EXPECT_TRUE(IconUtil::CreateSkBitmapFromHICON(nullptr, icon_size).isNull()); |
| |
| // The following code should succeed. |
| EXPECT_FALSE( |
| IconUtil::CreateSkBitmapFromHICON(icon.get(), icon_size).drawsNothing()); |
| } |
| |
| // The following test case makes sure IconUtil::CreateHICONFromSkBitmap fails |
| // gracefully when called with invalid input parameters. |
| TEST_F(IconUtilTest, TestBitmapToIconInvalidParameters) { |
| ScopedHICON icon; |
| std::unique_ptr<SkBitmap> bitmap; |
| |
| // Wrong bitmap format. |
| bitmap = std::make_unique<SkBitmap>(); |
| ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL)); |
| bitmap->setInfo(SkImageInfo::MakeA8(kSmallIconWidth, kSmallIconHeight)); |
| icon = IconUtil::CreateHICONFromSkBitmap(*bitmap); |
| EXPECT_FALSE(icon.is_valid()); |
| |
| // Invalid bitmap size. |
| bitmap = std::make_unique<SkBitmap>(); |
| ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL)); |
| bitmap->setInfo(SkImageInfo::MakeN32Premul(0, 0)); |
| icon = IconUtil::CreateHICONFromSkBitmap(*bitmap); |
| EXPECT_FALSE(icon.is_valid()); |
| |
| // Valid bitmap configuration but no pixels allocated. |
| bitmap = std::make_unique<SkBitmap>(); |
| ASSERT_NE(bitmap.get(), static_cast<SkBitmap*>(NULL)); |
| bitmap->setInfo(SkImageInfo::MakeN32Premul(kSmallIconWidth, |
| kSmallIconHeight)); |
| icon = IconUtil::CreateHICONFromSkBitmap(*bitmap); |
| EXPECT_FALSE(icon.is_valid()); |
| } |
| |
| // The following test case makes sure IconUtil::CreateIconFileFromImageFamily |
| // fails gracefully when called with invalid input parameters. |
| TEST_F(IconUtilTest, TestCreateIconFileInvalidParameters) { |
| gfx::ImageFamily image_family; |
| base::FilePath valid_icon_filename = |
| temp_directory_.GetPath().AppendASCII(kTempIconFilename); |
| base::FilePath invalid_icon_filename = |
| temp_directory_.GetPath().AppendASCII("<>?.ico"); |
| |
| // Invalid file name. |
| SkBitmap bitmap; |
| image_family.clear(); |
| bitmap.allocN32Pixels(1, 1); |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(bitmap)); |
| EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| invalid_icon_filename)); |
| EXPECT_FALSE(base::PathExists(invalid_icon_filename)); |
| } |
| |
| // This test case makes sure IconUtil::CreateIconFileFromImageFamily fails if |
| // the image family is empty or invalid. |
| TEST_F(IconUtilTest, TestCreateIconFileEmptyImageFamily) { |
| base::FilePath icon_filename = |
| temp_directory_.GetPath().AppendASCII(kTempIconFilename); |
| |
| // Empty image family. |
| EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(gfx::ImageFamily(), |
| icon_filename)); |
| EXPECT_FALSE(base::PathExists(icon_filename)); |
| |
| // Image family with only an empty image. |
| gfx::ImageFamily image_family; |
| image_family.Add(gfx::Image()); |
| EXPECT_FALSE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| EXPECT_FALSE(base::PathExists(icon_filename)); |
| } |
| |
| // This test case makes sure that when we load an icon from disk and convert |
| // the HICON into a bitmap, the bitmap has the expected format and dimensions. |
| TEST_F(IconUtilTest, TestCreateSkBitmapFromHICON) { |
| base::FilePath small_icon_filename = |
| test_data_dir_.AppendASCII(kSmallIconName); |
| gfx::Size small_icon_size(kSmallIconWidth, kSmallIconHeight); |
| ScopedHICON small_icon(LoadIconFromFile( |
| small_icon_filename, small_icon_size.width(), small_icon_size.height())); |
| ASSERT_TRUE(small_icon.is_valid()); |
| SkBitmap bitmap = |
| IconUtil::CreateSkBitmapFromHICON(small_icon.get(), small_icon_size); |
| ASSERT_FALSE(bitmap.isNull()); |
| EXPECT_EQ(bitmap.width(), small_icon_size.width()); |
| EXPECT_EQ(bitmap.height(), small_icon_size.height()); |
| EXPECT_EQ(bitmap.colorType(), kN32_SkColorType); |
| |
| base::FilePath large_icon_filename = |
| test_data_dir_.AppendASCII(kLargeIconName); |
| gfx::Size large_icon_size(kLargeIconWidth, kLargeIconHeight); |
| ScopedHICON large_icon(LoadIconFromFile( |
| large_icon_filename, large_icon_size.width(), large_icon_size.height())); |
| ASSERT_TRUE(large_icon.is_valid()); |
| bitmap = IconUtil::CreateSkBitmapFromHICON(large_icon.get(), large_icon_size); |
| ASSERT_FALSE(bitmap.isNull()); |
| EXPECT_EQ(bitmap.width(), large_icon_size.width()); |
| EXPECT_EQ(bitmap.height(), large_icon_size.height()); |
| EXPECT_EQ(bitmap.colorType(), kN32_SkColorType); |
| } |
| |
| // This test case makes sure that when an HICON is created from an SkBitmap, |
| // the returned handle is valid and refers to an icon with the expected |
| // dimensions color depth etc. |
| TEST_F(IconUtilTest, TestBasicCreateHICONFromSkBitmap) { |
| SkBitmap bitmap = CreateBlackSkBitmap(kSmallIconWidth, kSmallIconHeight); |
| ScopedHICON icon(IconUtil::CreateHICONFromSkBitmap(bitmap)); |
| EXPECT_TRUE(icon.is_valid()); |
| ICONINFO icon_info; |
| ASSERT_TRUE(GetIconInfo(icon.get(), &icon_info)); |
| EXPECT_TRUE(icon_info.fIcon); |
| |
| // Now that have the icon information, we should obtain the specification of |
| // the icon's bitmap and make sure it matches the specification of the |
| // SkBitmap we started with. |
| // |
| // The bitmap handle contained in the icon information is a handle to a |
| // compatible bitmap so we need to call ::GetDIBits() in order to retrieve |
| // the bitmap's header information. |
| BITMAPINFO bitmap_info; |
| ::ZeroMemory(&bitmap_info, sizeof(BITMAPINFO)); |
| bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFO); |
| HDC hdc = ::GetDC(NULL); |
| int result = ::GetDIBits(hdc, |
| icon_info.hbmColor, |
| 0, |
| kSmallIconWidth, |
| NULL, |
| &bitmap_info, |
| DIB_RGB_COLORS); |
| ASSERT_GT(result, 0); |
| EXPECT_EQ(bitmap_info.bmiHeader.biWidth, kSmallIconWidth); |
| EXPECT_EQ(bitmap_info.bmiHeader.biHeight, kSmallIconHeight); |
| EXPECT_EQ(bitmap_info.bmiHeader.biPlanes, 1); |
| EXPECT_EQ(bitmap_info.bmiHeader.biBitCount, 32); |
| ::ReleaseDC(NULL, hdc); |
| } |
| |
| // This test case makes sure that CreateIconFileFromImageFamily creates a |
| // valid .ico file given an ImageFamily, and appropriately creates all icon |
| // sizes from the given input. |
| TEST_F(IconUtilTest, TestCreateIconFileFromImageFamily) { |
| gfx::ImageFamily image_family; |
| base::FilePath icon_filename = |
| temp_directory_.GetPath().AppendASCII(kTempIconFilename); |
| |
| // Test with only a 16x16 icon. Should only scale up to 48x48. |
| image_family.Add(gfx::Image::CreateFrom1xBitmap( |
| CreateBlackSkBitmap(kSmallIconWidth, kSmallIconHeight))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 48); |
| |
| // Test with a 48x48 icon. Should only scale down. |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(48, 48))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 48); |
| |
| // Test with a 64x64 icon. Should scale up to 256x256. |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(64, 64))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 256); |
| |
| // Test with a 256x256 icon. Should include the 256x256 in the output. |
| image_family.Add(gfx::Image::CreateFrom1xBitmap( |
| CreateBlackSkBitmap(256, 256))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 256); |
| |
| // Test with a 49x49 icon. Should scale up to 256x256, but exclude the |
| // original 49x49 representation from the output. |
| image_family.clear(); |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(49, 49))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 256); |
| |
| // Test with a non-square 16x32 icon. Should scale up to 48, but exclude the |
| // original 16x32 representation from the output. |
| image_family.clear(); |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(16, 32))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 48); |
| |
| // Test with a non-square 32x49 icon. Should scale up to 256, but exclude the |
| // original 32x49 representation from the output. |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(32, 49))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 256); |
| |
| // Test with an empty and non-empty image. |
| // The empty image should be ignored. |
| image_family.clear(); |
| image_family.Add(gfx::Image()); |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(CreateBlackSkBitmap(16, 16))); |
| ASSERT_TRUE(IconUtil::CreateIconFileFromImageFamily(image_family, |
| icon_filename)); |
| CheckAllIconSizes(icon_filename, 48); |
| } |
| |
| TEST_F(IconUtilTest, TestCreateImageFamilyFromIconResource) { |
| HMODULE module = GetModuleHandle(NULL); |
| std::unique_ptr<gfx::ImageFamily> family( |
| IconUtil::CreateImageFamilyFromIconResource(module, IDR_MAINFRAME)); |
| ASSERT_TRUE(family.get()); |
| EXPECT_FALSE(family->empty()); |
| std::vector<gfx::Image> images; |
| for (const auto& image : *family) |
| images.push_back(image); |
| |
| // Assert that the family contains all of the images from the icon resource. |
| EXPECT_EQ(5u, images.size()); |
| EXPECT_EQ(16, images[0].Width()); |
| EXPECT_EQ(24, images[1].Width()); |
| EXPECT_EQ(32, images[2].Width()); |
| EXPECT_EQ(48, images[3].Width()); |
| EXPECT_EQ(256, images[4].Width()); |
| } |
| |
| // This tests that kNumIconDimensionsUpToMediumSize has the correct value. |
| TEST_F(IconUtilTest, TestNumIconDimensionsUpToMediumSize) { |
| ASSERT_LE(IconUtil::kNumIconDimensionsUpToMediumSize, |
| IconUtil::kNumIconDimensions); |
| EXPECT_EQ(IconUtil::kMediumIconSize, |
| IconUtil::kIconDimensions[ |
| IconUtil::kNumIconDimensionsUpToMediumSize - 1]); |
| } |
| |
| TEST_F(IconUtilTest, TestTransparentIcon) { |
| base::FilePath icon_filename = |
| temp_directory_.GetPath().AppendASCII(kTempIconFilename); |
| int size = 48; |
| auto semi_transparent_red = SkColorSetARGB(0x77, 0xFF, 0x00, 0x00); |
| |
| // Create a bitmap with a semi transparent red dot. |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(size, size, false); |
| EXPECT_EQ(bitmap.alphaType(), kPremul_SkAlphaType); |
| { |
| SkCanvas canvas(bitmap, SkSurfaceProps{}); |
| canvas.drawColor(SK_ColorWHITE); |
| SkPaint paint; |
| paint.setColor(semi_transparent_red); |
| paint.setBlendMode(SkBlendMode::kSrc); |
| canvas.drawPoint(1, 1, paint); |
| } |
| |
| // Create icon from that bitmap. |
| gfx::ImageFamily image_family; |
| image_family.Add(gfx::Image::CreateFrom1xBitmap(bitmap)); |
| ASSERT_TRUE( |
| IconUtil::CreateIconFileFromImageFamily(image_family, icon_filename)); |
| |
| // Load icon and check that dot has same color. |
| ScopedHICON icon(LoadIconFromFile(icon_filename, size, size)); |
| ASSERT_TRUE(icon.is_valid()); |
| SkBitmap bitmap_loaded = |
| IconUtil::CreateSkBitmapFromHICON(icon.get(), gfx::Size(size, size)); |
| EXPECT_EQ(bitmap_loaded.getColor(1, 1), semi_transparent_red); |
| } |