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