blob: 8201e2c1c5c1dc0f6dbd6331e0cb4d33c8825ed4 [file] [log] [blame]
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Author: Huibao Lin
#include <vector>
#include "base/logging.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/stdio_file_system.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/image/image_converter.h"
#include "pagespeed/kernel/image/image_util.h"
#include "pagespeed/kernel/image/png_optimizer.h"
#include "pagespeed/kernel/image/read_image.h"
#include "pagespeed/kernel/image/test_utils.h"
#include "pagespeed/kernel/image/webp_optimizer.h"
namespace {
using net_instaweb::MockMessageHandler;
using net_instaweb::NullMutex;
using pagespeed::image_compression::FrameSpec;
using pagespeed::image_compression::ImageConverter;
using pagespeed::image_compression::ImageSpec;
using pagespeed::image_compression::IMAGE_GIF;
using pagespeed::image_compression::IMAGE_PNG;
using pagespeed::image_compression::IMAGE_WEBP;
using pagespeed::image_compression::kPngSuiteTestDir;
using pagespeed::image_compression::kMessagePatternPixelFormat;
using pagespeed::image_compression::kMessagePatternStats;
using pagespeed::image_compression::kMessagePatternWritingToWebp;
using pagespeed::image_compression::kTestRootDir;
using pagespeed::image_compression::kWebpTestDir;
using pagespeed::image_compression::size_px;
using pagespeed::image_compression::PixelFormat;
using pagespeed::image_compression::PngScanlineReaderRaw;
using pagespeed::image_compression::QUIRKS_CHROME;
using pagespeed::image_compression::ReadFile;
using pagespeed::image_compression::ReadTestFile;
using pagespeed::image_compression::RGB_888;
using pagespeed::image_compression::RGBA_8888;
using pagespeed::image_compression::SCANLINE_STATUS_INVOCATION_ERROR;
using pagespeed::image_compression::SCANLINE_STATUS_SUCCESS;
using pagespeed::image_compression::ScanlineReaderInterface;
using pagespeed::image_compression::ScanlineStatus;
using pagespeed::image_compression::ScanlineWriterInterface;
using pagespeed::image_compression::WebpConfiguration;
using pagespeed::image_compression::WebpScanlineReader;
struct ImageInfo {
const char* original_file;
const char* gold_file;
};
// Each file in kValidImages is saved in both PNG format and WebP format.
// alpha_32x32.webp is lossless, while opaque_32x20.webp is lossy.
// alpha_32x32.png and opaque_32x20.png were generated by 'dwebp' from the
// corresponding WebP images. pagespeed_32x32_gray.png and
// pagespeed_32x32_rgb.png have the same pixel values, but are stored in
// different formats.
const ImageInfo kValidImages[] = {
{"alpha_32x32", "alpha_32x32"}, // size 32-by-32 with alpha.
{"opaque_32x20", "opaque_32x20"}, // size 32-by-20 without alpha.
{"gray_saved_as_gray", "gray_saved_as_rgb"}, // images of same contents.
};
const size_t kValidImageCount = arraysize(kValidImages);
const char kFileCorruptedHeader[] = "corrupt_header";
const char kFileCorruptedBody[] = "corrupt_body";
const double kMinPSNR = 33.0;
// Message to ignore.
const char kMessagePatternInvalidWebPData[] = "Invalid WebP data.";
class WebpScanlineOptimizerTest : public testing::Test {
public:
WebpScanlineOptimizerTest()
: message_handler_(new NullMutex),
reader_(&message_handler_),
scanline_(NULL) {
}
bool Initialize(const char* file_name) {
if (!ReadTestFile(kWebpTestDir, file_name, "webp", &input_image_)) {
PS_LOG_ERROR((&message_handler_), "Failed to read file: %s", file_name);
return false;
}
return reader_.Initialize(input_image_.c_str(), input_image_.length());
}
void ConvertPngToWebp(const GoogleString& png_image,
const WebpConfiguration& webp_config,
GoogleString* webp_image) {
PngScanlineReaderRaw png_reader(&message_handler_);
// Initialize a PNG reader for reading the original image.
ASSERT_TRUE(png_reader.Initialize(
png_image.data(), png_image.length()));
// Get the sizes and pixel format of the original image.
const size_t width = png_reader.GetImageWidth();
const size_t height = png_reader.GetImageHeight();
const PixelFormat pixel_format = png_reader.GetPixelFormat();
// Create a WebP writer.
net_instaweb::scoped_ptr<ScanlineWriterInterface> webp_writer(
CreateScanlineWriter(pagespeed::image_compression::IMAGE_WEBP,
pixel_format, width, height, &webp_config,
webp_image, &message_handler_));
ASSERT_NE(reinterpret_cast<ScanlineWriterInterface*>(NULL),
webp_writer.get());
// Read the scanlines from the original image and write them to the new one.
while (png_reader.HasMoreScanLines()) {
uint8* scanline = NULL;
ASSERT_TRUE(png_reader.ReadNextScanline(
reinterpret_cast<void**>(&scanline)));
ASSERT_TRUE(webp_writer->WriteNextScanline(
reinterpret_cast<void*>(scanline)));
}
ASSERT_TRUE(webp_writer->FinalizeWrite());
}
protected:
virtual void SetUp() {
message_handler_.AddPatternToSkipPrinting(kMessagePatternInvalidWebPData);
message_handler_.AddPatternToSkipPrinting(kMessagePatternPixelFormat);
message_handler_.AddPatternToSkipPrinting(kMessagePatternStats);
message_handler_.AddPatternToSkipPrinting(kMessagePatternWritingToWebp);
}
protected:
MockMessageHandler message_handler_;
WebpScanlineReader reader_;
GoogleString input_image_;
void* scanline_;
private:
DISALLOW_COPY_AND_ASSIGN(WebpScanlineOptimizerTest);
};
// Test both WebP writer and reader. It is done by encoding an image to WebP
// format, then decoding the image and comparing with the gold image. The image
// is encoded as lossy format.
TEST_F(WebpScanlineOptimizerTest, ConvertToAndReadLossyWebp) {
WebpConfiguration webp_config;
webp_config.lossless = false;
webp_config.quality = 90;
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString original_image, gold_image, webp_image;
ReadTestFile(kWebpTestDir, kValidImages[i].original_file, "png",
&original_image);
ConvertPngToWebp(original_image, webp_config, &webp_image);
ReadTestFile(kWebpTestDir, kValidImages[i].gold_file, "png",
&gold_image);
DecodeAndCompareImagesByPSNR(IMAGE_PNG, gold_image.c_str(),
gold_image.length(), IMAGE_WEBP,
webp_image.c_str(), webp_image.length(),
kMinPSNR,
true, // ignore_transparent_rgb
&message_handler_);
}
}
// Test both WebP writer and reader. It is done by encoding an image to WebP
// format, then decoding the image and comparing with the gold image. The image
// is encoded as lossless format.
TEST_F(WebpScanlineOptimizerTest, ConvertToAndReadLosslessWebp) {
WebpConfiguration webp_config;
webp_config.lossless = true;
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString original_image, gold_image, webp_image;
ReadTestFile(kWebpTestDir, kValidImages[i].original_file, "png",
&original_image);
ConvertPngToWebp(original_image, webp_config, &webp_image);
ReadTestFile(kWebpTestDir, kValidImages[i].gold_file, "png",
&gold_image);
DecodeAndCompareImages(IMAGE_PNG, gold_image.c_str(), gold_image.length(),
IMAGE_WEBP, webp_image.c_str(),
webp_image.length(),
true, // ignore_transparent_rgb
&message_handler_);
}
}
// Verify that decoded image is accurate as determined by PSNR.
// The gold data was loaded from disk.
TEST_F(WebpScanlineOptimizerTest, CompareToWebpGolds) {
WebpConfiguration webp_config;
webp_config.lossless = true;
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString png_image, webp_image;
ReadTestFile(kWebpTestDir, kValidImages[i].gold_file, "png", &png_image);
ReadTestFile(kWebpTestDir, kValidImages[i].gold_file, "webp", &webp_image);
DecodeAndCompareImagesByPSNR(IMAGE_PNG, png_image.c_str(),
png_image.length(), IMAGE_WEBP,
webp_image.c_str(), webp_image.length(), 55,
true, // ignore_transparent_rgb
&message_handler_);
}
}
TEST_F(WebpScanlineOptimizerTest, InitializeWithoutRead) {
ASSERT_TRUE(Initialize(kValidImages[0].original_file));
}
TEST_F(WebpScanlineOptimizerTest, ReadOneRow) {
ASSERT_TRUE(Initialize(kValidImages[0].original_file));
ASSERT_TRUE(reader_.ReadNextScanline(&scanline_));
}
TEST_F(WebpScanlineOptimizerTest, ReinitializeAfterOneRow) {
ASSERT_TRUE(Initialize(kValidImages[0].original_file));
ASSERT_TRUE(reader_.ReadNextScanline(&scanline_));
ASSERT_TRUE(Initialize(kValidImages[1].original_file));
ASSERT_TRUE(reader_.ReadNextScanline(&scanline_));
}
TEST_F(WebpScanlineOptimizerTest, ReInitializeAfterLastRow) {
ASSERT_TRUE(Initialize(kValidImages[0].original_file));
while (reader_.HasMoreScanLines()) {
ASSERT_TRUE(reader_.ReadNextScanline(&scanline_));
}
// After depleting the scanlines, any further call to
// ReadNextScanline leads to death in debugging mode, or a
// false in release mode.
#ifdef NDEBUG
EXPECT_FALSE(reader_.ReadNextScanline(&scanline_));
#else
EXPECT_DEATH(reader_.ReadNextScanline(&scanline_),
"The reader was not initialized or the image does not "
"have any more scanlines.");
#endif
ASSERT_TRUE(Initialize(kValidImages[1].original_file));
ASSERT_TRUE(reader_.ReadNextScanline(&scanline_));
}
TEST_F(WebpScanlineOptimizerTest, InvalidWebpHeader) {
ASSERT_FALSE(Initialize(kFileCorruptedHeader));
}
TEST_F(WebpScanlineOptimizerTest, InvalidWebpBody) {
ASSERT_TRUE(Initialize(kFileCorruptedBody));
ASSERT_FALSE(reader_.ReadNextScanline(&scanline_));
}
class AnimatedWebpTest : public testing::Test {
public:
AnimatedWebpTest() : message_handler_(new NullMutex) {}
void ConvertGifToWebp(const char* filename,
const GoogleString& input_image,
WebpConfiguration* webp_config,
GoogleString* webp_image) {
ScanlineStatus status;
reader_.reset(
CreateImageFrameReader(IMAGE_GIF,
input_image.c_str(), input_image.length(),
QUIRKS_CHROME, &message_handler_, &status));
ASSERT_TRUE(status.Success());
writer_.reset(CreateImageFrameWriter(IMAGE_WEBP, webp_config, webp_image,
&message_handler_, &status));
ASSERT_TRUE(status.Success());
EXPECT_TRUE(
ImageConverter::ConvertMultipleFrameImage(reader_.get(),
writer_.get()).Success())
<< " for '" << filename << "'";
}
bool PicturesEqual(const GoogleString& compare,
const GoogleString& input_file_name,
const GoogleString& output_file_name) {
// TODO(vchudnov): Fill this in.
return true;
}
void CheckGifVsWebP(const char* filename, WebpConfiguration* webp_config,
bool check_pixels) {
GoogleString input_path = net_instaweb::StrCat(net_instaweb::GTestSrcDir(),
kTestRootDir, filename);
GoogleString input_image;
ASSERT_TRUE(ReadFile(input_path, &input_image));
DVLOG(1) << "Read image: " << input_path;
GoogleString output_image;
ConvertGifToWebp(filename, input_image, webp_config, &output_image);
GoogleString output_path =
net_instaweb::StrCat(net_instaweb::GTestTempDir(),
kTestRootDir,
filename, ".webp");
net_instaweb::StdioFileSystem file_system;
file_system.WriteFile(output_path.c_str(), output_image, &message_handler_);
DVLOG(1) << "Wrote image: " << output_path;
if (check_pixels) {
// TODO(vchudnov): Employ a pixel-by-pixel comparison program.
GoogleString compare;
EXPECT_TRUE(PicturesEqual(compare, input_path, output_path));
}
}
void PrepareWriterFor5x5Image(size_px num_frames) {
webp_config_.lossless = false;
ScanlineStatus status;
writer_.reset(
CreateImageFrameWriter(IMAGE_WEBP, &webp_config_, &output_image_,
&message_handler_, &status));
ASSERT_TRUE(status.Success());
image_spec_.width = 5;
image_spec_.height = 5;
image_spec_.num_frames = num_frames;
image_spec_.loop_count = 1;
EXPECT_TRUE(writer_->PrepareImage(&image_spec_, &status));
}
protected:
MockMessageHandler message_handler_;
net_instaweb::scoped_ptr<
pagespeed::image_compression::MultipleFrameWriter> writer_;
private:
net_instaweb::scoped_ptr<
pagespeed::image_compression::MultipleFrameReader> reader_;
WebpConfiguration webp_config_;
GoogleString output_image_;
ImageSpec image_spec_;
};
struct ProgressData {
net_instaweb::MessageHandler* handler;
int times_called;
};
bool UpdateProgress(int percent, void* user_data) {
ProgressData* progress_data = static_cast<ProgressData*>(user_data);
progress_data->times_called++;
return true;
}
TEST_F(AnimatedWebpTest, ConvertGifs) {
ProgressData progress_data;
progress_data.handler = &message_handler_;
WebpConfiguration webp_config;
webp_config.lossless = true;
webp_config.quality = 50;
webp_config.progress_hook = UpdateProgress;
webp_config.user_data = &progress_data;
progress_data.times_called = 0;
CheckGifVsWebP("gif/animated.gif", &webp_config, true);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// Fails because of b/15484578 [google]
CheckGifVsWebP("gif/completely_transparent.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: canvas reporting issue: b/15755291 [google]
CheckGifVsWebP("gif/square2loop.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: loop count mismatch: b/15758805 [google]
CheckGifVsWebP("gif/full2loop.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: loop count mismatch: b/15758805 [google]
CheckGifVsWebP("gif/interlaced.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: SEGV: b/15758908 [google]
CheckGifVsWebP("gif/red_empty_screen.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: loop count mismatch: b/15758805 [google]
CheckGifVsWebP("gif/red_unused_invalid_background.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: loop count mismatch: b/15758805 [google]
CheckGifVsWebP("gif/transparent.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: SEGV: b/15758908 [google]
CheckGifVsWebP("gif/zero_size_animation.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: canvas reporting issue: b/15755291 [google]
CheckGifVsWebP("webp/multiple_frame_opaque.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
progress_data.times_called = 0;
// animdiff bug: canvas reporting issue: b/15755291 [google]
CheckGifVsWebP("webp/multiple_frame_opaque_gray.gif", &webp_config, false);
EXPECT_LT(3, progress_data.times_called);
}
TEST_F(AnimatedWebpTest, RequireFirstScanline) {
PrepareWriterFor5x5Image(2);
ScanlineStatus status;
FrameSpec frame_spec;
frame_spec.width = 5;
frame_spec.height = 5;
frame_spec.top = 0;
frame_spec.left = 0;
frame_spec.pixel_format = RGB_888;
EXPECT_TRUE(writer_->PrepareNextFrame(&frame_spec, &status));
#ifndef NDEBUG
ASSERT_DEATH(writer_->PrepareNextFrame(&frame_spec, &status),
"FRAME_WEBPWRITER/SCANLINE_STATUS_INVOCATION_ERROR "
"CacheCurrentFrame: not all scanlines written");
#else
EXPECT_FALSE(writer_->PrepareNextFrame(&frame_spec, &status));
EXPECT_EQ(SCANLINE_STATUS_INVOCATION_ERROR, status.type());
#endif
}
TEST_F(AnimatedWebpTest, RequireAllScanlines) {
PrepareWriterFor5x5Image(2);
ScanlineStatus status;
FrameSpec frame_spec;
frame_spec.width = 5;
frame_spec.height = 5;
frame_spec.top = 0;
frame_spec.left = 0;
frame_spec.pixel_format = RGB_888;
EXPECT_TRUE(writer_->PrepareNextFrame(&frame_spec, &status));
uint8_t scanline[300];
memset(scanline, 0x80, GetBytesPerPixel(RGB_888)*frame_spec.width);
EXPECT_TRUE(writer_->WriteNextScanline(scanline, &status));
#ifndef NDEBUG
ASSERT_DEATH(writer_->PrepareNextFrame(&frame_spec, &status),
"FRAME_WEBPWRITER/SCANLINE_STATUS_INVOCATION_ERROR "
"CacheCurrentFrame: not all scanlines written");
#else
EXPECT_FALSE(writer_->PrepareNextFrame(&frame_spec, &status));
EXPECT_EQ(SCANLINE_STATUS_INVOCATION_ERROR, status.type());
#endif
}
TEST_F(AnimatedWebpTest, RejectExtraScanlines) {
PrepareWriterFor5x5Image(1);
ScanlineStatus status;
FrameSpec frame_spec;
frame_spec.width = 3;
frame_spec.height = 3;
frame_spec.top = 0;
frame_spec.left = 0;
frame_spec.pixel_format = RGB_888;
EXPECT_TRUE(writer_->PrepareNextFrame(&frame_spec, &status));
uint8_t scanline[300];
memset(scanline, 0x80, GetBytesPerPixel(RGB_888)*frame_spec.width);
for (int j = 0; j < frame_spec.height; ++j) {
EXPECT_TRUE(writer_->WriteNextScanline(scanline, &status));
}
#ifndef NDEBUG
ASSERT_DEATH(writer_->WriteNextScanline(scanline, &status),
"FRAME_WEBPWRITER/SCANLINE_STATUS_INVOCATION_ERROR "
"WriteNextScanline: too many scanlines");
#else
EXPECT_FALSE(writer_->WriteNextScanline(scanline, &status));
#endif
}
TEST_F(AnimatedWebpTest, FrameAtOriginFallingOffImageFails) {
PrepareWriterFor5x5Image(1);
ScanlineStatus status;
FrameSpec frame_spec;
frame_spec.top = 0;
frame_spec.left = 0;
frame_spec.pixel_format = RGB_888;
frame_spec.width = 6;
frame_spec.height = 5;
EXPECT_FALSE(writer_->PrepareNextFrame(&frame_spec, &status));
EXPECT_EQ(SCANLINE_STATUS_INVOCATION_ERROR, status.type());
status = ScanlineStatus(SCANLINE_STATUS_SUCCESS);
frame_spec.width = 5;
frame_spec.height = 6;
EXPECT_FALSE(writer_->PrepareNextFrame(&frame_spec, &status));
EXPECT_EQ(SCANLINE_STATUS_INVOCATION_ERROR, status.type());
status = ScanlineStatus(SCANLINE_STATUS_SUCCESS);
frame_spec.width = 5;
frame_spec.height = 5;
EXPECT_TRUE(writer_->PrepareNextFrame(&frame_spec, &status));
}
TEST_F(AnimatedWebpTest, FrameInMiddleFallingOffImageFails) {
PrepareWriterFor5x5Image(1);
ScanlineStatus status;
FrameSpec frame_spec;
frame_spec.width = 5;
frame_spec.height = 5;
frame_spec.pixel_format = RGB_888;
frame_spec.top = 1;
frame_spec.left = 0;
EXPECT_FALSE(writer_->PrepareNextFrame(&frame_spec, &status));
EXPECT_EQ(SCANLINE_STATUS_INVOCATION_ERROR, status.type());
status = ScanlineStatus(SCANLINE_STATUS_SUCCESS);
frame_spec.top = 0;
frame_spec.left = 1;
EXPECT_FALSE(writer_->PrepareNextFrame(&frame_spec, &status));
EXPECT_EQ(SCANLINE_STATUS_INVOCATION_ERROR, status.type());
status = ScanlineStatus(SCANLINE_STATUS_SUCCESS);
frame_spec.top = 0;
frame_spec.left = 0;
EXPECT_TRUE(writer_->PrepareNextFrame(&frame_spec, &status));
}
} // namespace