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