blob: 653b3aaaeaa62bdaf397b8f63fb50ac492d9f6e6 [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
#include <stdbool.h>
#include <cstdlib>
#include "pagespeed/kernel/base/basictypes.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/string.h"
#include "pagespeed/kernel/image/gif_reader.h"
#include "pagespeed/kernel/image/png_optimizer.h"
#include "pagespeed/kernel/image/read_image.h"
#include "pagespeed/kernel/image/scanline_utils.h"
#include "pagespeed/kernel/image/test_utils.h"
extern "C" {
#ifdef USE_SYSTEM_LIBPNG
#include "png.h" // NOLINT
#else
#include "third_party/libpng/src/png.h"
#endif
}
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::kValidGifImageCount;
using pagespeed::image_compression::kValidGifImages;
using pagespeed::image_compression::GifReader;
using pagespeed::image_compression::GRAY_8;
using pagespeed::image_compression::ImageCompressionInfo;
using pagespeed::image_compression::IMAGE_PNG;
using pagespeed::image_compression::PixelFormat;
using pagespeed::image_compression::PngCompressParams;
using pagespeed::image_compression::PngOptimizer;
using pagespeed::image_compression::PngReader;
using pagespeed::image_compression::PngReaderInterface;
using pagespeed::image_compression::PngScanlineReaderRaw;
using pagespeed::image_compression::PngScanlineReader;
using pagespeed::image_compression::PngScanlineWriter;
using pagespeed::image_compression::ReadTestFile;
using pagespeed::image_compression::RGB_888;
using pagespeed::image_compression::ScanlineReaderInterface;
using pagespeed::image_compression::ScanlineWriterInterface;
using pagespeed::image_compression::ScopedPngStruct;
using pagespeed::image_compression::kMessagePatternAnimatedGif;
using pagespeed::image_compression::kMessagePatternFailedToRead;
using pagespeed::image_compression::kMessagePatternLibpngError;
using pagespeed::image_compression::kMessagePatternLibpngFailure;
using pagespeed::image_compression::kMessagePatternLibpngWarning;
using pagespeed::image_compression::kMessagePatternUnexpectedEOF;
// Message to ignore.
const char kMessagePatternBadGifDescriptor[] =
"Failed to get image descriptor.";
const char kMessagePatternBadGifLine[] = "Failed to DGifGetLine";
const char kMessagePatternUnrecognizedColor[] = "Unrecognized color type.";
// "rgb_alpha.png" and "gray_alpha.png" have the same image contents, but
// with different format.
const char kImageRGBA[] = "rgb_alpha";
const char kImageGA[] = "gray_alpha";
// Structure that holds metadata and actual pixel data for a decoded
// PNG.
struct ReadPngDescriptor {
unsigned char* img_bytes; // The actual pixel data.
unsigned char* img_rgba_bytes; // RGBA data for the image.
unsigned long width; // NOLINT
unsigned long height; // NOLINT
int channels; // 3 for RGB, 4 for RGB+alpha
unsigned long row_bytes; // number of bytes in a row // NOLINT
unsigned char bg_red, bg_green, bg_blue;
bool bgcolor_retval;
ReadPngDescriptor() : img_bytes(NULL), img_rgba_bytes(NULL) {}
~ReadPngDescriptor() {
free(img_bytes);
if (channels != 4) {
free(img_rgba_bytes);
}
}
};
// Decode the PNG and store the decoded data and metadata in the
// ReadPngDescriptor struct.
void PopulateDescriptor(const GoogleString& img,
ReadPngDescriptor* desc,
const char* identifier) {
MockMessageHandler message_handler(new NullMutex);
PngScanlineReader scanline_reader(&message_handler);
scanline_reader.set_transform(
// Expand paletted colors into true RGB triplets
// Expand grayscale images to full 8 bits from 1, 2, or 4 bits/pixel
// Expand paletted or RGB images with transparency to full alpha
// channels so the data will be available as RGBA quartets.
PNG_TRANSFORM_EXPAND |
// Downsample images with 16bits per channel to 8bits per channel.
PNG_TRANSFORM_STRIP_16 |
// Convert grayscale images to RGB images.
PNG_TRANSFORM_GRAY_TO_RGB);
if (setjmp(*scanline_reader.GetJmpBuf())) {
FAIL();
}
PngReader reader(&message_handler);
if (!scanline_reader.InitializeRead(reader, img)) {
FAIL();
}
int channels;
switch (scanline_reader.GetPixelFormat()) {
case pagespeed::image_compression::RGB_888:
channels = 3;
break;
case pagespeed::image_compression::RGBA_8888:
channels = 4;
break;
case pagespeed::image_compression::GRAY_8:
channels = 1;
break;
default:
PS_LOG_INFO((&message_handler), \
"Unexpected pixel format: %d", scanline_reader.GetPixelFormat());
channels = -1;
break;
}
desc->width = scanline_reader.GetImageWidth();
desc->height = scanline_reader.GetImageHeight();
desc->channels = channels;
desc->row_bytes = scanline_reader.GetBytesPerScanline();
desc->img_bytes = static_cast<unsigned char*>(
malloc(desc->row_bytes * desc->height));
if (channels == 4) {
desc->img_rgba_bytes = desc->img_bytes;
} else {
desc->img_rgba_bytes = static_cast<unsigned char*>(
malloc(4 * desc->width * desc->height));
}
desc->bgcolor_retval = scanline_reader.GetBackgroundColor(
&desc->bg_red, &desc->bg_green, &desc->bg_blue);
unsigned char* next_scanline_to_write = desc->img_bytes;
while (scanline_reader.HasMoreScanLines()) {
void* scanline = NULL;
if (scanline_reader.ReadNextScanline(&scanline)) {
memcpy(next_scanline_to_write, scanline,
scanline_reader.GetBytesPerScanline());
next_scanline_to_write += scanline_reader.GetBytesPerScanline();
}
}
if (channels != 4) {
memset(desc->img_rgba_bytes, 0xff, 4 * desc->height * desc->width);
unsigned char* next_img_byte = desc->img_bytes;
unsigned char* next_rgba_byte = desc->img_rgba_bytes;
for (unsigned long pixel_count = 0; // NOLINT
pixel_count < desc->height * desc->width;
++pixel_count) {
if (channels == 3) {
memcpy(next_rgba_byte, next_img_byte, channels);
} else if (channels == 1) {
memset(next_rgba_byte, *next_img_byte, 3);
} else {
FAIL() << "Unexpected number of channels: " << channels;
}
next_img_byte += channels;
next_rgba_byte += 4;
}
}
}
void AssertPngEq(
const GoogleString& orig, const GoogleString& opt, const char* identifier,
const GoogleString& in_rgba) {
// Gather data and metadata for the original and optimized PNGs.
ReadPngDescriptor orig_desc;
PopulateDescriptor(orig, &orig_desc, identifier);
ReadPngDescriptor opt_desc;
PopulateDescriptor(opt, &opt_desc, identifier);
// Verify that the dimensions match.
EXPECT_EQ(orig_desc.width, opt_desc.width)
<< "width mismatch for " << identifier;
EXPECT_EQ(orig_desc.height, opt_desc.height)
<< "height mismatch for " << identifier;
// If PNG background chunks are supported, verify that the
// background chunks are not present in the optimized image.
#if defined(PNG_bKGD_SUPPORTED) || defined(PNG_READ_BACKGROUND_SUPPORTED)
EXPECT_FALSE(opt_desc.bgcolor_retval) << "Unexpected: bgcolor";
#endif
// Verify that the number of channels matches (should be 3 for RGB
// or 4 for RGB+alpha)
EXPECT_EQ(orig_desc.channels, opt_desc.channels)
<< "channel mismatch for " << identifier;
// Verify that the number of bytes in a row matches.
EXPECT_EQ(orig_desc.row_bytes, opt_desc.row_bytes)
<< "row_bytes mismatch for " << identifier;
// Verify that the actual image data matches.
if (orig_desc.row_bytes == opt_desc.row_bytes &&
orig_desc.height == opt_desc.height) {
const unsigned long img_bytes_size = // NOLINT
orig_desc.row_bytes * orig_desc.height;
EXPECT_EQ(0,
memcmp(orig_desc.img_bytes, opt_desc.img_bytes, img_bytes_size))
<< "image data mismatch for " << identifier;
}
if (!in_rgba.empty()) {
unsigned long num_rgba_bytes = // NOLINT
4 * opt_desc.height * opt_desc.width;
EXPECT_EQ(in_rgba.length(), num_rgba_bytes)
<< "rgba data size mismatch for " << identifier;
EXPECT_EQ(0,
memcmp(opt_desc.img_rgba_bytes, in_rgba.c_str(), num_rgba_bytes))
<< "rgba data bytes mismatch for " << identifier;
}
}
// If allow_expand_colors is set to false, both readers must have the same
// color values, dimension, and pixel format. If it is set to true,
// additionally reader1 can be GRAY_8 while reader2 can be RGB_888 and the
// colors will be expanded prior to comparison.
void AssertReadersMatch(ScanlineReaderInterface* reader1,
ScanlineReaderInterface* reader2,
bool allow_expand_colors) {
MockMessageHandler message_handler(new NullMutex);
// Make sure the images sizes and the pixel formats are the same.
ASSERT_EQ(reader1->GetImageWidth(), reader2->GetImageWidth());
ASSERT_EQ(reader1->GetImageHeight(), reader2->GetImageHeight());
bool expand_colors = false;
if (!allow_expand_colors) {
ASSERT_EQ(reader1->GetPixelFormat(), reader2->GetPixelFormat());
} else {
expand_colors = (reader1->GetPixelFormat() == GRAY_8 &&
reader2->GetPixelFormat() == RGB_888);
ASSERT_TRUE(expand_colors ||
(reader1->GetPixelFormat(), reader2->GetPixelFormat()));
}
const int width = reader1->GetImageWidth();
const int num_channels =
GetNumChannelsFromPixelFormat(reader1->GetPixelFormat(),
&message_handler);
uint8* pixels1 = NULL;
uint8* pixels2 = NULL;
// Decode and check the image a scanline at a time.
while (reader1->HasMoreScanLines() &&
reader2->HasMoreScanLines()) {
ASSERT_TRUE(reader1->ReadNextScanline(
reinterpret_cast<void**>(&pixels1)));
ASSERT_TRUE(reader2->ReadNextScanline(
reinterpret_cast<void**>(&pixels2)));
if (!expand_colors) {
for (int i = 0; i < width*num_channels; ++i) {
ASSERT_EQ(pixels1[i], pixels2[i]);
}
} else {
for (int i = 0; i < width; ++i) {
ASSERT_EQ(pixels1[i], pixels2[3*i]);
ASSERT_EQ(pixels1[i], pixels2[3*i+1]);
ASSERT_EQ(pixels1[i], pixels2[3*i+2]);
}
}
}
// Make sure both readers have exhausted all scanlines.
ASSERT_FALSE(reader1->HasMoreScanLines());
ASSERT_FALSE(reader2->HasMoreScanLines());
}
// These images were obtained from
// http://www.libpng.org/pub/png/pngsuite.html
ImageCompressionInfo kValidImages[] = {
ImageCompressionInfo("basi0g01", 217, 208, 217, 32, 32, 1, 0, 1, 0),
ImageCompressionInfo("basi0g02", 154, 154, 154, 32, 32, 2, 0, 2, 0),
ImageCompressionInfo("basi0g04", 247, 145, 247, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("basi0g08", 254, 250, 799, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("basi0g16", 299, 285, 1223, 32, 32, 16, 0, 16, 0),
ImageCompressionInfo("basi2c08", 315, 313, 1509, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("basi2c16", 595, 557, 2863, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("basi3p01", 132, 132, 132, 32, 32, 1, 3, 1, 3),
ImageCompressionInfo("basi3p02", 193, 178, 178, 32, 32, 2, 3, 2, 3),
ImageCompressionInfo("basi3p04", 327, 312, 312, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("basi3p08", 1527, 1518, 1527, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("basi4a08", 214, 209, 1450, 32, 32, 8, 4, 8, 4),
ImageCompressionInfo("basi4a16", 2855, 1980, 1980, 32, 32, 16, 4, 16, 4),
ImageCompressionInfo("basi6a08", 361, 350, 1591, 32, 32, 8, 6, 8, 6),
ImageCompressionInfo("basi6a16", 4180, 4133, 4423, 32, 32, 16, 6, 16, 6),
ImageCompressionInfo("basn0g01", 164, 164, 164, 32, 32, 1, 0, 1, 0),
ImageCompressionInfo("basn0g02", 104, 104, 104, 32, 32, 2, 0, 2, 0),
ImageCompressionInfo("basn0g04", 145, 103, 145, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("basn0g08", 138, 132, 730, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("basn0g16", 167, 152, 645, 32, 32, 16, 0, 16, 0),
ImageCompressionInfo("basn2c08", 145, 145, 1441, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("basn2c16", 302, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("basn3p01", 112, 112, 112, 32, 32, 1, 3, 1, 3),
ImageCompressionInfo("basn3p02", 146, 131, 131, 32, 32, 2, 3, 2, 3),
ImageCompressionInfo("basn3p04", 216, 201, 201, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("basn3p08", 1286, 1286, 1286, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("basn4a08", 126, 121, 1433, 32, 32, 8, 4, 8, 4),
ImageCompressionInfo("basn4a16", 2206, 1185, 1185, 32, 32, 16, 4, 16, 4),
ImageCompressionInfo("basn6a08", 184, 176, 1435, 32, 32, 8, 6, 8, 6),
ImageCompressionInfo("basn6a16", 3435, 3271, 4181, 32, 32, 16, 6, 16, 6),
ImageCompressionInfo("bgai4a08", 214, 209, 1450, 32, 32, 8, 4, 8, 4),
ImageCompressionInfo("bgai4a16", 2855, 1980, 1980, 32, 32, 16, 4, 16, 4),
ImageCompressionInfo("bgan6a08", 184, 176, 1435, 32, 32, 8, 6, 8, 6),
ImageCompressionInfo("bgan6a16", 3435, 3271, 4181, 32, 32, 16, 6, 16, 6),
ImageCompressionInfo("bgbn4a08", 140, 121, 1433, 32, 32, 8, 4, 8, 4),
ImageCompressionInfo("bggn4a16", 2220, 1185, 1185, 32, 32, 16, 4, 16, 4),
ImageCompressionInfo("bgwn6a08", 202, 176, 1435, 32, 32, 8, 6, 8, 6),
ImageCompressionInfo("bgyn6a16", 3453, 3271, 4181, 32, 32, 16, 6, 16, 6),
ImageCompressionInfo("ccwn2c08", 1514, 1456, 1742, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("ccwn3p08", 1554, 1499, 1510, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("cdfn2c08", 404, 498, 532, 8, 32, 8, 2, 8, 3),
ImageCompressionInfo("cdhn2c08", 344, 476, 491, 32, 8, 8, 2, 8, 3),
ImageCompressionInfo("cdsn2c08", 232, 255, 258, 8, 8, 8, 2, 8, 3),
ImageCompressionInfo("cdun2c08", 724, 928, 942, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("ch1n3p04", 258, 201, 201, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("ch2n3p08", 1810, 1286, 1286, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("cm0n0g04", 292, 271, 273, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("cm7n0g04", 292, 271, 273, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("cm9n0g04", 292, 271, 273, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("cs3n2c16", 214, 178, 216, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("cs3n3p08", 259, 244, 244, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("cs5n2c08", 186, 226, 256, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("cs5n3p08", 271, 256, 256, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("cs8n2c08", 149, 226, 256, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("cs8n3p08", 256, 256, 256, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("ct0n0g04", 273, 271, 273, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("ct1n0g04", 792, 271, 273, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("ctzn0g04", 753, 271, 273, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("f00n0g08", 319, 312, 319, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("f00n2c08", 2475, 1070, 2475, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("f01n0g08", 321, 246, 283, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("f01n2c08", 1180, 965, 2546, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("f02n0g08", 355, 289, 297, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("f02n2c08", 1729, 1024, 2512, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("f03n0g08", 389, 292, 296, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("f03n2c08", 1291, 1062, 2509, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("f04n0g08", 269, 273, 281, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("f04n2c08", 985, 985, 2546, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("g03n0g16", 345, 273, 308, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("g03n2c08", 370, 396, 490, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("g03n3p04", 214, 214, 214, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("g04n0g16", 363, 287, 310, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("g04n2c08", 377, 399, 493, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("g04n3p04", 219, 219, 219, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("g05n0g16", 339, 275, 306, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("g05n2c08", 350, 402, 488, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("g05n3p04", 206, 206, 206, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("g07n0g16", 321, 261, 305, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("g07n2c08", 340, 401, 488, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("g07n3p04", 207, 207, 207, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("g10n0g16", 262, 210, 306, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("g10n2c08", 285, 403, 495, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("g10n3p04", 214, 214, 214, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("g25n0g16", 383, 305, 305, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("g25n2c08", 405, 399, 470, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("g25n3p04", 215, 215, 215, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("oi1n0g16", 167, 152, 645, 32, 32, 16, 0, 16, 0),
ImageCompressionInfo("oi1n2c16", 302, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("oi2n0g16", 179, 152, 645, 32, 32, 16, 0, 16, 0),
ImageCompressionInfo("oi2n2c16", 314, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("oi4n0g16", 203, 152, 645, 32, 32, 16, 0, 16, 0),
ImageCompressionInfo("oi4n2c16", 338, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("oi9n0g16", 1283, 152, 645, 32, 32, 16, 0, 16, 0),
ImageCompressionInfo("oi9n2c16", 3038, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("pp0n2c16", 962, 934, 3347, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("pp0n6a08", 818, 818, 3666, 32, 32, 8, 6, 8, 6),
ImageCompressionInfo("ps1n0g08", 1477, 132, 730, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("ps1n2c16", 1641, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("ps2n0g08", 2341, 132, 730, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("ps2n2c16", 2505, 274, 2687, 32, 32, 16, 2, 16, 2),
ImageCompressionInfo("s01i3p01", 113, 98, 98, 1, 1, 1, 3, 1, 3),
ImageCompressionInfo("s01n3p01", 113, 98, 98, 1, 1, 1, 3, 1, 3),
ImageCompressionInfo("s02i3p01", 114, 99, 99, 2, 2, 1, 3, 1, 3),
ImageCompressionInfo("s02n3p01", 115, 100, 100, 2, 2, 1, 3, 1, 3),
ImageCompressionInfo("s03i3p01", 118, 103, 103, 3, 3, 1, 3, 1, 3),
ImageCompressionInfo("s03n3p01", 120, 105, 105, 3, 3, 1, 3, 1, 3),
ImageCompressionInfo("s04i3p01", 126, 111, 111, 4, 4, 1, 3, 1, 3),
ImageCompressionInfo("s04n3p01", 121, 106, 106, 4, 4, 1, 3, 1, 3),
ImageCompressionInfo("s05i3p02", 134, 119, 119, 5, 5, 2, 3, 2, 3),
ImageCompressionInfo("s05n3p02", 129, 114, 114, 5, 5, 2, 3, 2, 3),
ImageCompressionInfo("s06i3p02", 143, 128, 128, 6, 6, 2, 3, 2, 3),
ImageCompressionInfo("s06n3p02", 131, 116, 116, 6, 6, 2, 3, 2, 3),
ImageCompressionInfo("s07i3p02", 149, 134, 134, 7, 7, 2, 3, 2, 3),
ImageCompressionInfo("s07n3p02", 138, 123, 123, 7, 7, 2, 3, 2, 3),
ImageCompressionInfo("s08i3p02", 149, 134, 134, 8, 8, 2, 3, 2, 3),
ImageCompressionInfo("s08n3p02", 139, 124, 124, 8, 8, 2, 3, 2, 3),
ImageCompressionInfo("s09i3p02", 147, 132, 132, 9, 9, 2, 3, 2, 3),
ImageCompressionInfo("s09n3p02", 143, 128, 128, 9, 9, 2, 3, 2, 3),
ImageCompressionInfo("s32i3p04", 355, 340, 340, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("s32n3p04", 263, 248, 248, 32, 32, 4, 3, 4, 3),
ImageCompressionInfo("s33i3p04", 385, 370, 370, 33, 33, 4, 3, 4, 3),
ImageCompressionInfo("s33n3p04", 329, 314, 314, 33, 33, 4, 3, 4, 3),
ImageCompressionInfo("s34i3p04", 349, 332, 334, 34, 34, 4, 3, 4, 3),
ImageCompressionInfo("s34n3p04", 248, 229, 233, 34, 34, 4, 3, 4, 3),
ImageCompressionInfo("s35i3p04", 399, 384, 384, 35, 35, 4, 3, 4, 3),
ImageCompressionInfo("s35n3p04", 338, 313, 323, 35, 35, 4, 3, 4, 3),
ImageCompressionInfo("s36i3p04", 356, 339, 341, 36, 36, 4, 3, 4, 3),
ImageCompressionInfo("s36n3p04", 258, 240, 243, 36, 36, 4, 3, 4, 3),
ImageCompressionInfo("s37i3p04", 393, 378, 378, 37, 37, 4, 3, 4, 3),
ImageCompressionInfo("s37n3p04", 336, 317, 321, 37, 37, 4, 3, 4, 3),
ImageCompressionInfo("s38i3p04", 357, 339, 342, 38, 38, 4, 3, 4, 3),
ImageCompressionInfo("s38n3p04", 245, 228, 230, 38, 38, 4, 3, 4, 3),
ImageCompressionInfo("s39i3p04", 420, 405, 405, 39, 39, 4, 3, 4, 3),
ImageCompressionInfo("s39n3p04", 352, 336, 337, 39, 39, 4, 3, 4, 3),
ImageCompressionInfo("s40i3p04", 357, 340, 342, 40, 40, 4, 3, 4, 3),
ImageCompressionInfo("s40n3p04", 256, 237, 241, 40, 40, 4, 3, 4, 3),
ImageCompressionInfo("tbbn1g04", 419, 405, 405, 32, 32, 4, 0, 4, 0),
ImageCompressionInfo("tbbn2c16", 1994, 1095, 1113, 32, 32, 16, 2, 8, 3),
ImageCompressionInfo("tbbn3p08", 1128, 1095, 1115, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("tbgn2c16", 1994, 1095, 1113, 32, 32, 16, 2, 8, 3),
ImageCompressionInfo("tbgn3p08", 1128, 1095, 1115, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("tbrn2c08", 1347, 1095, 1113, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("tbwn1g16", 1146, 582, 599, 32, 32, 16, 0, 8, 0),
ImageCompressionInfo("tbwn3p08", 1131, 1095, 1115, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("tbyn3p08", 1131, 1095, 1115, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("tp0n1g08", 689, 568, 585, 32, 32, 8, 0, 8, 0),
ImageCompressionInfo("tp0n2c08", 1311, 1099, 1119, 32, 32, 8, 2, 8, 3),
ImageCompressionInfo("tp0n3p08", 1120, 1098, 1120, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("tp1n3p08", 1115, 1095, 1115, 32, 32, 8, 3, 8, 3),
ImageCompressionInfo("z00n2c08", 3172, 224, 1956, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("z03n2c08", 232, 224, 1956, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("z06n2c08", 224, 224, 1956, 32, 32, 8, 2, 8, 2),
ImageCompressionInfo("z09n2c08", 224, 224, 1956, 32, 32, 8, 2, 8, 2),
};
const char* kInvalidFiles[] = {
"emptyfile",
"x00n0g01",
"xcrn0g04",
"xlfn0g04",
};
struct OpaqueImageInfo {
const char* filename;
bool is_opaque;
int in_color_type;
int out_color_type;
};
OpaqueImageInfo kOpaqueImagesWithAlpha[] = {
{ "rgba_opaque", 1, 6, 2 },
{ "grey_alpha_opaque", 1, 4, 0 },
{ "bgai4a16", 0, 4, 4 }
};
void AssertMatch(const GoogleString& in,
const GoogleString& ref,
PngReaderInterface* reader,
const ImageCompressionInfo& info,
GoogleString in_rgba) {
MockMessageHandler message_handler(new NullMutex);
PngReader png_reader(&message_handler);
int width, height, bit_depth, color_type;
GoogleString out;
EXPECT_EQ(info.original_size, in.size())
<< info.filename;
ASSERT_TRUE(reader->GetAttributes(
in, &width, &height, &bit_depth, &color_type)) << info.filename;
EXPECT_EQ(info.width, width) << info.filename;
EXPECT_EQ(info.height, height) << info.filename;
EXPECT_EQ(info.original_bit_depth, bit_depth) << info.filename;
EXPECT_EQ(info.original_color_type, color_type) << info.filename;
ASSERT_TRUE(PngOptimizer::OptimizePng(*reader, in, &out, &message_handler))
<< info.filename;
EXPECT_EQ(info.compressed_size_default, out.size()) << info.filename;
AssertPngEq(ref, out, info.filename, in_rgba);
ASSERT_TRUE(png_reader.GetAttributes(
out, &width, &height, &bit_depth, &color_type)) << info.filename;
EXPECT_EQ(info.compressed_bit_depth, bit_depth) << info.filename;
EXPECT_EQ(info.compressed_color_type, color_type) << info.filename;
ASSERT_TRUE(PngOptimizer::OptimizePngBestCompression(*reader, in, &out,
&message_handler)) << info.filename;
EXPECT_EQ(info.compressed_size_best, out.size()) << info.filename;
AssertPngEq(ref, out, info.filename, in_rgba);
ASSERT_TRUE(png_reader.GetAttributes(
out, &width, &height, &bit_depth, &color_type)) << info.filename;
EXPECT_EQ(info.compressed_bit_depth, bit_depth) << info.filename;
EXPECT_EQ(info.compressed_color_type, color_type) << info.filename;
}
void AssertMatch(const GoogleString& in,
const GoogleString& ref,
PngReaderInterface* reader,
const ImageCompressionInfo& info) {
static GoogleString in_rgba;
AssertMatch(in, ref, reader, info, in_rgba);
}
bool InitializeEntireReader(const GoogleString& image_string,
const PngReader& png_reader,
PngScanlineReader* entire_image_reader) {
// Initialize entire_image_reader.
if (!entire_image_reader->Reset()) {
return false;
}
entire_image_reader->set_transform(
PNG_TRANSFORM_EXPAND |
PNG_TRANSFORM_STRIP_16);
if (entire_image_reader->InitializeRead(png_reader, image_string) == false) {
return false;
}
// Skip the images which are not supported by PngScanlineReader.
return (entire_image_reader->GetPixelFormat()
!= pagespeed::image_compression::UNSUPPORTED);
}
const size_t kValidImageCount = arraysize(kValidImages);
const size_t kInvalidFileCount = arraysize(kInvalidFiles);
const size_t kOpaqueImagesWithAlphaCount = arraysize(kOpaqueImagesWithAlpha);
class PngOptimizerTest : public testing::Test {
public:
PngOptimizerTest()
: message_handler_(new NullMutex) {
}
protected:
virtual void SetUp() {
message_handler_.AddPatternToSkipPrinting(kMessagePatternAnimatedGif);
message_handler_.AddPatternToSkipPrinting(kMessagePatternBadGifDescriptor);
message_handler_.AddPatternToSkipPrinting(kMessagePatternBadGifLine);
message_handler_.AddPatternToSkipPrinting(kMessagePatternFailedToRead);
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngError);
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngFailure);
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngWarning);
message_handler_.AddPatternToSkipPrinting(kMessagePatternUnexpectedEOF);
}
protected:
MockMessageHandler message_handler_;
net_instaweb::scoped_ptr<PngReaderInterface> reader_;
private:
DISALLOW_COPY_AND_ASSIGN(PngOptimizerTest);
};
class PngScanlineReaderRawTest : public testing::Test {
public:
PngScanlineReaderRawTest()
: message_handler_(new NullMutex) {
}
protected:
MockMessageHandler message_handler_;
private:
DISALLOW_COPY_AND_ASSIGN(PngScanlineReaderRawTest);
};
class PngScanlineWriterTest : public testing::Test {
public:
PngScanlineWriterTest()
: params_(PngCompressParams(PNG_FILTER_NONE, Z_DEFAULT_STRATEGY, false)),
message_handler_(new NullMutex) {
}
bool Initialize() {
writer_.reset(CreateScanlineWriter(
pagespeed::image_compression::IMAGE_PNG, pixel_format_, width_,
height_, &params_, &output_, &message_handler_));
return (writer_ != NULL);
}
void TestRewritePng(bool best_compression, int* total_bytes);
protected:
net_instaweb::scoped_ptr<ScanlineWriterInterface> writer_;
GoogleString output_;
PngCompressParams params_;
unsigned char scanline_[3];
static const int width_ = 3;
static const int height_ = 2;
static const PixelFormat pixel_format_ = pagespeed::image_compression::GRAY_8;
MockMessageHandler message_handler_;
private:
DISALLOW_COPY_AND_ASSIGN(PngScanlineWriterTest);
};
TEST_F(PngOptimizerTest, ValidPngs) {
reader_.reset(new PngReader(&message_handler_));
for (size_t i = 0; i < kValidImageCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png", &in);
AssertMatch(in, in, reader_.get(), kValidImages[i]);
}
}
TEST(PngScanlineReaderTest, InitializeRead_validPngs) {
MockMessageHandler message_handler(new NullMutex);
PngScanlineReader scanline_reader(&message_handler);
if (setjmp(*scanline_reader.GetJmpBuf())) {
ASSERT_FALSE(true) << "Execution should never reach here";
}
for (size_t i = 0; i < kValidImageCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png", &in);
PngReader png_reader(&message_handler);
ASSERT_TRUE(scanline_reader.Reset());
int width, height, bit_depth, color_type;
ASSERT_TRUE(png_reader.GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(kValidImages[i].original_color_type, color_type);
ASSERT_TRUE(scanline_reader.InitializeRead(png_reader, in));
EXPECT_EQ(kValidImages[i].original_color_type,
scanline_reader.GetColorType());
}
for (size_t i = 0; i < kOpaqueImagesWithAlphaCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kOpaqueImagesWithAlpha[i].filename, "png",
&in);
PngReader png_reader(&message_handler);
ASSERT_TRUE(scanline_reader.Reset());
int width, height, bit_depth, color_type;
ASSERT_TRUE(png_reader.GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(kOpaqueImagesWithAlpha[i].in_color_type, color_type);
ASSERT_TRUE(scanline_reader.InitializeRead(png_reader, in));
EXPECT_EQ(kOpaqueImagesWithAlpha[i].out_color_type,
scanline_reader.GetColorType());
}
}
TEST_F(PngOptimizerTest, ValidPngs_isOpaque) {
ScopedPngStruct read(ScopedPngStruct::READ, &message_handler_);
for (size_t i = 0; i < kOpaqueImagesWithAlphaCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kOpaqueImagesWithAlpha[i].filename, "png",
&in);
reader_.reset(new PngReader(&message_handler_));
ASSERT_TRUE(reader_->ReadPng(in, read.png_ptr(), read.info_ptr(), 0));
EXPECT_EQ(kOpaqueImagesWithAlpha[i].is_opaque,
PngReaderInterface::IsAlphaChannelOpaque(
read.png_ptr(), read.info_ptr(), &message_handler_));
ASSERT_TRUE(read.reset());
}
}
TEST_F(PngOptimizerTest, LargerPng) {
reader_.reset(new PngReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kPngTestDir, "this_is_a_test", "png", &in);
ASSERT_EQ(static_cast<size_t>(20316), in.length());
ASSERT_TRUE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_TRUE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(640, width);
EXPECT_EQ(400, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(2, color_type);
ASSERT_TRUE(reader_->GetAttributes(
out, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(640, width);
EXPECT_EQ(400, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(0, color_type);
}
TEST_F(PngOptimizerTest, InvalidPngs) {
reader_.reset(new PngReader(&message_handler_));
for (size_t i = 0; i < kInvalidFileCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kInvalidFiles[i], "png", &in);
ASSERT_FALSE(PngOptimizer::OptimizePngBestCompression(*reader_,
in, &out, &message_handler_));
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
const bool get_attributes_result = reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type);
bool expected_get_attributes_result = false;
if (strcmp("x00n0g01", kInvalidFiles[i]) == 0) {
// Special case: even though the image is invalid, it has a
// valid IDAT chunk, so we can read its attributes.
expected_get_attributes_result = true;
}
EXPECT_EQ(expected_get_attributes_result, get_attributes_result)
<< kInvalidFiles[i];
}
}
TEST_F(PngOptimizerTest, FixPngOutOfBoundReadCrash) {
reader_.reset(new PngReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kPngTestDir, "read_from_stream_crash", "png", &in);
ASSERT_EQ(static_cast<size_t>(193), in.length());
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_TRUE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(32, width);
EXPECT_EQ(32, height);
EXPECT_EQ(2, bit_depth);
EXPECT_EQ(3, color_type);
}
TEST_F(PngOptimizerTest, PartialPng) {
reader_.reset(new PngReader(&message_handler_));
GoogleString in, out;
int width, height, bit_depth, color_type;
ReadTestFile(kPngTestDir, "pagespeed-128", "png", &in);
ASSERT_NE(static_cast<size_t>(0), in.length());
// Loop, removing the last byte repeatedly to generate every
// possible partial version of the animated PNG.
while (true) {
if (in.size() == 0) {
break;
}
// Remove the last byte.
in.erase(in.length() - 1);
EXPECT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
// See if we can extract image attributes. Doing so requires that
// at least 33 bytes are available (signature plus full IDAT
// chunk).
bool png_header_available = (in.size() >= 33);
bool get_attributes_result =
reader_->GetAttributes(in, &width, &height, &bit_depth, &color_type);
EXPECT_EQ(png_header_available, get_attributes_result) << in.size();
if (get_attributes_result) {
EXPECT_EQ(128, width);
EXPECT_EQ(128, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(3, color_type);
}
}
}
TEST_F(PngOptimizerTest, ValidGifs) {
reader_.reset(new GifReader(&message_handler_));
for (size_t i = 0; i < kValidGifImageCount; i++) {
GoogleString in, ref, gif_rgba;
ReadTestFile(
kPngSuiteGifTestDir, kValidGifImages[i].filename, "gif", &in);
ReadTestFile(
kPngSuiteGifTestDir, kValidGifImages[i].filename, "gif.rgba",
&gif_rgba);
ReadTestFile(kPngSuiteTestDir, kValidGifImages[i].filename, "png", &ref);
AssertMatch(in, ref, reader_.get(), kValidGifImages[i], gif_rgba);
}
}
TEST_F(PngOptimizerTest, AnimatedGif) {
reader_.reset(new GifReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kGifTestDir, "animated", "gif", &in);
ASSERT_NE(static_cast<size_t>(0), in.length());
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_TRUE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(120, width);
EXPECT_EQ(50, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(3, color_type);
}
TEST_F(PngOptimizerTest, InterlacedGif) {
reader_.reset(new GifReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kGifTestDir, "interlaced", "gif", &in);
ASSERT_NE(static_cast<size_t>(0), in.length());
ASSERT_TRUE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_TRUE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(213, width);
EXPECT_EQ(323, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(3, color_type);
}
TEST_F(PngOptimizerTest, TransparentGif) {
reader_.reset(new GifReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kGifTestDir, "transparent", "gif", &in);
ASSERT_NE(static_cast<size_t>(0), in.length());
ASSERT_TRUE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_TRUE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
EXPECT_EQ(320, width);
EXPECT_EQ(320, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(3, color_type);
}
// Verify that we fail gracefully when processing partial versions of
// the animated GIF.
TEST_F(PngOptimizerTest, PartialAnimatedGif) {
reader_.reset(new GifReader(&message_handler_));
GoogleString in, out;
int width, height, bit_depth, color_type;
ReadTestFile(kGifTestDir, "animated", "gif", &in);
ASSERT_NE(static_cast<size_t>(0), in.length());
// Loop, removing the last byte repeatedly to generate every
// possible partial version of the animated gif.
while (!in.empty()) {
// Remove the last byte.
in.erase(in.length() - 1);
// Since we have only a partial file, optimization should fail.
EXPECT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
// See if we can extract image attributes. Doing so requires that
// at least 10 bytes are available.
bool gif_header_available = (in.size() >= 10);
bool get_attributes_result =
reader_->GetAttributes(in, &width, &height, &bit_depth, &color_type);
EXPECT_EQ(gif_header_available, get_attributes_result) << in.size();
if (get_attributes_result) {
EXPECT_EQ(120, width);
EXPECT_EQ(50, height);
EXPECT_EQ(8, bit_depth);
EXPECT_EQ(3, color_type);
}
}
}
// Make sure we do not leak memory when attempting to optimize a GIF
// that fails to decode.
TEST_F(PngOptimizerTest, BadGifNoLeak) {
reader_.reset(new GifReader(&message_handler_));
GoogleString in, out;
ReadTestFile(kGifTestDir, "bad", "gif", &in);
ASSERT_NE(static_cast<size_t>(0), in.length());
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_FALSE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
}
TEST_F(PngOptimizerTest, InvalidGifs) {
// Verify that we fail gracefully when trying to parse PNGs using
// the GIF reader.
reader_.reset(new GifReader(&message_handler_));
for (size_t i = 0; i < kValidImageCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png", &in);
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_FALSE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
}
// Also verify we fail gracefully for the invalid PNG images.
for (size_t i = 0; i < kInvalidFileCount; i++) {
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kInvalidFiles[i], "png", &in);
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_FALSE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
}
}
// Make sure that after we fail, we're still able to successfully
// compress valid images.
TEST_F(PngOptimizerTest, SuccessAfterFailure) {
reader_.reset(new PngReader(&message_handler_));
for (size_t i = 0; i < kInvalidFileCount; i++) {
{
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kInvalidFiles[i], "png", &in);
ASSERT_FALSE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
}
{
GoogleString in, out;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png", &in);
ASSERT_TRUE(PngOptimizer::OptimizePng(*reader_, in, &out,
&message_handler_));
int width, height, bit_depth, color_type;
ASSERT_TRUE(reader_->GetAttributes(
in, &width, &height, &bit_depth, &color_type));
}
}
}
TEST_F(PngOptimizerTest, ScopedPngStruct) {
ScopedPngStruct read(ScopedPngStruct::READ, &message_handler_);
ASSERT_TRUE(read.valid());
ASSERT_NE(static_cast<png_structp>(NULL), read.png_ptr());
ASSERT_NE(static_cast<png_infop>(NULL), read.info_ptr());
ScopedPngStruct write(ScopedPngStruct::WRITE, &message_handler_);
ASSERT_TRUE(write.valid());
ASSERT_NE(static_cast<png_structp>(NULL), write.png_ptr());
ASSERT_NE(static_cast<png_infop>(NULL), write.info_ptr());
#ifdef NDEBUG
ScopedPngStruct invalid(static_cast<ScopedPngStruct::Type>(-1),
&message_handler_);
ASSERT_FALSE(invalid.valid());
ASSERT_EQ(static_cast<png_structp>(NULL), invalid.png_ptr());
ASSERT_EQ(static_cast<png_infop>(NULL), invalid.info_ptr());
#else
ASSERT_DEATH(ScopedPngStruct t =
ScopedPngStruct(static_cast<ScopedPngStruct::Type>(-1),
&message_handler_),
"Check failed: type == READ \\|\\| type == WRITE");
#endif
}
TEST(PngReaderTest, ReadTransparentPng) {
MockMessageHandler message_handler(new NullMutex);
PngReader reader(&message_handler);
ScopedPngStruct read(ScopedPngStruct::READ, &message_handler);
GoogleString in;
ReadTestFile(kPngSuiteTestDir, "basn4a16", "png", &in);
// Don't require_opaque.
ASSERT_TRUE(reader.ReadPng(in, read.png_ptr(), read.info_ptr(),
PNG_TRANSFORM_IDENTITY, false));
ASSERT_FALSE(reader.IsAlphaChannelOpaque(read.png_ptr(), read.info_ptr(),
&message_handler));
ASSERT_TRUE(read.reset());
// Don't transform but require opaque.
ASSERT_FALSE(reader.ReadPng(in, read.png_ptr(), read.info_ptr(),
PNG_TRANSFORM_IDENTITY, true));
ASSERT_TRUE(read.reset());
// Strip the alpha channel and require opaque.
ASSERT_TRUE(reader.ReadPng(in, read.png_ptr(), read.info_ptr(),
PNG_TRANSFORM_STRIP_ALPHA, true));
#ifndef NDEBUG
ASSERT_DEATH(reader.IsAlphaChannelOpaque(read.png_ptr(), read.info_ptr(),
&message_handler),
"IsAlphaChannelOpaque called for image without alpha channel.");
#else
ASSERT_FALSE(reader.IsAlphaChannelOpaque(read.png_ptr(), read.info_ptr(),
&message_handler));
#endif
ASSERT_TRUE(read.reset());
// Strip the alpha channel and don't require opaque.
ASSERT_TRUE(reader.ReadPng(in, read.png_ptr(), read.info_ptr(),
PNG_TRANSFORM_STRIP_ALPHA, false));
#ifndef NDEBUG
ASSERT_DEATH(reader.IsAlphaChannelOpaque(read.png_ptr(), read.info_ptr(),
&message_handler),
"IsAlphaChannelOpaque called for image without alpha channel.");
#else
ASSERT_FALSE(reader.IsAlphaChannelOpaque(read.png_ptr(), read.info_ptr(),
&message_handler));
#endif
ASSERT_TRUE(read.reset());
}
TEST_F(PngScanlineReaderRawTest, ValidPngsRow) {
// Create a reader which tries to read a row of image at a time.
PngScanlineReaderRaw per_row_reader(&message_handler_);
// Create a reader which reads the entire image.
PngReader png_reader(&message_handler_);
PngScanlineReader entire_image_reader(&message_handler_);
if (setjmp(*entire_image_reader.GetJmpBuf()) != 0) {
FAIL();
}
for (size_t i = 0; i < kValidImageCount; i++) {
GoogleString image_string;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png",
&image_string);
// Initialize entire_image_reader (PngScanlineReader).
if (!InitializeEntireReader(image_string, png_reader,
&entire_image_reader)) {
// This image is not supported by PngScanlineReader. Skip it.
continue;
}
// Initialize per_row_reader.
ASSERT_TRUE(per_row_reader.Initialize(image_string.data(),
image_string.length()));
// Make sure the images sizes and the pixel formats are the same.
ASSERT_EQ(entire_image_reader.GetImageWidth(),
per_row_reader.GetImageWidth());
ASSERT_EQ(entire_image_reader.GetImageHeight(),
per_row_reader.GetImageHeight());
ASSERT_EQ(entire_image_reader.GetPixelFormat(),
per_row_reader.GetPixelFormat());
const int width = per_row_reader.GetImageWidth();
const int num_channels =
GetNumChannelsFromPixelFormat(per_row_reader.GetPixelFormat(),
&message_handler_);
uint8* buffer_per_row = NULL;
uint8* buffer_entire = NULL;
// Decode and check the image a row at a time.
while (per_row_reader.HasMoreScanLines() &&
entire_image_reader.HasMoreScanLines()) {
ASSERT_TRUE(entire_image_reader.ReadNextScanline(
reinterpret_cast<void**>(&buffer_entire)));
ASSERT_TRUE(per_row_reader.ReadNextScanline(
reinterpret_cast<void**>(&buffer_per_row)));
for (int i = 0; i < width*num_channels; ++i) {
ASSERT_EQ(buffer_entire[i], buffer_per_row[i]);
}
}
// Make sure both readers have exhausted all image rows.
ASSERT_FALSE(per_row_reader.HasMoreScanLines());
ASSERT_FALSE(entire_image_reader.HasMoreScanLines());
}
}
TEST_F(PngScanlineReaderRawTest, ValidPngsEntire) {
PngReader png_reader(&message_handler_);
PngScanlineReader entire_image_reader(&message_handler_);
if (setjmp(*entire_image_reader.GetJmpBuf()) != 0) {
FAIL();
}
for (size_t i = 0; i < kValidImageCount; ++i) {
GoogleString image_string;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png",
&image_string);
// Initialize entire_image_reader (PngScanlineReader).
if (!InitializeEntireReader(image_string, png_reader,
&entire_image_reader)) {
// This image is not supported by PngScanlineReader. Skip it.
continue;
}
size_t width, bytes_per_row;
void* buffer_for_raw_reader;
pagespeed::image_compression::PixelFormat pixel_format;
ASSERT_TRUE(ReadImage(pagespeed::image_compression::IMAGE_PNG,
image_string.data(), image_string.length(),
&buffer_for_raw_reader, &pixel_format, &width, NULL,
&bytes_per_row, &message_handler_));
uint8* buffer_per_row = static_cast<uint8*>(buffer_for_raw_reader);
int num_channels = GetNumChannelsFromPixelFormat(pixel_format,
&message_handler_);
// Check the image row by row.
while (entire_image_reader.HasMoreScanLines()) {
uint8* buffer_entire = NULL;
ASSERT_TRUE(entire_image_reader.ReadNextScanline(
reinterpret_cast<void**>(&buffer_entire)));
for (size_t i = 0; i < width*num_channels; ++i) {
ASSERT_EQ(buffer_entire[i], buffer_per_row[i]);
}
buffer_per_row += bytes_per_row;
}
free(buffer_for_raw_reader);
}
}
TEST_F(PngScanlineReaderRawTest, PartialRead) {
uint8* buffer = NULL;
GoogleString image_string;
ReadTestFile(kPngSuiteTestDir, kValidImages[0].filename, "png",
&image_string);
// Initialize a reader but do not read any scanline.
PngScanlineReaderRaw reader1(&message_handler_);
ASSERT_TRUE(reader1.Initialize(image_string.data(), image_string.length()));
// Initialize a reader and read one scanline.
PngScanlineReaderRaw reader2(&message_handler_);
ASSERT_TRUE(reader2.Initialize(image_string.data(), image_string.length()));
ASSERT_TRUE(reader2.ReadNextScanline(reinterpret_cast<void**>(&buffer)));
// Initialize a reader, and try to read a scanline after the image has been
// depleted.
PngScanlineReaderRaw reader3(&message_handler_);
ASSERT_TRUE(reader3.Initialize(image_string.data(), image_string.length()));
while (reader3.HasMoreScanLines()) {
ASSERT_TRUE(reader3.ReadNextScanline(reinterpret_cast<void**>(&buffer)));
}
// After depleting the scanlines, any further call to
// ReadNextScanline leads to death in debugging mode, or a
// false in release mode.
#ifdef NDEBUG
ASSERT_FALSE(reader3.ReadNextScanline(reinterpret_cast<void**>(&buffer)));
#else
ASSERT_DEATH(reader3.ReadNextScanline(reinterpret_cast<void**>(&buffer)),
"The reader was not initialized or the image does not have any "
"more scanlines.");
#endif
}
TEST_F(PngScanlineReaderRawTest, ReadAfterReset) {
uint8* buffer = NULL;
GoogleString image_string;
ReadTestFile(kPngSuiteTestDir, kValidImages[0].filename, "png",
&image_string);
// Initialize a reader and read one scanline.
PngScanlineReaderRaw reader(&message_handler_);
ASSERT_TRUE(reader.Initialize(image_string.data(), image_string.length()));
ASSERT_TRUE(reader.ReadNextScanline(reinterpret_cast<void**>(&buffer)));
// Now re-initialize the reader.
ASSERT_TRUE(reader.Initialize(image_string.data(), image_string.length()));
// Decode the entire image using ReadImage(). This copy of image will be used
// as the baseline.
size_t width, bytes_per_row;
void* buffer_for_raw_reader;
pagespeed::image_compression::PixelFormat pixel_format;
ASSERT_TRUE(ReadImage(pagespeed::image_compression::IMAGE_PNG,
image_string.data(), image_string.length(),
&buffer_for_raw_reader, &pixel_format, &width, NULL,
&bytes_per_row, &message_handler_));
uint8* buffer_entire = static_cast<uint8*>(buffer_for_raw_reader);
int num_channels = GetNumChannelsFromPixelFormat(pixel_format,
&message_handler_);
// Compare the image row by row.
while (reader.HasMoreScanLines()) {
uint8* buffer_row = NULL;
ASSERT_TRUE(reader.ReadNextScanline(
reinterpret_cast<void**>(&buffer_row)));
for (size_t i = 0; i < width*num_channels; ++i) {
ASSERT_EQ(buffer_entire[i], buffer_row[i]);
}
buffer_entire += bytes_per_row;
}
free(buffer_for_raw_reader);
}
TEST_F(PngScanlineReaderRawTest, InvalidPngs) {
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngError);
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngFailure);
message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngWarning);
message_handler_.AddPatternToSkipPrinting(kMessagePatternUnexpectedEOF);
PngScanlineReaderRaw reader(&message_handler_);
for (size_t i = 0; i < kInvalidFileCount; i++) {
GoogleString image_string;
ReadTestFile(kPngSuiteTestDir, kInvalidFiles[i], "png", &image_string);
ASSERT_FALSE(reader.Initialize(image_string.data(), image_string.length()));
ASSERT_FALSE(ReadImage(pagespeed::image_compression::IMAGE_PNG,
image_string.data(), image_string.length(),
NULL, NULL, NULL, NULL, NULL, &message_handler_));
}
}
// Make sure that PNG files are written correctly. We firstly decompress
// a PNG image; then compress it to a new PNG image; finally verify that
// the new PNG is the same as the original one. Information to be verified
// include pixel values, pixel type, and image size.
//
// Libpng provides several options for writing a PNG. To verify that
// PngScanlineWriter works on all of these options, the test uses a rotation
// of configurations and verifies that all of the rewritten images are good.
void PngScanlineWriterTest::TestRewritePng(bool best_compression,
int* total_bytes) {
*total_bytes = 0;
message_handler_.AddPatternToSkipPrinting(kMessagePatternUnrecognizedColor);
PngScanlineReaderRaw original_reader(&message_handler_);
PngScanlineReaderRaw rewritten_reader(&message_handler_);
// List of filters supported by libpng.
const int png_filter_list[] = {
PNG_FILTER_NONE, // 0x08
PNG_FILTER_SUB, // 0x10
PNG_FILTER_UP, // 0x20
PNG_FILTER_AVG, // 0x40
PNG_FILTER_PAETH // 0x80
};
for (size_t i = 0; i < kValidImageCount; i++) {
GoogleString original_image;
GoogleString rewritten_image;
ReadTestFile(kPngSuiteTestDir, kValidImages[i].filename, "png",
&original_image);
// Initialize a PNG reader for reading the original image.
if (original_reader.Initialize(original_image.data(),
original_image.length()) == false) {
// Some images in kValidImages[] have unsupported formats, for example,
// GRAY_ALPHA. These images are skipped.
continue;
}
// Get the sizes and pixel format of the original image.
const size_t width = original_reader.GetImageWidth();
const size_t height = original_reader.GetImageHeight();
const PixelFormat pixel_format = original_reader.GetPixelFormat();
// Use a new combination of filter and compression level for writing
// this image.
const int num_z = Z_FIXED - Z_DEFAULT_STRATEGY + 1;
int compression_strategy = Z_DEFAULT_STRATEGY + (i % num_z);
int filter_level = png_filter_list[(i / num_z) % 5];
net_instaweb::scoped_ptr<PngCompressParams> params;
if (best_compression) {
params.reset(new PngCompressParams(true /*best compression*/,
true /*progressive*/));
} else {
params.reset(new PngCompressParams (filter_level, compression_strategy,
false));
}
// Initialize the writer.
writer_.reset(CreateScanlineWriter(
pagespeed::image_compression::IMAGE_PNG, pixel_format, width,
height, params.get(), &rewritten_image, &message_handler_));
ASSERT_NE(static_cast<ScanlineWriterInterface *>(NULL), writer_.get());
// Read the scanlines from the original image and write them to the new one.
while (original_reader.HasMoreScanLines()) {
uint8* scanline = NULL;
ASSERT_TRUE(original_reader.ReadNextScanline(
reinterpret_cast<void**>(&scanline)));
ASSERT_TRUE(writer_->WriteNextScanline(
reinterpret_cast<void*>(scanline)));
}
// Make sure that the readers has exhausted the original image.
ASSERT_FALSE(original_reader.HasMoreScanLines());
// Make sure that the writer has received all of the image data, and
// finalize it.
ASSERT_TRUE(writer_->FinalizeWrite());
// Now create readers for reading the original and the rewritten images.
ASSERT_TRUE(original_reader.Initialize(original_image.data(),
original_image.length()));
ASSERT_TRUE(rewritten_reader.Initialize(rewritten_image.data(),
rewritten_image.length()));
// Now make sure that the original and rewritten images have the
// same dimensions, types, and pixel values. When "best_compression"
// is true, the pixel format (i.e., number of color channels) may change,
// so we allow expanding colors.
AssertReadersMatch(&original_reader, &rewritten_reader,
best_compression /* allow expanding colors */);
*total_bytes += rewritten_image.length();
}
}
TEST_F(PngScanlineWriterTest, RewritePng) {
int total_bytes = 0;
int total_bytes_best = 0;
TestRewritePng(false /* no best compression */, &total_bytes);
TestRewritePng(true /* best compression */, &total_bytes_best);
// PNG compression performance is sensitive to image data. For a given image,
// we cannot gurantee that the "best compression" output will be smaller than
// the original, or smaller than the "no best compression". However, for a
// large corpus of images, "best compression" should be better than the other
// two overall.
EXPECT_GT(total_bytes, total_bytes_best);
}
// Attempt to finalize without writing all of the scanlines.
TEST_F(PngScanlineWriterTest, EarlyFinalize) {
ASSERT_TRUE(Initialize());
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
#ifndef NDEBUG
ASSERT_DEATH(writer_->FinalizeWrite(),
"SCANLINE_PNGWRITER/SCANLINE_STATUS_INVOCATION_ERROR "
"not initialized or not all rows written");
#else
ASSERT_FALSE(writer_->FinalizeWrite());
#endif
}
// Write insufficient number of scanlines and do not finalize at the end.
TEST_F(PngScanlineWriterTest, MissingScanlines) {
ASSERT_TRUE(Initialize());
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
}
// Write too many scanlines.
TEST_F(PngScanlineWriterTest, TooManyScanlines) {
ASSERT_TRUE(Initialize());
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
#ifndef NDEBUG
ASSERT_DEATH(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)),
"SCANLINE_PNGWRITER/SCANLINE_STATUS_INVOCATION_ERROR "
"failed preconditions to write scanline");
#else
ASSERT_FALSE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
#endif
}
// Write a scanline, and then re-initialize and write too many scanlines.
TEST_F(PngScanlineWriterTest, ReinitializeAndTooManyScanlines) {
ASSERT_TRUE(Initialize());
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
ASSERT_TRUE(Initialize());
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
ASSERT_TRUE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
#ifndef NDEBUG
ASSERT_DEATH(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)),
"SCANLINE_PNGWRITER/SCANLINE_STATUS_INVOCATION_ERROR "
"failed preconditions to write scanline");
#else
ASSERT_FALSE(writer_->WriteNextScanline(reinterpret_cast<void*>(scanline_)));
#endif
}
TEST_F(PngScanlineWriterTest, DecodeGrayAlpha) {
GoogleString rgba_image, ga_image;
ASSERT_TRUE(ReadTestFile(kPngTestDir, kImageRGBA, "png", &rgba_image));
ASSERT_TRUE(ReadTestFile(kPngTestDir, kImageGA, "png", &ga_image));
DecodeAndCompareImages(IMAGE_PNG, rgba_image.c_str(), rgba_image.length(),
IMAGE_PNG, ga_image.c_str(), ga_image.length(),
false, // ignore_transparent_rgb
&message_handler_);
}
} // namespace