blob: 87724df13ecf2b5c594993331ee92f78ebe8644c [file] [log] [blame]
// Copyright 2016 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 "third_party/blink/renderer/core/html/canvas/canvas_async_blob_creator.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/html/canvas/image_data.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/graphics/color_correction_test_utils.h"
#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/skia/include/core/SkSurface.h"
namespace blink {
typedef CanvasAsyncBlobCreator::IdleTaskStatus IdleTaskStatus;
class MockCanvasAsyncBlobCreator : public CanvasAsyncBlobCreator {
public:
MockCanvasAsyncBlobCreator(scoped_refptr<StaticBitmapImage> image,
ImageEncodingMimeType mime_type,
Document* document,
bool fail_encoder_initialization = false)
: CanvasAsyncBlobCreator(
image,
CanvasAsyncBlobCreator::GetImageEncodeOptionsForMimeType(mime_type),
kHTMLCanvasToBlobCallback,
nullptr,
TimeTicks(),
document,
nullptr) {
if (fail_encoder_initialization)
fail_encoder_initialization_for_test_ = true;
enforce_idle_encoding_for_test_ = true;
}
CanvasAsyncBlobCreator::IdleTaskStatus GetIdleTaskStatus() {
return idle_task_status_;
}
MOCK_METHOD0(SignalTaskSwitchInStartTimeoutEventForTesting, void());
MOCK_METHOD0(SignalTaskSwitchInCompleteTimeoutEventForTesting, void());
protected:
void CreateBlobAndReturnResult() override {}
void CreateNullAndReturnResult() override {}
void SignalAlternativeCodePathFinishedForTesting() override;
void PostDelayedTaskToCurrentThread(const base::Location&,
base::OnceClosure,
double delay_ms) override;
};
void MockCanvasAsyncBlobCreator::SignalAlternativeCodePathFinishedForTesting() {
test::ExitRunLoop();
}
void MockCanvasAsyncBlobCreator::PostDelayedTaskToCurrentThread(
const base::Location& location,
base::OnceClosure task,
double delay_ms) {
DCHECK(IsMainThread());
Thread::Current()->GetTaskRunner()->PostTask(location, std::move(task));
}
//==============================================================================
class MockCanvasAsyncBlobCreatorWithoutStart
: public MockCanvasAsyncBlobCreator {
public:
MockCanvasAsyncBlobCreatorWithoutStart(scoped_refptr<StaticBitmapImage> image,
Document* document)
: MockCanvasAsyncBlobCreator(image, kMimeTypePng, document) {}
protected:
void ScheduleInitiateEncoding(double) override {
// Deliberately make scheduleInitiateEncoding do nothing so that idle
// task never starts
}
};
//==============================================================================
class MockCanvasAsyncBlobCreatorWithoutComplete
: public MockCanvasAsyncBlobCreator {
public:
MockCanvasAsyncBlobCreatorWithoutComplete(
scoped_refptr<StaticBitmapImage> image,
Document* document,
bool fail_encoder_initialization = false)
: MockCanvasAsyncBlobCreator(image,
kMimeTypePng,
document,
fail_encoder_initialization) {}
protected:
void ScheduleInitiateEncoding(double quality) override {
Thread::Current()->GetTaskRunner()->PostTask(
FROM_HERE,
WTF::Bind(&MockCanvasAsyncBlobCreatorWithoutComplete::InitiateEncoding,
WrapPersistent(this), quality, TimeTicks::Max()));
}
void IdleEncodeRows(TimeTicks deadline) override {
// Deliberately make idleEncodeRows do nothing so that idle task never
// completes
}
};
//==============================================================================
class CanvasAsyncBlobCreatorTest : public PageTestBase {
public:
void PrepareMockCanvasAsyncBlobCreatorWithoutStart();
void PrepareMockCanvasAsyncBlobCreatorWithoutComplete();
void PrepareMockCanvasAsyncBlobCreatorFail();
protected:
CanvasAsyncBlobCreatorTest();
MockCanvasAsyncBlobCreator* AsyncBlobCreator() {
return async_blob_creator_.Get();
}
void TearDown() override;
private:
Persistent<MockCanvasAsyncBlobCreator> async_blob_creator_;
};
CanvasAsyncBlobCreatorTest::CanvasAsyncBlobCreatorTest() = default;
scoped_refptr<StaticBitmapImage> CreateTransparentImage(int width, int height) {
sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(width, height);
if (!surface)
return nullptr;
return StaticBitmapImage::Create(surface->makeImageSnapshot());
}
void CanvasAsyncBlobCreatorTest::
PrepareMockCanvasAsyncBlobCreatorWithoutStart() {
async_blob_creator_ = new MockCanvasAsyncBlobCreatorWithoutStart(
CreateTransparentImage(20, 20), &GetDocument());
}
void CanvasAsyncBlobCreatorTest::
PrepareMockCanvasAsyncBlobCreatorWithoutComplete() {
async_blob_creator_ = new MockCanvasAsyncBlobCreatorWithoutComplete(
CreateTransparentImage(20, 20), &GetDocument());
}
void CanvasAsyncBlobCreatorTest::PrepareMockCanvasAsyncBlobCreatorFail() {
// We reuse the class MockCanvasAsyncBlobCreatorWithoutComplete because
// this test case is expected to fail at initialization step before
// completion.
async_blob_creator_ = new MockCanvasAsyncBlobCreatorWithoutComplete(
CreateTransparentImage(20, 20), &GetDocument(), true);
}
void CanvasAsyncBlobCreatorTest::TearDown() {
async_blob_creator_ = nullptr;
}
//==============================================================================
TEST_F(CanvasAsyncBlobCreatorTest,
IdleTaskNotStartedWhenStartTimeoutEventHappens) {
// This test mocks the scenario when idle task is not started when the
// StartTimeoutEvent is inspecting the idle task status.
// The whole image encoding process (including initialization) will then
// become carried out in the alternative code path instead.
PrepareMockCanvasAsyncBlobCreatorWithoutStart();
EXPECT_CALL(*(AsyncBlobCreator()),
SignalTaskSwitchInStartTimeoutEventForTesting());
AsyncBlobCreator()->ScheduleAsyncBlobCreation(1.0);
test::EnterRunLoop();
testing::Mock::VerifyAndClearExpectations(AsyncBlobCreator());
EXPECT_EQ(IdleTaskStatus::kIdleTaskSwitchedToImmediateTask,
AsyncBlobCreator()->GetIdleTaskStatus());
}
TEST_F(CanvasAsyncBlobCreatorTest,
IdleTaskNotCompletedWhenCompleteTimeoutEventHappens) {
// This test mocks the scenario when idle task is not completed when the
// CompleteTimeoutEvent is inspecting the idle task status.
// The remaining image encoding process (excluding initialization) will
// then become carried out in the alternative code path instead.
PrepareMockCanvasAsyncBlobCreatorWithoutComplete();
EXPECT_CALL(*(AsyncBlobCreator()),
SignalTaskSwitchInCompleteTimeoutEventForTesting());
AsyncBlobCreator()->ScheduleAsyncBlobCreation(1.0);
test::EnterRunLoop();
testing::Mock::VerifyAndClearExpectations(AsyncBlobCreator());
EXPECT_EQ(IdleTaskStatus::kIdleTaskSwitchedToImmediateTask,
AsyncBlobCreator()->GetIdleTaskStatus());
}
TEST_F(CanvasAsyncBlobCreatorTest, IdleTaskFailedWhenStartTimeoutEventHappens) {
// This test mocks the scenario when idle task is not failed during when
// either the StartTimeoutEvent or the CompleteTimeoutEvent is inspecting
// the idle task status.
PrepareMockCanvasAsyncBlobCreatorFail();
AsyncBlobCreator()->ScheduleAsyncBlobCreation(1.0);
test::EnterRunLoop();
EXPECT_EQ(IdleTaskStatus::kIdleTaskFailed,
AsyncBlobCreator()->GetIdleTaskStatus());
}
static sk_sp<SkImage> DrawAndReturnImage(
const std::pair<sk_sp<SkColorSpace>, SkColorType>& color_space_param) {
SkPaint transparentRed, transparentGreen, transparentBlue, transparentBlack;
transparentRed.setARGB(128, 155, 27, 27);
transparentGreen.setARGB(128, 27, 155, 27);
transparentBlue.setARGB(128, 27, 27, 155);
transparentBlack.setARGB(128, 27, 27, 27);
SkImageInfo info = SkImageInfo::Make(2, 2, color_space_param.second,
SkAlphaType::kPremul_SkAlphaType,
color_space_param.first);
sk_sp<SkSurface> surface = SkSurface::MakeRaster(info);
surface->getCanvas()->drawRect(SkRect::MakeXYWH(0, 0, 1, 1), transparentRed);
surface->getCanvas()->drawRect(SkRect::MakeXYWH(1, 0, 1, 1),
transparentGreen);
surface->getCanvas()->drawRect(SkRect::MakeXYWH(0, 1, 1, 1), transparentBlue);
surface->getCanvas()->drawRect(SkRect::MakeXYWH(1, 1, 1, 1),
transparentBlack);
return surface->makeImageSnapshot();
}
TEST_F(CanvasAsyncBlobCreatorTest, ColorManagedConvertToBlob) {
std::list<std::pair<sk_sp<SkColorSpace>, SkColorType>> color_space_params;
color_space_params.push_back(std::pair<sk_sp<SkColorSpace>, SkColorType>(
SkColorSpace::MakeSRGB(), kN32_SkColorType));
color_space_params.push_back(std::pair<sk_sp<SkColorSpace>, SkColorType>(
SkColorSpace::MakeSRGBLinear(), kRGBA_F16_SkColorType));
color_space_params.push_back(std::pair<sk_sp<SkColorSpace>, SkColorType>(
SkColorSpace::MakeRGB(SkColorSpace::kLinear_RenderTargetGamma,
SkColorSpace::kDCIP3_D65_Gamut),
kRGBA_F16_SkColorType));
color_space_params.push_back(std::pair<sk_sp<SkColorSpace>, SkColorType>(
SkColorSpace::MakeRGB(SkColorSpace::kLinear_RenderTargetGamma,
SkColorSpace::kRec2020_Gamut),
kRGBA_F16_SkColorType));
std::list<String> blob_mime_types = {"image/png", "image/webp", "image/jpeg"};
std::list<String> blob_color_spaces = {kSRGBImageColorSpaceName,
kDisplayP3ImageColorSpaceName,
kRec2020ImageColorSpaceName};
std::list<String> blob_pixel_formats = {
kRGBA8ImagePixelFormatName, kRGBA16ImagePixelFormatName,
};
// The maximum difference locally observed is 2.
const unsigned uint8_color_tolerance = 2;
const float f16_color_tolerance = 0.01;
for (auto color_space_param : color_space_params) {
for (auto blob_mime_type : blob_mime_types) {
for (auto blob_color_space : blob_color_spaces) {
for (auto blob_pixel_format : blob_pixel_formats) {
// Create the StaticBitmapImage in canvas_color_space
sk_sp<SkImage> source_image = DrawAndReturnImage(color_space_param);
scoped_refptr<StaticBitmapImage> source_bitmap_image =
StaticBitmapImage::Create(source_image);
// Prepare encoding options
ImageEncodeOptions* options = ImageEncodeOptions::Create();
options->setQuality(1);
options->setType(blob_mime_type);
options->setColorSpace(blob_color_space);
options->setPixelFormat(blob_pixel_format);
// Encode the image using CanvasAsyncBlobCreator
CanvasAsyncBlobCreator* async_blob_creator =
CanvasAsyncBlobCreator::Create(
source_bitmap_image, options,
CanvasAsyncBlobCreator::ToBlobFunctionType::
kHTMLCanvasConvertToBlobPromise,
TimeTicks(), &GetDocument(), nullptr);
ASSERT_TRUE(async_blob_creator->EncodeImageForConvertToBlobTest());
sk_sp<SkData> sk_data = SkData::MakeWithCopy(
async_blob_creator->GetEncodedImageForConvertToBlobTest().data(),
async_blob_creator->GetEncodedImageForConvertToBlobTest().size());
sk_sp<SkImage> decoded_img = SkImage::MakeFromEncoded(sk_data);
sk_sp<SkColorSpace> expected_color_space =
CanvasAsyncBlobCreator::BlobColorSpaceToSkColorSpace(
blob_color_space);
SkColorType expected_color_type =
(blob_pixel_format == kRGBA8ImagePixelFormatName)
? kN32_SkColorType
: kRGBA_F16_SkColorType;
scoped_refptr<StaticBitmapImage> ref_bitmap =
source_bitmap_image->ConvertToColorSpace(expected_color_space,
expected_color_type);
sk_sp<SkImage> ref_image =
ref_bitmap->PaintImageForCurrentFrame().GetSkImage();
// Jpeg does not support transparent images.
bool compare_alpha = (blob_mime_type != "image/jpeg");
ASSERT_TRUE(ColorCorrectionTestUtils::MatchSkImages(
ref_image, decoded_img, uint8_color_tolerance,
f16_color_tolerance, compare_alpha));
}
}
}
}
}
}