| /* |
| * 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 |