| // Copyright 2014 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 "core/layout/ImageQualityController.h" |
| |
| #include "core/layout/LayoutImage.h" |
| #include "core/layout/LayoutTestHelper.h" |
| #include "platform/graphics/GraphicsContext.h" |
| #include "platform/graphics/paint/PaintController.h" |
| #include "platform/scheduler/test/fake_web_task_runner.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "wtf/PtrUtil.h" |
| #include <memory> |
| |
| namespace blink { |
| |
| class ImageQualityControllerTest : public RenderingTest { |
| protected: |
| ImageQualityController* controller() { return m_controller; } |
| |
| private: |
| void SetUp() override { |
| m_controller = ImageQualityController::imageQualityController(); |
| RenderingTest::SetUp(); |
| } |
| |
| ImageQualityController* m_controller; |
| }; |
| |
| TEST_F(ImageQualityControllerTest, RegularImage) { |
| setBodyInnerHTML("<img src='myimage'></img>"); |
| LayoutObject* obj = document().body()->firstChild()->layoutObject(); |
| |
| EXPECT_EQ(InterpolationDefault, controller()->chooseInterpolationQuality( |
| *obj, nullptr, nullptr, LayoutSize())); |
| } |
| |
| TEST_F(ImageQualityControllerTest, ImageRenderingPixelated) { |
| setBodyInnerHTML( |
| "<img src='myimage' style='image-rendering: pixelated'></img>"); |
| LayoutObject* obj = document().body()->firstChild()->layoutObject(); |
| |
| EXPECT_EQ(InterpolationNone, controller()->chooseInterpolationQuality( |
| *obj, nullptr, nullptr, LayoutSize())); |
| } |
| |
| #if !USE(LOW_QUALITY_IMAGE_INTERPOLATION) |
| |
| class TestImageAnimated : public Image { |
| public: |
| bool maybeAnimated() override { return true; } |
| bool currentFrameKnownToBeOpaque(MetadataMode = UseCurrentMetadata) override { |
| return false; |
| } |
| IntSize size() const override { return IntSize(); } |
| void destroyDecodedData() override {} |
| void draw(SkCanvas*, |
| const SkPaint&, |
| const FloatRect& dstRect, |
| const FloatRect& srcRect, |
| RespectImageOrientationEnum, |
| ImageClampingMode, |
| const ColorBehavior&) override {} |
| sk_sp<SkImage> imageForCurrentFrame(const ColorBehavior&) override { |
| return nullptr; |
| } |
| }; |
| |
| TEST_F(ImageQualityControllerTest, ImageMaybeAnimated) { |
| setBodyInnerHTML("<img src='myimage'></img>"); |
| LayoutImage* img = |
| toLayoutImage(document().body()->firstChild()->layoutObject()); |
| |
| RefPtr<TestImageAnimated> testImage = adoptRef(new TestImageAnimated); |
| EXPECT_EQ(InterpolationMedium, |
| controller()->chooseInterpolationQuality(*img, testImage.get(), |
| nullptr, LayoutSize())); |
| } |
| |
| class TestImageWithContrast : public Image { |
| public: |
| bool maybeAnimated() override { return true; } |
| bool currentFrameKnownToBeOpaque(MetadataMode = UseCurrentMetadata) override { |
| return false; |
| } |
| IntSize size() const override { return IntSize(); } |
| void destroyDecodedData() override {} |
| void draw(SkCanvas*, |
| const SkPaint&, |
| const FloatRect& dstRect, |
| const FloatRect& srcRect, |
| RespectImageOrientationEnum, |
| ImageClampingMode, |
| const ColorBehavior&) override {} |
| |
| bool isBitmapImage() const override { return true; } |
| sk_sp<SkImage> imageForCurrentFrame(const ColorBehavior&) override { |
| return nullptr; |
| } |
| }; |
| |
| TEST_F(ImageQualityControllerTest, LowQualityFilterForContrast) { |
| setBodyInnerHTML( |
| "<img src='myimage' style='image-rendering: " |
| "-webkit-optimize-contrast'></img>"); |
| LayoutImage* img = |
| toLayoutImage(document().body()->firstChild()->layoutObject()); |
| |
| RefPtr<TestImageWithContrast> testImage = adoptRef(new TestImageWithContrast); |
| EXPECT_EQ(InterpolationLow, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize())); |
| } |
| |
| class TestImageLowQuality : public Image { |
| public: |
| bool maybeAnimated() override { return true; } |
| bool currentFrameKnownToBeOpaque(MetadataMode = UseCurrentMetadata) override { |
| return false; |
| } |
| IntSize size() const override { return IntSize(1, 1); } |
| void destroyDecodedData() override {} |
| void draw(SkCanvas*, |
| const SkPaint&, |
| const FloatRect& dstRect, |
| const FloatRect& srcRect, |
| RespectImageOrientationEnum, |
| ImageClampingMode, |
| const ColorBehavior&) override {} |
| |
| bool isBitmapImage() const override { return true; } |
| sk_sp<SkImage> imageForCurrentFrame(const ColorBehavior&) override { |
| return nullptr; |
| } |
| }; |
| |
| TEST_F(ImageQualityControllerTest, MediumQualityFilterForUnscaledImage) { |
| setBodyInnerHTML("<img src='myimage'></img>"); |
| LayoutImage* img = |
| toLayoutImage(document().body()->firstChild()->layoutObject()); |
| |
| RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); |
| EXPECT_EQ(InterpolationMedium, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(1, 1))); |
| } |
| |
| // TODO(alexclarke): Remove this when possible. |
| class MockTimer : public TaskRunnerTimer<ImageQualityController> { |
| public: |
| using TimerFiredFunction = |
| typename TaskRunnerTimer<ImageQualityController>::TimerFiredFunction; |
| |
| static std::unique_ptr<MockTimer> create(ImageQualityController* o, |
| TimerFiredFunction f) { |
| auto taskRunner = adoptRef(new scheduler::FakeWebTaskRunner); |
| return WTF::wrapUnique(new MockTimer(std::move(taskRunner), o, f)); |
| } |
| |
| void fire() { |
| fired(); |
| stop(); |
| } |
| |
| void setTime(double newTime) { m_taskRunner->setTime(newTime); } |
| |
| private: |
| MockTimer(RefPtr<scheduler::FakeWebTaskRunner> taskRunner, |
| ImageQualityController* o, |
| TimerFiredFunction f) |
| : TaskRunnerTimer(taskRunner, o, f), |
| m_taskRunner(std::move(taskRunner)) {} |
| |
| RefPtr<scheduler::FakeWebTaskRunner> m_taskRunner; |
| |
| DISALLOW_COPY_AND_ASSIGN(MockTimer); |
| }; |
| |
| TEST_F(ImageQualityControllerTest, LowQualityFilterForResizingImage) { |
| MockTimer* mockTimer = |
| MockTimer::create(controller(), |
| &ImageQualityController::highQualityRepaintTimerFired) |
| .release(); |
| controller()->setTimer(WTF::wrapUnique(mockTimer)); |
| setBodyInnerHTML("<img src='myimage'></img>"); |
| LayoutImage* img = |
| toLayoutImage(document().body()->firstChild()->layoutObject()); |
| |
| RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); |
| std::unique_ptr<PaintController> paintController = PaintController::create(); |
| GraphicsContext context(*paintController); |
| |
| // Paint once. This will kick off a timer to see if we resize it during that |
| // timer's execution. |
| EXPECT_EQ(InterpolationMedium, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(2, 2))); |
| |
| // Go into low-quality mode now that the size changed. |
| EXPECT_EQ(InterpolationLow, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(3, 3))); |
| |
| // Stay in low-quality mode since the size changed again. |
| EXPECT_EQ(InterpolationLow, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(4, 4))); |
| |
| mockTimer->fire(); |
| // The timer fired before painting at another size, so this doesn't count as |
| // animation. Therefore not painting at low quality. |
| EXPECT_EQ(InterpolationMedium, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(4, 4))); |
| } |
| |
| TEST_F(ImageQualityControllerTest, |
| MediumQualityFilterForNotAnimatedWhileAnotherAnimates) { |
| MockTimer* mockTimer = |
| MockTimer::create(controller(), |
| &ImageQualityController::highQualityRepaintTimerFired) |
| .release(); |
| controller()->setTimer(WTF::wrapUnique(mockTimer)); |
| setBodyInnerHTML( |
| "<img id='myAnimatingImage' src='myimage'></img> <img " |
| "id='myNonAnimatingImage' src='myimage2'></img>"); |
| LayoutImage* animatingImage = toLayoutImage( |
| document().getElementById("myAnimatingImage")->layoutObject()); |
| LayoutImage* nonAnimatingImage = toLayoutImage( |
| document().getElementById("myNonAnimatingImage")->layoutObject()); |
| |
| RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); |
| std::unique_ptr<PaintController> paintController = PaintController::create(); |
| GraphicsContext context(*paintController); |
| |
| // Paint once. This will kick off a timer to see if we resize it during that |
| // timer's execution. |
| EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality( |
| *animatingImage, testImage.get(), |
| testImage.get(), LayoutSize(2, 2))); |
| |
| // Go into low-quality mode now that the size changed. |
| EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality( |
| *animatingImage, testImage.get(), |
| testImage.get(), LayoutSize(3, 3))); |
| |
| // The non-animating image receives a medium-quality filter, even though the |
| // other one is animating. |
| EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality( |
| *nonAnimatingImage, testImage.get(), |
| testImage.get(), LayoutSize(4, 4))); |
| |
| // Now the second image has animated, so it also gets painted with a |
| // low-quality filter. |
| EXPECT_EQ(InterpolationLow, controller()->chooseInterpolationQuality( |
| *nonAnimatingImage, testImage.get(), |
| testImage.get(), LayoutSize(3, 3))); |
| |
| mockTimer->fire(); |
| // The timer fired before painting at another size, so this doesn't count as |
| // animation. Therefore not painting at low quality for any image. |
| EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality( |
| *animatingImage, testImage.get(), |
| testImage.get(), LayoutSize(4, 4))); |
| EXPECT_EQ(InterpolationMedium, controller()->chooseInterpolationQuality( |
| *nonAnimatingImage, testImage.get(), |
| testImage.get(), LayoutSize(4, 4))); |
| } |
| |
| TEST_F(ImageQualityControllerTest, |
| DontKickTheAnimationTimerWhenPaintingAtTheSameSize) { |
| MockTimer* mockTimer = |
| MockTimer::create(controller(), |
| &ImageQualityController::highQualityRepaintTimerFired) |
| .release(); |
| controller()->setTimer(WTF::wrapUnique(mockTimer)); |
| setBodyInnerHTML("<img src='myimage'></img>"); |
| LayoutImage* img = |
| toLayoutImage(document().body()->firstChild()->layoutObject()); |
| |
| RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); |
| |
| // Paint once. This will kick off a timer to see if we resize it during that |
| // timer's execution. |
| EXPECT_EQ(InterpolationMedium, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(2, 2))); |
| |
| // Go into low-quality mode now that the size changed. |
| EXPECT_EQ(InterpolationLow, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(3, 3))); |
| |
| // Stay in low-quality mode since the size changed again. |
| EXPECT_EQ(InterpolationLow, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(4, 4))); |
| |
| mockTimer->stop(); |
| EXPECT_FALSE(mockTimer->isActive()); |
| // Painted at the same size, so even though timer is still executing, don't go |
| // to low quality. |
| EXPECT_EQ(InterpolationLow, |
| controller()->chooseInterpolationQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(4, 4))); |
| // Check that the timer was not kicked. It should not have been, since the |
| // image was painted at the same size as last time. |
| EXPECT_FALSE(mockTimer->isActive()); |
| } |
| |
| TEST_F(ImageQualityControllerTest, DontRestartTimerUnlessAdvanced) { |
| MockTimer* mockTimer = |
| MockTimer::create(controller(), |
| &ImageQualityController::highQualityRepaintTimerFired) |
| .release(); |
| controller()->setTimer(WTF::wrapUnique(mockTimer)); |
| setBodyInnerHTML("<img src='myimage'></img>"); |
| LayoutImage* img = |
| toLayoutImage(document().body()->firstChild()->layoutObject()); |
| |
| RefPtr<TestImageLowQuality> testImage = adoptRef(new TestImageLowQuality); |
| |
| // Paint once. This will kick off a timer to see if we resize it during that |
| // timer's execution. |
| mockTimer->setTime(0.1); |
| EXPECT_FALSE(controller()->shouldPaintAtLowQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(2, 2), 0.1)); |
| EXPECT_EQ(ImageQualityController::cLowQualityTimeThreshold, |
| mockTimer->nextFireInterval()); |
| |
| // Go into low-quality mode now that the size changed. |
| double nextTime = 0.1 + ImageQualityController::cTimerRestartThreshold / 2.0; |
| mockTimer->setTime(nextTime); |
| EXPECT_EQ(true, controller()->shouldPaintAtLowQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(3, 3), |
| nextTime)); |
| // The fire interval has decreased, because we have not restarted the timer. |
| EXPECT_EQ(ImageQualityController::cLowQualityTimeThreshold - |
| ImageQualityController::cTimerRestartThreshold / 2.0, |
| mockTimer->nextFireInterval()); |
| |
| // This animation is far enough in the future to make the timer restart, since |
| // it is half over. |
| nextTime = 0.1 + ImageQualityController::cTimerRestartThreshold + 0.01; |
| EXPECT_EQ(true, controller()->shouldPaintAtLowQuality( |
| *img, testImage.get(), testImage.get(), LayoutSize(4, 4), |
| nextTime)); |
| // Now the timer has restarted, leading to a larger fire interval. |
| EXPECT_EQ(ImageQualityController::cLowQualityTimeThreshold, |
| mockTimer->nextFireInterval()); |
| } |
| |
| #endif |
| |
| } // namespace blink |