| /* |
| * 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_url_encoder.h" |
| |
| #include "base/logging.h" |
| #include "net/instaweb/rewriter/cached_result.pb.h" |
| #include "net/instaweb/rewriter/public/request_properties.h" |
| #include "net/instaweb/rewriter/public/resource_namer.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/util/url_escaper.h" |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| const char kCodeSeparator = 'x'; |
| const char kCodeWebpLossy = 'w'; // for decoding legacy URLs. |
| const char kCodeWebpLossyLosslessAlpha = 'v'; // for decoding legacy URLs. |
| const char kCodeMobileUserAgent = 'm'; // for decoding legacy URLs. |
| const char kMissingDimension = 'N'; |
| |
| // Constants for UserAgent cache key entries. |
| const char kWebpLossyUserAgentKey[] = "w"; |
| const char kWebpLossyLossLessAlphaUserAgentKey[] = "v"; |
| const char kWebpAnimatedUserAgentKey[] = "a"; |
| // This used to not have a separate key, but we mixed up animated and it |
| // at one point, so this is now here to force a flush. |
| const char kWebpNoneUserAgentKey[] = "."; |
| const char kMobileUserAgentKey[] = "m"; |
| const char kSaveDataKey[] = "d"; |
| const char kSmallScreenKey[] = "ss"; |
| |
| bool IsValidCode(char code) { |
| return ((code == kCodeSeparator) || |
| (code == kCodeWebpLossy) || |
| (code == kCodeWebpLossyLosslessAlpha) || |
| (code == kCodeMobileUserAgent)); |
| } |
| |
| // Decodes a single dimension (either N or an integer), removing it from *in and |
| // ensuring at least one character remains. Returns true on success. When N is |
| // seen, *has_dimension is set to true. If decoding fails, *ok is set to false. |
| // |
| // Ensures that *in contains at least one character on exit. |
| uint32 DecodeDimension(StringPiece* in, bool* ok, bool* has_dimension) { |
| uint32 result = 0; |
| if (in->size() < 2) { |
| *ok = false; |
| *has_dimension = false; |
| } else if ((*in)[0] == kMissingDimension) { |
| // Dimension is absent. |
| in->remove_prefix(1); |
| *ok = true; |
| *has_dimension = false; |
| } else { |
| *ok = false; |
| *has_dimension = true; |
| while (in->size() >= 2 && AccumulateDecimalValue((*in)[0], &result)) { |
| in->remove_prefix(1); |
| *ok = true; |
| } |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| ImageUrlEncoder::~ImageUrlEncoder() { } |
| |
| void ImageUrlEncoder::Encode(const StringVector& urls, |
| const ResourceContext* data, |
| GoogleString* rewritten_url) const { |
| DCHECK(data != NULL) << "null data passed to ImageUrlEncoder::Encode"; |
| DCHECK_EQ(1U, urls.size()); |
| if (data != NULL) { |
| if (HasDimension(*data)) { |
| const ImageDim& dims = data->desired_image_dims(); |
| if (dims.has_width()) { |
| rewritten_url->append(IntegerToString(dims.width())); |
| } else { |
| rewritten_url->push_back(kMissingDimension); |
| } |
| if (dims.has_height()) { |
| StrAppend(rewritten_url, |
| StringPiece(&kCodeSeparator, 1), |
| IntegerToString(dims.height())); |
| } else { |
| StrAppend(rewritten_url, |
| StringPiece(&kCodeSeparator, 1), |
| StringPiece(&kMissingDimension, 1)); |
| } |
| } |
| rewritten_url->push_back(kCodeSeparator); |
| } |
| |
| UrlEscaper::EncodeToUrlSegment(urls[0], rewritten_url); |
| } |
| |
| namespace { |
| |
| // Stateless helper function for ImageUrlEncoder::Decode. |
| // Removes read dimensions from remaining, sets dims and returns true if |
| // dimensions are correctly parsed, returns false and leaves dims untouched on |
| // parse failure. |
| bool DecodeImageDimensions(StringPiece* remaining, ImageDim* dims) { |
| if (remaining->size() < 4) { |
| // url too short to hold dimensions. |
| return false; |
| } |
| bool ok, has_width, has_height; |
| uint32 width = DecodeDimension(remaining, &ok, &has_width); |
| if (!ok || ((*remaining)[0] != kCodeSeparator)) { // And check the separator |
| return false; |
| } |
| |
| // Consume the separator |
| remaining->remove_prefix(1); |
| uint32 height = DecodeDimension(remaining, &ok, &has_height); |
| if (remaining->size() < 1 || !ok) { |
| return false; |
| } |
| if (!IsValidCode((*remaining)[0])) { // And check the terminator |
| return false; |
| } |
| // Parsed successfully. |
| // Now store the dimensions that were present. |
| if (has_width) { |
| dims->set_width(width); |
| } |
| if (has_height) { |
| dims->set_height(height); |
| } else if (!has_width) { |
| // Both dimensions are missing! NxN[xw] is not allowed, as it's ambiguous |
| // with the shorter encoding. We should never get here in real life. |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| // The generic Decode interface is supplied so that |
| // RewriteContext and/or RewriteDriver can decode any |
| // ResourceNamer::name() field and find the set of URLs that are |
| // referenced. |
| bool ImageUrlEncoder::Decode(const StringPiece& encoded, |
| StringVector* urls, |
| ResourceContext* data, |
| MessageHandler* handler) const { |
| if (encoded.empty()) { |
| return false; |
| } |
| ImageDim* dims = data->mutable_desired_image_dims(); |
| // Note that "remaining" is shortened from the left as we parse. |
| StringPiece remaining(encoded); |
| char terminator = remaining[0]; |
| if (IsValidCode(terminator)) { |
| // No dimensions. x... or w... or mx... or mw... |
| // Do nothing. |
| } else if (DecodeImageDimensions(&remaining, dims)) { |
| // We've parsed the dimensions and they've been stripped from remaining. |
| // Now set terminator properly. |
| terminator = remaining[0]; |
| } else { |
| return false; |
| } |
| // Remove the terminator |
| remaining.remove_prefix(1); |
| |
| // Set mobile user agent & set webp only if its a legacy encoding. |
| if (terminator == kCodeMobileUserAgent) { |
| data->set_mobile_user_agent(true); |
| // There must be a final kCodeWebpLossy, |
| // kCodeWebpLossyLosslessAlpha, or kCodeSeparator. Otherwise, |
| // invalid. |
| // Check and strip it. |
| if (remaining.empty()) { |
| return false; |
| } |
| terminator = remaining[0]; |
| if (terminator != kCodeWebpLossy && |
| terminator != kCodeWebpLossyLosslessAlpha && |
| terminator != kCodeSeparator) { |
| return false; |
| } |
| remaining.remove_prefix(1); |
| } |
| // Following terminator check is for Legacy Url Encoding. |
| // If it's a legacy "x" encoding, we don't overwrite the libwebp_level. |
| // Example: if a webp-capable UA requested a legacy "x"-encoded url, we would |
| // wind up with a ResourceContext specifying a different webp-version of the |
| // original resourcem, but at least it's safe to send that to the UA, |
| // since we know it can handle it. |
| // |
| // In case it doesn't hit either of the following two conditions, |
| // the libwebp level is taken as the one set previously. This will happen |
| // mostly when the url is a Non-Legacy encoded one. |
| if (terminator == kCodeWebpLossy) { |
| data->set_libwebp_level(ResourceContext::LIBWEBP_LOSSY_ONLY); |
| } else if (terminator == kCodeWebpLossyLosslessAlpha) { |
| data->set_libwebp_level(ResourceContext::LIBWEBP_LOSSY_LOSSLESS_ALPHA); |
| } |
| |
| GoogleString* url = StringVectorAdd(urls); |
| if (UrlEscaper::DecodeFromUrlSegment(remaining, url)) { |
| return true; |
| } else { |
| urls->pop_back(); |
| return false; |
| } |
| } |
| |
| void ImageUrlEncoder::SetLibWebpLevel( |
| const RewriteOptions& options, |
| const RequestProperties& request_properties, |
| ResourceContext* resource_context) { |
| ResourceContext::LibWebpLevel libwebp_level = ResourceContext::LIBWEBP_NONE; |
| // We do enabled checks before Setting the Webp Level, since it avoids writing |
| // two metadata cache keys for same output if webp rewriting is disabled. |
| if (request_properties.SupportsWebpAnimated() && |
| (options.Enabled(RewriteOptions::kRecompressWebp) || |
| options.Enabled(RewriteOptions::kConvertToWebpAnimated))) { |
| libwebp_level = ResourceContext::LIBWEBP_ANIMATED; |
| } else if (request_properties.SupportsWebpLosslessAlpha() && |
| (options.Enabled(RewriteOptions::kRecompressWebp) || |
| options.Enabled(RewriteOptions::kConvertToWebpLossless))) { |
| libwebp_level = ResourceContext::LIBWEBP_LOSSY_LOSSLESS_ALPHA; |
| } else if (request_properties.SupportsWebpRewrittenUrls() && |
| (options.Enabled(RewriteOptions::kRecompressWebp) || |
| options.Enabled(RewriteOptions::kConvertToWebpLossless) || |
| options.Enabled(RewriteOptions::kConvertJpegToWebp))) { |
| libwebp_level = ResourceContext::LIBWEBP_LOSSY_ONLY; |
| } |
| resource_context->set_libwebp_level(libwebp_level); |
| } |
| |
| bool ImageUrlEncoder::IsWebpRewrittenUrl(const GoogleUrl& gurl) { |
| ResourceNamer namer; |
| if (!namer.DecodeIgnoreHashAndSignature(gurl.LeafSansQuery())) { |
| return false; |
| } |
| |
| // We only convert images to WebP whose URLs were created by |
| // ImageRewriteFilter, whose ID is "ic". Note that this code will |
| // not ordinarily be awakened for other filters (notabley .ce.) but |
| // is left in for paranoia in case this code is live for some path |
| // of in-place resource optimization of cache-extended images. |
| if (namer.id() != RewriteOptions::kImageCompressionId) { |
| return false; |
| } |
| |
| StringPiece webp_extension_with_dot = kContentTypeWebp.file_extension(); |
| return namer.ext() == webp_extension_with_dot.substr(1); |
| } |
| |
| void ImageUrlEncoder::SetWebpAndMobileUserAgent( |
| const RewriteDriver& driver, |
| ResourceContext* context) { |
| const RewriteOptions* options = driver.options(); |
| if (context == NULL) { |
| return; |
| } |
| |
| if (driver.options()->serve_rewritten_webp_urls_to_any_agent() && |
| !driver.fetch_url().empty() && |
| IsWebpRewrittenUrl(driver.decoded_base_url())) { |
| // See https://developers.google.com/speed/webp/faq#which_web_browsers_natively_support_webp |
| // which indicates that the latest versions of all browsers that support |
| // webp, support webp lossless as well. |
| context->set_libwebp_level(ResourceContext::LIBWEBP_LOSSY_LOSSLESS_ALPHA); |
| } else { |
| SetLibWebpLevel(*options, *driver.request_properties(), context); |
| } |
| |
| if (options->Enabled(RewriteOptions::kDelayImages) && |
| options->Enabled(RewriteOptions::kResizeMobileImages) && |
| driver.request_properties()->IsMobile()) { |
| context->set_mobile_user_agent(true); |
| } |
| } |
| |
| void ImageUrlEncoder::SetSmallScreen(const RewriteDriver& driver, |
| ResourceContext* context) { |
| // We used to do checking based on screen resolution, but we actually care |
| // about is physically small screens even if they're high-density. |
| context->set_may_use_small_screen_quality( |
| driver.options()->HasValidSmallScreenQualities() && |
| driver.request_properties()->IsMobile()); |
| } |
| |
| // Each image in lossless format may have up to 2 optimized versions |
| // (2 formats: Webp and GIF/PNG), while each image in lossy format may have up |
| // to 6 optimized versions (2 formats: WebP and JPEG; 3 qualities: Save-Data |
| // quality, mobile quality, and regular quality). |
| // |
| // mobile_user_agent, if applies, doubles the optimized versions. However, |
| // this flag is usually not effective. |
| GoogleString ImageUrlEncoder::CacheKeyFromResourceContext( |
| const ResourceContext& resource_context) { |
| GoogleString user_agent_cache_key = ""; |
| switch (resource_context.libwebp_level()) { |
| case ResourceContext::LIBWEBP_NONE: |
| StrAppend(&user_agent_cache_key, kWebpNoneUserAgentKey); |
| break; |
| case ResourceContext::LIBWEBP_LOSSY_LOSSLESS_ALPHA: |
| StrAppend(&user_agent_cache_key, kWebpLossyLossLessAlphaUserAgentKey); |
| break; |
| case ResourceContext::LIBWEBP_LOSSY_ONLY: |
| StrAppend(&user_agent_cache_key, kWebpLossyUserAgentKey); |
| break; |
| case ResourceContext::LIBWEBP_ANIMATED: |
| StrAppend(&user_agent_cache_key, kWebpAnimatedUserAgentKey); |
| break; |
| } |
| if (resource_context.mobile_user_agent()) { |
| StrAppend(&user_agent_cache_key, kMobileUserAgentKey); |
| } |
| |
| // If the image will be compressed to a quality different than the regular |
| // one, add a key to cache. The quality for Save-Data has higher precedence |
| // than that for mobile, so does the key. |
| if (resource_context.may_use_save_data_quality()) { |
| StrAppend(&user_agent_cache_key, kSaveDataKey); |
| } else if (resource_context.may_use_small_screen_quality()) { |
| StrAppend(&user_agent_cache_key, kSmallScreenKey); |
| } |
| |
| return user_agent_cache_key; |
| } |
| |
| bool ImageUrlEncoder::AllowVaryOnUserAgent( |
| const RewriteOptions& options, |
| const RequestProperties& request_properties) { |
| return (options.AllowVaryOnUserAgent() || |
| (options.AllowVaryOnAuto() && !request_properties.HasViaHeader())); |
| } |
| |
| bool ImageUrlEncoder::AllowVaryOnAccept( |
| const RewriteOptions& options, |
| const RequestProperties& request_properties) { |
| return (options.AllowVaryOnAccept() || |
| (options.AllowVaryOnAuto() && request_properties.HasViaHeader())); |
| } |
| |
| } // namespace net_instaweb |