• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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