1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/extensions/extension_icon_image.h" 6 7 #include "base/json/json_file_value_serializer.h" 8 #include "base/message_loop/message_loop.h" 9 #include "base/path_service.h" 10 #include "chrome/browser/extensions/image_loader.h" 11 #include "chrome/common/chrome_paths.h" 12 #include "chrome/common/extensions/extension_constants.h" 13 #include "chrome/common/extensions/manifest_handlers/icons_handler.h" 14 #include "chrome/test/base/testing_profile.h" 15 #include "content/public/test/test_browser_thread.h" 16 #include "extensions/common/extension.h" 17 #include "extensions/common/manifest.h" 18 #include "grit/theme_resources.h" 19 #include "skia/ext/image_operations.h" 20 #include "testing/gtest/include/gtest/gtest.h" 21 #include "ui/base/resource/resource_bundle.h" 22 #include "ui/gfx/image/image_skia_source.h" 23 #include "ui/gfx/skia_util.h" 24 25 using content::BrowserThread; 26 using extensions::Extension; 27 using extensions::IconImage; 28 using extensions::Manifest; 29 30 namespace { 31 32 SkBitmap CreateBlankBitmapForScale(int size_dip, ui::ScaleFactor scale_factor) { 33 SkBitmap bitmap; 34 const float scale = ui::GetImageScale(scale_factor); 35 bitmap.setConfig(SkBitmap::kARGB_8888_Config, 36 static_cast<int>(size_dip * scale), 37 static_cast<int>(size_dip * scale)); 38 bitmap.allocPixels(); 39 bitmap.eraseColor(SkColorSetARGB(0, 0, 0, 0)); 40 return bitmap; 41 } 42 43 SkBitmap EnsureBitmapSize(const SkBitmap& original, int size) { 44 if (original.width() == size && original.height() == size) 45 return original; 46 47 SkBitmap resized = skia::ImageOperations::Resize( 48 original, skia::ImageOperations::RESIZE_LANCZOS3, size, size); 49 return resized; 50 } 51 52 // Used to test behavior including images defined by an image skia source. 53 // |GetImageForScale| simply returns image representation from the image given 54 // in the ctor. 55 class MockImageSkiaSource : public gfx::ImageSkiaSource { 56 public: 57 explicit MockImageSkiaSource(const gfx::ImageSkia& image) 58 : image_(image) { 59 } 60 virtual ~MockImageSkiaSource() {} 61 62 virtual gfx::ImageSkiaRep GetImageForScale(float scale) OVERRIDE { 63 return image_.GetRepresentation(scale); 64 } 65 66 private: 67 gfx::ImageSkia image_; 68 }; 69 70 // Helper class for synchronously loading extension image resource. 71 class TestImageLoader { 72 public: 73 explicit TestImageLoader(const Extension* extension) 74 : extension_(extension), 75 waiting_(false), 76 image_loaded_(false) { 77 } 78 virtual ~TestImageLoader() {} 79 80 void OnImageLoaded(const gfx::Image& image) { 81 image_ = image; 82 image_loaded_ = true; 83 if (waiting_) 84 base::MessageLoop::current()->Quit(); 85 } 86 87 SkBitmap LoadBitmap(const std::string& path, 88 int size) { 89 image_loaded_ = false; 90 91 image_loader_.LoadImageAsync( 92 extension_, extension_->GetResource(path), gfx::Size(size, size), 93 base::Bind(&TestImageLoader::OnImageLoaded, 94 base::Unretained(this))); 95 96 // If |image_| still hasn't been loaded (i.e. it is being loaded 97 // asynchronously), wait for it. 98 if (!image_loaded_) { 99 waiting_ = true; 100 base::MessageLoop::current()->Run(); 101 waiting_ = false; 102 } 103 104 EXPECT_TRUE(image_loaded_); 105 106 return image_.IsEmpty() ? SkBitmap() : *image_.ToSkBitmap(); 107 } 108 109 private: 110 const Extension* extension_; 111 bool waiting_; 112 bool image_loaded_; 113 gfx::Image image_; 114 extensions::ImageLoader image_loader_; 115 116 DISALLOW_COPY_AND_ASSIGN(TestImageLoader); 117 }; 118 119 class ExtensionIconImageTest : public testing::Test, 120 public IconImage::Observer { 121 public: 122 ExtensionIconImageTest() 123 : image_loaded_count_(0), 124 quit_in_image_loaded_(false), 125 ui_thread_(BrowserThread::UI, &ui_loop_), 126 file_thread_(BrowserThread::FILE), 127 io_thread_(BrowserThread::IO) { 128 } 129 130 virtual ~ExtensionIconImageTest() {} 131 132 void WaitForImageLoad() { 133 quit_in_image_loaded_ = true; 134 base::MessageLoop::current()->Run(); 135 quit_in_image_loaded_ = false; 136 } 137 138 int ImageLoadedCount() { 139 int result = image_loaded_count_; 140 image_loaded_count_ = 0; 141 return result; 142 } 143 144 scoped_refptr<Extension> CreateExtension(const char* name, 145 Manifest::Location location) { 146 // Create and load an extension. 147 base::FilePath test_file; 148 if (!PathService::Get(chrome::DIR_TEST_DATA, &test_file)) { 149 EXPECT_FALSE(true); 150 return NULL; 151 } 152 test_file = test_file.AppendASCII("extensions").AppendASCII(name); 153 int error_code = 0; 154 std::string error; 155 JSONFileValueSerializer serializer(test_file.AppendASCII("app.json")); 156 scoped_ptr<DictionaryValue> valid_value( 157 static_cast<DictionaryValue*>(serializer.Deserialize(&error_code, 158 &error))); 159 EXPECT_EQ(0, error_code) << error; 160 if (error_code != 0) 161 return NULL; 162 163 EXPECT_TRUE(valid_value.get()); 164 if (!valid_value) 165 return NULL; 166 167 return Extension::Create(test_file, location, *valid_value, 168 Extension::NO_FLAGS, &error); 169 } 170 171 // testing::Test overrides: 172 virtual void SetUp() OVERRIDE { 173 file_thread_.Start(); 174 io_thread_.Start(); 175 } 176 177 // IconImage::Delegate overrides: 178 virtual void OnExtensionIconImageChanged(IconImage* image) OVERRIDE { 179 image_loaded_count_++; 180 if (quit_in_image_loaded_) 181 base::MessageLoop::current()->Quit(); 182 } 183 184 gfx::ImageSkia GetDefaultIcon() { 185 return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 186 IDR_EXTENSIONS_FAVICON); 187 } 188 189 // Loads an image to be used in test from the extension. 190 // The image will be loaded from the relative path |path|. 191 SkBitmap GetTestBitmap(const Extension* extension, 192 const std::string& path, 193 int size) { 194 TestImageLoader image_loader(extension); 195 return image_loader.LoadBitmap(path, size); 196 } 197 198 private: 199 int image_loaded_count_; 200 bool quit_in_image_loaded_; 201 base::MessageLoop ui_loop_; 202 content::TestBrowserThread ui_thread_; 203 content::TestBrowserThread file_thread_; 204 content::TestBrowserThread io_thread_; 205 206 DISALLOW_COPY_AND_ASSIGN(ExtensionIconImageTest); 207 }; 208 209 } // namespace 210 211 TEST_F(ExtensionIconImageTest, Basic) { 212 std::vector<ui::ScaleFactor> supported_factors; 213 supported_factors.push_back(ui::SCALE_FACTOR_100P); 214 supported_factors.push_back(ui::SCALE_FACTOR_200P); 215 ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors); 216 scoped_ptr<Profile> profile(new TestingProfile()); 217 scoped_refptr<Extension> extension(CreateExtension( 218 "extension_icon_image", Manifest::INVALID_LOCATION)); 219 ASSERT_TRUE(extension.get() != NULL); 220 221 gfx::ImageSkia default_icon = GetDefaultIcon(); 222 223 // Load images we expect to find as representations in icon_image, so we 224 // can later use them to validate icon_image. 225 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 226 ASSERT_FALSE(bitmap_16.empty()); 227 228 // There is no image of size 32 defined in the extension manifest, so we 229 // should expect manifest image of size 48 resized to size 32. 230 SkBitmap bitmap_48_resized_to_32 = 231 GetTestBitmap(extension.get(), "48.png", 32); 232 ASSERT_FALSE(bitmap_48_resized_to_32.empty()); 233 234 IconImage image(profile.get(), 235 extension.get(), 236 extensions::IconsInfo::GetIcons(extension.get()), 237 16, 238 default_icon, 239 this); 240 241 // No representations in |image_| yet. 242 gfx::ImageSkia::ImageSkiaReps image_reps = image.image_skia().image_reps(); 243 ASSERT_EQ(0u, image_reps.size()); 244 245 // Gets representation for a scale factor. 246 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 247 248 // Before the image representation is loaded, image should contain blank 249 // image representation. 250 EXPECT_TRUE(gfx::BitmapsAreEqual( 251 representation.sk_bitmap(), 252 CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_100P))); 253 254 WaitForImageLoad(); 255 EXPECT_EQ(1, ImageLoadedCount()); 256 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 257 258 representation = image.image_skia().GetRepresentation(1.0f); 259 260 // We should get the right representation now. 261 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16)); 262 EXPECT_EQ(16, representation.pixel_width()); 263 264 // Gets representation for an additional scale factor. 265 representation = image.image_skia().GetRepresentation(2.0f); 266 267 EXPECT_TRUE(gfx::BitmapsAreEqual( 268 representation.sk_bitmap(), 269 CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P))); 270 271 WaitForImageLoad(); 272 EXPECT_EQ(1, ImageLoadedCount()); 273 ASSERT_EQ(2u, image.image_skia().image_reps().size()); 274 275 representation = image.image_skia().GetRepresentation(2.0f); 276 277 // Image should have been resized. 278 EXPECT_EQ(32, representation.pixel_width()); 279 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 280 bitmap_48_resized_to_32)); 281 } 282 283 // There is no resource with either exact or bigger size, but there is a smaller 284 // resource. 285 TEST_F(ExtensionIconImageTest, FallbackToSmallerWhenNoBigger) { 286 std::vector<ui::ScaleFactor> supported_factors; 287 supported_factors.push_back(ui::SCALE_FACTOR_100P); 288 supported_factors.push_back(ui::SCALE_FACTOR_200P); 289 ui::test::ScopedSetSupportedScaleFactors scoped_supported(supported_factors); 290 scoped_ptr<Profile> profile(new TestingProfile()); 291 scoped_refptr<Extension> extension(CreateExtension( 292 "extension_icon_image", Manifest::INVALID_LOCATION)); 293 ASSERT_TRUE(extension.get() != NULL); 294 295 gfx::ImageSkia default_icon = GetDefaultIcon(); 296 297 // Load images we expect to find as representations in icon_image, so we 298 // can later use them to validate icon_image. 299 SkBitmap bitmap_48 = GetTestBitmap(extension.get(), "48.png", 48); 300 ASSERT_FALSE(bitmap_48.empty()); 301 302 IconImage image(profile.get(), 303 extension.get(), 304 extensions::IconsInfo::GetIcons(extension.get()), 305 32, 306 default_icon, 307 this); 308 309 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(2.0f); 310 311 WaitForImageLoad(); 312 EXPECT_EQ(1, ImageLoadedCount()); 313 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 314 315 representation = image.image_skia().GetRepresentation(2.0f); 316 317 // We should have loaded the biggest smaller resource resized to the actual 318 // size. 319 EXPECT_EQ(2.0f, representation.scale()); 320 EXPECT_EQ(64, representation.pixel_width()); 321 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 322 EnsureBitmapSize(bitmap_48, 64))); 323 } 324 325 // There is no resource with exact size, but there is a smaller and a bigger 326 // one. Requested size is smaller than 32 though, so the smaller resource should 327 // be loaded. 328 TEST_F(ExtensionIconImageTest, FallbackToSmaller) { 329 scoped_ptr<Profile> profile(new TestingProfile()); 330 scoped_refptr<Extension> extension(CreateExtension( 331 "extension_icon_image", Manifest::INVALID_LOCATION)); 332 ASSERT_TRUE(extension.get() != NULL); 333 334 gfx::ImageSkia default_icon = GetDefaultIcon(); 335 336 // Load images we expect to find as representations in icon_image, so we 337 // can later use them to validate icon_image. 338 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 339 ASSERT_FALSE(bitmap_16.empty()); 340 341 IconImage image(profile.get(), 342 extension.get(), 343 extensions::IconsInfo::GetIcons(extension.get()), 344 17, 345 default_icon, 346 this); 347 348 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 349 350 WaitForImageLoad(); 351 EXPECT_EQ(1, ImageLoadedCount()); 352 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 353 354 representation = image.image_skia().GetRepresentation(1.0f); 355 356 // We should have loaded smaller (resized) resource. 357 EXPECT_EQ(1.0f, representation.scale()); 358 EXPECT_EQ(17, representation.pixel_width()); 359 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 360 EnsureBitmapSize(bitmap_16, 17))); 361 } 362 363 // If resource set is empty, |GetRepresentation| should synchronously return 364 // default icon, without notifying observer of image change. 365 TEST_F(ExtensionIconImageTest, NoResources) { 366 scoped_ptr<Profile> profile(new TestingProfile()); 367 scoped_refptr<Extension> extension(CreateExtension( 368 "extension_icon_image", Manifest::INVALID_LOCATION)); 369 ASSERT_TRUE(extension.get() != NULL); 370 371 ExtensionIconSet empty_icon_set; 372 gfx::ImageSkia default_icon = GetDefaultIcon(); 373 374 const int kRequestedSize = 24; 375 IconImage image(profile.get(), 376 extension.get(), 377 empty_icon_set, 378 kRequestedSize, 379 default_icon, 380 this); 381 382 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 383 EXPECT_TRUE(gfx::BitmapsAreEqual( 384 representation.sk_bitmap(), 385 EnsureBitmapSize( 386 default_icon.GetRepresentation(1.0f).sk_bitmap(), 387 kRequestedSize))); 388 389 EXPECT_EQ(0, ImageLoadedCount()); 390 // We should have a default icon representation. 391 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 392 393 representation = image.image_skia().GetRepresentation(1.0f); 394 EXPECT_TRUE(gfx::BitmapsAreEqual( 395 representation.sk_bitmap(), 396 EnsureBitmapSize( 397 default_icon.GetRepresentation(1.0f).sk_bitmap(), 398 kRequestedSize))); 399 } 400 401 // If resource set is invalid, image load should be done asynchronously and 402 // the observer should be notified when it's done. |GetRepresentation| should 403 // return the default icon representation once image load is done. 404 TEST_F(ExtensionIconImageTest, InvalidResource) { 405 scoped_ptr<Profile> profile(new TestingProfile()); 406 scoped_refptr<Extension> extension(CreateExtension( 407 "extension_icon_image", Manifest::INVALID_LOCATION)); 408 ASSERT_TRUE(extension.get() != NULL); 409 410 const int kInvalidIconSize = 24; 411 ExtensionIconSet invalid_icon_set; 412 invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); 413 414 gfx::ImageSkia default_icon = GetDefaultIcon(); 415 416 IconImage image(profile.get(), 417 extension.get(), 418 invalid_icon_set, 419 kInvalidIconSize, 420 default_icon, 421 this); 422 423 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 424 EXPECT_TRUE(gfx::BitmapsAreEqual( 425 representation.sk_bitmap(), 426 CreateBlankBitmapForScale(kInvalidIconSize, ui::SCALE_FACTOR_100P))); 427 428 WaitForImageLoad(); 429 EXPECT_EQ(1, ImageLoadedCount()); 430 // We should have default icon representation now. 431 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 432 433 representation = image.image_skia().GetRepresentation(1.0f); 434 EXPECT_TRUE(gfx::BitmapsAreEqual( 435 representation.sk_bitmap(), 436 EnsureBitmapSize( 437 default_icon.GetRepresentation(1.0f).sk_bitmap(), 438 kInvalidIconSize))); 439 } 440 441 // Test that IconImage works with lazily (but synchronously) created default 442 // icon when IconImage returns synchronously. 443 TEST_F(ExtensionIconImageTest, LazyDefaultIcon) { 444 scoped_ptr<Profile> profile(new TestingProfile()); 445 scoped_refptr<Extension> extension(CreateExtension( 446 "extension_icon_image", Manifest::INVALID_LOCATION)); 447 ASSERT_TRUE(extension.get() != NULL); 448 449 gfx::ImageSkia default_icon = GetDefaultIcon(); 450 gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), 451 default_icon.size()); 452 453 ExtensionIconSet empty_icon_set; 454 455 const int kRequestedSize = 128; 456 IconImage image(profile.get(), 457 extension.get(), 458 empty_icon_set, 459 kRequestedSize, 460 lazy_default_icon, 461 this); 462 463 ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f)); 464 465 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 466 467 // The resouce set is empty, so we should get the result right away. 468 EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f)); 469 EXPECT_TRUE(gfx::BitmapsAreEqual( 470 representation.sk_bitmap(), 471 EnsureBitmapSize( 472 default_icon.GetRepresentation(1.0f).sk_bitmap(), 473 kRequestedSize))); 474 475 // We should have a default icon representation. 476 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 477 } 478 479 // Test that IconImage works with lazily (but synchronously) created default 480 // icon when IconImage returns asynchronously. 481 TEST_F(ExtensionIconImageTest, LazyDefaultIcon_AsyncIconImage) { 482 scoped_ptr<Profile> profile(new TestingProfile()); 483 scoped_refptr<Extension> extension(CreateExtension( 484 "extension_icon_image", Manifest::INVALID_LOCATION)); 485 ASSERT_TRUE(extension.get() != NULL); 486 487 gfx::ImageSkia default_icon = GetDefaultIcon(); 488 gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), 489 default_icon.size()); 490 491 const int kInvalidIconSize = 24; 492 ExtensionIconSet invalid_icon_set; 493 invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); 494 495 IconImage image(profile.get(), 496 extension.get(), 497 invalid_icon_set, 498 kInvalidIconSize, 499 lazy_default_icon, 500 this); 501 502 ASSERT_FALSE(lazy_default_icon.HasRepresentation(1.0f)); 503 504 gfx::ImageSkiaRep representation = image.image_skia().GetRepresentation(1.0f); 505 506 WaitForImageLoad(); 507 EXPECT_EQ(1, ImageLoadedCount()); 508 // We should have default icon representation now. 509 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 510 511 EXPECT_TRUE(lazy_default_icon.HasRepresentation(1.0f)); 512 513 representation = image.image_skia().GetRepresentation(1.0f); 514 EXPECT_TRUE(gfx::BitmapsAreEqual( 515 representation.sk_bitmap(), 516 EnsureBitmapSize( 517 default_icon.GetRepresentation(1.0f).sk_bitmap(), 518 kInvalidIconSize))); 519 } 520 521 // Tests behavior of image created by IconImage after IconImage host goes 522 // away. The image should still return loaded representations. If requested 523 // representation was not loaded while IconImage host was around, transparent 524 // representations should be returned. 525 TEST_F(ExtensionIconImageTest, IconImageDestruction) { 526 scoped_ptr<Profile> profile(new TestingProfile()); 527 scoped_refptr<Extension> extension(CreateExtension( 528 "extension_icon_image", Manifest::INVALID_LOCATION)); 529 ASSERT_TRUE(extension.get() != NULL); 530 531 gfx::ImageSkia default_icon = GetDefaultIcon(); 532 533 // Load images we expect to find as representations in icon_image, so we 534 // can later use them to validate icon_image. 535 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 536 ASSERT_FALSE(bitmap_16.empty()); 537 538 scoped_ptr<IconImage> image( 539 new IconImage(profile.get(), 540 extension.get(), 541 extensions::IconsInfo::GetIcons(extension.get()), 542 16, 543 default_icon, 544 this)); 545 546 // Load an image representation. 547 gfx::ImageSkiaRep representation = 548 image->image_skia().GetRepresentation(1.0f); 549 550 WaitForImageLoad(); 551 EXPECT_EQ(1, ImageLoadedCount()); 552 ASSERT_EQ(1u, image->image_skia().image_reps().size()); 553 554 // Stash loaded image skia, and destroy |image|. 555 gfx::ImageSkia image_skia = image->image_skia(); 556 image.reset(); 557 extension = NULL; 558 559 // Image skia should still be able to get previously loaded representation. 560 representation = image_skia.GetRepresentation(1.0f); 561 562 EXPECT_EQ(1.0f, representation.scale()); 563 EXPECT_EQ(16, representation.pixel_width()); 564 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16)); 565 566 // When requesting another representation, we should get blank image. 567 representation = image_skia.GetRepresentation(2.0f); 568 569 EXPECT_TRUE(gfx::BitmapsAreEqual( 570 representation.sk_bitmap(), 571 CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P))); 572 } 573