449 lines
15 KiB
C++
449 lines
15 KiB
C++
/*
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
// Author: huibao@google.com (Huibao Lin)
|
|
|
|
#include "pagespeed/kernel/image/image_optimizer.h"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
|
|
extern "C" {
|
|
#ifdef USE_SYSTEM_LIBPNG
|
|
#include "png.h" // NOLINT
|
|
#else
|
|
#include "third_party/libpng/src/png.h"
|
|
#endif
|
|
} // extern "C"
|
|
|
|
#include "base/logging.h"
|
|
#include "pagespeed/kernel/base/message_handler.h"
|
|
#include "pagespeed/kernel/base/string.h"
|
|
#include "pagespeed/kernel/base/timer.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/pixel_format_optimizer.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/webp_optimizer.h"
|
|
|
|
using net_instaweb::MessageHandler;
|
|
using net_instaweb::Timer;
|
|
using pagespeed::image_compression::ImageDimensions;
|
|
using pagespeed::image_compression::WebpConfiguration;
|
|
using pagespeed::image_compression::ConversionTimeoutHandler;
|
|
using pagespeed::image_compression::JpegCompressionOptions;
|
|
using pagespeed::image_compression::PngCompressParams;
|
|
using pagespeed::image_compression::ScanlineWriterConfig;
|
|
using pagespeed::image_compression::ImageFormat;
|
|
using pagespeed::image_compression::MultipleFrameReader;
|
|
using pagespeed::image_compression::MultipleFrameWriter;
|
|
using pagespeed::image_compression::ScanlineStatus;
|
|
using pagespeed::image_compression::ImageOptions;
|
|
using pagespeed::image_compression::ShouldConvertToProgressive;
|
|
using pagespeed::image_compression::PixelFormatOptimizer;
|
|
using pagespeed::image_compression::ScanlineReaderInterface;
|
|
using pagespeed::image_compression::ScanlineResizer;
|
|
using pagespeed::image_compression::ScanlineWriterInterface;
|
|
using pagespeed::image_compression::ImageConverter;
|
|
using pagespeed::image_compression::ComputeImageType;
|
|
using pagespeed::image_compression::IMAGE_GIF;
|
|
using pagespeed::image_compression::IMAGE_PNG;
|
|
using pagespeed::image_compression::IMAGE_JPEG;
|
|
using pagespeed::image_compression::IMAGE_WEBP;
|
|
using pagespeed::image_compression::IMAGE_UNKNOWN;
|
|
|
|
namespace pagespeed {
|
|
|
|
namespace image_compression {
|
|
|
|
bool ImageOptimizer::ComputeDesiredFormat() {
|
|
desired_lossless_ = false;
|
|
optimized_format_ = IMAGE_UNKNOWN;
|
|
if (is_animated_) {
|
|
if (options_.allow_webp_animated()) {
|
|
optimized_format_ = IMAGE_WEBP;
|
|
desired_lossless_ = true;
|
|
}
|
|
} else if (is_transparent_) {
|
|
if (options_.allow_webp_lossless_or_alpha()) {
|
|
optimized_format_ = IMAGE_WEBP;
|
|
desired_lossless_ = true;
|
|
} else if (options_.allow_png()) {
|
|
optimized_format_ = IMAGE_PNG;
|
|
desired_lossless_ = true;
|
|
}
|
|
} else {
|
|
// single frame and opaque
|
|
if (is_photo_ &&
|
|
(original_format_ == IMAGE_JPEG ||
|
|
options_.allow_convert_lossless_to_lossy())) {
|
|
// We can use lossy format.
|
|
if (options_.allow_webp_lossy()) {
|
|
optimized_format_ = IMAGE_WEBP;
|
|
} else if (options_.allow_jpeg()) {
|
|
optimized_format_ = IMAGE_JPEG;
|
|
}
|
|
} else if (options_.allow_webp_lossless_or_alpha()) {
|
|
optimized_format_ = IMAGE_WEBP;
|
|
desired_lossless_ = true;
|
|
} else if (options_.allow_png()) {
|
|
optimized_format_ = IMAGE_PNG;
|
|
desired_lossless_ = true;
|
|
}
|
|
}
|
|
|
|
return optimized_format_ != IMAGE_UNKNOWN;
|
|
}
|
|
|
|
// Computes the dimension for the resized image. In the requested dimensions
|
|
// you can specify the width, height, or both. This method will compute
|
|
// the unspecified dimension. If the input dimensions are valid and the
|
|
// output dimensions are smaller than the input, this method returns TRUE.
|
|
bool ImageOptimizer::ComputeResizedDimension() {
|
|
if (original_width_ < 1 || original_height_ < 1 ||
|
|
(requested_dim_.has_width() && requested_dim_.width() < 1) ||
|
|
(requested_dim_.has_height() && requested_dim_.height() < 1)) {
|
|
return false;
|
|
}
|
|
|
|
// Do not resize the image in these cases:
|
|
// - input is an animated image
|
|
// - requested dimension is larger than the original in either way.
|
|
// By returning TRUE, the image can still have other optimizations.
|
|
if (is_animated_ ||
|
|
(requested_dim_.has_width() &&
|
|
requested_dim_.width() > original_width_) ||
|
|
(requested_dim_.has_height() &&
|
|
requested_dim_.height() > original_height_)) {
|
|
optimized_width_ = original_width_;
|
|
optimized_height_ = original_height_;
|
|
return true;
|
|
}
|
|
|
|
if (!requested_dim_.has_width() && !requested_dim_.has_height()) {
|
|
optimized_width_ = original_width_;
|
|
optimized_height_ = original_height_;
|
|
} else if (!requested_dim_.has_width()) {
|
|
optimized_height_ = requested_dim_.height();
|
|
optimized_width_ =
|
|
(optimized_height_ * original_width_ + original_height_ / 2) /
|
|
original_height_;
|
|
} else if (!requested_dim_.has_height()) {
|
|
optimized_width_ = requested_dim_.width();
|
|
optimized_height_ =
|
|
(optimized_width_ * original_height_ + original_width_ / 2) /
|
|
original_width_;
|
|
} else {
|
|
optimized_width_ = requested_dim_.width();
|
|
optimized_height_ = requested_dim_.height();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ImageOptimizer::ComputeDesiredQualityProgressive() {
|
|
// Determines quality level and whether to use progressive format.
|
|
desired_progressive_ = false;
|
|
int quality = original_quality_;
|
|
if (quality == -1) {
|
|
quality = 100;
|
|
}
|
|
if (optimized_format_ == IMAGE_JPEG) {
|
|
quality = std::min(quality, options_.max_jpeg_quality());
|
|
const int kMinJpegProgressiveBytes = 10240;
|
|
desired_progressive_ =
|
|
ShouldConvertToProgressive(
|
|
quality, kMinJpegProgressiveBytes,
|
|
original_contents_.length(), optimized_width_, optimized_height_);
|
|
|
|
} else if (is_animated_) {
|
|
quality = std::min(quality, options_.max_webp_animated_quality());
|
|
} else {
|
|
quality = std::min(quality, options_.max_webp_quality());
|
|
}
|
|
|
|
if (quality >= 0 && quality <= 100) {
|
|
desired_quality_ = quality;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO(huibao): Unify ImageFormat and ImageType.
|
|
ImageFormat ImageOptimizer::ImageTypeToImageFormat(
|
|
net_instaweb::ImageType image_type) {
|
|
ImageFormat image_format = IMAGE_UNKNOWN;
|
|
switch (image_type) {
|
|
case net_instaweb::IMAGE_UNKNOWN:
|
|
image_format = IMAGE_UNKNOWN;
|
|
break;
|
|
case net_instaweb::IMAGE_JPEG:
|
|
image_format = IMAGE_JPEG;
|
|
break;
|
|
case net_instaweb::IMAGE_PNG:
|
|
image_format = IMAGE_PNG;
|
|
break;
|
|
case net_instaweb::IMAGE_GIF:
|
|
image_format = IMAGE_GIF;
|
|
break;
|
|
case net_instaweb::IMAGE_WEBP:
|
|
case net_instaweb::IMAGE_WEBP_LOSSLESS_OR_ALPHA:
|
|
case net_instaweb::IMAGE_WEBP_ANIMATED:
|
|
image_format = IMAGE_WEBP;
|
|
break;
|
|
}
|
|
return image_format;
|
|
}
|
|
|
|
// Returns a configuration for writing JPEG, PNG, or WebP image.
|
|
// Caller needs to cast the pointer back to the correct class.
|
|
bool ImageOptimizer::ConfigureWriter() {
|
|
std::unique_ptr<PngCompressParams> png_config;
|
|
std::unique_ptr<JpegCompressionOptions> jpeg_config;
|
|
std::unique_ptr<WebpConfiguration> webp_config;
|
|
|
|
bool result = false;
|
|
switch (optimized_format_) {
|
|
case IMAGE_UNKNOWN:
|
|
case IMAGE_GIF:
|
|
break;
|
|
case IMAGE_PNG:
|
|
png_config.reset(
|
|
new PngCompressParams(
|
|
options_.try_best_compression_for_png(),
|
|
false /* never use progressive format */));
|
|
writer_config_.reset(png_config.release());
|
|
result = true;
|
|
break;
|
|
case IMAGE_JPEG:
|
|
jpeg_config.reset(new JpegCompressionOptions);
|
|
jpeg_config->retain_color_profile = false;
|
|
jpeg_config->retain_exif_data = false;
|
|
jpeg_config->lossy = true;
|
|
jpeg_config->progressive = desired_progressive_;
|
|
jpeg_config->lossy_options.quality = desired_quality_;
|
|
writer_config_.reset(jpeg_config.release());
|
|
result = true;
|
|
break;
|
|
case IMAGE_WEBP:
|
|
webp_config.reset(new WebpConfiguration);
|
|
// 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->kmin = 3;
|
|
webp_config->kmax = 5;
|
|
webp_config->user_data = timeout_handler_.get();
|
|
webp_config->progress_hook = ConversionTimeoutHandler::Continue;
|
|
webp_config->lossless = desired_lossless_;
|
|
|
|
// In the lossless mode, the "quality" aprameter does not affect the
|
|
// visual quality of encoded image, however, it affects the number of
|
|
// bytes which the encoded image has. For consistent output, we set it
|
|
// to a constant value.
|
|
webp_config->quality = (desired_lossless_ ? 100 : desired_quality_);
|
|
|
|
if (is_transparent_) {
|
|
webp_config->alpha_quality = 100;
|
|
webp_config->alpha_compression = 1;
|
|
} else {
|
|
webp_config->alpha_quality = 0;
|
|
webp_config->alpha_compression = 0;
|
|
}
|
|
writer_config_.reset(webp_config.release());
|
|
result = true;
|
|
break;
|
|
// no default:
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Rewrites a single frame image. Optimizations to apply includes
|
|
// resizing dimensions, reducing color channels, and converting to better
|
|
// format.
|
|
bool ImageOptimizer::RewriteSingleFrameImage() {
|
|
std::unique_ptr<ScanlineReaderInterface> reader(
|
|
CreateScanlineReader(original_format_, original_contents_.data(),
|
|
original_contents_.length(), message_handler_));
|
|
|
|
if (reader == nullptr) {
|
|
PS_LOG_INFO(message_handler_, "Cannot open the image.");
|
|
return false;
|
|
}
|
|
|
|
std::unique_ptr<PixelFormatOptimizer> optimizer(
|
|
new PixelFormatOptimizer(message_handler_));
|
|
if (!optimizer->Initialize(reader.release()).Success()) {
|
|
return false;
|
|
}
|
|
|
|
bool need_resizing =
|
|
(optimized_width_ < static_cast<int>(optimizer->GetImageWidth()) ||
|
|
optimized_height_ < static_cast<int>(optimizer->GetImageHeight()));
|
|
|
|
ScanlineReaderInterface* processor = nullptr;
|
|
std::unique_ptr<ScanlineResizer> resizer;
|
|
if (need_resizing) {
|
|
resizer.reset(new ScanlineResizer(message_handler_));
|
|
if (!resizer->Initialize(optimizer.get(), optimized_width_,
|
|
optimized_height_)) {
|
|
return false;
|
|
}
|
|
processor = resizer.get();
|
|
} else {
|
|
processor = optimizer.get();
|
|
}
|
|
|
|
std::unique_ptr<ScanlineWriterInterface> writer(
|
|
CreateScanlineWriter(optimized_format_, processor->GetPixelFormat(),
|
|
processor->GetImageWidth(),
|
|
processor->GetImageHeight(),
|
|
writer_config_.get(), optimized_contents_,
|
|
message_handler_));
|
|
if (writer == nullptr) {
|
|
PS_LOG_INFO(message_handler_,
|
|
"Cannot create an image for output.");
|
|
return false;
|
|
}
|
|
|
|
bool result = ImageConverter::ConvertImage(processor, writer.get());
|
|
return result;
|
|
}
|
|
|
|
// Rewrite an animated image. Currently this is limited to converting
|
|
// an animated GIF image to animated WebP.
|
|
//
|
|
// TODO(huibao): Apply resizing and pixel format optimization to animated
|
|
// image.
|
|
bool ImageOptimizer::RewriteAnimatedImage() {
|
|
ScanlineStatus status;
|
|
std::unique_ptr<MultipleFrameReader>
|
|
reader(
|
|
CreateImageFrameReader(
|
|
IMAGE_GIF,
|
|
original_contents_.data(), original_contents_.length(),
|
|
message_handler_, &status));
|
|
if (!status.Success()) {
|
|
PS_LOG_INFO(message_handler_, "Cannot read the animated GIF image.");
|
|
return false;
|
|
}
|
|
|
|
std::unique_ptr<MultipleFrameWriter>
|
|
writer(
|
|
CreateImageFrameWriter(
|
|
IMAGE_WEBP,
|
|
writer_config_.get(), optimized_contents_, message_handler_,
|
|
&status));
|
|
if (!status.Success()) {
|
|
PS_LOG_INFO(message_handler_,
|
|
"Cannot create an animated WebP image for output.");
|
|
return false;
|
|
}
|
|
|
|
status =
|
|
ImageConverter::ConvertMultipleFrameImage(reader.get(), writer.get());
|
|
return status.Success();
|
|
}
|
|
|
|
bool ImageOptimizer::Run() {
|
|
if (options_.max_timeout_ms() > 0 && timer_ != nullptr) {
|
|
timeout_handler_.reset(
|
|
new ConversionTimeoutHandler(options_.max_timeout_ms(), timer_,
|
|
message_handler_));
|
|
if (timeout_handler_ != nullptr) {
|
|
timeout_handler_->Start(optimized_contents_);
|
|
}
|
|
} else {
|
|
timeout_handler_.reset();
|
|
}
|
|
|
|
original_format_ =
|
|
ImageTypeToImageFormat(ComputeImageType(original_contents_));
|
|
if (original_format_ == IMAGE_UNKNOWN || original_format_ == IMAGE_WEBP) {
|
|
return false;
|
|
}
|
|
|
|
if (!AnalyzeImage(original_format_, original_contents_.data(),
|
|
original_contents_.length(), &original_width_,
|
|
&original_height_, &is_progressive_, &is_animated_,
|
|
&is_transparent_, &is_photo_, &original_quality_, nullptr,
|
|
message_handler_)) {
|
|
return false;
|
|
}
|
|
|
|
if (!ComputeDesiredFormat() ||
|
|
!ComputeResizedDimension() ||
|
|
!ComputeDesiredQualityProgressive() ||
|
|
!ConfigureWriter()) {
|
|
return false;
|
|
}
|
|
|
|
bool result = false;
|
|
optimized_contents_->clear();
|
|
if (is_animated_) {
|
|
result = RewriteAnimatedImage();
|
|
} else {
|
|
result = RewriteSingleFrameImage();
|
|
}
|
|
|
|
// Stops timer and reports whether timeout happened.
|
|
was_timed_out_ = false;
|
|
if (timeout_handler_ != nullptr) {
|
|
timeout_handler_->Stop();
|
|
was_timed_out_ = timeout_handler_->was_timed_out();
|
|
}
|
|
|
|
if (result && options_.must_reduce_bytes() &&
|
|
optimized_contents_->length() > original_contents_.length()) {
|
|
result = false;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool ImageOptimizer::Optimize(
|
|
StringPiece original_contents, GoogleString* optimized_contents,
|
|
ImageFormat* optimized_format) {
|
|
// This method can only be called once.
|
|
CHECK(is_valid_);
|
|
is_valid_ = false;
|
|
|
|
// All output buffers cannot be NULL.
|
|
CHECK(optimized_contents != nullptr && optimized_format != nullptr);
|
|
|
|
original_contents_ = original_contents;
|
|
optimized_contents_ = optimized_contents;
|
|
|
|
bool result = Run();
|
|
if (result) {
|
|
*optimized_format = optimized_format_;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} // namespace image_compression
|
|
|
|
} // namespace pagespeed
|