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.h" 13 #include "chrome/common/extensions/extension_constants.h" 14 #include "chrome/common/extensions/manifest.h" 15 #include "chrome/common/extensions/manifest_handlers/icons_handler.h" 16 #include "chrome/test/base/testing_profile.h" 17 #include "content/public/test/test_browser_thread.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::GetScaleFactorScale(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( 63 ui::ScaleFactor scale_factor) OVERRIDE { 64 return image_.GetRepresentation(scale_factor); 65 } 66 67 private: 68 gfx::ImageSkia image_; 69 }; 70 71 // Helper class for synchronously loading extension image resource. 72 class TestImageLoader { 73 public: 74 explicit TestImageLoader(const Extension* extension) 75 : extension_(extension), 76 waiting_(false), 77 image_loaded_(false) { 78 } 79 virtual ~TestImageLoader() {} 80 81 void OnImageLoaded(const gfx::Image& image) { 82 image_ = image; 83 image_loaded_ = true; 84 if (waiting_) 85 base::MessageLoop::current()->Quit(); 86 } 87 88 SkBitmap LoadBitmap(const std::string& path, 89 int size) { 90 image_loaded_ = false; 91 92 image_loader_.LoadImageAsync( 93 extension_, extension_->GetResource(path), gfx::Size(size, size), 94 base::Bind(&TestImageLoader::OnImageLoaded, 95 base::Unretained(this))); 96 97 // If |image_| still hasn't been loaded (i.e. it is being loaded 98 // asynchronously), wait for it. 99 if (!image_loaded_) { 100 waiting_ = true; 101 base::MessageLoop::current()->Run(); 102 waiting_ = false; 103 } 104 105 EXPECT_TRUE(image_loaded_); 106 107 return image_.IsEmpty() ? SkBitmap() : *image_.ToSkBitmap(); 108 } 109 110 private: 111 const Extension* extension_; 112 bool waiting_; 113 bool image_loaded_; 114 gfx::Image image_; 115 extensions::ImageLoader image_loader_; 116 117 DISALLOW_COPY_AND_ASSIGN(TestImageLoader); 118 }; 119 120 class ExtensionIconImageTest : public testing::Test, 121 public IconImage::Observer { 122 public: 123 ExtensionIconImageTest() 124 : image_loaded_count_(0), 125 quit_in_image_loaded_(false), 126 ui_thread_(BrowserThread::UI, &ui_loop_), 127 file_thread_(BrowserThread::FILE), 128 io_thread_(BrowserThread::IO) { 129 } 130 131 virtual ~ExtensionIconImageTest() {} 132 133 void WaitForImageLoad() { 134 quit_in_image_loaded_ = true; 135 base::MessageLoop::current()->Run(); 136 quit_in_image_loaded_ = false; 137 } 138 139 int ImageLoadedCount() { 140 int result = image_loaded_count_; 141 image_loaded_count_ = 0; 142 return result; 143 } 144 145 scoped_refptr<Extension> CreateExtension(const char* name, 146 Manifest::Location location) { 147 // Create and load an extension. 148 base::FilePath test_file; 149 if (!PathService::Get(chrome::DIR_TEST_DATA, &test_file)) { 150 EXPECT_FALSE(true); 151 return NULL; 152 } 153 test_file = test_file.AppendASCII("extensions").AppendASCII(name); 154 int error_code = 0; 155 std::string error; 156 JSONFileValueSerializer serializer(test_file.AppendASCII("app.json")); 157 scoped_ptr<DictionaryValue> valid_value( 158 static_cast<DictionaryValue*>(serializer.Deserialize(&error_code, 159 &error))); 160 EXPECT_EQ(0, error_code) << error; 161 if (error_code != 0) 162 return NULL; 163 164 EXPECT_TRUE(valid_value.get()); 165 if (!valid_value) 166 return NULL; 167 168 return Extension::Create(test_file, location, *valid_value, 169 Extension::NO_FLAGS, &error); 170 } 171 172 // testing::Test overrides: 173 virtual void SetUp() OVERRIDE { 174 file_thread_.Start(); 175 io_thread_.Start(); 176 } 177 178 // IconImage::Delegate overrides: 179 virtual void OnExtensionIconImageChanged(IconImage* image) OVERRIDE { 180 image_loaded_count_++; 181 if (quit_in_image_loaded_) 182 base::MessageLoop::current()->Quit(); 183 } 184 185 gfx::ImageSkia GetDefaultIcon() { 186 return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 187 IDR_EXTENSIONS_FAVICON); 188 } 189 190 // Loads an image to be used in test from the extension. 191 // The image will be loaded from the relative path |path|. 192 SkBitmap GetTestBitmap(const Extension* extension, 193 const std::string& path, 194 int size) { 195 TestImageLoader image_loader(extension); 196 return image_loader.LoadBitmap(path, size); 197 } 198 199 private: 200 int image_loaded_count_; 201 bool quit_in_image_loaded_; 202 base::MessageLoop ui_loop_; 203 content::TestBrowserThread ui_thread_; 204 content::TestBrowserThread file_thread_; 205 content::TestBrowserThread io_thread_; 206 207 DISALLOW_COPY_AND_ASSIGN(ExtensionIconImageTest); 208 }; 209 210 } // namespace 211 212 TEST_F(ExtensionIconImageTest, Basic) { 213 scoped_ptr<Profile> profile(new TestingProfile()); 214 scoped_refptr<Extension> extension(CreateExtension( 215 "extension_icon_image", Manifest::INVALID_LOCATION)); 216 ASSERT_TRUE(extension.get() != NULL); 217 218 gfx::ImageSkia default_icon = GetDefaultIcon(); 219 220 // Load images we expect to find as representations in icon_image, so we 221 // can later use them to validate icon_image. 222 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 223 ASSERT_FALSE(bitmap_16.empty()); 224 225 // There is no image of size 32 defined in the extension manifest, so we 226 // should expect manifest image of size 48 resized to size 32. 227 SkBitmap bitmap_48_resized_to_32 = 228 GetTestBitmap(extension.get(), "48.png", 32); 229 ASSERT_FALSE(bitmap_48_resized_to_32.empty()); 230 231 IconImage image(profile.get(), 232 extension.get(), 233 extensions::IconsInfo::GetIcons(extension.get()), 234 16, 235 default_icon, 236 this); 237 238 // No representations in |image_| yet. 239 gfx::ImageSkia::ImageSkiaReps image_reps = image.image_skia().image_reps(); 240 ASSERT_EQ(0u, image_reps.size()); 241 242 // Gets representation for a scale factor. 243 gfx::ImageSkiaRep representation = 244 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 245 246 // Before the image representation is loaded, image should contain blank 247 // image representation. 248 EXPECT_TRUE(gfx::BitmapsAreEqual( 249 representation.sk_bitmap(), 250 CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_100P))); 251 252 WaitForImageLoad(); 253 EXPECT_EQ(1, ImageLoadedCount()); 254 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 255 256 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 257 258 // We should get the right representation now. 259 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), bitmap_16)); 260 EXPECT_EQ(16, representation.pixel_width()); 261 262 // Gets representation for an additional scale factor. 263 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); 264 265 EXPECT_TRUE(gfx::BitmapsAreEqual( 266 representation.sk_bitmap(), 267 CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P))); 268 269 WaitForImageLoad(); 270 EXPECT_EQ(1, ImageLoadedCount()); 271 ASSERT_EQ(2u, image.image_skia().image_reps().size()); 272 273 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); 274 275 // Image should have been resized. 276 EXPECT_EQ(32, representation.pixel_width()); 277 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 278 bitmap_48_resized_to_32)); 279 } 280 281 // There is no resource with either exact or bigger size, but there is a smaller 282 // resource. 283 TEST_F(ExtensionIconImageTest, FallbackToSmallerWhenNoBigger) { 284 scoped_ptr<Profile> profile(new TestingProfile()); 285 scoped_refptr<Extension> extension(CreateExtension( 286 "extension_icon_image", Manifest::INVALID_LOCATION)); 287 ASSERT_TRUE(extension.get() != NULL); 288 289 gfx::ImageSkia default_icon = GetDefaultIcon(); 290 291 // Load images we expect to find as representations in icon_image, so we 292 // can later use them to validate icon_image. 293 SkBitmap bitmap_48 = GetTestBitmap(extension.get(), "48.png", 48); 294 ASSERT_FALSE(bitmap_48.empty()); 295 296 IconImage image(profile.get(), 297 extension.get(), 298 extensions::IconsInfo::GetIcons(extension.get()), 299 32, 300 default_icon, 301 this); 302 303 gfx::ImageSkiaRep representation = 304 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); 305 306 WaitForImageLoad(); 307 EXPECT_EQ(1, ImageLoadedCount()); 308 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 309 310 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_200P); 311 312 // We should have loaded the biggest smaller resource resized to the actual 313 // size. 314 EXPECT_EQ(ui::SCALE_FACTOR_200P, representation.scale_factor()); 315 EXPECT_EQ(64, representation.pixel_width()); 316 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 317 EnsureBitmapSize(bitmap_48, 64))); 318 } 319 320 // There is no resource with exact size, but there is a smaller and a bigger 321 // one. Requested size is smaller than 32 though, so the smaller resource should 322 // be loaded. 323 TEST_F(ExtensionIconImageTest, FallbackToSmaller) { 324 scoped_ptr<Profile> profile(new TestingProfile()); 325 scoped_refptr<Extension> extension(CreateExtension( 326 "extension_icon_image", Manifest::INVALID_LOCATION)); 327 ASSERT_TRUE(extension.get() != NULL); 328 329 gfx::ImageSkia default_icon = GetDefaultIcon(); 330 331 // Load images we expect to find as representations in icon_image, so we 332 // can later use them to validate icon_image. 333 SkBitmap bitmap_16 = GetTestBitmap(extension.get(), "16.png", 16); 334 ASSERT_FALSE(bitmap_16.empty()); 335 336 IconImage image(profile.get(), 337 extension.get(), 338 extensions::IconsInfo::GetIcons(extension.get()), 339 17, 340 default_icon, 341 this); 342 343 gfx::ImageSkiaRep representation = 344 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 345 346 WaitForImageLoad(); 347 EXPECT_EQ(1, ImageLoadedCount()); 348 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 349 350 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 351 352 // We should have loaded smaller (resized) resource. 353 EXPECT_EQ(ui::SCALE_FACTOR_100P, representation.scale_factor()); 354 EXPECT_EQ(17, representation.pixel_width()); 355 EXPECT_TRUE(gfx::BitmapsAreEqual(representation.sk_bitmap(), 356 EnsureBitmapSize(bitmap_16, 17))); 357 } 358 359 // If resource set is empty, |GetRepresentation| should synchronously return 360 // default icon, without notifying observer of image change. 361 TEST_F(ExtensionIconImageTest, NoResources) { 362 scoped_ptr<Profile> profile(new TestingProfile()); 363 scoped_refptr<Extension> extension(CreateExtension( 364 "extension_icon_image", Manifest::INVALID_LOCATION)); 365 ASSERT_TRUE(extension.get() != NULL); 366 367 ExtensionIconSet empty_icon_set; 368 gfx::ImageSkia default_icon = GetDefaultIcon(); 369 370 const int kRequestedSize = 24; 371 IconImage image(profile.get(), 372 extension.get(), 373 empty_icon_set, 374 kRequestedSize, 375 default_icon, 376 this); 377 378 gfx::ImageSkiaRep representation = 379 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 380 EXPECT_TRUE(gfx::BitmapsAreEqual( 381 representation.sk_bitmap(), 382 EnsureBitmapSize( 383 default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), 384 kRequestedSize))); 385 386 EXPECT_EQ(0, ImageLoadedCount()); 387 // We should have a default icon representation. 388 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 389 390 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 391 EXPECT_TRUE(gfx::BitmapsAreEqual( 392 representation.sk_bitmap(), 393 EnsureBitmapSize( 394 default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), 395 kRequestedSize))); 396 } 397 398 // If resource set is invalid, image load should be done asynchronously and 399 // the observer should be notified when it's done. |GetRepresentation| should 400 // return the default icon representation once image load is done. 401 TEST_F(ExtensionIconImageTest, InvalidResource) { 402 scoped_ptr<Profile> profile(new TestingProfile()); 403 scoped_refptr<Extension> extension(CreateExtension( 404 "extension_icon_image", Manifest::INVALID_LOCATION)); 405 ASSERT_TRUE(extension.get() != NULL); 406 407 const int kInvalidIconSize = 24; 408 ExtensionIconSet invalid_icon_set; 409 invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); 410 411 gfx::ImageSkia default_icon = GetDefaultIcon(); 412 413 IconImage image(profile.get(), 414 extension.get(), 415 invalid_icon_set, 416 kInvalidIconSize, 417 default_icon, 418 this); 419 420 gfx::ImageSkiaRep representation = 421 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 422 EXPECT_TRUE(gfx::BitmapsAreEqual( 423 representation.sk_bitmap(), 424 CreateBlankBitmapForScale(kInvalidIconSize, ui::SCALE_FACTOR_100P))); 425 426 WaitForImageLoad(); 427 EXPECT_EQ(1, ImageLoadedCount()); 428 // We should have default icon representation now. 429 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 430 431 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 432 EXPECT_TRUE(gfx::BitmapsAreEqual( 433 representation.sk_bitmap(), 434 EnsureBitmapSize( 435 default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), 436 kInvalidIconSize))); 437 } 438 439 // Test that IconImage works with lazily (but synchronously) created default 440 // icon when IconImage returns synchronously. 441 TEST_F(ExtensionIconImageTest, LazyDefaultIcon) { 442 scoped_ptr<Profile> profile(new TestingProfile()); 443 scoped_refptr<Extension> extension(CreateExtension( 444 "extension_icon_image", Manifest::INVALID_LOCATION)); 445 ASSERT_TRUE(extension.get() != NULL); 446 447 gfx::ImageSkia default_icon = GetDefaultIcon(); 448 gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), 449 default_icon.size()); 450 451 ExtensionIconSet empty_icon_set; 452 453 const int kRequestedSize = 128; 454 IconImage image(profile.get(), 455 extension.get(), 456 empty_icon_set, 457 kRequestedSize, 458 lazy_default_icon, 459 this); 460 461 ASSERT_FALSE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); 462 463 gfx::ImageSkiaRep representation = 464 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 465 466 // The resouce set is empty, so we should get the result right away. 467 EXPECT_TRUE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); 468 EXPECT_TRUE(gfx::BitmapsAreEqual( 469 representation.sk_bitmap(), 470 EnsureBitmapSize( 471 default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).sk_bitmap(), 472 kRequestedSize))); 473 474 // We should have a default icon representation. 475 ASSERT_EQ(1u, image.image_skia().image_reps().size()); 476 } 477 478 // Test that IconImage works with lazily (but synchronously) created default 479 // icon when IconImage returns asynchronously. 480 TEST_F(ExtensionIconImageTest, LazyDefaultIcon_AsyncIconImage) { 481 scoped_ptr<Profile> profile(new TestingProfile()); 482 scoped_refptr<Extension> extension(CreateExtension( 483 "extension_icon_image", Manifest::INVALID_LOCATION)); 484 ASSERT_TRUE(extension.get() != NULL); 485 486 gfx::ImageSkia default_icon = GetDefaultIcon(); 487 gfx::ImageSkia lazy_default_icon(new MockImageSkiaSource(default_icon), 488 default_icon.size()); 489 490 const int kInvalidIconSize = 24; 491 ExtensionIconSet invalid_icon_set; 492 invalid_icon_set.Add(kInvalidIconSize, "invalid.png"); 493 494 IconImage image(profile.get(), 495 extension.get(), 496 invalid_icon_set, 497 kInvalidIconSize, 498 lazy_default_icon, 499 this); 500 501 ASSERT_FALSE(lazy_default_icon.HasRepresentation(ui::SCALE_FACTOR_100P)); 502 503 gfx::ImageSkiaRep representation = 504 image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 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(ui::SCALE_FACTOR_100P)); 512 513 representation = image.image_skia().GetRepresentation(ui::SCALE_FACTOR_100P); 514 EXPECT_TRUE(gfx::BitmapsAreEqual( 515 representation.sk_bitmap(), 516 EnsureBitmapSize( 517 default_icon.GetRepresentation(ui::SCALE_FACTOR_100P).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(ui::SCALE_FACTOR_100P); 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(ui::SCALE_FACTOR_100P); 561 562 EXPECT_EQ(ui::SCALE_FACTOR_100P, representation.scale_factor()); 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(ui::SCALE_FACTOR_200P); 568 569 EXPECT_TRUE(gfx::BitmapsAreEqual( 570 representation.sk_bitmap(), 571 CreateBlankBitmapForScale(16, ui::SCALE_FACTOR_200P))); 572 } 573