// Copyright (c) 2012 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 "chrome/browser/extensions/extension_icon_image.h" #include "base/json/json_file_value_serializer.h" #include "base/message_loop/message_loop.h" #include "base/path_service.h" #include "chrome/browser/extensions/image_loader.h" #include "chrome/common/chrome_paths.h" #include "chrome/common/extensions/extension.h" #include "chrome/common/extensions/extension_constants.h" #include "chrome/common/extensions/manifest_handlers/icons_handler.h" #include "chrome/test/base/testing_profile.h" #include "content/public/test/test_browser_thread.h" #include "extensions/common/manifest.h" #include "grit/theme_resources.h" #include "skia/ext/image_operations.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/image/image_skia_source.h" #include "ui/gfx/skia_util.h" using content::BrowserThread; using extensions::Extension; using extensions::IconImage; using extensions::Manifest; namespace { SkBitmap CreateBlankBitmapForScale(int size_dip, ui::ScaleFactor scale_factor) { SkBitmap bitmap; const float scale = ui::GetScaleFactorScale(scale_factor); bitmap.setConfig(SkBitmap::kARGB_8888_Config, static_cast(size_dip * scale), static_cast(size_dip * scale)); bitmap.allocPixels(); bitmap.eraseColor(SkColorSetARGB(0, 0, 0, 0)); return bitmap; } SkBitmap EnsureBitmapSize(const SkBitmap& original, int size) { if (original.width() == size && original.height() == size) return original; SkBitmap resized = skia::ImageOperations::Resize( original, skia::ImageOperations::RESIZE_LANCZOS3, size, size); return resized; } // Used to test behavior including images defined by an image skia source. // |GetImageForScale| simply returns image representation from the image given // in the ctor. class MockImageSkiaSource : public gfx::ImageSkiaSource { public: explicit MockImageSkiaSource(const gfx::ImageSkia& image) : image_(image) { } virtual ~MockImageSkiaSource() {} virtual gfx::ImageSkiaRep GetImageForScale( ui::ScaleFactor scale_factor) OVERRIDE { return image_.GetRepresentation(scale_factor); } private: gfx::ImageSkia image_; }; // Helper class for synchronously loading extension image resource. class TestImageLoader { public: explicit TestImageLoader(const Extension* extension) : extension_(extension), waiting_(false), image_loaded_(false) { } virtual ~TestImageLoader() {} void OnImageLoaded(const gfx::Image& image) { image_ = image; image_loaded_ = true; if (waiting_) base::MessageLoop::current()->Quit(); } SkBitmap LoadBitmap(const std::string& path, int size) { image_loaded_ = false; image_loader_.LoadImageAsync( extension_, extension_->GetResource(path), gfx::Size(size, size), base::Bind(&TestImageLoader::OnImageLoaded, base::Unretained(this))); // If |image_| still hasn't been loaded (i.e. it is being loaded // asynchronously), wait for it. if (!image_loaded_) { waiting_ = true; base::MessageLoop::current()->Run(); waiting_ = false; } EXPECT_TRUE(image_loaded_); return image_.IsEmpty() ? SkBitmap() : *image_.ToSkBitmap(); } private: const Extension* extension_; bool waiting_; bool image_loaded_; gfx::Image image_; extensions::ImageLoader image_loader_; DISALLOW_COPY_AND_ASSIGN(TestImageLoader); }; class ExtensionIconImageTest : public testing::Test, public IconImage::Observer { public: ExtensionIconImageTest() : image_loaded_count_(0), quit_in_image_loaded_(false), ui_thread_(BrowserThread::UI, &ui_loop_), file_thread_(BrowserThread::FILE), io_thread_(BrowserThread::IO) { } virtual ~ExtensionIconImageTest() {} void WaitForImageLoad() { quit_in_image_loaded_ = true; base::MessageLoop::current()->Run(); quit_in_image_loaded_ = false; } int ImageLoadedCount() { int result = image_loaded_count_; image_loaded_count_ = 0; return result; } scoped_refptr CreateExtension(const char* name, Manifest::Location location) { // Create and load an extension. base::FilePath test_file; if (!PathService::Get(chrome::DIR_TEST_DATA, &test_file)) { EXPECT_FALSE(true); return NULL; } test_file = test_file.AppendASCII("extensions").AppendASCII(name); int error_code = 0; std::string error; JSONFileValueSerializer serializer(test_file.AppendASCII("app.json")); scoped_ptr valid_value( static_cast(serializer.Deserialize(&error_code, &error))); EXPECT_EQ(0, error_code) << error; if (error_code != 0) return NULL; EXPECT_TRUE(valid_value.get()); if (!valid_value) return NULL; return Extension::Create(test_file, location, *valid_value, Extension::NO_FLAGS, &error); } // testing::Test overrides: virtual void SetUp() OVERRIDE { file_thread_.Start(); io_thread_.Start(); } // IconImage::Delegate overrides: virtual void OnExtensionIconImageChanged(IconImage* image) OVERRIDE { image_loaded_count_++; if (quit_in_image_loaded_) base::MessageLoop::current()->Quit(); } gfx::ImageSkia GetDefaultIcon() { return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( IDR_EXTENSIONS_FAVICON); } // Loads an image to be used in test from the extension. // The image will be loaded from the relative path |path|. SkBitmap GetTestBitmap(const Extension* extension, const std::string& path, int size) { TestImageLoader image_loader(extension); return image_loader.LoadBitmap(path, size); } private: int image_loaded_count_; bool quit_in_image_loaded_; base::MessageLoop ui_loop_; content::TestBrowserThread ui_thread_; content::TestBrowserThread file_thread_; content::TestBrowserThread io_thread_; DISALLOW_COPY_AND_ASSIGN(ExtensionIconImageTest); }; } // namespace TEST_F(ExtensionIconImageTest, Basic) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); gfx::ImageSkia default_icon = GetDefaultIcon(); // Load images we expect to find as representations in icon_image, so we // can later use them to validate icon_image. SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); ASSERT_FALSE(bitmap_16.empty()); // There is no image of size 32 defined in the extension manifest, so we // should expect manifest image of size 48 resized to size 32. SkBitmap bitmap_48_resized_to_32 = GetTestBitmap(extension.get(), "48.png", 32); ASSERT_FALSE(bitmap_48_resized_to_32.empty()); IconImage image(profile.get(), extension.get(), extensions::IconsInfo::GetIcons(extension.get()), 16, default_icon, this); // No representations in |image_| yet. gfx::ImageSkia::ImageSkiaReps image_reps = image.image_skia().image_reps(); ASSERT_EQ(0u, image_reps.size()); // Gets representation for a scale factor. gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); // Before the image representation is loaded, image should contain blank // image representation. EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_100P))); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); ASSERT_EQ(1u, image.image_skia().image_reps().size()); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); // We should get the right representation now. EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16)); EXPECT_EQ(16, representation.pixel_width()); // Gets representation for an additional scale factor. representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P))); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); ASSERT_EQ(2u, image.image_skia().image_reps().size()); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); // Image should have been resized. EXPECT_EQ(32, representation.pixel_width()); EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_48_resized_to_32)); } // There is no resource with either exact or bigger size, but there is a smaller // resource. TEST_F(ExtensionIconImageTest, FallbackToSmallerWhenNoBigger) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); gfx::ImageSkia default_icon = GetDefaultIcon(); // Load images we expect to find as representations in icon_image, so we // can later use them to validate icon_image. SkBitmap bitmap_48 = GetTestBitmap(extension.get(), "48.png", 48); ASSERT_FALSE(bitmap_48.empty()); IconImage image(profile.get(), extension.get(), extensions::IconsInfo::GetIcons(extension.get()), 32, default_icon, this); gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); ASSERT_EQ(1u, image.image_skia().image_reps().size()); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); // We should have loaded the biggest smaller resource resized to the actual // size. EXPECT_EQ(ui::SCALE_FACTOR_200P, representation.scale_factor()); EXPECT_EQ(64, representation.pixel_width()); EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), EnsureBitmapSize(bitmap_48, 64))); } // There is no resource with exact size, but there is a smaller and a bigger // one. Requested size is smaller than 32 though, so the smaller resource should // be loaded. TEST_F(ExtensionIconImageTest, FallbackToSmaller) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); gfx::ImageSkia default_icon = GetDefaultIcon(); // Load images we expect to find as representations in icon_image, so we // can later use them to validate icon_image. SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); ASSERT_FALSE(bitmap_16.empty()); IconImage image(profile.get(), extension.get(), extensions::IconsInfo::GetIcons(extension.get()), 17, default_icon, this); gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); ASSERT_EQ(1u, image.image_skia().image_reps().size()); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); // We should have loaded smaller (resized) resource. EXPECT_EQ(ui::SCALE_FACTOR_100P, representation.scale_factor()); EXPECT_EQ(17, representation.pixel_width()); EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), EnsureBitmapSize(bitmap_16, 17))); } // If resource set is empty, |GetRepresentation| should synchronously return // default icon, without notifying observer of image change. TEST_F(ExtensionIconImageTest, NoResources) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); ExtensionIconSet empty_icon_set; gfx::ImageSkia default_icon = GetDefaultIcon(); const int kRequestedSize = 24; IconImage image(profile.get(), extension.get(), empty_icon_set, kRequestedSize, default_icon, this); gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), EnsureBitmapSize( default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), kRequestedSize))); EXPECT_EQ(0, ImageLoadedCount()); // We should have a default icon representation. ASSERT_EQ(1u, image.image_skia().image_reps().size()); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), EnsureBitmapSize( default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), kRequestedSize))); } // If resource set is invalid, image load should be done asynchronously and // the observer should be notified when it's done. |GetRepresentation| should // return the default icon representation once image load is done. TEST_F(ExtensionIconImageTest, InvalidResource) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); const int kInvalidIconSize = 24; ExtensionIconSet invalid_icon_set; invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); gfx::ImageSkia default_icon = GetDefaultIcon(); IconImage image(profile.get(), extension.get(), invalid_icon_set, kInvalidIconSize, default_icon, this); gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), CreateBlankBitmapForScale(kInvalidIconSize, ui::SCALE_FACTOR_100P))); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); // We should have default icon representation now. ASSERT_EQ(1u, image.image_skia().image_reps().size()); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), EnsureBitmapSize( default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), kInvalidIconSize))); } // Test that IconImage works with lazily (but synchronously) created default // icon when IconImage returns synchronously. TEST_F(ExtensionIconImageTest, LazyDefaultIcon) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); gfx::ImageSkia default_icon = GetDefaultIcon(); gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), default_icon.size()); ExtensionIconSet empty_icon_set; const int kRequestedSize = 128; IconImage image(profile.get(), extension.get(), empty_icon_set, kRequestedSize, lazy_default_icon, this); ASSERT_FALSE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); // The resouce set is empty, so we should get the result right away. EXPECT_TRUE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), EnsureBitmapSize( default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), kRequestedSize))); // We should have a default icon representation. ASSERT_EQ(1u, image.image_skia().image_reps().size()); } // Test that IconImage works with lazily (but synchronously) created default // icon when IconImage returns asynchronously. TEST_F(ExtensionIconImageTest, LazyDefaultIcon_AsyncIconImage) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); gfx::ImageSkia default_icon = GetDefaultIcon(); gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), default_icon.size()); const int kInvalidIconSize = 24; ExtensionIconSet invalid_icon_set; invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); IconImage image(profile.get(), extension.get(), invalid_icon_set, kInvalidIconSize, lazy_default_icon, this); ASSERT_FALSE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); // We should have default icon representation now. ASSERT_EQ(1u, image.image_skia().image_reps().size()); EXPECT_TRUE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), EnsureBitmapSize( default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), kInvalidIconSize))); } // Tests behavior of image created by IconImage after IconImage host goes // away. The image should still return loaded representations. If requested // representation was not loaded while IconImage host was around, transparent // representations should be returned. TEST_F(ExtensionIconImageTest, IconImageDestruction) { scoped_ptr profile(new TestingProfile()); scoped_refptr extension(CreateExtension( "extension_icon_image", Manifest::INVALID_LOCATION)); ASSERT_TRUE(extension.get() != NULL); gfx::ImageSkia default_icon = GetDefaultIcon(); // Load images we expect to find as representations in icon_image, so we // can later use them to validate icon_image. SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); ASSERT_FALSE(bitmap_16.empty()); scoped_ptr image( new IconImage(profile.get(), extension.get(), extensions::IconsInfo::GetIcons(extension.get()), 16, default_icon, this)); // Load an image representation. gfx::ImageSkiaRep representation = image->image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); WaitForImageLoad(); EXPECT_EQ(1, ImageLoadedCount()); ASSERT_EQ(1u, image->image_skia().image_reps().size()); // Stash loaded image skia, and destroy |image|. gfx::ImageSkia image_skia = image->image_skia(); image.reset(); extension = NULL; // Image skia should still be able to get previously loaded representation. representation = image_skia.GetRepresentation(ui::SCALE_FACTOR_100P); EXPECT_EQ(ui::SCALE_FACTOR_100P, representation.scale_factor()); EXPECT_EQ(16, representation.pixel_width()); EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16)); // When requesting another representation, we should get blank image. representation = image_skia.GetRepresentation(ui::SCALE_FACTOR_200P); EXPECT_TRUE(gfx::BitmapsAreEqual( representation.sk_bitmap(), CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P))); }