blob: 63b515fb49e6f5a186adbcbabebef33670d54e3e [file] [log] [blame]
/*
* Copyright 2009 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: Bryan McQuade, Matthew Steele
#include <vector>
#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/string.h"
#include "pagespeed/kernel/image/jpeg_optimizer.h"
#include "pagespeed/kernel/image/jpeg_optimizer_test_helper.h"
#include "pagespeed/kernel/image/test_utils.h"
// DO NOT INCLUDE LIBJPEG HEADERS HERE. Doing so causes build errors
// on Windows. If you need to call out to libjpeg, please add helper
// methods in jpeg_optimizer_test_helper.h.
namespace {
using net_instaweb::MockMessageHandler;
using net_instaweb::NullMutex;
using pagespeed::image_compression::ColorSampling;
using pagespeed::image_compression::JpegCompressionOptions;
using pagespeed::image_compression::JpegLossyOptions;
using pagespeed::image_compression::kJpegTestDir;
using pagespeed::image_compression::OptimizeJpeg;
using pagespeed::image_compression::OptimizeJpegWithOptions;
using pagespeed::image_compression::ReadTestFileWithExt;
using pagespeed_testing::image_compression::GetNumScansInJpeg;
using pagespeed_testing::image_compression::GetJpegNumComponentsAndSamplingFactors; // NOLINT
using pagespeed_testing::image_compression::IsJpegSegmentPresent;
using pagespeed_testing::image_compression::GetColorProfileMarker;
using pagespeed_testing::image_compression::GetExifDataMarker;
// The JPEG_TEST_DIR_PATH macro is set by the gyp target that builds this file.
const char kAppSegmentsJpegFile[] = "app_segments.jpg";
struct ImageCompressionInfo {
const char* filename;
size_t original_size;
size_t compressed_size;
size_t lossy_compressed_size;
size_t progressive_size;
size_t progressive_and_lossy_compressed_size;
};
ImageCompressionInfo kValidImages[] = {
{ "sjpeg1.jpg", 1552, 1536, 1165, 1774, 1410 },
{ "sjpeg3.jpg", 44084, 41664, 26924, 40997, 25814 },
{ "sjpeg6.jpg", 149600, 147163, 89671, 146038, 84641 },
{ "testgray.jpg", 5014, 3072, 3060, 3094, 3078 },
{ "sjpeg2.jpg", 3612, 3283, 3652, 3475, 3833 },
{ "sjpeg4.jpg", 168895, 168240, 50936, 162867, 48731 },
{ "test411.jpg", 6883, 4367, 3705, 4540, 3849 },
{ "test420.jpg", 6173, 3657, 3651, 3796, 3787 },
{ "test422.jpg", 6501, 3985, 3709, 4152, 3852 },
};
const char *kInvalidFiles[] = {
"notajpeg.png", // A png.
"notajpeg.gif", // A gif.
"emptyfile.jpg", // A zero-byte file.
"corrupt.jpg", // Invalid huffman code in the image data section.
};
const size_t kValidImageCount = arraysize(kValidImages);
const size_t kInvalidFileCount = arraysize(kInvalidFiles);
class JpegOptimizerTest : public testing::Test {
public:
JpegOptimizerTest()
: message_handler_(new NullMutex) {
}
void AssertColorSampling(const GoogleString& data,
int expected_h_sampling_factor,
int expected_v_sampling_factor) {
int num_components, h_sampling_factor, v_sampling_factor;
ASSERT_TRUE(GetJpegNumComponentsAndSamplingFactors(data,
&num_components,
&h_sampling_factor,
&v_sampling_factor));
ASSERT_LE(1, num_components);
ASSERT_EQ(expected_h_sampling_factor, h_sampling_factor);
ASSERT_EQ(expected_v_sampling_factor, v_sampling_factor);
}
void AssertJpegOptimizeWithSampling(
const GoogleString &src_data, GoogleString* dest_data,
ColorSampling color_sampling, int h_sampling_factor,
int v_sampling_factor) {
dest_data->clear();
JpegCompressionOptions options;
options.lossy = true;
options.lossy_options.color_sampling = color_sampling;
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, dest_data, options,
&message_handler_));
AssertColorSampling(*dest_data, h_sampling_factor, v_sampling_factor);
}
protected:
net_instaweb::MockMessageHandler message_handler_;
private:
DISALLOW_COPY_AND_ASSIGN(JpegOptimizerTest);
};
TEST_F(JpegOptimizerTest, ValidJpegs) {
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
GoogleString dest_data;
ASSERT_TRUE(OptimizeJpeg(src_data, &dest_data, &message_handler_));
EXPECT_EQ(kValidImages[i].original_size, src_data.size())
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].compressed_size, dest_data.size())
<< kValidImages[i].filename;
ASSERT_LE(dest_data.size(), src_data.size());
}
}
TEST_F(JpegOptimizerTest, ValidJpegsLossy) {
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
pagespeed::image_compression::JpegCompressionOptions options;
options.lossy = true;
GoogleString dest_data;
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_))
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].original_size, src_data.size())
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].lossy_compressed_size, dest_data.size())
<< kValidImages[i].filename;
}
}
TEST_F(JpegOptimizerTest, ValidJpegLossyAndColorSampling) {
int test_422_file_idx = 8;
GoogleString src_data;
GoogleString src_filename = kValidImages[test_422_file_idx].filename;
ReadTestFileWithExt(kJpegTestDir, src_filename.c_str(), &src_data);
JpegCompressionOptions options;
options.lossy = true;
GoogleString dest_data;
// Calling optimize will use default color sampling which is 420.
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
size_t lossy_420_size =
kValidImages[test_422_file_idx].lossy_compressed_size;
EXPECT_EQ(lossy_420_size, dest_data.size()) << src_filename;
AssertColorSampling(dest_data, 2, 2);
// Calling optimize with ColorSampling::YUV420 will give samping as 420.
AssertJpegOptimizeWithSampling(src_data, &dest_data,
pagespeed::image_compression::YUV420, 2, 2);
EXPECT_EQ(lossy_420_size, dest_data.size()) << src_filename;
// Calling optimize with ColorSampling::RETAIN will leave samping as 422.
AssertJpegOptimizeWithSampling(src_data, &dest_data,
pagespeed::image_compression::RETAIN, 2, 1);
size_t lossy_retain_size = dest_data.size();
EXPECT_GT(lossy_retain_size, lossy_420_size) << src_filename;
// Calling optimize with ColorSampling::YUV422 will give samping as 422.
AssertJpegOptimizeWithSampling(src_data, &dest_data,
pagespeed::image_compression::YUV422, 2, 1);
EXPECT_EQ(lossy_retain_size, dest_data.size()) << src_filename;
// Calling optimize with ColorSampling::YUV444 will give samping as 444.
AssertJpegOptimizeWithSampling(src_data, &dest_data,
pagespeed::image_compression::YUV444, 1, 1);
EXPECT_LT(lossy_retain_size, dest_data.size()) << src_filename;
}
TEST_F(JpegOptimizerTest, ValidJpegRetainColorProfile) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kAppSegmentsJpegFile, &src_data);
GoogleString dest_data;
// Testing lossless flow.
JpegCompressionOptions options;
options.retain_color_profile = true;
ASSERT_TRUE(IsJpegSegmentPresent(src_data, GetColorProfileMarker()));
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));
options.retain_color_profile = false;
dest_data.clear();
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));
// Testing lossy flow.
options.lossy = true;
options.retain_color_profile = true;
dest_data.clear();
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));
options.retain_color_profile = false;
dest_data.clear();
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetColorProfileMarker()));
}
TEST_F(JpegOptimizerTest, ValidJpegRetainExifData) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kAppSegmentsJpegFile, &src_data);
GoogleString dest_data;
// Testing lossless flow.
JpegCompressionOptions options;
options.retain_exif_data = true;
ASSERT_TRUE(IsJpegSegmentPresent(src_data, GetExifDataMarker()));
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));
options.retain_exif_data = false;
dest_data.clear();
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));
// Testing lossy flow.
options.lossy = true;
options.retain_exif_data = true;
dest_data.clear();
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_TRUE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));
options.retain_exif_data = false;
dest_data.clear();
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
ASSERT_FALSE(IsJpegSegmentPresent(dest_data, GetExifDataMarker()));
}
TEST_F(JpegOptimizerTest, ValidJpegLossyWithNProgressiveScans) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kAppSegmentsJpegFile, &src_data);
GoogleString dest_data;
// Testing lossless flow.
JpegCompressionOptions options;
options.progressive = true;
EXPECT_EQ(1, GetNumScansInJpeg(src_data));
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
int num_scans = GetNumScansInJpeg(dest_data);
EXPECT_LT(1, num_scans);
dest_data.clear();
options.lossy = true;
options.lossy_options.num_scans = 3;
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
EXPECT_EQ(3, GetNumScansInJpeg(dest_data));
dest_data.clear();
// jpeg has max scan limit based on image color spaces, So trying to set
// num scans to a large value should be handled gracefully and default to
// jpeg scan limit if the specified value is greater.
options.lossy_options.num_scans = 1000;
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
EXPECT_EQ(num_scans, GetNumScansInJpeg(dest_data));
}
TEST_F(JpegOptimizerTest, ValidJpegsProgressive) {
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
pagespeed::image_compression::JpegCompressionOptions options;
options.progressive = true;
GoogleString dest_data;
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_))
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].original_size, src_data.size())
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].progressive_size, dest_data.size())
<< kValidImages[i].filename;
}
}
TEST_F(JpegOptimizerTest, ValidJpegsProgressiveAndLossy) {
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
pagespeed::image_compression::JpegCompressionOptions options;
options.lossy = true;
options.progressive = true;
GoogleString dest_data;
ASSERT_TRUE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_))
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].original_size, src_data.size())
<< kValidImages[i].filename;
EXPECT_EQ(kValidImages[i].progressive_and_lossy_compressed_size,
dest_data.size()) << kValidImages[i].filename;
}
}
TEST_F(JpegOptimizerTest, InvalidJpegs) {
for (size_t i = 0; i < kInvalidFileCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
GoogleString dest_data;
ASSERT_FALSE(OptimizeJpeg(src_data, &dest_data, &message_handler_));
}
}
TEST_F(JpegOptimizerTest, InvalidJpegsLossy) {
for (size_t i = 0; i < kInvalidFileCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
JpegCompressionOptions options;
options.lossy = true;
GoogleString dest_data;
ASSERT_FALSE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
}
}
TEST_F(JpegOptimizerTest, InvalidJpegsProgressive) {
for (size_t i = 0; i < kInvalidFileCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
JpegCompressionOptions options;
options.progressive = true;
GoogleString dest_data;
ASSERT_FALSE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
}
}
TEST_F(JpegOptimizerTest, InvalidJpegsProgressiveAndLossy) {
for (size_t i = 0; i < kInvalidFileCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &src_data);
JpegCompressionOptions options;
options.lossy = true;
options.progressive = true;
GoogleString dest_data;
ASSERT_FALSE(OptimizeJpegWithOptions(src_data, &dest_data, options,
&message_handler_));
}
}
// Test that after reading an invalid jpeg, the reader cleans its state so that
// it can read a correct jpeg again.
TEST_F(JpegOptimizerTest, CleanupAfterReadingInvalidJpeg) {
// Compress each input image with a reinitialized JpegOptimizer.
// We will compare these files with the output we get from
// a JpegOptimizer that had an error.
std::vector<GoogleString> correctly_compressed;
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString src_data;
ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename, &src_data);
correctly_compressed.push_back("");
GoogleString &dest_data = correctly_compressed.back();
ASSERT_TRUE(OptimizeJpeg(src_data, &dest_data, &message_handler_));
}
// The invalid files are all invalid in different ways, and we want to cover
// all the ways jpeg decoding can fail. So, we want at least as many valid
// images as invalid ones.
ASSERT_GE(kValidImageCount, kInvalidFileCount);
for (size_t i = 0; i < kInvalidFileCount; ++i) {
GoogleString invalid_src_data;
ReadTestFileWithExt(kJpegTestDir, kInvalidFiles[i], &invalid_src_data);
GoogleString invalid_dest_data;
GoogleString valid_src_data;
ReadTestFileWithExt(kJpegTestDir, kValidImages[i].filename,
&valid_src_data);
GoogleString valid_dest_data;
ASSERT_FALSE(OptimizeJpeg(invalid_src_data, &invalid_dest_data,
&message_handler_));
ASSERT_TRUE(OptimizeJpeg(valid_src_data, &valid_dest_data,
&message_handler_));
// Diff the jpeg created by CreateOptimizedJpeg() with the one created
// with a reinitialized JpegOptimizer.
ASSERT_EQ(valid_dest_data, correctly_compressed.at(i));
}
}
} // namespace