blob: 526d08aed9c5afcdff439eed25bd2c79f2ae2365 [file] [log] [blame]
/*
* Copyright 2011 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: Satyanarayana Manyam
#include <cstddef>
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/image/gif_reader.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/scanline_interface.h"
#include "pagespeed/kernel/image/test_utils.h"
namespace {
using net_instaweb::MockMessageHandler;
using net_instaweb::NullMutex;
using pagespeed::image_compression::kGifTestDir;
using pagespeed::image_compression::kPngSuiteTestDir;
using pagespeed::image_compression::kPngSuiteGifTestDir;
using pagespeed::image_compression::kPngTestDir;
using pagespeed::image_compression::GifReader;
using pagespeed::image_compression::ImageConverter;
using pagespeed::image_compression::IMAGE_GIF;
using pagespeed::image_compression::IMAGE_PNG;
using pagespeed::image_compression::IMAGE_WEBP;
using pagespeed::image_compression::JpegLossyOptions;
using pagespeed::image_compression::PngOptimizer;
using pagespeed::image_compression::PngReader;
using pagespeed::image_compression::PngReaderInterface;
using pagespeed::image_compression::WebpConfiguration;
using pagespeed::image_compression::ReadTestFile;
using pagespeed::image_compression::ScanlineReaderInterface;
using pagespeed::image_compression::ScanlineStatus;
using pagespeed::image_compression::ScanlineWriterInterface;
using pagespeed::image_compression::kMessagePatternLibpngError;
using pagespeed::image_compression::kMessagePatternLibpngWarning;
using pagespeed::image_compression::kMessagePatternPixelFormat;
using pagespeed::image_compression::kMessagePatternStats;
using pagespeed::image_compression::kMessagePatternUnexpectedEOF;
using pagespeed::image_compression::kMessagePatternWritingToWebp;
struct ImageCompressionInfo {
const char* filename;
size_t original_size;
size_t compressed_size;
bool is_png;
};
// These images were obtained from
// http://www.libpng.org/pub/png/pngsuite.html
ImageCompressionInfo kValidImages[] = {
{ "basi0g01", 217, 208, 1},
{ "basi0g02", 154, 154, 1},
{ "basi0g04", 247, 145, 1},
{ "basi0g08", 254, 250, 1},
{ "basi0g16", 299, 285, 1},
{ "basi2c08", 315, 313, 1},
{ "basi2c16", 595, 419, 0},
{ "basi3p01", 132, 132, 1},
{ "basi3p02", 193, 178, 1},
{ "basi3p04", 327, 312, 1},
{ "basi4a08", 214, 209, 1},
{ "basi4a16", 2855, 1980, 1},
{ "basi6a08", 361, 350, 1},
{ "basi6a16", 4180, 4133, 1},
{ "basn0g01", 164, 164, 1},
{ "basn0g02", 104, 104, 1},
{ "basn0g04", 145, 103, 1},
{ "basn0g08", 138, 132, 1},
{ "basn0g16", 167, 152, 1},
{ "basn2c08", 145, 145, 1},
{ "basn2c16", 302, 274, 1},
{ "basn3p01", 112, 112, 1},
{ "basn3p02", 146, 131, 1},
{ "basn3p04", 216, 201, 1},
{ "basn4a08", 126, 121, 1},
{ "basn4a16", 2206, 1185, 1},
{ "basn6a08", 184, 176, 1},
{ "basn6a16", 3435, 3271, 1},
{ "bgai4a08", 214, 209, 1},
{ "bgai4a16", 2855, 1980, 1},
{ "bgan6a08", 184, 176, 1},
{ "bgan6a16", 3435, 3271, 1},
{ "bgbn4a08", 140, 121, 1},
{ "bggn4a16", 2220, 1185, 1},
{ "bgwn6a08", 202, 176, 1},
{ "bgyn6a16", 3453, 3271, 1},
{ "cdfn2c08", 404, 498, 1},
{ "cdhn2c08", 344, 476, 1},
{ "cdsn2c08", 232, 255, 1},
{ "cdun2c08", 724, 928, 1},
{ "ch1n3p04", 258, 201, 1},
{ "cm0n0g04", 292, 271, 1},
{ "cm7n0g04", 292, 271, 1},
{ "cm9n0g04", 292, 271, 1},
{ "cs3n2c16", 214, 178, 1},
{ "cs3n3p08", 259, 244, 1},
{ "cs5n2c08", 186, 226, 1},
{ "cs5n3p08", 271, 256, 1},
{ "cs8n2c08", 149, 226, 1},
{ "cs8n3p08", 256, 256, 1},
{ "ct0n0g04", 273, 271, 1},
{ "ct1n0g04", 792, 271, 1},
{ "ctzn0g04", 753, 271, 1},
{ "f00n0g08", 319, 312, 1},
{ "f01n0g08", 321, 246, 1},
{ "f02n0g08", 355, 289, 1},
{ "f03n0g08", 389, 292, 1},
{ "f04n0g08", 269, 273, 1},
{ "g03n0g16", 345, 273, 1},
{ "g03n2c08", 370, 396, 1},
{ "g03n3p04", 214, 214, 1},
{ "g04n0g16", 363, 287, 1},
{ "g04n2c08", 377, 399, 1},
{ "g04n3p04", 219, 219, 1},
{ "g05n0g16", 339, 275, 1},
{ "g05n2c08", 350, 402, 1},
{ "g05n3p04", 206, 206, 1},
{ "g07n0g16", 321, 261, 1},
{ "g07n2c08", 340, 401, 1},
{ "g07n3p04", 207, 207, 1},
{ "g10n0g16", 262, 210, 1},
{ "g10n2c08", 285, 403, 1},
{ "g10n3p04", 214, 214, 1},
{ "g25n0g16", 383, 305, 1},
{ "g25n2c08", 405, 399, 1},
{ "g25n3p04", 215, 215, 1},
{ "oi1n0g16", 167, 152, 1},
{ "oi1n2c16", 302, 274, 1},
{ "oi2n0g16", 179, 152, 1},
{ "oi2n2c16", 314, 274, 1},
{ "oi4n0g16", 203, 152, 1},
{ "oi4n2c16", 338, 274, 1},
{ "oi9n0g16", 1283, 152, 1},
{ "oi9n2c16", 3038, 274, 1},
{ "pp0n2c16", 962, 419, 0},
{ "pp0n6a08", 818, 818, 1},
{ "ps1n0g08", 1477, 132, 1},
{ "ps1n2c16", 1641, 274, 1},
{ "ps2n0g08", 2341, 132, 1},
{ "ps2n2c16", 2505, 274, 1},
{ "s01i3p01", 113, 98, 1},
{ "s01n3p01", 113, 98, 1},
{ "s02i3p01", 114, 99, 1},
{ "s02n3p01", 115, 100, 1},
{ "s03i3p01", 118, 103, 1},
{ "s03n3p01", 120, 105, 1},
{ "s04i3p01", 126, 111, 1},
{ "s04n3p01", 121, 106, 1},
{ "s05i3p02", 134, 119, 1},
{ "s05n3p02", 129, 114, 1},
{ "s06i3p02", 143, 128, 1},
{ "s06n3p02", 131, 116, 1},
{ "s07i3p02", 149, 134, 1},
{ "s07n3p02", 138, 123, 1},
{ "s08i3p02", 149, 134, 1},
{ "s08n3p02", 139, 124, 1},
{ "s09i3p02", 147, 132, 1},
{ "s09n3p02", 143, 128, 1},
{ "s32i3p04", 355, 340, 1},
{ "s32n3p04", 263, 248, 1},
{ "s33i3p04", 385, 370, 1},
{ "s33n3p04", 329, 314, 1},
{ "s34i3p04", 349, 332, 1},
{ "s34n3p04", 248, 229, 1},
{ "s35i3p04", 399, 384, 1},
{ "s35n3p04", 338, 313, 1},
{ "s36i3p04", 356, 339, 1},
{ "s36n3p04", 258, 240, 1},
{ "s37i3p04", 393, 378, 1},
{ "s37n3p04", 336, 317, 1},
{ "s38i3p04", 357, 339, 1},
{ "s38n3p04", 245, 228, 1},
{ "s39i3p04", 420, 405, 1},
{ "s39n3p04", 352, 336, 1},
{ "s40i3p04", 357, 340, 1},
{ "s40n3p04", 256, 237, 1},
{ "tbbn1g04", 419, 405, 1},
{ "tbbn2c16", 1994, 1095, 1},
{ "tbbn3p08", 1128, 1095, 1},
{ "tbgn2c16", 1994, 1095, 1},
{ "tbgn3p08", 1128, 1095, 1},
{ "tbrn2c08", 1347, 1095, 1},
{ "tbwn1g16", 1146, 582, 1},
{ "tbwn3p08", 1131, 1095, 1},
{ "tbyn3p08", 1131, 1095, 1},
{ "tp0n1g08", 689, 568, 1},
{ "tp1n3p08", 1115, 1095, 1},
{ "z00n2c08", 3172, 224, 1},
{ "z03n2c08", 232, 224, 1},
{ "z06n2c08", 224, 224, 1},
{ "z09n2c08", 224, 224, 1},
{ "basi3p08", 1527, 567, 0},
{ "basn3p08", 1286, 567, 0},
{ "ccwn2c08", 1514, 757, 0},
{ "ccwn3p08", 1554, 775, 0},
{ "ch2n3p08", 1810, 567, 0},
{ "f00n2c08", 2475, 695, 0},
{ "f01n2c08", 1180, 648, 0},
{ "f02n2c08", 1729, 688, 0},
{ "f03n2c08", 1291, 690, 0},
{ "f04n2c08", 985, 653, 0},
{ "tp0n2c08", 1311, 863, 0},
{ "tp0n3p08", 1120, 863, 0},
};
const char* kInvalidFiles[] = {
"nosuchfile",
"emptyfile",
"x00n0g01",
"xcrn0g04",
"xlfn0g04",
};
struct GifImageCompressionInfo {
const char* filename;
size_t original_size;
size_t png_size;
size_t jpeg_size;
size_t webp_size;
};
GifImageCompressionInfo kValidGifImages[] = {
{ "basi0g01", 153, 166, 1036, 120},
{ "basi0g02", 185, 112, 664, 74},
{ "basi0g04", 344, 144, 439, 104},
{ "basi0g08", 1736, 116, 468, 582},
{ "basn0g01", 153, 166, 1036, 120},
{ "basn0g02", 185, 112, 664, 74},
{ "basn0g04", 344, 144, 439, 104},
{ "basn0g08", 1736, 116, 468, 582},
{ "basi3p01", 138, 96, 793, 56},
{ "basi3p02", 186, 115, 1162, 74},
{ "basi3p04", 344, 185, 1002, 136},
{ "basi3p08", 1737, 1270, 936, 810},
{ "basn3p01", 138, 96, 793, 56},
{ "basn3p02", 186, 115, 1162, 74},
{ "basn3p04", 344, 185, 1002, 136},
{ "basn3p08", 1737, 1270, 936, 810}
};
const size_t kValidImageCount = arraysize(kValidImages);
const size_t kValidGifImageCount = arraysize(kValidGifImages);
const size_t kInvalidFileCount = arraysize(kInvalidFiles);
class ImageConverterTest : public testing::Test {
public:
ImageConverterTest()
: message_handler_(new NullMutex) {
}
protected:
virtual void SetUp() {
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngError);
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngWarning);
message_handler_.AddPatternToSkipPrinting(kMessagePatternPixelFormat);
message_handler_.AddPatternToSkipPrinting(kMessagePatternStats);
message_handler_.AddPatternToSkipPrinting(kMessagePatternUnexpectedEOF);
message_handler_.AddPatternToSkipPrinting(kMessagePatternWritingToWebp);
}
protected:
MockMessageHandler message_handler_;
net_instaweb::scoped_ptr<PngReaderInterface> png_struct_reader_;
private:
DISALLOW_COPY_AND_ASSIGN(ImageConverterTest);
};
TEST_F(ImageConverterTest, OptimizePngOrConvertToJpeg_invalidPngs) {
png_struct_reader_.reset(new PngReader(&message_handler_));
pagespeed::image_compression::JpegCompressionOptions options;
for (size_t i = 0; i < kInvalidFileCount; i++) {
GoogleString in, out;
bool is_out_png;
ReadTestFile(kPngSuiteTestDir, kInvalidFiles[i], "png", &in);
ASSERT_FALSE(ImageConverter::OptimizePngOrConvertToJpeg(
*png_struct_reader_, in, options, &out, &is_out_png,
&message_handler_));
}
}
TEST_F(ImageConverterTest, OptimizePngOrConvertToJpeg) {
png_struct_reader_.reset(new PngReader(&message_handler_));
pagespeed::image_compression::JpegCompressionOptions options;
// We are using default lossy options for conversion.
options.lossy = true;
options.progressive = false;
for (size_t i = 0; i < kValidImageCount; i++) {
GoogleString in, out;
bool is_out_png;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png", &in);
ASSERT_TRUE(ImageConverter::OptimizePngOrConvertToJpeg(
*png_struct_reader_, in, options, &out, &is_out_png,
&message_handler_));
// Verify that the size matches.
EXPECT_EQ(kValidImages[i].compressed_size, out.size())
<< "size mismatch for " << kValidImages[i].filename;
// Verify that out put image type matches.
EXPECT_EQ(kValidImages[i].is_png, is_out_png)
<< "image type mismatch for " << kValidImages[i].filename;
}
}
TEST_F(ImageConverterTest, ConvertPngToWebp_invalidPngs) {
png_struct_reader_.reset(new PngReader(&message_handler_));
WebpConfiguration webp_config;
for (size_t i = 0; i < kInvalidFileCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kInvalidFiles[i], "png", &in);
bool is_opaque = false;
ASSERT_FALSE(ImageConverter::ConvertPngToWebp(
*png_struct_reader_, in, webp_config, &out, &is_opaque,
&message_handler_));
}
}
TEST_F(ImageConverterTest, ConvertOpaqueGifToPng) {
png_struct_reader_.reset(new GifReader(&message_handler_));
for (size_t i = 0; i < kValidGifImageCount; i++) {
GoogleString in, out;
ReadTestFile(
kPngSuiteGifTestDir, kValidGifImages[i].filename, "gif", &in);
EXPECT_EQ(kValidGifImages[i].original_size, in.size())
<< "input size mismatch for " << kValidGifImages[i].filename;
ASSERT_TRUE(PngOptimizer::OptimizePngBestCompression(
*png_struct_reader_, in, &out, &message_handler_));
// Verify that the size matches.
EXPECT_EQ(kValidGifImages[i].png_size, out.size())
<< "output size mismatch for " << kValidGifImages[i].filename;
}
}
TEST_F(ImageConverterTest, ConvertOpaqueGifToJpeg) {
png_struct_reader_.reset(new GifReader(&message_handler_));
pagespeed::image_compression::JpegCompressionOptions options;
options.lossy = true;
options.progressive = false;
options.lossy_options.quality = 100;
for (size_t i = 0; i < kValidGifImageCount; i++) {
GoogleString in, out;
ReadTestFile(
kPngSuiteGifTestDir, kValidGifImages[i].filename, "gif", &in);
EXPECT_EQ(kValidGifImages[i].original_size, in.size())
<< "input size mismatch for " << kValidGifImages[i].filename;
ASSERT_TRUE(ImageConverter::ConvertPngToJpeg(
*png_struct_reader_, in, options, &out, &message_handler_));
// Verify that the size matches.
EXPECT_EQ(kValidGifImages[i].jpeg_size, out.size())
<< "output size mismatch for " << kValidGifImages[i].filename;
}
}
TEST_F(ImageConverterTest, ConvertOpaqueGifToWebp) {
png_struct_reader_.reset(new GifReader(&message_handler_));
pagespeed::image_compression::WebpConfiguration options;
for (size_t i = 0; i < kValidGifImageCount; i++) {
GoogleString in, out;
ReadTestFile(
kPngSuiteGifTestDir, kValidGifImages[i].filename, "gif", &in);
EXPECT_EQ(kValidGifImages[i].original_size, in.size())
<< "input size mismatch for " << kValidGifImages[i].filename;
bool is_opaque = false;
ASSERT_TRUE(ImageConverter::ConvertPngToWebp(
*png_struct_reader_, in, options, &out, &is_opaque, &message_handler_));
// Verify that the size matches.
// TODO(vchudnov): Have a more thorough comparison.
EXPECT_LT(kValidGifImages[i].webp_size, in.size())
<< "webp size is not smaller for " << kValidGifImages[i].filename;
EXPECT_TRUE(is_opaque) << kValidGifImages[i].filename;
}
}
TEST_F(ImageConverterTest, ConvertTransparentGifToPng) {
png_struct_reader_.reset(new GifReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kGifTestDir, "transparent", "gif", &in);
EXPECT_EQ(static_cast<size_t>(55800), in.size())
<< "input size mismatch";
ASSERT_TRUE(PngOptimizer::OptimizePngBestCompression(
*png_struct_reader_, in, &out, &message_handler_));
// Verify that the size matches.
EXPECT_EQ(static_cast<size_t>(25020), out.size())
<< "output size mismatch";
}
TEST_F(ImageConverterTest, ConvertTransparentGifToWebp) {
png_struct_reader_.reset(new GifReader(&message_handler_));
pagespeed::image_compression::WebpConfiguration options;
GoogleString in, out;
ReadTestFile(kGifTestDir, "transparent", "gif", &in);
EXPECT_EQ(static_cast<size_t>(55800), in.size())
<< "input size mismatch";
bool is_opaque = false;
ASSERT_TRUE(ImageConverter::ConvertPngToWebp(
*png_struct_reader_, in, options, &out, &is_opaque, &message_handler_));
// Verify that the size matches.
// TODO(vchudnov): Have a more thorough comparison.
EXPECT_LT(out.size(), in.size())
<< "webpsize is not smaller";
EXPECT_FALSE(is_opaque);
}
TEST_F(ImageConverterTest, NotConvertTransparentGifToJpeg) {
png_struct_reader_.reset(new GifReader(&message_handler_));
pagespeed::image_compression::JpegCompressionOptions options;
options.lossy = true;
options.progressive = false;
options.lossy_options.quality = 100;
GoogleString in, out;
ReadTestFile(kGifTestDir, "transparent", "gif", &in);
EXPECT_EQ(static_cast<size_t>(55800), in.size())
<< "input size mismatch";
ASSERT_FALSE(ImageConverter::ConvertPngToJpeg(
*png_struct_reader_, in, options, &out, &message_handler_));
// Verify that the size matches.
EXPECT_EQ(static_cast<size_t>(0), out.size())
<< "output size mismatch";
}
TEST_F(ImageConverterTest, ConvertPaddedGifToWebp) {
GoogleString in, out;
WebpConfiguration options;
ScanlineStatus status;
ReadTestFile(kGifTestDir, "frame_smaller_than_screen", "gif", &in);
EXPECT_EQ(static_cast<size_t>(45), in.size())
<< "input size mismatch";
net_instaweb::scoped_ptr<ScanlineReaderInterface> reader(
CreateScanlineReader(IMAGE_GIF, in.c_str(), in.size(),
&message_handler_, &status));
EXPECT_TRUE(status.Success()) << status.ToString();
net_instaweb::scoped_ptr<ScanlineWriterInterface> writer(
CreateScanlineWriter(IMAGE_WEBP, reader->GetPixelFormat(),
reader->GetImageWidth(), reader->GetImageHeight(),
&options, &out, &message_handler_, &status));
EXPECT_TRUE(status.Success()) << status.ToString();
status = ImageConverter::ConvertImageWithStatus(reader.get(), writer.get());
EXPECT_TRUE(status.Success()) << status.ToString();
}
// To manually inspect all gif conversions tested, uncomment the lines
// indicated in the *Convert*GifTo* test cases above, run this
// test, and then generate an html page as follows:
//
/*
(echo '<table border="1" style="background-color: gray;">'
echo '<tr><th>name</th><th>gif</th><th>png</th>'
echo '<th>jpeg</th><th>webp</th></tr>'
ls /tmp/image_converter_test/gif-*gif | sed -s 's/\.gif//' | \
xargs --replace=X \
echo '<tr><td>X</td><td><img src="X.gif"></td><td><img src="X.png"></td>' \
'<td><img src="X.jpg"></td><td><img src="X.webp"></td></tr>'
echo '</table>') > /tmp/allimages.html
*/
// TODO(vchudnov): add webp tests to do pixel-for-pixel comparisons
// and to test GetSmallestOfPngJpegWebp
} // namespace