blob: 992b1b870530524588e6b0e05ec860d69509e173 [file] [log] [blame]
/*
* Copyright 2010 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: jmaessen@google.com (Jan Maessen)
#include "net/instaweb/rewriter/public/image.h"
#include <algorithm>
#include <cstddef>
#include "base/logging.h"
#include "net/instaweb/rewriter/cached_result.pb.h"
#include "net/instaweb/rewriter/public/image_data_lookup.h"
#include "net/instaweb/rewriter/public/image_url_encoder.h"
#include "net/instaweb/rewriter/public/webp_optimizer.h"
#include "pagespeed/kernel/base/annotated_message_handler.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/image/gif_reader.h"
#include "pagespeed/kernel/image/image_analysis.h"
#include "pagespeed/kernel/image/image_converter.h"
#include "pagespeed/kernel/image/image_frame_interface.h"
#include "pagespeed/kernel/image/image_resizer.h"
#include "pagespeed/kernel/image/image_util.h"
#include "pagespeed/kernel/image/jpeg_optimizer.h"
#include "pagespeed/kernel/image/jpeg_utils.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/scanline_status.h"
#include "pagespeed/kernel/image/scanline_utils.h"
#include "pagespeed/kernel/image/webp_optimizer.h"
extern "C" {
#ifdef USE_SYSTEM_LIBWEBP
#include "webp/decode.h"
#else
#include "third_party/libwebp/src/webp/decode.h"
#endif
#ifdef USE_SYSTEM_LIBPNG
#include "png.h" // NOLINT
#else
#include "third_party/libpng/src/png.h"
#endif
}
using pagespeed::image_compression::AnalyzeImage;
using pagespeed::image_compression::ConversionTimeoutHandler;
using pagespeed::image_compression::CreateScanlineReader;
using pagespeed::image_compression::CreateScanlineWriter;
using pagespeed::image_compression::GifReader;
using pagespeed::image_compression::GRAY_8;
using pagespeed::image_compression::ImageConverter;
using pagespeed::image_compression::ImageFormat;
using pagespeed::image_compression::ImageFormatToString;
using pagespeed::image_compression::JpegCompressionOptions;
using pagespeed::image_compression::JpegScanlineWriter;
using pagespeed::image_compression::JpegUtils;
using pagespeed::image_compression::OptimizeJpegWithOptions;
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::PngScanlineWriter;
using pagespeed::image_compression::PreferredLibwebpLevel;
using pagespeed::image_compression::RETAIN;
using pagespeed::image_compression::RGB_888;
using pagespeed::image_compression::RGBA_8888;
using pagespeed::image_compression::ScanlineReaderInterface;
using pagespeed::image_compression::ScanlineResizer;
using pagespeed::image_compression::ScanlineWriterInterface;
using pagespeed::image_compression::WebpConfiguration;
using pagespeed::image_compression::WEBP_NONE;
using pagespeed::image_compression::WEBP_LOSSY;
using pagespeed::image_compression::WEBP_LOSSLESS;
using pagespeed::image_compression::WEBP_ANIMATED;
namespace net_instaweb {
namespace ImageHeaders {
const char kPngHeader[] = "\x89PNG\r\n\x1a\n";
const size_t kPngHeaderLength = STATIC_STRLEN(kPngHeader);
const char kPngIHDR[] = "\0\0\0\x0dIHDR";
const size_t kPngIntSize = 4;
const size_t kPngSectionHeaderLength = 2 * kPngIntSize;
const size_t kIHDRDataStart = kPngHeaderLength + kPngSectionHeaderLength;
const size_t kPngSectionMinSize = kPngSectionHeaderLength + kPngIntSize;
const size_t kPngColourTypeOffset = kIHDRDataStart + 2 * kPngIntSize + 1;
const char kPngAlphaChannel = 0x4; // bit of ColourType set for alpha channel
const char kPngIDAT[] = "IDAT";
const char kPngtRNS[] = "tRNS";
const char kGifHeader[] = "GIF8";
const size_t kGifHeaderLength = STATIC_STRLEN(kGifHeader);
const size_t kGifDimStart = kGifHeaderLength + 2;
const size_t kGifIntSize = 2;
const size_t kJpegIntSize = 2;
const int64 kMaxJpegQuality = 100;
const int64 kQualityForJpegWithUnkownQuality = 85;
} // namespace ImageHeaders
namespace {
const char kGifString[] = "gif";
const char kPngString[] = "png";
const uint8 kAlphaOpaque = 255;
void UpdateWebpStats(bool ok, bool was_timed_out, int64 time_elapsed_ms,
Image::ConversionVariables::VariableType var_type,
Image::ConversionVariables* conversion_vars) {
if (conversion_vars != NULL) {
Image::ConversionBySourceVariable* the_var = conversion_vars->Get(var_type);
if (the_var != NULL) {
if (was_timed_out) {
the_var->timeout_count->Add(1);
DCHECK(!ok);
} else {
if (ok) {
the_var->success_ms->Add(time_elapsed_ms);
} else {
the_var->failure_ms->Add(time_elapsed_ms);
}
}
}
}
}
// TODO(huibao): Unify ImageType and ImageFormat.
ImageFormat ImageTypeToImageFormat(ImageType type) {
ImageFormat format = pagespeed::image_compression::IMAGE_UNKNOWN;
switch (type) {
case IMAGE_UNKNOWN:
format = pagespeed::image_compression::IMAGE_UNKNOWN;
break;
case IMAGE_JPEG:
format = pagespeed::image_compression::IMAGE_JPEG;
break;
case IMAGE_PNG:
format = pagespeed::image_compression::IMAGE_PNG;
break;
case IMAGE_GIF:
format = pagespeed::image_compression::IMAGE_GIF;
break;
case IMAGE_WEBP:
case IMAGE_WEBP_LOSSLESS_OR_ALPHA:
case IMAGE_WEBP_ANIMATED:
format = pagespeed::image_compression::IMAGE_WEBP;
break;
}
return format;
}
ImageFormat GetOutputImageFormat(ImageFormat in_format) {
if (in_format == pagespeed::image_compression::IMAGE_GIF) {
return pagespeed::image_compression::IMAGE_PNG;
} else {
return in_format;
}
}
ScanlineWriterInterface* CreateUncompressedPngWriter(
size_t width, size_t height, GoogleString* output,
MessageHandler* handler, bool use_transparent_for_blank_image) {
PngCompressParams config(PNG_FILTER_NONE, Z_NO_COMPRESSION, false);
PixelFormat pixel_format =
use_transparent_for_blank_image ?
RGBA_8888 :
RGB_888;
return CreateScanlineWriter(
pagespeed::image_compression::IMAGE_PNG,
pixel_format, width, height, &config, output, handler);
}
} // namespace
// TODO(jmaessen): Put ImageImpl into private namespace.
class ImageImpl : public Image {
public:
ImageImpl(const StringPiece& original_contents,
const GoogleString& url,
const StringPiece& file_prefix,
CompressionOptions* options,
Timer* timer,
MessageHandler* handler);
ImageImpl(int width, int height, ImageType type,
const StringPiece& tmp_dir,
Timer* timer, MessageHandler* handler,
CompressionOptions* options);
virtual void Dimensions(ImageDim* natural_dim);
virtual bool ResizeTo(const ImageDim& new_dim);
virtual bool DrawImage(Image* image, int x, int y);
virtual bool EnsureLoaded(bool output_useful);
virtual bool ShouldConvertToProgressive(int64 quality) const;
virtual void SetResizedDimensions(const ImageDim& dims) { dims_ = dims; }
virtual void SetTransformToLowRes();
virtual const GoogleString& url() { return url_; }
virtual const GoogleString& debug_message() { return debug_message_; }
virtual const GoogleString& resize_debug_message() {
return resize_debug_message_;
}
bool GenerateBlankImage();
StringPiece original_contents() { return original_contents_; }
private:
// Maximum number of libpagespeed conversion attempts.
// TODO(vchudnov): Consider making this tunable.
static const int kMaxConversionAttempts = 2;
// Concrete helper methods called by parent class
virtual void ComputeImageType();
virtual bool ComputeOutputContents();
bool ComputeOutputContentsFromGifOrPng(
const GoogleString& string_for_image,
const PngReaderInterface* png_reader,
bool fall_back_to_png,
const char* dbg_input_format,
ImageType input_type,
ConversionVariables::VariableType var_type);
// Helper methods
static bool ComputePngTransparency(const StringPiece& buf);
// Internal methods used only in the implementation
void UndoChange();
void FindJpegSize();
void FindPngSize();
void FindGifSize();
void FindWebpSize();
// Convert the given options object to jpeg compression options.
void ConvertToJpegOptions(const Image::CompressionOptions& options,
JpegCompressionOptions* jpeg_options);
// Optimizes the png image_data, readable via png_reader.
bool OptimizePng(
const PngReaderInterface& png_reader,
const GoogleString& image_data);
// Converts image_data, readable via png_reader, to a jpeg if
// possible or a png if not, using the settings in options_.
bool OptimizePngOrConvertToJpeg(
const PngReaderInterface& png_reader,
const GoogleString& image_data);
// Converts image_data, readable via png_reader, to a webp using the
// settings in options_, if allowed by those settings. The alpha channel
// is always losslessly compressed, while the color may be lossily or
// or losslessly compressed, depending on 'compress_color_losslessly'.
bool ConvertPngToWebp(
const PngReaderInterface& png_reader,
const GoogleString& image_data,
bool compress_color_losslessly,
bool has_transparency,
ConversionVariables::VariableType var_type);
// Convert the JPEG in original_jpeg to WebP format in
// compressed_webp using the quality specified in
// configured_quality.
bool ConvertJpegToWebp(
const GoogleString& original_jpeg, int configured_quality,
GoogleString* compressed_webp);
static bool ContinueWebpConversion(
int percent,
void* user_data);
// Determines whether we can attempt a libpagespeed conversion
// without exceeding kMaxConversionAttempts. If so, increments the
// number of attempts.
bool MayConvert() {
if (options_.get()) {
VLOG(1) << "Conversions attempted: "
<< options_->conversions_attempted;
if (options_->conversions_attempted < kMaxConversionAttempts) {
++options_->conversions_attempted;
return true;
}
}
return false;
}
int GetJpegQualityFromImage(const StringPiece& contents) {
const int quality = JpegUtils::GetImageQualityFromImage(contents.data(),
contents.size(),
handler_.get());
return quality;
}
// Quality level for compressing the resized image.
int EstimateQualityForResizedJpeg();
bool ConvertAnimatedGifToWebp(bool has_transparency);
const GoogleString file_prefix_;
scoped_ptr<MessageHandler> handler_;
bool changed_;
const GoogleString url_;
ImageDim dims_;
ImageDim resized_dimensions_;
GoogleString resized_image_;
scoped_ptr<Image::CompressionOptions> options_;
bool low_quality_enabled_;
Timer* timer_;
GoogleString debug_message_;
GoogleString resize_debug_message_;
DISALLOW_COPY_AND_ASSIGN(ImageImpl);
};
void ImageImpl::SetTransformToLowRes() {
// TODO(vchudnov): Deprecate low_quality_enabled_.
low_quality_enabled_ = true;
// TODO(vchudnov): All these settings should probably be tunable.
if (options_->preferred_webp != WEBP_NONE) {
options_->preferred_webp = WEBP_LOSSY;
}
options_->webp_quality = 10;
options_->webp_animated_quality = 10;
options_->jpeg_quality = 10;
}
Image::Image(const StringPiece& original_contents)
: image_type_(IMAGE_UNKNOWN),
original_contents_(original_contents),
output_contents_(),
output_valid_(false),
rewrite_attempted_(false) { }
ImageImpl::ImageImpl(const StringPiece& original_contents,
const GoogleString& url,
const StringPiece& file_prefix,
CompressionOptions* options,
Timer* timer,
MessageHandler* handler)
: Image(original_contents),
file_prefix_(file_prefix.data(), file_prefix.size()),
changed_(false),
url_(url),
options_(options),
low_quality_enabled_(false),
timer_(timer) {
const GoogleString annotation = StrCat(url, ": ");
handler_.reset(new AnnotatedMessageHandler(annotation, handler));
}
Image* NewImage(const StringPiece& original_contents,
const GoogleString& url,
const StringPiece& file_prefix,
Image::CompressionOptions* options,
Timer* timer,
MessageHandler* handler) {
return new ImageImpl(original_contents, url, file_prefix, options,
timer, handler);
}
Image::Image(ImageType type)
: image_type_(type),
original_contents_(),
output_contents_(),
output_valid_(false),
rewrite_attempted_(false) { }
ImageImpl::ImageImpl(int width, int height, ImageType type,
const StringPiece& tmp_dir,
Timer* timer, MessageHandler* handler,
CompressionOptions* options)
: Image(type),
file_prefix_(tmp_dir.data(), tmp_dir.size()),
changed_(false),
low_quality_enabled_(false),
timer_(timer) {
options_.reset(options);
dims_.set_width(width);
dims_.set_height(height);
handler_.reset(new AnnotatedMessageHandler(handler));
}
bool ImageImpl::GenerateBlankImage() {
DCHECK(image_type_ == IMAGE_PNG) << "Blank image must be a PNG.";
if (pagespeed::image_compression::GenerateBlankImage(dims_.width(),
dims_.height(), options_->use_transparent_for_blank_image,
&output_contents_, handler_.get())) {
output_valid_ = true;
return true;
}
return false;
}
Image* BlankImageWithOptions(int width, int height, ImageType type,
const StringPiece& tmp_dir,
Timer* timer, MessageHandler* handler,
Image::CompressionOptions* options) {
scoped_ptr<ImageImpl> image(new ImageImpl(width, height, type, tmp_dir,
timer, handler, options));
if (image != NULL && image->GenerateBlankImage()) {
return image.release();
}
return NULL;
}
Image::~Image() {
}
// Looks through blocks of jpeg stream to find SOFn block
// indicating encoding and dimensions of image.
// Loosely based on code and FAQs found here:
// http://www.faqs.org/faqs/jpeg-faq/part1/
void ImageImpl::FindJpegSize() {
const StringPiece& buf = original_contents_;
size_t pos = 2; // Position of first data block after header.
while (pos < buf.size()) {
// Read block identifier
int id = CharToInt(buf[pos++]);
if (id == 0xff) { // Padding byte
continue;
}
// At this point pos points to first data byte in block. In any block,
// first two data bytes are size (including these 2 bytes). But first,
// make sure block wasn't truncated on download.
if (pos + ImageHeaders::kJpegIntSize > buf.size()) {
break;
}
int length = JpegIntAtPosition(buf, pos);
// Now check for a SOFn header, which describes image dimensions.
if (0xc0 <= id && id <= 0xcf && // SOFn header
length >= 8 && // Valid SOFn block size
pos + 1 + 3 * ImageHeaders::kJpegIntSize <= buf.size() &&
// Above avoids case where dimension data was truncated
id != 0xc4 && id != 0xc8 && id != 0xcc) {
// 0xc4, 0xc8, 0xcc aren't actually valid SOFn headers.
// NOTE: we don't care if we have the whole SOFn block,
// just that we can fetch both dimensions without trouble.
// Our image download could be truncated at this point for
// all we care.
// We're a bit sloppy about SOFn block size, as it's
// actually 8 + 3 * buf[pos+2], but for our purposes this
// will suffice as we don't parse subsequent metadata (which
// describes the formatting of chunks of image data).
dims_.set_height(
JpegIntAtPosition(buf, pos + 1 + ImageHeaders::kJpegIntSize));
dims_.set_width(
JpegIntAtPosition(buf, pos + 1 + 2 * ImageHeaders::kJpegIntSize));
break;
}
pos += length;
}
if (!ImageUrlEncoder::HasValidDimensions(dims_) ||
(dims_.height() <= 0) || (dims_.width() <= 0)) {
dims_.Clear();
PS_LOG_INFO(handler_, "Couldn't find jpeg dimensions (data truncated?).");
}
}
// Looks at first (IHDR) block of png stream to find image dimensions.
// See also: http://www.w3.org/TR/PNG/
void ImageImpl::FindPngSize() {
const StringPiece& buf = original_contents_;
// Here we make sure that buf contains at least enough data that we'll be able
// to decipher the image dimensions first, before we actually check for the
// headers and attempt to decode the dimensions (which are the first two ints
// after the IHDR section label).
if ((buf.size() >= // Not truncated
ImageHeaders::kIHDRDataStart + 2 * ImageHeaders::kPngIntSize) &&
(StringPiece(buf.data() + ImageHeaders::kPngHeaderLength,
ImageHeaders::kPngSectionHeaderLength) ==
StringPiece(ImageHeaders::kPngIHDR,
ImageHeaders::kPngSectionHeaderLength))) {
dims_.set_width(PngIntAtPosition(buf, ImageHeaders::kIHDRDataStart));
dims_.set_height(PngIntAtPosition(
buf, ImageHeaders::kIHDRDataStart + ImageHeaders::kPngIntSize));
} else {
PS_LOG_INFO(handler_, "Couldn't find png dimensions "
"(data truncated or IHDR missing).");
}
}
// Looks at header of GIF file to extract image dimensions.
// See also: http://en.wikipedia.org/wiki/Graphics_Interchange_Format
void ImageImpl::FindGifSize() {
const StringPiece& buf = original_contents_;
// Make sure that buf contains enough data that we'll be able to
// decipher the image dimensions before we attempt to do so.
if (buf.size() >=
ImageHeaders::kGifDimStart + 2 * ImageHeaders::kGifIntSize) {
// Not truncated
dims_.set_width(GifIntAtPosition(buf, ImageHeaders::kGifDimStart));
dims_.set_height(GifIntAtPosition(
buf, ImageHeaders::kGifDimStart + ImageHeaders::kGifIntSize));
} else {
PS_LOG_INFO(handler_, "Couldn't find gif dimensions (data truncated)");
}
}
void ImageImpl::FindWebpSize() {
const uint8* webp = reinterpret_cast<const uint8*>(original_contents_.data());
const int webp_size = original_contents_.size();
int width = 0, height = 0;
if (WebPGetInfo(webp, webp_size, &width, &height) > 0) {
dims_.set_width(width);
dims_.set_height(height);
} else {
PS_LOG_INFO(handler_, "Couldn't find webp dimensions ");
}
}
// Looks at image data in order to determine image type, and also fills in any
// dimension information it can (setting image_type_ and dims_).
void ImageImpl::ComputeImageType() {
image_type_ =
pagespeed::image_compression::ComputeImageType(original_contents_);
switch (image_type_) {
case IMAGE_JPEG:
FindJpegSize();
break;
case IMAGE_PNG:
FindPngSize();
break;
case IMAGE_GIF:
FindGifSize();
break;
case IMAGE_WEBP:
case IMAGE_WEBP_LOSSLESS_OR_ALPHA:
case IMAGE_WEBP_ANIMATED:
FindWebpSize();
break;
case IMAGE_UNKNOWN:
break;
}
}
const ContentType* Image::TypeToContentType(ImageType image_type) {
const ContentType* res = NULL;
switch (image_type) {
case IMAGE_UNKNOWN:
break;
case IMAGE_JPEG:
res = &kContentTypeJpeg;
break;
case IMAGE_PNG:
res = &kContentTypePng;
break;
case IMAGE_GIF:
res = &kContentTypeGif;
break;
case IMAGE_WEBP:
case IMAGE_WEBP_LOSSLESS_OR_ALPHA:
case IMAGE_WEBP_ANIMATED:
res = &kContentTypeWebp;
break;
}
return res;
}
// Compute whether a PNG can have transparent / semi-transparent pixels
// by walking the image data in accordance with the spec:
// http://www.w3.org/TR/PNG/
// If the colour type (UK spelling from spec) includes an alpha channel, or
// there is a tRNS section with at least one entry before IDAT, then we assume
// the image contains non-opaque pixels and return true.
bool ImageImpl::ComputePngTransparency(const StringPiece& buf) {
// We assume the image has transparency until we prove otherwise.
// This allows us to deal conservatively with truncation etc.
bool has_transparency = true;
if (buf.size() > ImageHeaders::kPngColourTypeOffset &&
((buf[ImageHeaders::kPngColourTypeOffset] &
ImageHeaders::kPngAlphaChannel) == 0)) {
// The colour type indicates that there is no dedicated alpha channel. Now
// we must look for a tRNS section indicating the existence of transparent
// colors or palette entries.
size_t section_start = ImageHeaders::kPngHeaderLength;
while (section_start + ImageHeaders::kPngSectionHeaderLength < buf.size()) {
size_t section_size = PngIntAtPosition(buf, section_start);
if (PngSectionIdIs(ImageHeaders::kPngIDAT, buf, section_start)) {
// tRNS section must occur before first IDAT. This image doesn't have a
// tRNS section, and thus doesn't have transparency.
has_transparency = false;
break;
} else if (PngSectionIdIs(ImageHeaders::kPngtRNS, buf, section_start) &&
section_size > 0) {
// Found a nonempty tRNS section. This image has_transparency.
break;
} else {
// Move on to next section.
section_start += section_size + ImageHeaders::kPngSectionMinSize;
}
}
}
return has_transparency;
}
bool ImageImpl::EnsureLoaded(bool output_useful) {
return true;
}
// Determine the quality level for compressing the resized image.
// If a JPEG image needs resizing, we decompress it first, then resize it,
// and finally compress it into a new JPEG image. To compress the output image,
// We would like to use the quality level that was used in the input image,
// if such information can be calculated from the input image; otherwise, we
// will use the quality level set in the configuration; otherwise, we will use
// a predefined default quality.
int ImageImpl::EstimateQualityForResizedJpeg() {
int input_quality = GetJpegQualityFromImage(original_contents_);
int output_quality = std::min(ImageHeaders::kMaxJpegQuality,
options_->jpeg_quality);
if (input_quality > 0 && output_quality > 0) {
return std::min(input_quality, output_quality);
} else if (input_quality > 0) {
return input_quality;
} else if (output_quality > 0) {
return output_quality;
} else {
return ImageHeaders::kQualityForJpegWithUnkownQuality;
}
}
void ImageImpl::Dimensions(ImageDim* natural_dim) {
if (!ImageUrlEncoder::HasValidDimensions(dims_)) {
ComputeImageType();
}
*natural_dim = dims_;
}
bool ImageImpl::ResizeTo(const ImageDim& new_dim) {
CHECK(ImageUrlEncoder::HasValidDimensions(new_dim));
if ((new_dim.width() <= 0) || (new_dim.height() <= 0)) {
return false;
}
if (changed_) {
// If we already resized, drop data and work with original image.
UndoChange();
}
// TODO(huibao): Enable resizing for WebP and images with alpha channel.
// We have the tools ready but no tests.
const ImageFormat original_format = ImageTypeToImageFormat(image_type());
if (original_format == pagespeed::image_compression::IMAGE_WEBP) {
return false;
}
scoped_ptr<ScanlineReaderInterface> image_reader(
CreateScanlineReader(original_format,
original_contents_.data(),
original_contents_.length(),
handler_.get()));
if (image_reader == NULL) {
resize_debug_message_ = "Cannot resize: Cannot open the image to resize";
PS_LOG_INFO(handler_, "Cannot open the image to resize.");
return false;
}
ScanlineResizer resizer(handler_.get());
if (!resizer.Initialize(image_reader.get(), new_dim.width(),
new_dim.height())) {
resize_debug_message_ = "Cannot resize: Unable to initialize resizer";
return false;
}
// Create a writer.
scoped_ptr<ScanlineWriterInterface> writer;
const ImageFormat resized_format = GetOutputImageFormat(original_format);
switch (resized_format) {
case pagespeed::image_compression::IMAGE_JPEG:
{
JpegCompressionOptions jpeg_config;
jpeg_config.lossy = true;
jpeg_config.lossy_options.quality = EstimateQualityForResizedJpeg();
writer.reset(CreateScanlineWriter(resized_format,
resizer.GetPixelFormat(),
resizer.GetImageWidth(),
resizer.GetImageHeight(),
&jpeg_config,
&resized_image_,
handler_.get()));
}
break;
case pagespeed::image_compression::IMAGE_PNG:
{
PngCompressParams png_config(PNG_FILTER_NONE, Z_DEFAULT_STRATEGY,
false);
writer.reset(CreateScanlineWriter(resized_format,
resizer.GetPixelFormat(),
resizer.GetImageWidth(),
resizer.GetImageHeight(),
&png_config,
&resized_image_,
handler_.get()));
}
break;
default:
resize_debug_message_ = "Cannot resize: Unsupported image format";
PS_LOG_DFATAL(handler_, "Unsupported image format");
}
if (writer == NULL) {
return false;
}
// Resize the image and save the results in 'resized_image_'.
void* scanline = NULL;
while (resizer.HasMoreScanLines()) {
if (!resizer.ReadNextScanline(&scanline)) {
resize_debug_message_ = "Cannot resize: Reading image failed";
return false;
}
if (!writer->WriteNextScanline(scanline)) {
resize_debug_message_ = "Cannot resize: Writing image failed";
return false;
}
}
if (!writer->FinalizeWrite()) {
resize_debug_message_ = "Cannot resize: Finalizing writing image failed";
return false;
}
changed_ = true;
output_valid_ = false;
rewrite_attempted_ = false;
output_contents_.clear();
resized_dimensions_ = new_dim;
resize_debug_message_ = StringPrintf(
"Resized image from %dx%d to %dx%d", dims_.width(), dims_.height(),
resized_dimensions_.width(), resized_dimensions_.height());
return true;
}
void ImageImpl::UndoChange() {
if (changed_) {
output_valid_ = false;
rewrite_attempted_ = false;
output_contents_.clear();
resized_image_.clear();
image_type_ = IMAGE_UNKNOWN;
changed_ = false;
}
}
// TODO(huibao): Refactor image rewriting. We may have a centralized
// controller and a set of naive image writers. The controller looks at
// the input image type and the filter settings, and decides which output
// format(s) to try and the configuration for each output format. The writers
// simply write the output based on the specified configurations and should not
// be aware of the input type nor the filters.
//
// Here are some thoughts for the new design.
// 1. Create a scanline reader based on the type of input image.
// 2. If the image is going to be resized, wrap the reader into a resizer, which
// is also a scanline reader.
// 3. Create a scanline writer or mutliple writers based the filter settings.
// The parameters for the writer will also be determined by the filters.
//
// Transfer all of the scanlines from the reader to the writer and the image is
// rewritten (and resized)!
// Performs image optimization and output
bool ImageImpl::ComputeOutputContents() {
if (rewrite_attempted_) {
return output_valid_;
}
rewrite_attempted_ = true;
if (!output_valid_) {
StringPiece contents;
bool resized;
// Choose appropriate source for image contents.
// Favor original contents if image unchanged.
resized = !resized_image_.empty();
if (resized) {
contents = resized_image_;
} else {
contents = original_contents_;
}
// Take image contents and re-compress them.
// The basic logic is this:
// * low_quality_enabled_ acts as though convert_gif_to_png and
// convert_png_to_webp were both set for this image.
// * We compute the intended final end state of all the
// convert_X_to_Y options, and try to convert to the final
// option in one shot. If that fails, we back off by each of the stages.
// * We return as soon as any applicable conversion succeeds. We
// do not compare the sizes of alternative conversions.
// If we can't optimize the image, we'll fail.
bool ok = false;
// We copy the data to a string eagerly as we're very likely to need it
// (only unrecognized formats don't require it, in which case we probably
// don't get this far in the first place).
// TODO(jmarantz): The PageSpeed library should, ideally, take StringPiece
// args rather than const string&. We would save lots of string-copying
// if we made that change.
GoogleString string_for_image(contents.data(), contents.size());
scoped_ptr<PngReaderInterface> png_reader;
switch (image_type()) {
case IMAGE_UNKNOWN:
break;
case IMAGE_WEBP:
case IMAGE_WEBP_LOSSLESS_OR_ALPHA:
if (resized || options_->recompress_webp) {
ok = MayConvert() &&
ReduceWebpImageQuality(string_for_image,
options_->webp_quality,
&output_contents_);
}
// TODO(pulkitg): Convert a webp image to jpeg image if
// web_preferred_ is false.
break;
case IMAGE_WEBP_ANIMATED:
// TODO(huibao): Recompress animated WebP.
ok = false;
break;
case IMAGE_JPEG:
if (MayConvert() &&
options_->convert_jpeg_to_webp &&
(options_->preferred_webp != WEBP_NONE)) {
ok = ConvertJpegToWebp(string_for_image, options_->webp_quality,
&output_contents_);
VLOG(1) << "Image conversion: " << ok << " jpeg->webp for " << url_;
if (!ok) {
// Image is not going to be webp-converted!
PS_LOG_INFO(handler_, "Failed to create webp!");
}
}
if (ok) {
image_type_ = IMAGE_WEBP;
} else if (MayConvert() &&
(resized || options_->recompress_jpeg)) {
JpegCompressionOptions jpeg_options;
ConvertToJpegOptions(*options_.get(), &jpeg_options);
ok = OptimizeJpegWithOptions(string_for_image, &output_contents_,
jpeg_options, handler_.get());
VLOG(1) << "Image conversion: " << ok << " jpeg->jpeg for " << url_;
}
break;
case IMAGE_PNG:
png_reader.reset(new PngReader(handler_.get()));
ok = ComputeOutputContentsFromGifOrPng(
string_for_image,
png_reader.get(),
(resized || options_->recompress_png) /* fall_back_to_png */,
kPngString,
IMAGE_PNG,
Image::ConversionVariables::FROM_PNG);
break;
case IMAGE_GIF:
ImageType current_image_type = IMAGE_GIF;
if (resized) {
// If the GIF image has been resized, it has already been
// converted to a PNG image.
png_reader.reset(new PngReader(handler_.get()));
current_image_type = IMAGE_PNG;
} else if (options_->convert_gif_to_png || low_quality_enabled_ ||
options_->allow_webp_animated) {
png_reader.reset(new GifReader(handler_.get()));
} else {
break;
}
ok = ComputeOutputContentsFromGifOrPng(
string_for_image,
png_reader.get(),
options_->convert_gif_to_png /* fall_back_to_png */,
kGifString,
current_image_type,
Image::ConversionVariables::FROM_GIF);
break;
}
output_valid_ = ok;
}
return output_valid_;
}
inline bool ImageImpl::ConvertJpegToWebp(
const GoogleString& original_jpeg, int configured_quality,
GoogleString* compressed_webp) {
ConversionTimeoutHandler timeout_handler(options_->webp_conversion_timeout_ms,
timer_, handler_.get());
timeout_handler.Start(compressed_webp);
bool ok = OptimizeWebp(original_jpeg, configured_quality,
ConversionTimeoutHandler::Continue, &timeout_handler,
compressed_webp, handler_.get());
timeout_handler.Stop();
bool was_timed_out = timeout_handler.was_timed_out();
int64 time_elapsed_ms = timeout_handler.time_elapsed_ms();
UpdateWebpStats(ok, was_timed_out, time_elapsed_ms,
Image::ConversionVariables::FROM_JPEG,
options_->webp_conversion_variables);
UpdateWebpStats(ok, was_timed_out, time_elapsed_ms,
Image::ConversionVariables::OPAQUE,
options_->webp_conversion_variables);
return ok;
}
bool ImageImpl::ConvertAnimatedGifToWebp(bool has_transparency) {
ConversionTimeoutHandler timeout_handler(
options_->webp_conversion_timeout_ms, timer_, handler_.get());
timeout_handler.Start(&output_contents_);
// Parameters controlling WebP compression.
WebpConfiguration webp_config;
webp_config.quality = options_->webp_animated_quality;
webp_config.progress_hook = ConversionTimeoutHandler::Continue;
webp_config.user_data = &timeout_handler;
// TODO(huibao): Evaluate the following parameters.
webp_config.method = 3;
webp_config.kmin = 3;
webp_config.kmax = 5;
webp_config.lossless = false;
webp_config.alpha_quality = 100;
webp_config.alpha_compression = 1; // alpha plane compressed losslessly
pagespeed::image_compression::ScanlineStatus status;
scoped_ptr<pagespeed::image_compression::MultipleFrameReader> reader(
CreateImageFrameReader(
pagespeed::image_compression::IMAGE_GIF,
original_contents_.data(),
original_contents_.length(),
handler_.get(), &status));
if (!status.Success()) {
PS_LOG_ERROR(handler_, "Cannot read the animated GIF image.");
return false;
}
scoped_ptr<pagespeed::image_compression::MultipleFrameWriter> writer(
CreateImageFrameWriter(
pagespeed::image_compression::IMAGE_WEBP,
&webp_config,
&output_contents_,
handler_.get(), &status));
if (!status.Success()) {
PS_LOG_ERROR(handler_, "Cannot create an animated WebP image for output.");
return false;
}
// Copy all pixels in all frames from the reader to the writer. This will do
// format conversion and compression.
pagespeed::image_compression::ImageSpec image_spec;
pagespeed::image_compression::FrameSpec frame_spec;
const void* scan_row = NULL;
if (reader->GetImageSpec(&image_spec, &status) &&
writer->PrepareImage(&image_spec, &status)) {
while (reader->HasMoreFrames() &&
reader->PrepareNextFrame(&status) &&
reader->GetFrameSpec(&frame_spec, &status) &&
writer->PrepareNextFrame(&frame_spec, &status)) {
while (reader->HasMoreScanlines() &&
reader->ReadNextScanline(&scan_row, &status) &&
writer->WriteNextScanline(scan_row, &status)) {
// intentional empty loop body
}
}
}
writer->FinalizeWrite(&status);
timeout_handler.Stop();
bool was_timed_out = timeout_handler.was_timed_out();
int64 time_elapsed_ms = timeout_handler.time_elapsed_ms();
bool ok = status.Success();
UpdateWebpStats(ok, was_timed_out, time_elapsed_ms,
Image::ConversionVariables::FROM_GIF_ANIMATED,
options_->webp_conversion_variables);
UpdateWebpStats(ok, was_timed_out, time_elapsed_ms,
(has_transparency ?
Image::ConversionVariables::NONOPAQUE :
Image::ConversionVariables::OPAQUE),
options_->webp_conversion_variables);
return ok;
}
inline bool ImageImpl::ComputeOutputContentsFromGifOrPng(
const GoogleString& string_for_image,
const PngReaderInterface* png_reader,
bool fall_back_to_png,
const char* dbg_input_format,
ImageType input_type,
ConversionVariables::VariableType var_type) {
// Don't try to optimize empty images, it just messes things up.
if (dims_.width() <= 0 || dims_.height() <= 0) {
return false;
}
bool ok = false;
bool is_animated = false;
bool has_transparency = false;
bool is_photo = false;
bool compress_color_losslessly = false;
ImageType output_type = IMAGE_UNKNOWN;
AnalyzeImage(ImageTypeToImageFormat(input_type),
string_for_image.data(), string_for_image.length(),
NULL /* width */, NULL /* height */, NULL /* is_progressive */,
&is_animated, &has_transparency, &is_photo,
NULL /* quality */, NULL /* reader */, handler_.get());
debug_message_ = StringPrintf("Image has%s transparent pixels,"
" is%s sensitive to compression noise, and"
" has%s animation.",
(has_transparency ? "" : " no"),
(is_photo ? " not" : ""),
(is_animated ? "" : " no"));
// By default, a lossless image conversion is eligible for lossless webp
// conversion.
if (is_animated) {
if (options_->preferred_webp == WEBP_ANIMATED &&
options_->webp_animated_quality > 0) {
output_type = IMAGE_WEBP_ANIMATED;
}
// else we can't recompress this image
} else if (is_photo && options_->convert_png_to_jpeg &&
(input_type == IMAGE_PNG ||
(input_type == IMAGE_GIF && options_->convert_gif_to_png))) {
// Can be converted to lossy format.
if (!has_transparency) {
// No alpha; can be converted to WebP lossy or JPEG.
if (options_->preferred_webp != WEBP_NONE &&
options_->convert_jpeg_to_webp &&
options_->webp_quality > 0) {
compress_color_losslessly = false;
output_type = IMAGE_WEBP;
} else if (options_->jpeg_quality > 0) {
output_type = IMAGE_JPEG;
}
} else {
if (options_->allow_webp_alpha &&
options_->convert_jpeg_to_webp &&
options_->webp_quality > 0) {
compress_color_losslessly = false;
output_type = IMAGE_WEBP_LOSSLESS_OR_ALPHA;
}
}
} else {
// Must be converted to lossless format.
if (options_->preferred_webp == WEBP_ANIMATED ||
options_->preferred_webp == WEBP_LOSSLESS) {
compress_color_losslessly = true;
output_type = IMAGE_WEBP_LOSSLESS_OR_ALPHA;
}
}
if (output_type == IMAGE_WEBP_ANIMATED) {
ok = ConvertAnimatedGifToWebp(has_transparency);
} else {
if (output_type == IMAGE_WEBP ||
output_type == IMAGE_WEBP_LOSSLESS_OR_ALPHA) {
ok = MayConvert() &&
ConvertPngToWebp(*png_reader, string_for_image,
compress_color_losslessly, has_transparency,
var_type);
// TODO(huibao): Re-evaluate why we need to try a different format, if the
// conversion to WebP failed.
if (!ok) {
// If the conversion to WebP failed, we will try converting the image to
// jpeg or png.
if (output_type == IMAGE_WEBP) {
output_type = IMAGE_JPEG;
} else {
fall_back_to_png = true;
}
}
}
if (output_type == IMAGE_JPEG) {
JpegCompressionOptions jpeg_options;
ConvertToJpegOptions(*options_.get(), &jpeg_options);
ok = MayConvert() &&
ImageConverter::ConvertPngToJpeg(*png_reader, string_for_image,
jpeg_options, &output_contents_,
handler_.get());
}
if (!ok && fall_back_to_png) {
ok = MayConvert() &&
PngOptimizer::OptimizePngBestCompression(*png_reader,
string_for_image,
&output_contents_,
handler_.get());
output_type = IMAGE_PNG;
}
}
if (ok) {
image_type_ = output_type;
} else {
image_type_ = input_type;
}
VLOG(1) << "Image conversion: " << ok << " " << dbg_input_format << "->"
<< ImageFormatToString(ImageTypeToImageFormat(image_type_)) << " for "
<< url_;
return ok;
}
bool ImageImpl::ConvertPngToWebp(
const PngReaderInterface& png_reader,
const GoogleString& input_image,
bool compress_color_losslessly,
bool has_transparency,
ConversionVariables::VariableType var_type) {
ConversionTimeoutHandler timeout_handler(options_->webp_conversion_timeout_ms,
timer_, handler_.get());
WebpConfiguration webp_config;
// Quality/speed trade-off (0=fast, 6=slower-better).
// This is the default value in libpagespeed. We should evaluate
// whether this is the optimal value, and consider making it
// tunable.
webp_config.method = 3;
webp_config.quality = options_->webp_quality;
webp_config.progress_hook = ConversionTimeoutHandler::Continue;
webp_config.user_data = &timeout_handler;
ImageType target_image_type = IMAGE_WEBP_LOSSLESS_OR_ALPHA;
if (compress_color_losslessly) {
// Note that webp_config.alpha_quality and
// webp_config.alpha_compression are only meaningful in the
// lossy compression case.
webp_config.lossless = true;
} else {
webp_config.lossless = false;
if (has_transparency) {
webp_config.alpha_quality = 100;
webp_config.alpha_compression = 1;
} else {
webp_config.alpha_quality = 0;
webp_config.alpha_compression = 0;
image_type_ = IMAGE_WEBP;
}
}
// TODO(huibao): Remove "is_opaque" from the returned arguments in
// ConvertPngToWebp() and PngScanlineReader::InitializeRead().
// The technique they use can only detect some of the opaque images.
// PixelFormatOptimizer has a more expensive, but comprehensive solution.
bool not_used;
timeout_handler.Start(&output_contents_);
bool ok = ImageConverter::ConvertPngToWebp(
png_reader, input_image, webp_config,
&output_contents_, &not_used, handler_.get());
if (ok) {
image_type_ = target_image_type;
}
timeout_handler.Stop();
bool was_timed_out = timeout_handler.was_timed_out();
int64 time_elapsed_ms = timeout_handler.time_elapsed_ms();
UpdateWebpStats(ok, was_timed_out, time_elapsed_ms, var_type,
options_->webp_conversion_variables);
UpdateWebpStats(ok, was_timed_out, time_elapsed_ms,
(has_transparency ?
Image::ConversionVariables::NONOPAQUE :
Image::ConversionVariables::OPAQUE),
options_->webp_conversion_variables);
return ok;
}
bool ImageImpl::OptimizePng(
const PngReaderInterface& png_reader,
const GoogleString& image_data) {
bool ok = MayConvert() &&
PngOptimizer::OptimizePngBestCompression(png_reader,
image_data,
&output_contents_,
handler_.get());
if (ok) {
image_type_ = IMAGE_PNG;
}
return ok;
}
bool ImageImpl::OptimizePngOrConvertToJpeg(
const PngReaderInterface& png_reader,
const GoogleString& image_data) {
bool is_png;
JpegCompressionOptions jpeg_options;
ConvertToJpegOptions(*options_.get(), &jpeg_options);
bool ok = MayConvert() &&
ImageConverter::OptimizePngOrConvertToJpeg(
png_reader, image_data, jpeg_options,
&output_contents_, &is_png, handler_.get());
if (ok) {
if (is_png) {
image_type_ = IMAGE_PNG;
} else {
image_type_ = IMAGE_JPEG;
}
}
return ok;
}
void ImageImpl::ConvertToJpegOptions(const Image::CompressionOptions& options,
JpegCompressionOptions* jpeg_options) {
int input_quality = GetJpegQualityFromImage(original_contents_);
jpeg_options->retain_color_profile = options.retain_color_profile;
jpeg_options->retain_exif_data = options.retain_exif_data;
int output_quality = EstimateQualityForResizedJpeg();
if (options.jpeg_quality > 0) {
// If the source image is JPEG we want to fallback to lossless if the input
// quality is less than the quality we want to set for final compression and
// num progressive scans is not set. Incase we are not able to decode the
// input image quality, then we use lossless path.
if (image_type() != IMAGE_JPEG ||
options.jpeg_num_progressive_scans > 0 ||
input_quality > output_quality) {
jpeg_options->lossy = true;
jpeg_options->lossy_options.quality = output_quality;
if (options.progressive_jpeg) {
jpeg_options->lossy_options.num_scans =
options.jpeg_num_progressive_scans;
}
if (options.retain_color_sampling) {
jpeg_options->lossy_options.color_sampling = RETAIN;
}
}
}
jpeg_options->progressive = options.progressive_jpeg &&
ShouldConvertToProgressive(output_quality);
}
bool ImageImpl::ShouldConvertToProgressive(int64 quality) const {
bool progressive = false;
const ImageDim* expected_dimensions = &dims_;
if (ImageUrlEncoder::HasValidDimensions(resized_dimensions_)) {
expected_dimensions = &resized_dimensions_;
}
if (ImageUrlEncoder::HasValidDimensions(*expected_dimensions)) {
progressive = pagespeed::image_compression::ShouldConvertToProgressive(
quality, options_->progressive_jpeg_min_bytes,
original_contents_.size(), expected_dimensions->width(),
expected_dimensions->height());
} else {
progressive = (static_cast<int64>(original_contents_.size()) >=
options_->progressive_jpeg_min_bytes);
}
return progressive;
}
StringPiece Image::Contents() {
StringPiece contents;
if (this->image_type() != IMAGE_UNKNOWN) {
contents = original_contents_;
if (output_valid_ || ComputeOutputContents()) {
contents = output_contents_;
}
}
return contents;
}
bool ImageImpl::DrawImage(Image* image, int x, int y) {
// Create a reader for reading the original canvas image.
scoped_ptr<ScanlineReaderInterface> canvas_reader(CreateScanlineReader(
pagespeed::image_compression::IMAGE_PNG,
output_contents_.data(),
output_contents_.length(),
handler_.get()));
if (canvas_reader == NULL) {
PS_LOG_ERROR(handler_, "Cannot open canvas image.");
return false;
}
// Get the size and pixel format of the original canvas image.
const size_t canvas_width = canvas_reader->GetImageWidth();
const size_t canvas_height = canvas_reader->GetImageHeight();
const PixelFormat canvas_pixel_format = canvas_reader->GetPixelFormat();
// Initialize a reader for reading the image which will be sprited.
ImageImpl* impl = static_cast<ImageImpl*>(image);
scoped_ptr<ScanlineReaderInterface> image_reader(CreateScanlineReader(
ImageTypeToImageFormat(impl->image_type()),
impl->original_contents().data(),
impl->original_contents().length(),
handler_.get()));
if (image_reader == NULL) {
PS_LOG_INFO(handler_, "Cannot open the image which will be sprited.");
return false;
}
// Get the size of the image which will be sprited.
const size_t image_width = image_reader->GetImageWidth();
const size_t image_height = image_reader->GetImageHeight();
const PixelFormat image_pixel_format = image_reader->GetPixelFormat();
if (x + image_width > canvas_width || y + image_height > canvas_height) {
PS_LOG_INFO(handler_, "The new image cannot fit into the canvas.");
return false;
}
bool has_transparency = false;
PixelFormat output_pixel_format = RGB_888;
if (image_pixel_format == RGBA_8888 ||
canvas_pixel_format == RGBA_8888) {
has_transparency = true;
output_pixel_format = RGBA_8888;
}
const size_t bytes_per_pixel =
GetNumChannelsFromPixelFormat(output_pixel_format, handler_.get());
const size_t bytes_per_scanline = canvas_width * bytes_per_pixel;
scoped_array<uint8> scanline(new uint8[bytes_per_scanline]);
// Create a writer for writing the new canvas image.
GoogleString canvas_image;
scoped_ptr<ScanlineWriterInterface> canvas_writer(
CreateUncompressedPngWriter(canvas_width, canvas_height,
&canvas_image, handler_.get(),
has_transparency));
if (canvas_writer == NULL) {
PS_LOG_ERROR(handler_, "Failed to create canvas writer.");
return false;
}
// Overlay the new image onto the canvas image.
for (int row = 0; row < static_cast<int>(canvas_height); ++row) {
uint8* canvas_line = NULL;
if (!canvas_reader->ReadNextScanline(
reinterpret_cast<void**>(&canvas_line))) {
PS_LOG_ERROR(handler_, "Failed to read canvas image.");
return false;
}
if (row >= y && row < y + static_cast<int>(image_height)) {
uint8* image_line = NULL;
if (!image_reader->ReadNextScanline(
reinterpret_cast<void**>(&image_line))) {
PS_LOG_INFO(handler_,
"Failed to read the image which will be sprited.");
return false;
}
// Set the entire scanline to white. This operation has no effect
// on the webpage; it just gives a clean background to the
// sprite image.
memset(scanline.get(), kAlphaOpaque, x * bytes_per_pixel);
memset(scanline.get() + (x + image_width) * bytes_per_pixel,
kAlphaOpaque,
(canvas_width - image_width - x) * bytes_per_pixel);
ExpandPixelFormat(image_width, image_pixel_format, 0, image_line,
output_pixel_format, x, scanline.get(), handler_.get());
} else {
ExpandPixelFormat(canvas_width, canvas_pixel_format, 0, canvas_line,
output_pixel_format, 0, scanline.get(), handler_.get());
}
if (!canvas_writer->WriteNextScanline(
reinterpret_cast<void*>(scanline.get()))) {
PS_LOG_ERROR(handler_, "Failed to write canvas image.");
return false;
}
}
if (!canvas_writer->FinalizeWrite()) {
PS_LOG_ERROR(handler_, "Failed to close canvas file.");
return false;
}
output_contents_ = canvas_image;
output_valid_ = true;
return true;
}
} // namespace net_instaweb