blob: 4db05f2d3713036bf7b53e47bf9246604fd8096b [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_rewrite_filter.h"
#include <limits.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <algorithm>
#include <cstdarg>
#include <utility>
#include <vector>
#include "base/logging.h"
#include "net/instaweb/http/public/log_record.h"
#include "net/instaweb/http/public/logging_proto.h"
#include "net/instaweb/http/public/logging_proto_impl.h"
#include "net/instaweb/http/public/request_context.h"
#include "net/instaweb/rewriter/cached_result.pb.h"
#include "net/instaweb/rewriter/public/central_controller_interface_adapter.h"
#include "net/instaweb/rewriter/public/critical_images_beacon_filter.h"
#include "net/instaweb/rewriter/public/critical_images_finder.h"
#include "net/instaweb/rewriter/public/css_url_encoder.h"
#include "net/instaweb/rewriter/public/css_util.h"
#include "net/instaweb/rewriter/public/image.h"
#include "net/instaweb/rewriter/public/local_storage_cache_filter.h"
#include "net/instaweb/rewriter/public/output_resource.h"
#include "net/instaweb/rewriter/public/output_resource_kind.h"
#include "net/instaweb/rewriter/public/request_properties.h"
#include "net/instaweb/rewriter/public/resource.h"
#include "net/instaweb/rewriter/public/resource_namer.h"
#include "net/instaweb/rewriter/public/resource_slot.h"
#include "net/instaweb/rewriter/public/resource_tag_scanner.h"
#include "net/instaweb/rewriter/public/responsive_image_filter.h"
#include "net/instaweb/rewriter/public/rewrite_context.h"
#include "net/instaweb/rewriter/public/rewrite_driver.h"
#include "net/instaweb/rewriter/public/rewrite_driver_factory.h"
#include "net/instaweb/rewriter/public/rewrite_options.h"
#include "net/instaweb/rewriter/public/server_context.h"
#include "net/instaweb/rewriter/public/single_rewrite_context.h"
#include "net/instaweb/util/public/property_cache.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/escaping.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/base/timer.h"
#include "pagespeed/kernel/html/html_element.h"
#include "pagespeed/kernel/html/html_name.h"
#include "pagespeed/kernel/html/html_node.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/data_url.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/kernel/http/semantic_type.h"
#include "pagespeed/kernel/image/image_util.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/util/simple_random.h"
#include "pagespeed/opt/logging/enums.pb.h"
namespace net_instaweb {
namespace {
void DetermineQualities(const RewriteOptions& options,
const ResourceContext& resource_context,
const RequestProperties& request_properties,
Image::CompressionOptions* image_options) {
if (resource_context.may_use_save_data_quality()) {
// Use Save-Data qualities.
image_options->webp_quality = options.ImageWebpQualityForSaveData();
image_options->webp_animated_quality =
options.ImageWebpQualityForSaveData();
image_options->jpeg_quality = options.ImageJpegQualityForSaveData();
image_options->jpeg_num_progressive_scans =
options.image_jpeg_num_progressive_scans();
} else if (resource_context.may_use_small_screen_quality()) {
// Use small screen qualities.
image_options->webp_quality = options.ImageWebpQualityForSmallScreen();
image_options->webp_animated_quality = options.ImageWebpAnimatedQuality();
image_options->jpeg_quality = options.ImageJpegQualityForSmallScreen();
image_options->jpeg_num_progressive_scans =
options.ImageJpegNumProgressiveScansForSmallScreen();
} else {
// Use regular (desktop) qualities.
image_options->webp_quality = options.ImageWebpQuality();
image_options->webp_animated_quality = options.ImageWebpAnimatedQuality();
image_options->jpeg_quality = options.ImageJpegQuality();
image_options->jpeg_num_progressive_scans =
options.image_jpeg_num_progressive_scans();
}
}
int64 GetPageWidth(const int64 page_height,
const int64 image_width,
const int64 image_height) {
if (image_height > 0) {
return (page_height * image_width + image_height / 2) / image_height;
} else {
// The client should ensure that "image_height > 0". If this condition is
// not met, we protect against division by 0 by returning 0 so that resize
// attempts will fail.
return 0;
}
}
int64 GetPageHeight(const int64 page_width,
const int64 image_height,
const int64 image_width) {
if (image_height > 0) {
return (page_width * image_height + image_width / 2) / image_width;
} else {
// The client should ensure that "image_width > 0". If this condition is
// not met, we protect against division by 0 by returning 0 so that resize
// attempts will fail.
return 0;
}
}
void SetDesiredDimensionsIfRequired(ImageDim* desired_dim,
const ImageDim& image_dim) {
if (!ImageUrlEncoder::HasValidDimension(*desired_dim)) {
return;
}
int32 page_width = desired_dim->width(); // Rendered width.
int32 page_height = desired_dim->height(); // Rendered height.
const int64 image_width = image_dim.width();
const int64 image_height = image_dim.height();
if (!desired_dim->has_width()) {
// Fill in a missing page height:
// page_height * (image_width / image_height),
// rounding the result.
// To avoid fractions we instead group as
// (page_height * image_width) / image_height and do the
// math in int64 to avoid overflow in the numerator. The additional
// image_height / 2 causes us to round rather than truncate.
desired_dim->set_height(page_height);
desired_dim->set_width(static_cast<int32>(GetPageWidth(
page_height, image_width, image_height)));
} else if (!desired_dim->has_height()) {
desired_dim->set_width(page_width);
desired_dim->set_height(static_cast<int32>(GetPageHeight(
page_width, image_height, image_width)));
}
}
// Returns true if the low-res image can be inline-previewed.
bool ShouldInlinePreview(const int64 low_res_size, const int64 full_res_size,
const RewriteOptions* options) {
bool low_res_is_small = options->max_low_res_image_size_bytes() < 0 ||
low_res_size <= options->max_low_res_image_size_bytes();
bool low_res_smaller_than_full_res =
low_res_size * 100 < full_res_size *
options->max_low_res_to_full_res_image_size_percentage();
return (low_res_is_small && low_res_smaller_than_full_res);
}
const char* const kRelatedOptions[] = {
RewriteOptions::kImageJpegNumProgressiveScans,
RewriteOptions::kImageJpegNumProgressiveScansForSmallScreens,
RewriteOptions::kImageJpegRecompressionQuality,
RewriteOptions::kImageJpegRecompressionQualityForSmallScreens,
RewriteOptions::kImageJpegQualityForSaveData,
RewriteOptions::kImageLimitOptimizedPercent,
RewriteOptions::kImageLimitResizeAreaPercent,
RewriteOptions::kImageMaxRewritesAtOnce,
RewriteOptions::kImagePreserveURLs,
RewriteOptions::kImageRecompressionQuality,
RewriteOptions::kImageResolutionLimitBytes,
RewriteOptions::kImageWebpRecompressionQuality,
RewriteOptions::kImageWebpRecompressionQualityForSmallScreens,
RewriteOptions::kImageWebpAnimatedRecompressionQuality,
RewriteOptions::kImageWebpQualityForSaveData,
RewriteOptions::kProgressiveJpegMinBytes
};
} // namespace
// Expose kRelatedFilters as a class variable for the benefit of
// static-init-time merging in css_filter.cc.
const RewriteOptions::Filter ImageRewriteFilter::kRelatedFilters[] = {
RewriteOptions::kConvertGifToPng,
RewriteOptions::kConvertJpegToProgressive,
RewriteOptions::kConvertJpegToWebp,
RewriteOptions::kConvertPngToJpeg,
RewriteOptions::kConvertToWebpAnimated,
RewriteOptions::kConvertToWebpLossless,
RewriteOptions::kJpegSubsampling,
RewriteOptions::kRecompressJpeg,
RewriteOptions::kRecompressPng,
RewriteOptions::kRecompressWebp,
RewriteOptions::kResizeImages,
RewriteOptions::kResizeMobileImages,
RewriteOptions::kStripImageColorProfile,
RewriteOptions::kStripImageMetaData
};
const int ImageRewriteFilter::kRelatedFiltersSize = arraysize(kRelatedFilters);
StringPieceVector* ImageRewriteFilter::related_options_ = NULL;
// names for Statistics variables.
const char ImageRewriteFilter::kImageRewrites[] = "image_rewrites";
const char ImageRewriteFilter::kImageNoRewritesHighResolution[] =
"image_norewrites_high_resolution";
const char kImageRewritesDroppedIntentionally[] =
"image_rewrites_dropped_intentionally";
const char ImageRewriteFilter::kImageRewritesDroppedDecodeFailure[] =
"image_rewrites_dropped_decode_failure";
const char ImageRewriteFilter::kImageRewritesDroppedServerWriteFail[] =
"image_rewrites_dropped_server_write_fail";
const char ImageRewriteFilter::kImageRewritesDroppedMIMETypeUnknown[] =
"image_rewrites_dropped_mime_type_unknown";
const char ImageRewriteFilter::kImageRewritesDroppedNoSavingResize[] =
"image_rewrites_dropped_nosaving_resize";
const char ImageRewriteFilter::kImageRewritesDroppedNoSavingNoResize[] =
"image_rewrites_dropped_nosaving_noresize";
const char ImageRewriteFilter::kImageRewritesDroppedDueToLoad[] =
"image_rewrites_dropped_due_to_load";
const char ImageRewriteFilter::kImageRewritesSquashingForMobileScreen[] =
"image_rewrites_squashing_for_mobile_screen";
const char kImageRewriteTotalBytesSaved[] = "image_rewrite_total_bytes_saved";
const char kImageRewriteTotalOriginalBytes[] =
"image_rewrite_total_original_bytes";
const char kImageRewriteUses[] = "image_rewrite_uses";
const char kImageInline[] = "image_inline";
const char ImageRewriteFilter::kImageOngoingRewrites[] =
"image_ongoing_rewrites";
const char ImageRewriteFilter::kImageResizedUsingRenderedDimensions[] =
"image_resized_using_rendered_dimensions";
const char ImageRewriteFilter::kImageWebpRewrites[] = "image_webp_rewrites";
const char ImageRewriteFilter::kInlinableImageUrlsPropertyName[] =
"ImageRewriter-inlinable-urls";
const char ImageRewriteFilter::kImageRewriteLatencyOkMs[] =
"image_rewrite_latency_ok_ms";
const char ImageRewriteFilter::kImageRewriteLatencyFailedMs[] =
"image_rewrite_latency_failed_ms";
const char ImageRewriteFilter::kImageRewriteLatencyTotalMs[] =
"image_rewrite_latency_total_ms";
const char ImageRewriteFilter::kImageWebpFromGifTimeouts[] =
"image_webp_conversion_gif_timeouts";
const char ImageRewriteFilter::kImageWebpFromPngTimeouts[] =
"image_webp_conversion_png_timeouts";
const char ImageRewriteFilter::kImageWebpFromJpegTimeouts[] =
"image_webp_conversion_jpeg_timeouts";
const char ImageRewriteFilter::kImageWebpFromGifAnimatedTimeouts[] =
"image_webp_conversion_gif_animated_timeouts";
const char ImageRewriteFilter::kImageWebpFromGifSuccessMs[] =
"image_webp_conversion_gif_success_ms";
const char ImageRewriteFilter::kImageWebpFromPngSuccessMs[] =
"image_webp_conversion_png_success_ms";
const char ImageRewriteFilter::kImageWebpFromJpegSuccessMs[] =
"image_webp_conversion_jpeg_success_ms";
const char ImageRewriteFilter::kImageWebpFromGifAnimatedSuccessMs[] =
"image_webp_conversion_gif_animated_success_ms";
const char ImageRewriteFilter::kImageWebpFromGifFailureMs[] =
"image_webp_conversion_gif_failure_ms";
const char ImageRewriteFilter::kImageWebpFromPngFailureMs[] =
"image_webp_conversion_png_failure_ms";
const char ImageRewriteFilter::kImageWebpFromJpegFailureMs[] =
"image_webp_conversion_jpeg_failure_ms";
const char ImageRewriteFilter::kImageWebpFromGifAnimatedFailureMs[] =
"image_webp_conversion_gif_animated_failure_ms";
const char ImageRewriteFilter::kImageWebpWithAlphaTimeouts[] =
"image_webp_alpha_timeouts";
const char ImageRewriteFilter::kImageWebpWithAlphaSuccessMs[] =
"image_webp_alpha_success_ms";
const char ImageRewriteFilter::kImageWebpWithAlphaFailureMs[] =
"image_webp_alpha_failure_ms";
const char ImageRewriteFilter::kImageWebpOpaqueTimeouts[] =
"image_webp_opaque_timeouts";
const char ImageRewriteFilter::kImageWebpOpaqueSuccessMs[] =
"image_webp_opaque_success_ms";
const char ImageRewriteFilter::kImageWebpOpaqueFailureMs[] =
"image_webp_opaque_failure_ms";
const int kNotCriticalIndex = INT_MAX;
// This is the resized placeholder image width for mobile.
const int kDelayImageWidthForMobile = 320;
namespace {
void LogImageBackgroundRewriteActivity(
RewriteDriver* driver,
RewriterApplication::Status status,
const GoogleString& url,
const char* id,
int original_size,
int optimized_size,
bool is_recompressed,
ImageType original_image_type,
ImageType optimized_image_type,
bool is_resized,
int original_width,
int original_height,
bool is_resized_using_rendered_dimensions,
int resized_width,
int resized_height) {
const RewriteOptions* options = driver->options();
if (!options->log_background_rewrites()) {
return;
}
AbstractLogRecord* log_record =
driver->request_context()->GetBackgroundRewriteLog(
driver->server_context()->thread_system(),
options->allow_logging_urls_in_log_record(),
options->log_url_indices(),
options->max_rewrite_info_log_size());
// Write log for background rewrites.
log_record->LogImageBackgroundRewriteActivity(status, url, id, original_size,
optimized_size, is_recompressed, original_image_type,
optimized_image_type, is_resized, original_width, original_height,
is_resized_using_rendered_dimensions, resized_width, resized_height);
}
const char* MessageForInlineResult(InlineResult inline_result) {
const char* message = "";
switch (inline_result) {
case INLINE_SUCCESS:
// No message will be displayed.
break;
case INLINE_UNSUPPORTED_DEVICE:
message = "The image was not inlined because device does not support "
"inlinling.";
break;
case INLINE_NOT_CRITICAL:
message = "The image was not inlined because you have chosen to only "
"inline the critical images but this image is not critical.";
break;
case INLINE_NO_DATA:
case INLINE_TOO_LARGE:
message = "The image was not inlined because it has too many bytes.";
break;
case INLINE_CACHE_SMALL_IMAGES_UNREWRITTEN:
message = "The image was not inlined because CacheSmallImagesUnrewritten "
"has been set.";
break;
case INLINE_RESPONSIVE:
// Don't add any debug message for virtual responsive images. This virtual
// image will be deleted before the user sees it, so message won't be
// useful.
break;
case INLINE_SHORTCUT:
message = "The image was not inlined because it is a shortcut icon.";
break;
case INLINE_INTERNAL_ERROR:
message = "The image was not inlined because the internal data was "
"corrupted.";
break;
}
return message;
}
} // namespace
class ImageRewriteFilter::Context : public SingleRewriteContext {
public:
Context(int64 css_image_inline_max_bytes,
ImageRewriteFilter* filter, RewriteDriver* driver,
RewriteContext* parent, ResourceContext* resource_context,
bool is_css, int html_index, bool in_noscript_element,
bool is_resized_using_rendered_dimensions)
: SingleRewriteContext(driver, parent, resource_context),
css_image_inline_max_bytes_(css_image_inline_max_bytes),
filter_(filter),
is_css_(is_css),
html_index_(html_index),
in_noscript_element_(in_noscript_element),
is_resized_using_rendered_dimensions_(
is_resized_using_rendered_dimensions) {}
virtual ~Context() {}
virtual void Render();
virtual void RewriteSingle(const ResourcePtr& input,
const OutputResourcePtr& output);
virtual const char* id() const { return filter_->id(); }
virtual OutputResourceKind kind() const { return kRewrittenResource; }
virtual const UrlSegmentEncoder* encoder() const;
// Implements UserAgentCacheKey method of RewriteContext.
virtual GoogleString UserAgentCacheKey(
const ResourceContext* resource_context) const;
// Implements EncodeUserAgentIntoResourceContext of RewriteContext.
virtual void EncodeUserAgentIntoResourceContext(
ResourceContext* context);
private:
class InvokeRewriteFunction;
friend class ImageRewriteFilter;
int64 css_image_inline_max_bytes_;
ImageRewriteFilter* filter_;
bool is_css_;
const int html_index_;
bool in_noscript_element_;
bool is_resized_using_rendered_dimensions_;
DISALLOW_COPY_AND_ASSIGN(Context);
};
class ImageRewriteFilter::Context::InvokeRewriteFunction
: public ExpensiveOperationCallback {
public:
InvokeRewriteFunction(ImageRewriteFilter::Context* context,
ImageRewriteFilter* filter,
const ResourcePtr& input_resource,
const OutputResourcePtr& output_resource)
: ExpensiveOperationCallback(
context->Driver()->low_priority_rewrite_worker()),
context_(context),
filter_(filter),
input_resource_(input_resource),
output_resource_(output_resource) {}
virtual ~InvokeRewriteFunction() { }
protected:
virtual void RunImpl(scoped_ptr<ExpensiveOperationContext>* context) {
RewriteResult result = filter_->RewriteLoadedResourceImpl(
context_, input_resource_, output_resource_);
(*context)->Done();
context_->RewriteDone(result, 0);
}
virtual void CancelImpl() {
filter_->ReportDroppedRewrite();
filter_->InfoAndTrace(context_, "%s: Too busy to rewrite image.",
input_resource_->url().c_str());
context_->RewriteDone(kTooBusy, 0);
}
private:
ImageRewriteFilter::Context* context_;
ImageRewriteFilter* filter_;
const ResourcePtr input_resource_;
const OutputResourcePtr output_resource_;
DISALLOW_COPY_AND_ASSIGN(InvokeRewriteFunction);
};
// TODO(huibao): Move the logic for determining output format to a centralized
// method which should consider all relevant factors.
void SetWebpCompressionOptions(
const ResourceContext& resource_context,
const RewriteOptions& options,
const StringPiece& url,
Image::ConversionVariables* webp_conversion_variables,
Image::CompressionOptions* image_options) {
switch (resource_context.libwebp_level()) {
case ResourceContext::LIBWEBP_NONE:
image_options->preferred_webp = pagespeed::image_compression::WEBP_NONE;
image_options->allow_webp_alpha = false;
VLOG(1) << "User agent is not webp capable";
break;
case ResourceContext::LIBWEBP_LOSSY_ONLY:
image_options->preferred_webp =
pagespeed::image_compression::WEBP_LOSSY;
image_options->allow_webp_alpha = false;
VLOG(1) << "User agent is webp lossy capable ";
break;
case ResourceContext::LIBWEBP_ANIMATED:
if (options.Enabled(RewriteOptions::kConvertToWebpAnimated)) {
image_options->preferred_webp =
pagespeed::image_compression::WEBP_ANIMATED;
image_options->allow_webp_animated = true;
image_options->allow_webp_alpha = true;
break;
}
VLOG(1) << "User agent is webp animated capable ";
FALLTHROUGH_INTENDED;
case ResourceContext::LIBWEBP_LOSSY_LOSSLESS_ALPHA:
image_options->allow_webp_alpha = true;
if (options.Enabled(RewriteOptions::kConvertToWebpLossless)) {
image_options->preferred_webp =
pagespeed::image_compression::WEBP_LOSSLESS;
VLOG(1) << "User agent is webp lossless+alpha capable "
<< "and lossless images preferred";
} else {
image_options->preferred_webp =
pagespeed::image_compression::WEBP_LOSSY;
VLOG(1) << "User agent is webp lossless+alpha capable "
<< "and lossy images preferred";
}
break;
default:
LOG(DFATAL) << "Unhandled libwebp_level";
}
image_options->webp_conversion_variables = webp_conversion_variables;
}
void ImageRewriteFilter::Context::RewriteSingle(
const ResourcePtr& input_resource,
const OutputResourcePtr& output_resource) {
// If requested, drop random image rewrites. Eventually, frequently requested
// images will get optimized but the long tail won't be optimized much. We're
// not particularly concerned about the quality of the PRNG here as it's just
// deciding if we should optimize an image or not.
int drop_percentage = Options()->rewrite_random_drop_percentage();
if (drop_percentage > 0 && !IsNestedIn(RewriteOptions::kCssFilterId)) {
// Note that we don't randomly drop if this is a nested context of the CSS
// filter as we don't want to partially rewrite a CSS file.
SimpleRandom* simple_random = FindServerContext()->simple_random();
if (drop_percentage > static_cast<int>(simple_random->Next() % 100)) {
RewriteDone(kTooBusy, 0);
return;
}
}
bool is_ipro = IsNestedIn(RewriteOptions::kInPlaceRewriteId);
AttachDependentRequestTrace(is_ipro ? "IproProcessImage" : "ProcessImage");
AddLinkRelCanonical(input_resource, output_resource);
FindServerContext()->factory()->ScheduleExpensiveOperation(
new InvokeRewriteFunction(this, filter_, input_resource,
output_resource));
}
void ImageRewriteFilter::Context::Render() {
if (num_output_partitions() != 1) {
// Partition failed since one of the inputs was unavailable; nothing to do.
return;
}
CHECK_EQ(1, num_slots());
CachedResult* result = output_partition(0);
bool rewrote_url = false;
ResourceSlot* resource_slot = slot(0).get();
if (is_css_ || !has_parent()) {
InlineResult inline_result;
if (is_css_) {
rewrote_url = filter_->FinishRewriteCssImageUrl(
css_image_inline_max_bytes_, result, resource_slot, &inline_result);
} else { // html
// We use manual rendering for HTML, as we have to consider whether to
// inline, and may also pass in width and height attributes.
HtmlResourceSlot* html_slot = static_cast<HtmlResourceSlot*>(
resource_slot);
rewrote_url = filter_->FinishRewriteImageUrl(
result, resource_context(), html_slot->element(),
html_slot->attribute(), html_index_, html_slot, &inline_result);
// Register image metrics for images inside HTML here. We don't deal with
// images inside CSS here since we might not even run --- our work may get
// cached at CSS filter level.
if (Driver()->options()->Enabled(
RewriteOptions::kExperimentCollectMobImageInfo)) {
AssociatedImageInfo aii;
if (ExtractAssociatedImageInfo(result, this, &aii)) {
filter_->RegisterImageInfo(aii);
}
}
}
if (Driver()->options()->Enabled(RewriteOptions::kInlineImages)) {
// Show the debug message for inlining only when this option has been
// enabled.
// Note: Although debug message is saved to the cached_result, it is
// *not* cached because the cached_result has already been stored in
// the cache previously. This is good because the exact debug message
// here depends upon factors that may not be in the cache key (such
// as whether this is a responsive image). So we should not be storing
// the result in the cache.
filter_->SaveDebugMessageToCache(
MessageForInlineResult(inline_result), this, result);
}
// Use standard rendering in case the rewrite is nested and not inside CSS.
}
if (rewrote_url) {
// We wrote out the URL ourselves; don't let the default handling mess it up
// (in particular replacing data: with out-of-line version)
resource_slot->set_disable_rendering(true);
}
}
const UrlSegmentEncoder* ImageRewriteFilter::Context::encoder() const {
return filter_->encoder();
}
GoogleString ImageRewriteFilter::Context::UserAgentCacheKey(
const ResourceContext* resource_context) const {
if (resource_context != NULL) {
// cache-key is sensitive to whether the UA supports webp or not.
return ImageUrlEncoder::CacheKeyFromResourceContext(*resource_context);
}
return "";
}
void ImageRewriteFilter::Context::EncodeUserAgentIntoResourceContext(
ResourceContext* context) {
filter_->EncodeUserAgentIntoResourceContext(context);
}
ImageRewriteFilter::ImageRewriteFilter(RewriteDriver* driver)
: RewriteFilter(driver),
image_counter_(0),
saw_end_document_(false) {
Statistics* stats = server_context()->statistics();
image_rewrites_ = stats->GetVariable(kImageRewrites);
image_resized_using_rendered_dimensions_ =
stats->GetVariable(kImageResizedUsingRenderedDimensions);
image_norewrites_high_resolution_ = stats->GetVariable(
kImageNoRewritesHighResolution);
image_rewrites_dropped_intentionally_ =
stats->GetVariable(kImageRewritesDroppedIntentionally);
image_rewrites_dropped_decode_failure_ =
stats->GetVariable(kImageRewritesDroppedDecodeFailure);
image_rewrites_dropped_server_write_fail_ =
stats->GetVariable(kImageRewritesDroppedServerWriteFail);
image_rewrites_dropped_mime_type_unknown_ =
stats->GetVariable(kImageRewritesDroppedMIMETypeUnknown);
image_rewrites_dropped_nosaving_resize_ =
stats->GetVariable(kImageRewritesDroppedNoSavingResize);
image_rewrites_dropped_nosaving_noresize_ =
stats->GetVariable(kImageRewritesDroppedNoSavingNoResize);
image_rewrites_dropped_due_to_load_ =
stats->GetTimedVariable(kImageRewritesDroppedDueToLoad);
image_rewrites_squashing_for_mobile_screen_ =
stats->GetTimedVariable(kImageRewritesSquashingForMobileScreen);
image_rewrite_total_bytes_saved_ =
stats->GetVariable(kImageRewriteTotalBytesSaved);
image_rewrite_total_original_bytes_ =
stats->GetVariable(kImageRewriteTotalOriginalBytes);
image_rewrite_uses_ = stats->GetVariable(kImageRewriteUses);
image_inline_count_ = stats->GetVariable(kImageInline);
image_webp_rewrites_ = stats->GetVariable(kImageWebpRewrites);
image_rewrite_latency_total_ms_ =
stats->GetVariable(kImageRewriteLatencyTotalMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_GIF)->timeout_count =
stats->GetVariable(kImageWebpFromGifTimeouts);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_PNG)->timeout_count =
stats->GetVariable(kImageWebpFromPngTimeouts);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_JPEG)->timeout_count =
stats->GetVariable(kImageWebpFromJpegTimeouts);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_GIF_ANIMATED)->timeout_count =
stats->GetVariable(kImageWebpFromGifAnimatedTimeouts);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_GIF)->success_ms =
stats->GetHistogram(kImageWebpFromGifSuccessMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_PNG)->success_ms =
stats->GetHistogram(kImageWebpFromPngSuccessMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_JPEG)->success_ms =
stats->GetHistogram(kImageWebpFromJpegSuccessMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_GIF_ANIMATED)->success_ms =
stats->GetHistogram(kImageWebpFromGifAnimatedSuccessMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_GIF)->failure_ms =
stats->GetHistogram(kImageWebpFromGifFailureMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_PNG)->failure_ms =
stats->GetHistogram(kImageWebpFromPngFailureMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_JPEG)->failure_ms =
stats->GetHistogram(kImageWebpFromJpegFailureMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::FROM_GIF_ANIMATED)->failure_ms =
stats->GetHistogram(kImageWebpFromGifAnimatedFailureMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::NONOPAQUE)->timeout_count =
stats->GetVariable(kImageWebpWithAlphaTimeouts);
webp_conversion_variables_.Get(
Image::ConversionVariables::NONOPAQUE)->success_ms =
stats->GetHistogram(kImageWebpWithAlphaSuccessMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::NONOPAQUE)->failure_ms =
stats->GetHistogram(kImageWebpWithAlphaFailureMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::OPAQUE)->timeout_count =
stats->GetVariable(kImageWebpOpaqueTimeouts);
webp_conversion_variables_.Get(
Image::ConversionVariables::OPAQUE)->success_ms =
stats->GetHistogram(kImageWebpOpaqueSuccessMs);
webp_conversion_variables_.Get(
Image::ConversionVariables::OPAQUE)->failure_ms =
stats->GetHistogram(kImageWebpOpaqueFailureMs);
image_rewrite_latency_ok_ms_ = stats->GetHistogram(kImageRewriteLatencyOkMs);
image_rewrite_latency_failed_ms_ =
stats->GetHistogram(kImageRewriteLatencyFailedMs);
image_ongoing_rewrites_ = stats->GetUpDownCounter(kImageOngoingRewrites);
}
ImageRewriteFilter::~ImageRewriteFilter() {}
void ImageRewriteFilter::InitStats(Statistics* statistics) {
#ifndef NDEBUG
for (int i = 1; i < kRelatedFiltersSize; ++i) {
CHECK_LT(kRelatedFilters[i - 1], kRelatedFilters[i])
<< "kRelatedFilters not in enum-value order";
}
#endif
statistics->AddVariable(kImageRewrites);
statistics->AddVariable(kImageResizedUsingRenderedDimensions);
statistics->AddVariable(kImageNoRewritesHighResolution);
statistics->AddVariable(kImageRewritesDroppedIntentionally);
statistics->AddVariable(kImageRewritesDroppedDecodeFailure);
statistics->AddVariable(kImageRewritesDroppedMIMETypeUnknown);
statistics->AddVariable(kImageRewritesDroppedServerWriteFail);
statistics->AddVariable(kImageRewritesDroppedNoSavingResize);
statistics->AddVariable(kImageRewritesDroppedNoSavingNoResize);
statistics->AddTimedVariable(kImageRewritesDroppedDueToLoad,
ServerContext::kStatisticsGroup);
statistics->AddTimedVariable(kImageRewritesSquashingForMobileScreen,
ServerContext::kStatisticsGroup);
statistics->AddVariable(kImageRewriteTotalBytesSaved);
statistics->AddVariable(kImageRewriteTotalOriginalBytes);
statistics->AddVariable(kImageRewriteUses);
statistics->AddVariable(kImageInline);
statistics->AddVariable(kImageWebpRewrites);
statistics->AddVariable(kImageRewriteLatencyTotalMs);
statistics->AddUpDownCounter(kImageOngoingRewrites);
statistics->AddHistogram(kImageRewriteLatencyOkMs);
statistics->AddHistogram(kImageRewriteLatencyFailedMs);
statistics->AddVariable(kImageWebpFromGifTimeouts);
statistics->AddVariable(kImageWebpFromPngTimeouts);
statistics->AddVariable(kImageWebpFromJpegTimeouts);
statistics->AddVariable(kImageWebpFromGifAnimatedTimeouts);
statistics->AddHistogram(kImageWebpFromGifSuccessMs);
statistics->AddHistogram(kImageWebpFromPngSuccessMs);
statistics->AddHistogram(kImageWebpFromJpegSuccessMs);
statistics->AddHistogram(kImageWebpFromGifAnimatedSuccessMs);
statistics->AddHistogram(kImageWebpFromGifFailureMs);
statistics->AddHistogram(kImageWebpFromPngFailureMs);
statistics->AddHistogram(kImageWebpFromJpegFailureMs);
statistics->AddHistogram(kImageWebpFromGifAnimatedFailureMs);
statistics->AddVariable(kImageWebpWithAlphaTimeouts);
statistics->AddHistogram(kImageWebpWithAlphaSuccessMs);
statistics->AddHistogram(kImageWebpWithAlphaFailureMs);
statistics->AddVariable(kImageWebpOpaqueTimeouts);
statistics->AddHistogram(kImageWebpOpaqueSuccessMs);
statistics->AddHistogram(kImageWebpOpaqueFailureMs);
}
void ImageRewriteFilter::Initialize() {
CHECK(related_options_ == NULL);
related_options_ = new StringPieceVector;
ImageRewriteFilter::AddRelatedOptions(ImageRewriteFilter::related_options_);
std::sort(related_options_->begin(), related_options_->end());
}
void ImageRewriteFilter::Terminate() {
CHECK(related_options_ != NULL);
delete related_options_;
related_options_ = NULL;
}
void ImageRewriteFilter::AddRelatedOptions(StringPieceVector* target) {
for (int i = 0, n = arraysize(kRelatedOptions); i < n; ++i) {
target->push_back(kRelatedOptions[i]);
}
}
void ImageRewriteFilter::StartDocumentImpl() {
image_counter_ = 0;
saw_end_document_ = false;
inlinable_urls_.clear();
driver()->log_record()->LogRewriterHtmlStatus(
RewriteOptions::kImageCompressionId, RewriterHtmlApplication::ACTIVE);
}
void ImageRewriteFilter::EndDocument() {
saw_end_document_ = true;
}
void ImageRewriteFilter::RenderDone() {
// Only care about the very end, not every flush window; framework orders
// EndDocument before the last RenderDone (and after previous ones) so we
// use EndDocument() having been called to distinguish the last flush window
// from previous ones.
if (!saw_end_document_) {
return;
}
if (!image_info_.empty()) {
GoogleString code =
"psMobStaticImageInfo = {";
for (AssociatedImageInfoMap::iterator i = image_info_.begin(),
e = image_info_.end();
i != e; ++i) {
const AssociatedImageInfo& image_info = i->second;
EscapeToJsStringLiteral(image_info.url(), true /* want quotes */,
&code);
StrAppend(&code, ":{");
StrAppend(&code, "w:",
IntegerToString(image_info.dimensions().width()), ",");
StrAppend(&code, "h:",
IntegerToString(image_info.dimensions().height()), "},");
}
StrAppend(&code, "}");
HtmlElement* script = driver()->NewElement(NULL, HtmlName::kScript);
HtmlCharactersNode* chars = driver()->NewCharactersNode(script, code);
InsertNodeAtBodyEnd(script);
driver()->AppendChild(script, chars);
}
image_info_.clear();
}
// Allocate and initialize CompressionOptions object based on RewriteOptions and
// ResourceContext.
Image::CompressionOptions* ImageRewriteFilter::ImageOptionsForLoadedResource(
const ResourceContext& resource_context,
const ResourcePtr& input_resource) {
Image::CompressionOptions* image_options = new Image::CompressionOptions();
int64 input_size =
static_cast<int64>(input_resource->UncompressedContentsSize());
// Disable webp conversion for images in CSS if the original image size is
// greater than max_image_bytes_in_css_for_webp. This is because webp does not
// support progressive which causes a perceptible delay in the loading of
// large background images.
const RewriteOptions* options = driver()->options();
if (resource_context.libwebp_level() != ResourceContext::LIBWEBP_NONE) {
SetWebpCompressionOptions(resource_context, *options, input_resource->url(),
&webp_conversion_variables_, image_options);
}
DetermineQualities(*options, resource_context,
*driver()->request_properties(), image_options);
image_options->progressive_jpeg =
options->Enabled(RewriteOptions::kConvertJpegToProgressive) &&
input_size >= options->progressive_jpeg_min_bytes();
image_options->progressive_jpeg_min_bytes =
options->progressive_jpeg_min_bytes();
image_options->convert_png_to_jpeg =
options->Enabled(RewriteOptions::kConvertPngToJpeg);
image_options->convert_gif_to_png =
options->Enabled(RewriteOptions::kConvertGifToPng);
image_options->convert_jpeg_to_webp =
options->Enabled(RewriteOptions::kConvertJpegToWebp);
image_options->recompress_jpeg =
options->Enabled(RewriteOptions::kRecompressJpeg);
image_options->recompress_png =
options->Enabled(RewriteOptions::kRecompressPng);
image_options->recompress_webp =
options->Enabled(RewriteOptions::kRecompressWebp);
image_options->retain_color_profile =
!options->Enabled(RewriteOptions::kStripImageColorProfile);
image_options->retain_exif_data =
!options->Enabled(RewriteOptions::kStripImageMetaData);
image_options->retain_color_sampling =
!options->Enabled(RewriteOptions::kJpegSubsampling);
image_options->webp_conversion_timeout_ms =
options->image_webp_timeout_ms();
return image_options;
}
// Resize image if necessary, returning true if this resizing succeeds and false
// if it's unnecessary or fails.
bool ImageRewriteFilter::ResizeImageIfNecessary(
const RewriteContext* rewrite_context, const GoogleString& url,
ResourceContext* resource_context, Image* image, CachedResult* cached) {
bool resized = false;
// Begin by resizing the image if necessary
ImageDim image_dim;
image->Dimensions(&image_dim);
if (image_dim.width() <= 0 || image_dim.height() <= 0) {
cached->add_debug_message("Cannot resize: Image must be at least 1x1");
return false;
}
// Here we are computing the size of the image as described by the html on the
// page or as desired by mobile screen resolutions. If we succeed in doing so,
// that will be the desired image size. Otherwise we may fill in
// desired_image_dims later based on actual image size.
ImageDim* desired_dim = resource_context->mutable_desired_image_dims();
const ImageDim* post_resize_dim = &image_dim;
if (ShouldResize(*resource_context, url, image, desired_dim)) {
DCHECK_LT(0, desired_dim->width());
DCHECK_LT(0, desired_dim->height());
const char* message; // Informational message for logging only.
if (image->ResizeTo(*desired_dim)) {
post_resize_dim = desired_dim;
message = "Resized";
resized = true;
} else {
message = "Couldn't resize";
}
driver()->InfoAt(rewrite_context, "%s image `%s' from %dx%d to %dx%d",
message, url.c_str(),
image_dim.width(), image_dim.height(),
desired_dim->width(), desired_dim->height());
cached->add_debug_message(image->resize_debug_message());
} else {
cached->add_debug_message("Image does not appear to need resizing.");
}
// Cache image dimensions, including any resizing we did.
// This happens regardless of whether we rewrite the image contents.
if (ImageUrlEncoder::HasValidDimensions(*post_resize_dim)) {
ImageDim* dims = cached->mutable_image_file_dims();
dims->set_width(post_resize_dim->width());
dims->set_height(post_resize_dim->height());
}
return resized;
}
// Determines whether an image should be resized based on the current options.
//
// Returns the dimensions to resize to in *desired_dimensions.
bool ImageRewriteFilter::ShouldResize(const ResourceContext& resource_context,
const GoogleString& url,
Image* image,
ImageDim* desired_dim) {
const RewriteOptions* options = driver()->options();
if (!options->Enabled(RewriteOptions::kResizeImages) &&
!options->Enabled(RewriteOptions::kResizeToRenderedImageDimensions)) {
return false;
}
if (image->content_type()->type() != ContentType::kGif ||
options->Enabled(RewriteOptions::kConvertGifToPng) ||
options->Enabled(RewriteOptions::kDelayImages)) {
*desired_dim = resource_context.desired_image_dims();
ImageDim image_dim;
image->Dimensions(&image_dim);
if (options->Enabled(RewriteOptions::kResizeToRenderedImageDimensions)) {
// Respect the aspect ratio of the image when doing the resize.
SetDesiredDimensionsIfRequired(desired_dim, image_dim);
} else {
UpdateDesiredImageDimsIfNecessary(
image_dim, resource_context, desired_dim);
if (options->Enabled(RewriteOptions::kResizeImages) &&
ImageUrlEncoder::HasValidDimension(*desired_dim) &&
ImageUrlEncoder::HasValidDimensions(image_dim)) {
SetDesiredDimensionsIfRequired(desired_dim, image_dim);
}
}
if (ImageUrlEncoder::HasValidDimension(*desired_dim) &&
ImageUrlEncoder::HasValidDimensions(image_dim)) {
const int64 page_area =
static_cast<int64>(desired_dim->width()) *
desired_dim->height();
const int64 image_area =
static_cast<int64>(image_dim.width()) * image_dim.height();
if (page_area * 100 <
image_area * options->image_limit_resize_area_percent()) {
DCHECK_LT(0, desired_dim->width());
DCHECK_LT(0, desired_dim->height());
return true;
}
}
}
return false;
}
namespace {
int64 GetCurrentCpuTimeMs(Timer* timer) {
// See http://linux.die.net/man/2/getrusage -- RUSAGE_THREAD is supported
// on Linux since Linux 2.6.26, so fall back to wall-clock time.
#ifdef RUSAGE_THREAD
struct rusage start_rusage;
if (getrusage(RUSAGE_THREAD, &start_rusage) == 0) {
return ((start_rusage.ru_utime.tv_sec * 1000) +
(start_rusage.ru_utime.tv_usec / 1000));
}
#endif
return timer->NowMs();
}
} // namespace
// Format as InfoAt and using TracePrintf.
// TODO(jmaessen): Avoid formatting if neither applies.
void ImageRewriteFilter::InfoAndTrace(
Context* rewrite_context, const char* format, ...) {
va_list args;
va_start(args, format);
GoogleString message;
StringAppendV(&message, format, args);
driver()->InfoAt(rewrite_context, "%s", message.c_str());
driver()->TraceString(message);
va_end(args);
}
RewriteResult ImageRewriteFilter::RewriteLoadedResourceImpl(
Context* rewrite_context, const ResourcePtr& input_resource,
const OutputResourcePtr& result) {
rewrite_context->TracePrintf("Image rewrite: %s",
input_resource->url().c_str());
MessageHandler* message_handler = driver()->message_handler();
StringVector urls;
ResourceContext resource_context;
const RewriteOptions* options = driver()->options();
resource_context = *rewrite_context->resource_context();
if (!encoder_.Decode(result->name(),
&urls, &resource_context, message_handler)) {
image_rewrites_dropped_intentionally_->Add(1);
image_rewrites_dropped_decode_failure_->Add(1);
return kRewriteFailed;
}
Image::CompressionOptions* image_options =
ImageOptionsForLoadedResource(resource_context, input_resource);
scoped_ptr<Image> image(
NewImage(input_resource->ExtractUncompressedContents(),
input_resource->url(), server_context()->filename_prefix(),
image_options, driver()->timer(), message_handler));
// Initialize logging data.
ImageType original_image_type = image->image_type();
ImageType optimized_image_type = original_image_type;
int original_size = image->input_size();
int optimized_size = original_size;
bool is_recompressed = false;
bool is_resized = false;
if (original_image_type == IMAGE_UNKNOWN) {
image_rewrites_dropped_intentionally_->Add(1);
image_rewrites_dropped_mime_type_unknown_->Add(1);
driver()->InfoAt(
rewrite_context, "%s: Image MIME type could not be "
"discovered from reading magic bytes; rewriting dropped.",
input_resource->url().c_str());
return kRewriteFailed;
}
// We used to reject beacon images based on their size (1x1 or less) here, but
// now rely on caching headers instead as this was missing a lot of padding
// images that were ripe for inlining.
RewriteResult rewrite_result = kTooBusy;
ImageDim image_dim;
image->Dimensions(&image_dim);
int64 image_width = image_dim.width(), image_height = image_dim.height();
if ((image_width*image_height*4) > options->image_resolution_limit_bytes()) {
image_rewrites_dropped_intentionally_->Add(1);
image_norewrites_high_resolution_->Add(1);
return kRewriteFailed;
}
image_ongoing_rewrites_->Add(1);
rewrite_result = kRewriteFailed;
Timer* timer = server_context()->timer();
int64 rewrite_time_start_ms = GetCurrentCpuTimeMs(timer);
CachedResult* cached = result->EnsureCachedResultCreated();
is_resized = ResizeImageIfNecessary(
rewrite_context, input_resource->url(),
&resource_context, image.get(), cached);
// When the "resize_images" filter has been turned on and the IMG tag has
// width and/or height specified, we assume that the image will be resized so
// the new dimension will be embedded into the rewritten image URL. However,
// if reizing turns out to be a failure, we don't want the new dimension in
// the rewritten URL. For the latter case, we will reset the "name" of the
// output resource.
if (!is_resized) {
resource_context.clear_desired_image_dims();
GoogleString name;
GoogleUrl mapped_gurl; // Not used
GoogleString failure_reason; // Not used
if (driver()->GenerateOutputResourceNameAndUrl(
encoder(), &resource_context, input_resource, &name, &mapped_gurl,
&failure_reason)) {
result->mutable_full_name()->set_name(name);
} else {
LOG(DFATAL) << "Failed to generate name and URL for the output resource.";
return kRewriteFailed;
}
}
// Now re-compress the (possibly resized) image, and decide if it's
// saved us anything.
if (is_resized || options->ImageOptimizationEnabled()) {
// Call output_size() before image_type(). When output_size() is called,
// the image will be recompressed and the image type may be changed
// in order to get the smallest output.
optimized_size = image->output_size();
optimized_image_type = image->image_type();
is_recompressed = true;
// The image has been recompressed (and potentially resized). However,
// the recompressed image may not be used unless the file size is reduced.
if (image->output_size() * 100 <
image->input_size() * options->image_limit_optimized_percent()) {
// Here output image type could potentially be different from input
// type.
const ContentType* output_type =
ImageToContentType(input_resource->url(), image.get());
// Consider inlining output image (no need to check input, it's bigger)
// This needs to happen before Write to persist.
SaveIfInlinable(image->Contents(), image->image_type(), cached);
server_context()->MergeNonCachingResponseHeaders(
input_resource, result);
if (options->no_transform_optimized_images()) {
result->set_cache_control_suffix(",no-transform");
}
if (driver()->Write(
ResourceVector(1, input_resource), image->Contents(),
output_type, StringPiece() /* no charset for images */,
result.get())) {
driver()->InfoAt(
rewrite_context,
"Shrinking image `%s' (%u bytes) to `%s' (%u bytes)",
input_resource->url().c_str(),
static_cast<unsigned>(image->input_size()),
result->url().c_str(),
static_cast<unsigned>(image->output_size()));
// Update stats.
image_rewrites_->Add(1);
image_rewrite_total_bytes_saved_->Add(
image->input_size() - image->output_size());
image_rewrite_total_original_bytes_->Add(image->input_size());
if (result->type()->type() == ContentType::kWebp) {
image_webp_rewrites_->Add(1);
}
rewrite_result = kRewriteOk;
} else {
// Server fails to write merged files.
image_rewrites_dropped_server_write_fail_->Add(1);
InfoAndTrace(
rewrite_context,
"Server fails writing image content for `%s'; rewriting dropped.",
input_resource->url().c_str());
}
} else if (is_resized) {
// Eliminate any image dimensions from a resize operation that
// succeeded, but yielded overly-large output.
image_rewrites_dropped_nosaving_resize_->Add(1);
InfoAndTrace(
rewrite_context,
"Shrink of image `%s' (%u -> %u bytes) doesn't save space; "
"dropped.",
input_resource->url().c_str(),
static_cast<unsigned>(image->input_size()),
static_cast<unsigned>(image->output_size()));
ImageDim* dims = cached->mutable_image_file_dims();
dims->clear_width();
dims->clear_height();
} else if (options->ImageOptimizationEnabled()) {
// Fails due to overly-large output without resize.
image_rewrites_dropped_nosaving_noresize_->Add(1);
InfoAndTrace(
rewrite_context,
"Recompressing image `%s' (%u -> %u bytes) doesn't save space; "
"dropped.",
input_resource->url().c_str(),
static_cast<unsigned>(image->input_size()),
static_cast<unsigned>(image->output_size()));
}
}
cached->set_optimized_image_type(optimized_image_type);
cached->set_size(rewrite_result == kRewriteOk ? image->output_size() :
image->input_size());
SaveDebugMessageToCache(image->debug_message(), rewrite_context, cached);
// Try inlining input image if output hasn't been inlined already.
if (!cached->has_inlined_data()) {
SaveIfInlinable(input_resource->ExtractUncompressedContents(),
original_image_type, cached);
}
int64 image_size = static_cast<int64>(image->output_size());
if (options->Enabled(RewriteOptions::kDelayImages) &&
!rewrite_context->in_noscript_element_ &&
!cached->has_low_resolution_inlined_data() &&
image_size >= options->min_image_size_low_resolution_bytes() &&
image_size <= options->max_image_size_low_resolution_bytes()) {
Image::CompressionOptions* image_options =
new Image::CompressionOptions();
SetWebpCompressionOptions(resource_context, *options,
input_resource->url(),
&webp_conversion_variables_,
image_options);
image_options->jpeg_quality = options->ImageJpegQuality();
image_options->webp_quality = options->ImageWebpQuality();
image_options->webp_animated_quality = options->ImageWebpAnimatedQuality();
image_options->progressive_jpeg = false;
image_options->convert_png_to_jpeg =
options->Enabled(RewriteOptions::kConvertPngToJpeg);
// Set to true since we optimize a gif to png before resize.
image_options->convert_gif_to_png = true;
image_options->recompress_jpeg = true;
image_options->recompress_png = true;
image_options->recompress_webp = true;
// Since these are replaced with their high res versions, stripping
// them off for low res images will further reduce bytes.
image_options->retain_color_profile = false;
image_options->retain_exif_data = false;
image_options->retain_color_sampling = false;
image_options->jpeg_num_progressive_scans =
options->image_jpeg_num_progressive_scans();
scoped_ptr<Image> low_image;
if (driver()->options()->use_blank_image_for_inline_preview()) {
image_options->use_transparent_for_blank_image = true;
low_image.reset(BlankImageWithOptions(image_width, image_height,
IMAGE_PNG, server_context()->filename_prefix(),
timer, message_handler, image_options));
low_image->EnsureLoaded(true);
} else {
low_image.reset(NewImage(image->Contents(), input_resource->url(),
server_context()->filename_prefix(), image_options,
timer, message_handler));
}
low_image->SetTransformToLowRes();
if (ShouldInlinePreview(low_image->Contents().size(),
image->Contents().size(), options)) {
if (resource_context.mobile_user_agent()) {
ResizeLowQualityImage(low_image.get(), input_resource, cached);
} else {
cached->set_low_resolution_inlined_data(low_image->Contents().data(),
low_image->Contents().size());
}
cached->set_low_resolution_inlined_image_type(
static_cast<int>(low_image->image_type()));
}
}
image_ongoing_rewrites_->Add(-1);
int64 latency_ms = GetCurrentCpuTimeMs(timer) - rewrite_time_start_ms;
if (rewrite_result == kRewriteOk) {
image_rewrite_latency_ok_ms_->Add(latency_ms);
} else {
image_rewrite_latency_failed_ms_->Add(latency_ms);
}
// We track the total latency (including failed & OK) in its own
// variable so it can be easily scraped with wget. The ok/failed
// versions above are histograms and thus harder to scrape.
image_rewrite_latency_total_ms_->Add(latency_ms);
// All other conditions were updated in other code paths above.
if (rewrite_result == kRewriteFailed) {
image_rewrites_dropped_intentionally_->Add(1);
} else if (rewrite_result == kRewriteOk) {
rewrite_context->TracePrintf("Image rewrite success (%u -> %u)",
static_cast<unsigned>(image->input_size()),
static_cast<unsigned>(image->output_size()));
}
const ImageDim& post_resize_dim =
resource_context.desired_image_dims();
LogImageBackgroundRewriteActivity(driver(),
rewrite_result == kRewriteOk ?
RewriterApplication::APPLIED_OK : RewriterApplication::NOT_APPLIED,
input_resource->url(), LoggingId(), original_size, optimized_size,
is_recompressed, original_image_type, optimized_image_type, is_resized,
image_width, image_height,
rewrite_context->is_resized_using_rendered_dimensions_,
post_resize_dim.width(), post_resize_dim.height());
return rewrite_result;
}
// Generate resized low quality image if the image width is not smaller than
// kDelayImageWidthForMobile. If image width is smaller than
// kDelayImageWidthForMobile, "delay_images" optimization is not very useful
// and no low quality image will be generated.
void ImageRewriteFilter::ResizeLowQualityImage(
Image* low_image, const ResourcePtr& input_resource, CachedResult* cached) {
ImageDim image_dim;
low_image->Dimensions(&image_dim);
if (image_dim.width() >= kDelayImageWidthForMobile) {
const RewriteOptions* options = driver()->options();
Image::CompressionOptions* image_options =
new Image::CompressionOptions();
image_options->jpeg_quality = options->ImageJpegQuality();
image_options->webp_quality = options->ImageWebpQuality();
image_options->webp_animated_quality = options->ImageWebpAnimatedQuality();
image_options->progressive_jpeg = false;
image_options->convert_png_to_jpeg =
options->Enabled(RewriteOptions::kConvertPngToJpeg);
image_options->convert_gif_to_png =
options->Enabled(RewriteOptions::kConvertGifToPng);
image_options->recompress_jpeg =
options->Enabled(RewriteOptions::kRecompressJpeg);
image_options->recompress_png =
options->Enabled(RewriteOptions::kRecompressPng);
image_options->recompress_webp =
options->Enabled(RewriteOptions::kRecompressWebp);
scoped_ptr<Image> image(
NewImage(low_image->Contents(), input_resource->url(),
server_context()->filename_prefix(), image_options,
driver()->timer(), driver()->message_handler()));
image->SetTransformToLowRes();
ImageDim resized_dim;
resized_dim.set_width(kDelayImageWidthForMobile);
resized_dim.set_height((static_cast<int64>(resized_dim.width()) *
image_dim.height()) / image_dim.width());
MessageHandler* message_handler = driver()->message_handler();
bool resized = image->ResizeTo(resized_dim);
StringPiece contents = image->Contents();
StringPiece old_contents = low_image->Contents();
if (resized && contents.size() < old_contents.size()) {
cached->set_low_resolution_inlined_data(contents.data(), contents.size());
message_handler->Message(
kInfo,
"Resized low quality image (%s) from "
"%dx%d(%d bytes) to %dx%d(%d bytes)",
input_resource->url().c_str(),
image_dim.width(), image_dim.height(),
static_cast<int>(old_contents.size()),
resized_dim.width(), resized_dim.width(),
static_cast<int>(contents.size()));
} else {
message_handler->Message(
kInfo,
"Couldn't resize low quality image (%s) or resized image file is "
"not smaller: "
"%dx%d(%d bytes) => %dx%d(%d bytes)",
input_resource->url().c_str(),
image_dim.width(), image_dim.height(),
static_cast<int>(old_contents.size()),
resized_dim.width(), resized_dim.height(),
static_cast<int>(contents.size()));
}
}
}
void ImageRewriteFilter::SaveIfInlinable(const StringPiece& contents,
const ImageType image_type,
CachedResult* cached) {
// We retain inlining information if the image size is < the largest possible
// inlining threshold, as an image might be used in both html and css and we
// may see it first from the one with a smaller threshold. Note that this can
// cause us to save inline information for an image that won't ever actually
// be inlined (because it's too big to inline in html, say, and doesn't occur
// in css).
int64 image_inline_max_bytes =
driver()->options()->MaxImageInlineMaxBytes();
if (static_cast<int64>(contents.size()) < image_inline_max_bytes) {
cached->set_inlined_data(contents.data(), contents.size());
cached->set_inlined_image_type(static_cast<int>(image_type));
}
}
// Convert (possibly NULL) Image* to corresponding (possibly NULL) ContentType*
const ContentType* ImageRewriteFilter::ImageToContentType(
const GoogleString& origin_url, Image* image) {
const ContentType* content_type = NULL;
if (image != NULL) {
// Even if we know the content type from the extension coming
// in, the content-type can change as a result of compression,
// e.g. gif to png, or jpeg to webp.
return image->content_type();
}
return content_type;
}
void ImageRewriteFilter::BeginRewriteImageUrl(HtmlElement* element,
HtmlElement::Attribute* src) {
scoped_ptr<ResourceContext> resource_context(new ResourceContext);
const RewriteOptions* options = driver()->options();
bool is_resized_using_rendered_dimensions = false;
// In case of RewriteOptions::image_preserve_urls() we do not want to use
// image dimension information from HTML/CSS.
if (options->Enabled(RewriteOptions::kResizeImages) ||
options->Enabled(RewriteOptions::kResizeToRenderedImageDimensions)) {
ImageDim* desired_dim = resource_context->mutable_desired_image_dims();
GetDimensions(element, desired_dim, src,
&is_resized_using_rendered_dimensions);
if ((desired_dim->width() == 0 || desired_dim->height() == 0 ||
(desired_dim->width() == 1 && desired_dim->height() == 1))) {
// This is either a beacon image, or an attempt to prefetch. Drop the
// desired dimensions so that the image is not resized.
resource_context->clear_desired_image_dims();
}
}
StringPiece url(src->DecodedValueOrNull());
EncodeUserAgentIntoResourceContext(resource_context.get());
ResourcePtr input_resource(CreateInputResourceOrInsertDebugComment(
src->DecodedValueOrNull(), element));
if (input_resource.get() == NULL) {
return;
}
// If the image will be inlined and the local storage cache is enabled, add
// the LSC marker attribute to this element so that the LSC filter knows to
// insert the relevant javascript functions.
if (driver()->request_properties()->SupportsImageInlining()) {
LocalStorageCacheFilter::InlineState state;
LocalStorageCacheFilter::AddStorableResource(src->DecodedValueOrNull(),
driver(),
true /* ignore cookie */,
element, &state);
}
Context* context = new Context(0 /* No CSS inlining, it's html */,
this, driver(), NULL /*not nested */,
resource_context.release(),
false /*not css */, image_counter_++,
noscript_element() != NULL,
is_resized_using_rendered_dimensions);
ResourceSlotPtr slot(driver()->GetSlot(input_resource, element, src));
context->AddSlot(slot);
// Note that in RewriteOptions::Merge we turn off image_preserve_urls
// when merging into a configuration that has explicitly
// enabled cache_extend_images.
//
// Consider a hosting provider that turns on "optimize for
// bandwidth" mode, and then a site enables resize_images
// explicitly. That should override the image-url-preservation
// default that was set at root. Note that explicitly turning on
// RecompressImages doesn't mean we'll want to override
// image_preserve_urls rewrite URLs here, since we can still get
// the benefit of recompression via IPRO. But we make an
// exception for inlining and image-resizing directives since
// those can only be done via url-rewriting.
if (options->image_preserve_urls() &&
!options->Enabled(RewriteOptions::kResizeImages) &&
!options->Enabled(RewriteOptions::kResizeToRenderedImageDimensions) &&
!options->Enabled(RewriteOptions::kInlineImages)) {
slot->set_preserve_urls(true);
}
driver()->InitiateRewrite(context);
}
bool ImageRewriteFilter::FinishRewriteCssImageUrl(
int64 css_image_inline_max_bytes, const CachedResult* cached,
ResourceSlot* slot, InlineResult* inline_result) {
GoogleString data_url;
*inline_result = TryInline(false /*not html*/, false /*not critical*/,
css_image_inline_max_bytes, cached, slot,
&data_url);
if (*inline_result == INLINE_SUCCESS) {
// TODO(jmaessen): Can we make output URL reflect actual *usage*
// of image inlining and/or webp images?
const RewriteOptions* options = driver()->options();
DCHECK(!options->cache_small_images_unrewritten())
<< "Modifying a URL slot despite "
<< "image_inlining_identify_and_cache_without_rewriting set.";
if (slot->DirectSetUrl(data_url)) {
image_inline_count_->Add(1);
return true;
}
} else if (cached->optimizable()) {
image_rewrite_uses_->Add(1);
}
// Fall back to nested rewriting, which will also left trim the url if that
// is required.
return false;
}
namespace {
// Skip ascii whitespace, returning pointer to first non-whitespace character in
// accordance with:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/
// common-microsyntaxes.html#space-character
const char* SkipAsciiWhitespace(const char* position) {
while (*position <= ' ' && // Quickly skip if no leading whitespace
(*position == ' ' || *position == '\x09' || *position == '\x0A' ||
*position == '\x0C' || *position == '\x0D')) {
++position;
}
return position;
}
bool GetDimensionAttribute(
const HtmlElement* element, HtmlName::Keyword name, int* value) {
const HtmlElement::Attribute* attribute = element->FindAttribute(name);
if (attribute == NULL) {
return false;
}
const char* position = attribute->DecodedValueOrNull();
return ImageRewriteFilter::ParseDimensionAttribute(position, value);
}
// If the element has a width attribute, set it in page_dim.
void SetWidthFromAttribute(const HtmlElement* element, ImageDim* page_dim) {
int32 width;
if (GetDimensionAttribute(element, HtmlName::kWidth, &width)) {
page_dim->set_width(width);
}
}
// If the element has a height attribute, set it in page_dim.
void SetHeightFromAttribute(const HtmlElement* element, ImageDim* page_dim) {
int32 height;
if (GetDimensionAttribute(element, HtmlName::kHeight, &height)) {
page_dim->set_height(height);
}
}
void DeleteMatchingImageDimsAfterInline(
const CachedResult* cached, HtmlElement* element) {
// Never strip width= or height= attributes from non-img elements.
if (element->keyword() != HtmlName::kImg) {
return;
}
// We used to take the absence of desired_image_dims here as license to delete
// dimensions. That was incorrect, as sometimes there were dimensions in the
// page but the image was being enlarged on page and we can't strip the
// enlargement out safely. Now we also strip desired_image_dims when the
// image is 1x1 or less. As a result, we go back to the html to determine
// whether it's safe to strip the width and height attributes, doing so only
// if all dimensions that are present match the actual post-optimization image
// dimensions.
if (cached->has_image_file_dims()) {
int attribute_width, attribute_height = -1;
if (GetDimensionAttribute(element, HtmlName::kWidth, &attribute_width)) {
if (cached->image_file_dims().width() == attribute_width) {
// Width matches, height must either be absent or match.
if (!element->FindAttribute(HtmlName::kHeight)) {
// No height, just delete width.
element->DeleteAttribute(HtmlName::kWidth);
} else if (GetDimensionAttribute(
element, HtmlName::kHeight, &attribute_height) &&
cached->image_file_dims().height() == attribute_height) {
// Both dimensions match, delete both.
element->DeleteAttribute(HtmlName::kWidth);
element->DeleteAttribute(HtmlName::kHeight);
}
}
} else if (!element->FindAttribute(HtmlName::kWidth) &&
GetDimensionAttribute(element, HtmlName::kHeight, &attribute_height) &&
cached->image_file_dims().height() == attribute_height) {
// No width, matching height
element->DeleteAttribute(HtmlName::kHeight);
}
}
}
} // namespace
bool ImageRewriteFilter::FinishRewriteImageUrl(
const CachedResult* cached, const ResourceContext* resource_context,
HtmlElement* element, HtmlElement::Attribute* src, int image_index,
HtmlResourceSlot* slot, InlineResult* inline_result) {
GoogleString src_value(src->DecodedValueOrNull());
if (src_value.empty()) {
return false;
}
const RewriteOptions* options = driver()->options();
bool rewrote_url = false;
bool image_inlined = false;
const bool is_critical_image = IsHtmlCriticalImage(src_value);
// Don't inline images used by responsive filter (except for the ones
// explicitly marked as inlinable).
const char* responsive_attr =
element->AttributeValue(HtmlName::kDataPagespeedResponsiveTemp);
if (responsive_attr != NULL &&
StringPiece(responsive_attr) !=
ResponsiveImageFirstFilter::kInlinableVirtualImage) {
*inline_result = INLINE_RESPONSIVE;
} else if (element->keyword() == HtmlName::kLink) {
// Don't inline shortcut images. All shortcut images are on link tags, and
// no non-shortcut images are on link tags, so we can just check if this is
// a link tag. This is to exclude inlining on:
// * <link rel=icon ...>
// * <link rel=apple-touch-icon ...>
// * <link rel=apple-touch-icon-precomposed ...>
// * <link rel=apple-touch-startup-image ...>
*inline_result = INLINE_SHORTCUT;
} else {
// See if we have a data URL, and if so use it if the browser can handle it
// TODO(jmaessen): get rid of a string copy here. Tricky because
// src->SetValue() copies implicitly.
GoogleString data_url;
// TODO(sligocki): Use different threshold for responsive images?
*inline_result = TryInline(true /*in html*/, is_critical_image,
options->ImageInlineMaxBytes(),
cached, slot, &data_url);
if (*inline_result == INLINE_SUCCESS) {
DCHECK(!options->cache_small_images_unrewritten())
<< "Modifying a URL slot despite "
<< "image_inlining_identify_and_cache_without_rewriting set.";
src->SetValue(data_url);
// Note the use of the ORIGINAL url not the data url.
LocalStorageCacheFilter::AddLscAttributes(src_value, *cached,
driver(), element);
// AddLscAttributes uses the width and height attributes so must be called
// before we delete them with:
DeleteMatchingImageDimsAfterInline(cached, element);
image_inline_count_->Add(1);
rewrote_url = true;
image_inlined = true;
}
}
// Rewrite URL in case this image was not inlined (and URL rewriting allowed).
if (!image_inlined && !slot->preserve_urls()) {
// Not inlined means we cannot store it in local storage.
LocalStorageCacheFilter::RemoveLscAttributes(element, driver());
if (cached->optimizable()) {
// Rewritten HTTP url
src->SetValue(ResourceSlot::RelativizeOrPassthrough(
options, cached->url(), slot->url_relativity(),
driver()->base_url()));
image_rewrite_uses_->Add(1);
rewrote_url = true;
}
if (options->Enabled(RewriteOptions::kInsertImageDimensions) &&
(element->keyword() == HtmlName::kImg ||
element->keyword() == HtmlName::kInput) &&
!HasAnyDimensions(element) &&
cached->has_image_file_dims() &&
ImageUrlEncoder::HasValidDimensions(cached->image_file_dims())) {
// Add image dimensions. We don't bother to resize if either dimension is
// specified with units (em, %) rather than as absolute pixels. But note
// that we DO attempt to include image dimensions even if we otherwise
// choose not to optimize an image.
const ImageDim& file_dims = cached->image_file_dims();
driver()->AddAttribute(element, HtmlName::kWidth,
IntegerToString(file_dims.width()));
driver()->AddAttribute(element, HtmlName::kHeight,
IntegerToString(file_dims.height()));
}
if (element->FindAttribute(HtmlName::kDataPagespeedResponsiveTemp) != NULL
&& cached->has_image_file_dims()
&& ImageUrlEncoder::HasValidDimensions(cached->image_file_dims())) {
// If this is an image used by ResponsiveImageFilter, add information
// on actual final dimensions used. That way we can decide which to use
// in srcset and which to discard (because they are the same size as a
// lower density image).
const ImageDim& file_dims = cached->image_file_dims();
driver()->AddAttribute(element, HtmlName::kDataActualWidth,
IntegerToString(file_dims.width()));
driver()->AddAttribute(element, HtmlName::kDataActualHeight,
IntegerToString(file_dims.height()));
}
}
bool low_res_src_inserted = false;
bool try_low_res_src_insertion = false;
ImageType low_res_image_type = IMAGE_UNKNOWN;
if (options->Enabled(RewriteOptions::kDelayImages) &&
src->keyword() == HtmlName::kSrc &&
(element->keyword() == HtmlName::kImg ||
element->keyword() == HtmlName::kInput)) {
try_low_res_src_insertion = true;
int max_preview_image_index = options->max_inlined_preview_images_index();
if (!image_inlined &&
!slot->preserve_urls() &&
is_critical_image &&
driver()->request_properties()->SupportsImageInlining() &&
driver()->server_context()->critical_images_finder()->Available(
driver()) != CriticalImagesFinder::kNoDataYet &&
cached->has_low_resolution_inlined_data() &&
(max_preview_image_index < 0 ||
image_index < max_preview_image_index)) {
low_res_image_type = static_cast<ImageType>(
cached->low_resolution_inlined_image_type());
const ContentType* content_type =
Image::TypeToContentType(low_res_image_type);
DCHECK(content_type != NULL) << "Invalid Image Type: "
<< low_res_image_type;
if (content_type != NULL) {
GoogleString data_url;
DataUrl(*content_type, BASE64, cached->low_resolution_inlined_data(),
&data_url);
driver()->AddAttribute(
element, HtmlName::kDataPagespeedLowResSrc, data_url);
driver()->increment_num_inline_preview_images();
low_res_src_inserted = true;
} else {
driver()->message_handler()->Message(kError,
"Invalid low res image type: %d",
low_res_image_type);
}
}
}
// Absolutify the image url for logging.
GoogleUrl image_gurl(driver()->base_url(), src_value);
driver()->log_record()->LogImageRewriteActivity(
LoggingId(),
image_gurl.spec_c_str(),
(rewrote_url ?
RewriterApplication::APPLIED_OK :
RewriterApplication::NOT_APPLIED),
image_inlined,
is_critical_image,
cached->optimizable(),
cached->size(),
try_low_res_src_insertion,
low_res_src_inserted,
low_res_image_type,
cached->low_resolution_inlined_data().size());
return rewrote_url;
}
void ImageRewriteFilter::SaveDebugMessageToCache(const GoogleString& message,
Context* rewrite_context,
CachedResult* cached_result) {
if (!message.empty()) {
// We always save our result to our cache entry, since it will be propagated
// to the parent automatically, and we need to be replayable independently.
cached_result->add_debug_message(message);
}
}
bool ImageRewriteFilter::IsHtmlCriticalImage(StringPiece image_url) const {
CriticalImagesFinder* finder =
driver()->server_context()->critical_images_finder();
if (finder->Available(driver()) != CriticalImagesFinder::kAvailable) {
// Default to all images being critical if we don't have meaningful critical
// image information.
return true;
}
GoogleUrl image_gurl(driver()->base_url(), image_url);
return finder->IsHtmlCriticalImage(image_gurl.Spec(), driver());
}
bool ImageRewriteFilter::StoreUrlInPropertyCache(const StringPiece& url) {
if (url.length() == 0) {
return true;
}
PropertyPage* property_page = driver()->property_page();
if (property_page == NULL) {
LOG(WARNING) << "image_inlining_identify_and_cache_without_rewriting "
<< "without PropertyPage.";
return false;
}
const PropertyCache::Cohort* cohort =
driver()->server_context()->dom_cohort();
if (cohort == NULL) {
LOG(WARNING) << "image_inlining_identify_and_cache_without_rewriting "
<< "without configured DOM cohort.";
return false;
}
PropertyValue* value = property_page->GetProperty(
cohort, kInlinableImageUrlsPropertyName);
VLOG(3) << "image_inlining_identify_and_cache_without_rewriting value "
<< "inserted into pcache: " << url;
GoogleString new_value(StrCat("\"", url, "\""));
if (value->has_value()) {
StrAppend(&new_value, ",", value->value());
}
property_page->UpdateValue(
cohort, kInlinableImageUrlsPropertyName, new_value);
return true;
}
bool ImageRewriteFilter::HasAnyDimensions(HtmlElement* element) {
if (element->FindAttribute(HtmlName::kWidth)) {
return true;
}
if (element->FindAttribute(HtmlName::kHeight)) {
return true;
}
css_util::StyleExtractor extractor(element);
return extractor.HasAnyDimensions();
}
bool ImageRewriteFilter::ParseDimensionAttribute(
const char* position, int* value) {
if (position == NULL) {
return false;
}
// Note that we rely heavily on null-termination of char* here to cause our
// control flow to fall through when we reach end of string. Numbered steps
// correspond to the steps in the spec.
// http://www.whatwg.org/specs/web-apps/current-work/multipage/
// common-microsyntaxes.html#percentages-and-dimensions
// 3) Skip ascii whitespace
position = SkipAsciiWhitespace(position);
// 5) Skip leading plus
if (*position == '+') {
++position;
}
unsigned int result = 0; // unsigned for consistent overflow behavior.
// 6,7,9) Process digits
while ('0' <= *position && *position <= '9') {
unsigned int new_result = result * 10 + *position - '0';
if (new_result < result) {
// Integer overflow. Reject.
return false;
}
result = new_result;
++position;
}
// 6,7,8) Reject if no digits or only zeroes, or conversion to signed will
// fail.
if (result < 1 || INT_MAX < result) {
return false;
}
// 11) Process fraction (including 45. with nothing after the . )
if (*position == '.') {
++position;
if ('5' <= *position && *position <= '9' && result < INT_MAX) {
// Round based on leading fraction digit, avoiding overflow.
++result;
++position;
}
// Discard all fraction digits.
while ('0' <= *position && *position <= '9') {
++position;
}
}
// Skip whitespace before a possible trailing px. The spec allows other junk,
// or a trailing percent, but we can't resize percentages and older browsers
// don't resize when they encounter junk.
position = SkipAsciiWhitespace(position);
if (position[0] == 'p' && position[1] == 'x') {
position = SkipAsciiWhitespace(position + 2);
}
// Reject if there's trailing junk.
if (*position != '\0') {
return false;
}
// 14) return result as length.
*value = static_cast<int>(result);
return true;
}
void ImageRewriteFilter::GetDimensions(
HtmlElement* element,
ImageDim* page_dim,
const HtmlElement::Attribute* src,
bool* is_resized_using_rendered_dimensions) {
css_util::StyleExtractor extractor(element);
css_util::DimensionState state = extractor.state();
int32 width = extractor.width();
int32 height = extractor.height();
int32 rendered_width = 0;
int32 rendered_height = 0;
// If the image has rendered dimensions stored in the property cache, update
// the desired image dimensions. Don't use rendered image dimensions
// when beaconing, since it would cause improper instrumentation.
if (driver()->options()->Enabled(
RewriteOptions::kResizeToRenderedImageDimensions) &&
!CriticalImagesBeaconFilter::ShouldApply(driver())) {
StringPiece src_value(src->DecodedValueOrNull());
if (!src_value.empty()) {
GoogleUrl src_gurl(driver()->base_url(), src_value);
if (src_gurl.IsWebOrDataValid()) {
std::pair<int32, int32> dimensions;
CriticalImagesFinder* finder =
driver()->server_context()->critical_images_finder();
if (finder->GetRenderedImageDimensions(
driver(), src_gurl, &dimensions)) {
if (dimensions.first != 0 && dimensions.second != 0) {
rendered_width = dimensions.first;
rendered_height = dimensions.second;
}
}
}
}
}
// If we didn't get a height dimension above, but there is a height
// value in the style attribute, that means there's a height value
// we can't process. This height will trump the height attribute in the
// image tag, so we need to avoid resizing.
// The same is true of width.
switch (state) {
case css_util::kNotParsable:
break;
case css_util::kHasBothDimensions:
page_dim->set_width(width);
page_dim->set_height(height);
break;
case css_util::kHasHeightOnly:
page_dim->set_height(height);
SetWidthFromAttribute(element, page_dim);
break;
case css_util::kHasWidthOnly:
page_dim->set_width(width);
SetHeightFromAttribute(element, page_dim);
break;
case css_util::kNoDimensions:
SetWidthFromAttribute(element, page_dim);
SetHeightFromAttribute(element, page_dim);
break;
}
// If the area of image using rendered dimensions is less than the dimensions
// from the style or image tag attributes, then only resize using rendered
// dimensions.
int64 rendered_area = rendered_width * rendered_height;
int64 image_attribute_area = page_dim->width() * page_dim->height();
// Note: we check for image_attribute_area = 1 (-1 * -1 = 1) when we have
// -1(unset) for both height and width from the image attributes.
if (rendered_area != 0 && ((image_attribute_area != 1 &&
rendered_area < image_attribute_area) ||
(image_attribute_area == 1))) {
page_dim->set_width(rendered_width);
page_dim->set_height(rendered_height);
*is_resized_using_rendered_dimensions = true;
image_resized_using_rendered_dimensions_->Add(1);
}
}
InlineResult ImageRewriteFilter::TryInline(bool is_html, bool is_critical,
int64 image_inline_max_bytes, const CachedResult* cached_result,
ResourceSlot* slot, GoogleString* data_url) {
int32 image_type_value = cached_result->inlined_image_type();
if ((image_type_value < IMAGE_UNKNOWN) ||
(image_type_value > IMAGE_WEBP_LOSSLESS_OR_ALPHA)) {
// IMAGE_UNKNOWN and IMAGE_WEBP_LOSSLESS_OR_ALPHA must be the smallest
// and largest values, respectively, in ImageType enum.
LOG(DFATAL) << "Invalid inlined_image_type in cached_result";
return INLINE_INTERNAL_ERROR;
}
ImageType image_type = static_cast<ImageType>(image_type_value);
const RequestProperties* request_properties = driver()->request_properties();
if (!request_properties->SupportsImageInlining() ||
((image_type == IMAGE_WEBP ||
image_type == IMAGE_WEBP_LOSSLESS_OR_ALPHA) &&
request_properties->ForbidWebpInlining())) {
return INLINE_UNSUPPORTED_DEVICE;
}
if (is_html && driver()->options()->inline_only_critical_images() &&
!is_critical) {
return INLINE_NOT_CRITICAL;
}
if (!cached_result->has_inlined_data()) {
return INLINE_NO_DATA;
}
StringPiece data = cached_result->inlined_data();
if (static_cast<int64>(data.size()) >= image_inline_max_bytes) {
return INLINE_TOO_LARGE;
}
// This is the decision point for whether or not an image is suitable for
// inlining. After this point, we may skip inlining an image, but not
// because of properties of the image.
const RewriteOptions* options = driver()->options();
if (options->cache_small_images_unrewritten()) {
// Skip rewriting, record the URL for storage in the property cache,
// suppress future rewrites to this slot, and return immediately.
GoogleString url(slot->resource()->url());
// Duplicate URLs are suppressed.
if (inlinable_urls_.insert(url).second) {
// This write to the property value allows downstream filters to observe
// inlinable images within the same flush window. Note that this does not
// induce a write to the underlying cache -- the value is written only
// when the filter chain has finished execution.
StoreUrlInPropertyCache(url);
}
// We disable rendering to prevent any rewriting of the URL that we'll
// advertise in the property cache.
slot->set_disable_rendering(true);
return INLINE_CACHE_SMALL_IMAGES_UNREWRITTEN;
}
DataUrl(*Image::TypeToContentType(image_type), BASE64, data, data_url);
return INLINE_SUCCESS;
}
void ImageRewriteFilter::EndElementImpl(HtmlElement* element) {
// Don't rewrite if the image is broken by a flush.
if (driver()->HasChildrenInFlushWindow(element)) {
return;
}
// Don't rewrite if there is a pagespeed_no_transform or
// data-pagespeed-no-transform attribute.
if (element->FindAttribute(HtmlName::kDataPagespeedNoTransform)) {
// Remove the attribute
element->DeleteAttribute(HtmlName::kDataPagespeedNoTransform);
return;
}
if (element->FindAttribute(HtmlName::kPagespeedNoTransform)) {
// Remove the attribute
element->DeleteAttribute(HtmlName::kPagespeedNoTransform);
return;
}
// Rewrite any image-valued attributes we find.
resource_tag_scanner::UrlCategoryVector attributes;
resource_tag_scanner::ScanElement(element, driver()->options(), &attributes);
for (int i = 0, n = attributes.size(); i < n; ++i) {
if (attributes[i].category != semantic_type::kImage ||
attributes[i].url->DecodedValueOrNull() == NULL) {
continue;
}
// The LSC filter only knows how to handle the src attribute.
if (attributes[i].url->keyword() == HtmlName::kSrc) {
// Ask the LSC filter to work out how to handle this element. A return
// value of true means we don't have to rewrite it so can skip that.
// The state is carried forward to after we initiate rewriting since
// we might still have to modify the element.
LocalStorageCacheFilter::InlineState state;
if (LocalStorageCacheFilter::AddStorableResource(
attributes[i].url->DecodedValueOrNull(),
driver(),
false /* check cookie */,
element, &state)) {
continue;
}
}
BeginRewriteImageUrl(element, attributes[i].url);
}
}
const UrlSegmentEncoder* ImageRewriteFilter::encoder() const {
return &encoder_;
}
void ImageRewriteFilter::EncodeUserAgentIntoResourceContext(
ResourceContext* context) const {
ImageUrlEncoder::SetWebpAndMobileUserAgent(*driver(), context);
CssUrlEncoder::SetInliningImages(*driver()->request_properties(), context);
ImageUrlEncoder::SetSmallScreen(*driver(), context);
context->set_may_use_save_data_quality(
driver()->options()->SupportSaveData() &&
driver()->request_properties()->RequestsSaveData());
}
RewriteContext* ImageRewriteFilter::MakeRewriteContext() {
ResourceContext* resource_context = new ResourceContext;
EncodeUserAgentIntoResourceContext(resource_context);
return new Context(0 /*No CSS inlining, it's html */,
this, driver(), NULL /*not nested */,
resource_context, false /*not css */,
kNotCriticalIndex,
false /*not in noscript */,
false /*not resized by rendered dimensions*/);
}
RewriteContext* ImageRewriteFilter::MakeNestedRewriteContextForCss(
int64 css_image_inline_max_bytes, RewriteContext* parent,
const ResourceSlotPtr& slot) {
// Copy over the ResourceContext from the parent RewriteContext so that we
// preserve request specific options, such as whether WebP rewriting is
// allowed.
ResourceContext* cloned_context = new ResourceContext;
const ResourceContext* parent_context = parent->resource_context();
if (parent_context != NULL) {
*cloned_context = *parent_context;
}
if (cloned_context->libwebp_level() != ResourceContext::LIBWEBP_NONE) {
// Assignment from parent_context is not sufficient because parent_context
// checks only UserAgentSupportsWebp when creating the context, but while
// rewriting the image, rewrite options should also be checked.
ImageUrlEncoder::SetLibWebpLevel(
*driver()->options(), *driver()->request_properties(),
cloned_context);
}
Context* context = new Context(css_image_inline_max_bytes,
this, NULL /* driver*/, parent,
cloned_context, true /*is css */,
kNotCriticalIndex,
false /*not in noscript */,
false /*not resized by rendered dimensions*/);
context->AddSlot(slot);
return context;
}
RewriteContext* ImageRewriteFilter::MakeNestedRewriteContext(
RewriteContext* parent, const ResourceSlotPtr& slot) {
ResourceContext* resource_context = new ResourceContext;
DCHECK(parent != NULL);
DCHECK(parent->resource_context() != NULL);
if (parent != NULL && parent->resource_context() != NULL) {
*resource_context = *(parent->resource_context());
}
Context* context = new Context(
0 /*No Css inling */, this, NULL /* driver */, parent, resource_context,
false /*not css */, kNotCriticalIndex, false /*not in noscript */,
false /*not resized by rendered dimensions*/);
context->AddSlot(slot);
return context;
}
bool ImageRewriteFilter::UpdateDesiredImageDimsIfNecessary(
const ImageDim& image_dim, const ResourceContext& resource_context,
ImageDim* desired_dim) {
return false;
}
const RewriteOptions::Filter* ImageRewriteFilter::RelatedFilters(
int* num_filters) const {
*num_filters = kRelatedFiltersSize;
return kRelatedFilters;
}
void ImageRewriteFilter::DisableRelatedFilters(RewriteOptions* options) {
for (int i = 0; i < kRelatedFiltersSize; ++i) {
options->DisableFilter(kRelatedFilters[i]);
}
}
void ImageRewriteFilter::RegisterImageInfo(
const AssociatedImageInfo& image_info) {
if (!driver()->options()->Enabled(
RewriteOptions::kExperimentCollectMobImageInfo)) {
return;
}
image_info_[image_info.url()] = image_info;
}
void ImageRewriteFilter::ReportDroppedRewrite() {
image_rewrites_dropped_due_to_load_->IncBy(1);
}
bool ImageRewriteFilter::ExtractAssociatedImageInfo(
const CachedResult* result, RewriteContext* context,
AssociatedImageInfo* out) {
bool ret = false;
if (result->has_image_file_dims()) {
if (result->url().empty()) {
if (context->num_slots() == 1) {
out->set_url(context->slot(0)->resource()->url());
ret = true;
}
} else {
out->set_url(result->url());
ret = true;
}
}
if (ret) {
*out->mutable_dimensions() = result->image_file_dims();
}
return ret;
}
} // namespace net_instaweb