blob: 2a6bdffb86582f7ed87f5bb71e7719dec43a786e [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "renderer/win32/win32_image_util.h"
#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
#include <atlbase.h>
#include <atlapp.h>
#include <atlgdi.h>
#include <atlmisc.h>
#include <gdiplus.h>
#include <fstream>
#include <list>
#include <memory>
#include "base/file_stream.h"
#include "base/file_util.h"
#include "base/logging.h"
#include "base/mmap.h"
#include "base/util.h"
#include "base/win_font_test_helper.h"
#include "net/jsoncpp.h"
#include "testing/base/public/gunit.h"
DECLARE_string(test_srcdir);
namespace mozc {
namespace renderer {
namespace win32 {
namespace {
using ::mozc::renderer::win32::internal::GaussianBlur;
using ::mozc::renderer::win32::internal::SafeFrameBuffer;
using ::mozc::renderer::win32::internal::SubdivisionalPixel;
using ::mozc::renderer::win32::internal::TextLabel;
using ::std::unique_ptr;
using ::WTL::CBitmap;
using ::WTL::CDC;
using ::WTL::CLogFont;
using ::WTL::CPoint;
using ::WTL::CSize;
typedef SubdivisionalPixel::SubdivisionalPixelIterator
SubdivisionalPixelIterator;
class BalloonImageTest : public testing::Test,
public testing::WithParamInterface<const char *> {
public:
static void SetUpTestCase() {
InitGdiplus();
// On Windows XP, the availability of typical Japanese fonts such are as
// MS Gothic depends on the language edition and language packs.
// So we will register a private font for unit test.
EXPECT_TRUE(WinFontTestHelper::Initialize());
}
static void TearDownTestCase() {
// Free private fonts although the system automatically frees them when
// this process is terminated.
WinFontTestHelper::Uninitialize();
UninitGdiplus();
}
protected:
class TestableBalloonImage : public BalloonImage {
public:
using BalloonImage::CreateInternal;
};
static void BalloonImageTest::SaveTestImage(
const TestableBalloonImage::BalloonImageInfo &info,
const wstring filename) {
CPoint tail_offset;
CSize size;
vector<ARGBColor> buffer;
CBitmap dib = TestableBalloonImage::CreateInternal(
info, &tail_offset, &size, &buffer);
Json::Value tail;
BalloonInfoToJson(info, &tail);
tail["output"]["tail_offset_x"] = tail_offset.x;
tail["output"]["tail_offset_y"] = tail_offset.y;
Gdiplus::Bitmap bitmap(size.cx, size.cy);
for (size_t y = 0; y < size.cy; ++y) {
for (size_t x = 0; x < size.cx; ++x) {
ARGBColor argb = buffer[y * size.cx + x];
const Gdiplus::Color color(argb.a, argb.r, argb.g, argb.b);
bitmap.SetPixel(x, y, color);
}
}
bitmap.Save(filename.c_str(), &clsid_png_);
string utf8_filename;
Util::WideToUTF8(filename + L".json", &utf8_filename);
OutputFileStream os(utf8_filename.c_str());
Json::StyledWriter writer;
os << writer.write(tail);
}
static void BalloonInfoToJson(
const TestableBalloonImage::BalloonImageInfo &info, Json::Value *tail) {
Json::Value input(Json::objectValue);
input["frame_color"] = Json::Value(ColorToInteger(info.frame_color));
input["inside_color"] = Json::Value(ColorToInteger(info.inside_color));
input["label_color"] = Json::Value(ColorToInteger(info.label_color));
input["blur_color"] = Json::Value(ColorToInteger(info.blur_color));
input["blur_alpha"] = Json::Value(info.blur_alpha);
input["label_size"] = Json::Value(info.label_size);
input["label_font"] = Json::Value(info.label_font);
input["label"] = Json::Value(info.label);
input["rect_width"] = Json::Value(info.rect_width);
input["rect_height"] = Json::Value(info.rect_height);
input["frame_thickness"] = Json::Value(info.frame_thickness);
input["corner_radius"] = Json::Value(info.corner_radius);
input["tail_height"] = Json::Value(info.tail_height);
input["tail_width"] = Json::Value(info.tail_width);
input["tail_direction"] =
Json::Value(static_cast<int>(info.tail_direction));
input["blur_sigma"] = Json::Value(info.blur_sigma);
input["blur_offset_x"] = Json::Value(info.blur_offset_x);
input["blur_offset_y"] = Json::Value(info.blur_offset_y);
(*tail)["input"] = input;
}
static void JsonToBalloonInfo(
const Json::Value &tail, TestableBalloonImage::BalloonImageInfo *info) {
const Json::Value &input = tail["input"];
*info = TestableBalloonImage::BalloonImageInfo();
info->frame_color = IntegerToColor(input["frame_color"].asInt());
info->inside_color = IntegerToColor(input["inside_color"].asInt());
info->label_color = IntegerToColor(input["label_color"].asInt());
info->blur_color = IntegerToColor(input["blur_color"].asInt());
info->blur_alpha = input["blur_alpha"].asDouble();
info->label_size = input["label_size"].asInt();
info->label_font = input["label_font"].asString();
info->label = input["label"].asString();
info->rect_width = input["rect_width"].asDouble();
info->rect_height = input["rect_height"].asDouble();
info->frame_thickness = input["frame_thickness"].asDouble();
info->corner_radius = input["corner_radius"].asDouble();
info->tail_height = input["tail_height"].asDouble();
info->tail_width = input["tail_width"].asDouble();
info->tail_direction =
static_cast<TestableBalloonImage::BalloonImageInfo::TailDirection>(
input["tail_direction"].asInt());
info->blur_sigma = input["blur_sigma"].asDouble();
info->blur_offset_x = input["blur_offset_x"].asInt();
info->blur_offset_y = input["blur_offset_y"].asInt();
}
private:
static bool GetEncoderClsid(const wstring format, CLSID *clsid) {
UINT num_codecs = 0;
UINT codecs_buffer_size = 0;
Gdiplus::GetImageEncodersSize(&num_codecs, &codecs_buffer_size);
if (codecs_buffer_size == 0) {
return false;
}
unique_ptr<uint8[]> codesc_buffer(new uint8[codecs_buffer_size]);
Gdiplus::ImageCodecInfo *codecs =
reinterpret_cast<Gdiplus::ImageCodecInfo *>(codesc_buffer.get());
Gdiplus::GetImageEncoders(num_codecs, codecs_buffer_size, codecs);
for (size_t i = 0; i < num_codecs; ++i) {
const Gdiplus::ImageCodecInfo &info = codecs[i];
if (format == info.MimeType) {
*clsid = info.Clsid;
return true;
}
}
return false;
}
static void InitGdiplus() {
Gdiplus::GdiplusStartupInput input;
Gdiplus::GdiplusStartup(&gdiplus_token_, &input, nullptr);
if (!GetEncoderClsid(L"image/png", &clsid_png_)) {
clsid_png_ = CLSID_NULL;
}
if (!GetEncoderClsid(L"image/bmp", &clsid_bmp_)) {
clsid_bmp_ = CLSID_NULL;
}
}
static void UninitGdiplus() {
Gdiplus::GdiplusShutdown(gdiplus_token_);
}
static int32 ColorToInteger(RGBColor color) {
return static_cast<int32>(color.r) << 16 |
static_cast<int32>(color.g) << 8 |
static_cast<int32>(color.b);
}
static RGBColor IntegerToColor(int32 color) {
return RGBColor((color >> 16) & 0xff,
(color >> 8) & 0xff,
color & 0xff);
}
static CLSID clsid_png_;
static CLSID clsid_bmp_;
static HANDLE font_handle_;
static ULONG_PTR gdiplus_token_;
};
HANDLE BalloonImageTest::font_handle_;
CLSID BalloonImageTest::clsid_png_;
CLSID BalloonImageTest::clsid_bmp_;
ULONG_PTR BalloonImageTest::gdiplus_token_;
// Tests should be passed.
const char *kRenderingResultList[] = {
"data/test/renderer/win32/balloon_blur_alpha_-1.png",
"data/test/renderer/win32/balloon_blur_alpha_0.png",
"data/test/renderer/win32/balloon_blur_alpha_10.png",
"data/test/renderer/win32/balloon_blur_color_32_64_128.png",
"data/test/renderer/win32/balloon_blur_offset_-20_-10.png",
"data/test/renderer/win32/balloon_blur_offset_0_0.png",
"data/test/renderer/win32/balloon_blur_offset_20_5.png",
"data/test/renderer/win32/balloon_blur_sigma_0.0.png",
"data/test/renderer/win32/balloon_blur_sigma_0.5.png",
"data/test/renderer/win32/balloon_blur_sigma_1.0.png",
"data/test/renderer/win32/balloon_blur_sigma_2.0.png",
"data/test/renderer/win32/balloon_frame_thickness_-1.png",
"data/test/renderer/win32/balloon_frame_thickness_0.png",
"data/test/renderer/win32/balloon_frame_thickness_1.5.png",
"data/test/renderer/win32/balloon_frame_thickness_3.png",
"data/test/renderer/win32/balloon_inside_color_32_64_128.png",
"data/test/renderer/win32/balloon_no_label.png",
"data/test/renderer/win32/balloon_tail_bottom.png",
"data/test/renderer/win32/balloon_tail_left.png",
"data/test/renderer/win32/balloon_tail_right.png",
"data/test/renderer/win32/balloon_tail_top.png",
"data/test/renderer/win32/balloon_tail_width_height_-10_-10.png",
"data/test/renderer/win32/balloon_tail_width_height_0_0.png",
"data/test/renderer/win32/balloon_tail_width_height_10_20.png",
"data/test/renderer/win32/balloon_width_height_40_30.png",
};
INSTANTIATE_TEST_CASE_P(BalloonImageParameters,
BalloonImageTest,
::testing::ValuesIn(kRenderingResultList));
TEST_P(BalloonImageTest, TestImpl) {
string expected_image = GetParam();
const string &expected_image_path =
FileUtil::JoinPath(FLAGS_test_srcdir, expected_image);
ASSERT_TRUE(FileUtil::FileExists(expected_image_path))
<< "Reference file is not found: " << expected_image_path;
const string json_path = expected_image_path + ".json";
ASSERT_TRUE(FileUtil::FileExists(json_path))
<< "Manifest file is not found: " << json_path;
Json::Value tail;
{
InputFileStream fs(json_path.c_str());
ASSERT_TRUE(fs.good());
fs >> tail;
}
TestableBalloonImage::BalloonImageInfo info;
JsonToBalloonInfo(tail, &info);
CPoint actual_tail_offset;
CSize actual_size;
vector<ARGBColor> actual_buffer;
CBitmap dib = TestableBalloonImage::CreateInternal(
info, &actual_tail_offset, &actual_size, &actual_buffer);
EXPECT_EQ(tail["output"]["tail_offset_x"].asInt(), actual_tail_offset.x);
EXPECT_EQ(tail["output"]["tail_offset_y"].asInt(), actual_tail_offset.y);
wstring wide_path;
Util::UTF8ToWide(expected_image_path, &wide_path);
Gdiplus::Bitmap bitmap(wide_path.c_str());
ASSERT_EQ(bitmap.GetWidth(), actual_size.cx);
ASSERT_EQ(bitmap.GetHeight(), actual_size.cy);
for (size_t y = 0; y < actual_size.cy; ++y) {
for (size_t x = 0; x < actual_size.cx; ++x) {
ARGBColor argb = actual_buffer[y * actual_size.cx + x];
Gdiplus::Color color;
ASSERT_EQ(Gdiplus::Ok, bitmap.GetPixel(x, y, &color));
EXPECT_EQ(color.GetA(), argb.a) << "(x, y): (" << x << ", " << y << ")";
EXPECT_EQ(color.GetR(), argb.r) << "(x, y): (" << x << ", " << y << ")";
EXPECT_EQ(color.GetG(), argb.g) << "(x, y): (" << x << ", " << y << ")";
EXPECT_EQ(color.GetB(), argb.b) << "(x, y): (" << x << ", " << y << ")";
}
}
}
TEST(RGBColorTest, BasicTest) {
EXPECT_NE(RGBColor::kBlack, RGBColor::kWhite);
EXPECT_EQ(RGBColor::kWhite, RGBColor::kWhite);
}
TEST(ARGBColorTest, BasicTest) {
EXPECT_NE(ARGBColor::kBlack, ARGBColor::kWhite);
EXPECT_EQ(ARGBColor::kWhite, ARGBColor::kWhite);
}
TEST(SubdivisionalPixelTest, BasicTest) {
const RGBColor kBlue(0, 0, 255);
const RGBColor kGreen(0, 255, 0);
SubdivisionalPixel sub_pixel;
EXPECT_EQ(0.0, sub_pixel.GetCoverage())
<< "Should be zero for an empty pixel";
EXPECT_EQ(RGBColor::kBlack, sub_pixel.GetPixelColor())
<< "Should be black for an empty pixel";
// SetSubdivisionalPixel sets only sub-pixel specified.
sub_pixel.SetSubdivisionalPixel(SubdivisionalPixel::Fraction2D(0, 0),
RGBColor::kWhite);
EXPECT_NEAR(1.0 / 255.0, sub_pixel.GetCoverage(), 0.1);
EXPECT_EQ(RGBColor::kWhite, sub_pixel.GetPixelColor());
sub_pixel.SetColorToFilledPixels(kGreen);
EXPECT_NEAR(1.0 / 255.0, sub_pixel.GetCoverage(), 0.1);
EXPECT_EQ(kGreen, sub_pixel.GetPixelColor());
// SetPixel sets all the sub-pixels.
sub_pixel.SetPixel(kBlue);
EXPECT_NEAR(1.0, sub_pixel.GetCoverage(), 0.01);
EXPECT_EQ(kBlue, sub_pixel.GetPixelColor());
sub_pixel.SetSubdivisionalPixel(SubdivisionalPixel::Fraction2D(0, 0),
RGBColor::kWhite);
EXPECT_NEAR(1.0, sub_pixel.GetCoverage(), 0.01);
EXPECT_EQ(1, sub_pixel.GetPixelColor().r);
sub_pixel.SetColorToFilledPixels(kBlue);
EXPECT_NEAR(1.0, sub_pixel.GetCoverage(), 0.01);
EXPECT_EQ(kBlue, sub_pixel.GetPixelColor());
}
TEST(SubdivisionalPixelTest, IteratorTest) {
const RGBColor kBlue(0, 0, 255);
SubdivisionalPixel sub_pixel;
size_t count = 0;
for (SubdivisionalPixelIterator it(0, 0); !it.Done(); it.Next()) {
EXPECT_LE(0, it.GetFraction().x);
EXPECT_LE(0, it.GetFraction().y);
EXPECT_GT(SubdivisionalPixel::kDivision, it.GetFraction().x);
EXPECT_GT(SubdivisionalPixel::kDivision, it.GetFraction().y);
EXPECT_LE(0.0, it.GetX());
EXPECT_LE(0.0, it.GetY());
EXPECT_GE(1.0, it.GetX());
EXPECT_GE(1.0, it.GetY());
++count;
}
EXPECT_EQ(SubdivisionalPixel::kTotalPixels, count);
}
TEST(GaussianBlurTest, NoBlurTest) {
// When Gaussian blur sigma is 0.0, no blur effect should be applied.
GaussianBlur blur(0.0);
struct Source {
Source()
: call_count_(0) {}
double operator()(int x, int y) const {
EXPECT_EQ(0, x);
EXPECT_EQ(0, y);
++call_count_;
return 1.0;
}
mutable int call_count_;
};
Source source;
EXPECT_EQ(1.0, blur.Apply(0, 0, source));
EXPECT_EQ(1, source.call_count_);
}
TEST(GaussianBlurTest, InvalidParamBlurTest) {
// When Gaussian blur sigma is invalid (a negative value), no blur effect
// should be applied.
GaussianBlur blur(-100.0);
struct Source {
Source()
: call_count_(0) {}
double operator()(int x, int y) const {
EXPECT_EQ(0, x);
EXPECT_EQ(0, y);
++call_count_;
return 1.0;
}
mutable int call_count_;
};
Source source;
EXPECT_EQ(1.0, blur.Apply(0, 0, source));
EXPECT_EQ(1, source.call_count_);
}
TEST(GaussianBlurTest, NormalBlurTest) {
GaussianBlur blur(1.0);
struct Source {
explicit Source(int cutoff_length)
: call_count_(0),
cutoff_length_(cutoff_length) {}
double operator()(int x, int y) const {
EXPECT_GE(cutoff_length_, abs(x));
EXPECT_GE(cutoff_length_, abs(y));
++call_count_;
return 1.0;
}
mutable size_t call_count_;
int cutoff_length_;
};
Source source(blur.cutoff_length());
EXPECT_NEAR(1.0, blur.Apply(0, 0, source), 0.1);
const size_t matrix_length = blur.cutoff_length() * 2 + 1;
EXPECT_EQ(matrix_length * matrix_length, source.call_count_);
}
TEST(SafeFrameBufferTest, BasicTest) {
const ARGBColor kTransparent(0, 0, 0, 0);
const int kLeft = -10;
const int kTop = -20;
const int kWidth = 50;
const int kHeight = 100;
SafeFrameBuffer buffer(Rect(kLeft, kTop, kWidth, kHeight));
EXPECT_EQ(kTransparent, buffer.GetPixel(kLeft, kTop))
<< "Initial color should be transparent";
buffer.SetPixel(kLeft, kTop, ARGBColor::kWhite);
EXPECT_EQ(ARGBColor::kWhite, buffer.GetPixel(kLeft, kTop));
buffer.SetPixel(kLeft + kWidth, kTop, ARGBColor::kWhite);
EXPECT_EQ(kTransparent, buffer.GetPixel(kLeft + kWidth, kTop))
<< "(left + width) is outside.";
buffer.SetPixel(kLeft, kTop + kHeight, ARGBColor::kWhite);
EXPECT_EQ(kTransparent, buffer.GetPixel(kLeft, kTop + kHeight))
<< "(top + height) is outside.";
buffer.SetPixel(kLeft - 10, kTop - 10, ARGBColor::kWhite);
EXPECT_EQ(kTransparent, buffer.GetPixel(kLeft - 10, kTop - 10))
<< "Outside pixel should be kept as transparent.";
}
TEST(TextLabelTest, BoundingBoxTest) {
const TextLabel label(
-10.5, -5.1, 10.5, 5.0, "text", "font name", 10, RGBColor::kWhite);
EXPECT_EQ(-11, label.bounding_rect().Left());
EXPECT_EQ(-6, label.bounding_rect().Top());
EXPECT_EQ(0, label.bounding_rect().Right());
EXPECT_EQ(0, label.bounding_rect().Bottom());
}
} // namespace
} // namespace win32
} // namespace renderer
} // namespace mozc