| /* |
| * Copyright 2011 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: sligocki@google.com (Shawn Ligocki) |
| |
| #include "net/instaweb/rewriter/public/css_image_rewriter.h" |
| |
| #include <cstddef> |
| #include <map> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "net/instaweb/rewriter/cached_result.pb.h" |
| #include "net/instaweb/rewriter/public/cache_extender.h" |
| #include "net/instaweb/rewriter/public/css_filter.h" |
| #include "net/instaweb/rewriter/public/css_hierarchy.h" |
| #include "net/instaweb/rewriter/public/css_resource_slot.h" |
| #include "net/instaweb/rewriter/public/image_combine_filter.h" |
| #include "net/instaweb/rewriter/public/image_rewrite_filter.h" |
| #include "net/instaweb/rewriter/public/resource.h" |
| #include "net/instaweb/rewriter/public/resource_slot.h" |
| #include "net/instaweb/rewriter/public/rewrite_context.h" |
| #include "net/instaweb/rewriter/public/rewrite_driver.h" |
| #include "net/instaweb/rewriter/public/rewrite_options.h" |
| #include "pagespeed/kernel/base/message_handler.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" // for StrCat |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "util/utf8/public/unicodetext.h" |
| #include "webutil/css/parser.h" |
| #include "webutil/css/property.h" |
| #include "webutil/css/value.h" |
| |
| |
| namespace net_instaweb { |
| |
| namespace { |
| |
| GoogleString CannotImportMessage(StringPiece action, StringPiece url, |
| bool is_authorized) { |
| return StrCat("Cannot ", action, " ", url, |
| (is_authorized |
| ? " for an unknown reason" |
| : " as it is on an unauthorized domain")); |
| } |
| |
| } // namespace |
| |
| CssImageRewriter::CssImageRewriter(CssFilter::Context* root_context, |
| CssFilter* filter, |
| CacheExtender* cache_extender, |
| ImageRewriteFilter* image_rewriter, |
| ImageCombineFilter* image_combiner) |
| : filter_(filter), |
| root_context_(root_context), |
| // For now we use the same options as for rewriting and cache-extending |
| // images found in HTML. |
| cache_extender_(cache_extender), |
| image_combiner_(image_combiner), |
| image_rewriter_(image_rewriter) { |
| // TODO(morlovich): Unlike the original CssImageRewriter, this uses the same |
| // statistics as underlying filters like CacheExtender. Should it get separate |
| // stats instead? sligocki thinks it's useful to know how many images were |
| // optimized from CSS files, but people probably also want to know how many |
| // total images were cache-extended. |
| } |
| |
| CssImageRewriter::~CssImageRewriter() {} |
| |
| bool CssImageRewriter::RewritesEnabled( |
| int64 image_inline_max_bytes) const { |
| const RewriteOptions* options = driver()->options(); |
| return (image_inline_max_bytes > 0 || |
| options->ImageOptimizationEnabled() || |
| options->Enabled(RewriteOptions::kLeftTrimUrls) || |
| options->Enabled(RewriteOptions::kExtendCacheImages) || |
| options->Enabled(RewriteOptions::kSpriteImages)); |
| } |
| |
| bool CssImageRewriter::RewriteImport(RewriteContext* parent, |
| CssHierarchy* hierarchy, |
| bool* is_authorized) { |
| GoogleUrl import_url(hierarchy->url()); |
| ResourcePtr resource = driver()->CreateInputResource(import_url, |
| is_authorized); |
| if (resource.get() == NULL) { |
| return false; |
| } |
| |
| parent->AddNestedContext( |
| filter_->MakeNestedFlatteningContextInNewSlot( |
| resource, driver()->UrlLine(), root_context_, parent, hierarchy)); |
| return true; |
| } |
| |
| bool CssImageRewriter::RewriteImage(int64 image_inline_max_bytes, |
| const GoogleUrl& trim_url, |
| const GoogleUrl& original_url, |
| RewriteContext* parent, |
| Css::Values* values, |
| size_t value_index, |
| bool* is_authorized) { |
| const RewriteOptions* options = driver()->options(); |
| ResourcePtr resource = driver()->CreateInputResource(original_url, |
| is_authorized); |
| if (resource.get() == NULL) { |
| return false; |
| } |
| |
| CssResourceSlotPtr slot( |
| root_context_->slot_factory()->GetSlot(resource, trim_url, options, |
| values, value_index)); |
| if (options->image_preserve_urls()) { |
| slot->set_preserve_urls(true); |
| } |
| |
| RewriteSlot(ResourceSlotPtr(slot), image_inline_max_bytes, parent); |
| return true; |
| } |
| |
| void CssImageRewriter::RewriteSlot(const ResourceSlotPtr& slot, |
| int64 image_inline_max_bytes, |
| RewriteContext* parent) { |
| const RewriteOptions* options = driver()->options(); |
| if (options->ImageOptimizationEnabled() || image_inline_max_bytes > 0) { |
| // Do not rewrite external resource if preserve_urls is enabled unless |
| // we allow preemptive rewriting. |
| if (!slot->preserve_urls() || |
| options->in_place_preemptive_rewrite_css_images()) { |
| parent->AddNestedContext( |
| image_rewriter_->MakeNestedRewriteContextForCss( |
| image_inline_max_bytes, parent, slot)); |
| } |
| } |
| |
| if (driver()->MayCacheExtendImages()) { |
| parent->AddNestedContext( |
| cache_extender_->MakeNestedContext(parent, slot)); |
| } |
| |
| // TODO(sligocki): DomainRewriter or is this done automatically? |
| } |
| |
| void CssImageRewriter::InheritChildImageInfo(RewriteContext* context) { |
| typedef ImageRewriteFilter::AssociatedImageInfoMap AssociatedImageInfoMap; |
| if (!context->Driver()->options()->Enabled( |
| RewriteOptions::kExperimentCollectMobImageInfo)) { |
| return; |
| } |
| |
| if (context->num_outputs() != 1) { |
| LOG(DFATAL) << "InheritChildImageInfo on context with wrong # of outputs:" |
| << context->num_outputs(); |
| return; |
| } |
| |
| AssociatedImageInfoMap child_image_info; |
| for (int i = 0; i < context->num_nested(); ++i) { |
| RewriteContext* nested_context = context->nested(i); |
| for (int j = 0; j < nested_context->num_output_partitions(); ++j) { |
| const CachedResult* child_result = nested_context->output_partition(j); |
| |
| // Image info may be directly included inside the CachedResult, if |
| // child_result came from the image filter. |
| AssociatedImageInfo from_image_filter; |
| if (ImageRewriteFilter::ExtractAssociatedImageInfo(child_result, |
| nested_context, |
| &from_image_filter)) { |
| child_image_info[from_image_filter.url()] = from_image_filter; |
| } |
| |
| // Info on multiple images may be stored as AssociatedImageInfo by CSS |
| // rewriting or flattening. |
| for (int k = 0; k < child_result->associated_image_info_size(); ++k) { |
| const AssociatedImageInfo& image_info = |
| child_result->associated_image_info(k); |
| child_image_info[image_info.url()] = image_info; |
| } |
| } |
| } |
| |
| for (AssociatedImageInfoMap::iterator i = child_image_info.begin(), |
| e = child_image_info.end(); |
| i != e; ++i) { |
| *context->output_partition(0)->add_associated_image_info() = i->second; |
| } |
| } |
| |
| bool CssImageRewriter::RewriteCss(int64 image_inline_max_bytes, |
| RewriteContext* parent, |
| CssHierarchy* hierarchy, |
| MessageHandler* handler) { |
| const RewriteOptions* options = driver()->options(); |
| bool spriting_ok = options->Enabled(RewriteOptions::kSpriteImages); |
| |
| if (!driver()->FlattenCssImportsEnabled()) { |
| // If flattening is disabled completely, mark this hierarchy as having |
| // failed flattening, so that later RollUps do the right thing (nothing). |
| // This is not something we need to log in the statistics or in debug. |
| hierarchy->set_flattening_succeeded(false); |
| } else if (hierarchy->flattening_succeeded()) { |
| // Flattening of this hierarchy might have already failed because of a |
| // problem detected with the containing charset or media, in particular |
| // see CssFilter::Start(Inline|Attribute|External)Rewrite. |
| if (hierarchy->ExpandChildren()) { |
| for (int i = 0, n = hierarchy->children().size(); i < n; ++i) { |
| CssHierarchy* child = hierarchy->children()[i]; |
| if (child->NeedsRewriting()) { |
| bool is_authorized; |
| if (!RewriteImport(parent, child, &is_authorized)) { |
| hierarchy->set_flattening_succeeded(false); |
| hierarchy->AddFlatteningFailureReason( |
| CannotImportMessage("import", child->url_for_humans(), |
| is_authorized)); |
| } |
| } |
| } |
| } |
| } |
| |
| // TODO(jkarlin): We need a separate flag for CssImagePreserveURLs in case the |
| // user is willing to change image URLs in CSS but not in HTML. |
| bool is_enabled = RewritesEnabled(image_inline_max_bytes); |
| |
| if (is_enabled) { |
| if (spriting_ok) { |
| image_combiner_->Reset(parent, hierarchy->css_base_url(), |
| hierarchy->input_contents()); |
| } |
| Css::Rulesets& rulesets = |
| hierarchy->mutable_stylesheet()->mutable_rulesets(); |
| for (Css::Rulesets::iterator ruleset_iter = rulesets.begin(); |
| ruleset_iter != rulesets.end(); ++ruleset_iter) { |
| Css::Ruleset* ruleset = *ruleset_iter; |
| if (ruleset->type() != Css::Ruleset::RULESET) { |
| continue; |
| } |
| Css::Declarations& decls = ruleset->mutable_declarations(); |
| bool background_position_found = false; |
| bool background_image_found = false; |
| for (Css::Declarations::iterator decl_iter = decls.begin(); |
| decl_iter != decls.end(); ++decl_iter) { |
| Css::Declaration* decl = *decl_iter; |
| // Only edit image declarations. |
| switch (decl->prop()) { |
| case Css::Property::BACKGROUND_POSITION: |
| case Css::Property::BACKGROUND_POSITION_X: |
| case Css::Property::BACKGROUND_POSITION_Y: |
| background_position_found = true; |
| break; |
| case Css::Property::BACKGROUND: |
| case Css::Property::BACKGROUND_IMAGE: |
| case Css::Property::CONTENT: // In CSS2 but not CSS2.1 |
| case Css::Property::CURSOR: |
| case Css::Property::LIST_STYLE: |
| case Css::Property::LIST_STYLE_IMAGE: { |
| // Rewrite all URLs. Technically, background-image should only |
| // have a single value which is a URL, but background could have |
| // more values. |
| Css::Values* values = decl->mutable_values(); |
| for (size_t value_index = 0; value_index < values->size(); |
| value_index++) { |
| Css::Value* value = values->at(value_index); |
| if (value->GetLexicalUnitType() == Css::Value::URI) { |
| background_image_found = true; |
| GoogleString rel_url = |
| UnicodeTextToUTF8(value->GetStringValue()); |
| // TODO(abliss): only do this resolution once. |
| const GoogleUrl original_url(hierarchy->css_resolution_base(), |
| rel_url); |
| if (!original_url.IsWebValid()) { |
| continue; |
| } |
| if (!options->IsAllowed(original_url.Spec())) { |
| continue; |
| } |
| bool is_authorized; |
| if (spriting_ok) { |
| // TODO(sligocki): Pass in the correct base URL here. |
| // Specifically, the final base URL of the CSS that will |
| // be used to trim the final URLs. |
| // hierarchy->css_base_url(), hierarchy->css_trim_url(), |
| // or hierarchy->css_resolution_base()? |
| // Note that currently preserving URLs doesn't work for |
| // image combining filter, so we need to fix that before |
| // testing which URL is correct. |
| if (!image_combiner_->AddCssBackgroundContext( |
| original_url, hierarchy->css_trim_url(), |
| values, value_index, root_context_, &decls, |
| &is_authorized, handler)) { |
| // This doesn't fail flattening but we want to log it. |
| hierarchy->AddFlatteningFailureReason( |
| CannotImportMessage("rewrite", original_url.Spec(), |
| is_authorized)); |
| } |
| } |
| if (!RewriteImage(image_inline_max_bytes, |
| hierarchy->css_trim_url(), original_url, |
| parent, values, value_index, |
| &is_authorized)) { |
| // This doesn't fail flattening but we want to log it. |
| hierarchy->AddFlatteningFailureReason( |
| CannotImportMessage("rewrite", original_url.Spec(), |
| is_authorized)); |
| } |
| } |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| // All the declarations in this ruleset have been parsed. |
| if (spriting_ok && background_position_found && !background_image_found) { |
| // A ruleset that contains a background-position but no background image |
| // is a signal that we should not be spriting. |
| handler->Message(kInfo, |
| "Lone background-position found: Cannot sprite."); |
| spriting_ok = false; |
| } |
| } |
| |
| image_combiner_->RegisterOrReleaseContext(); |
| } else { |
| handler->Message(kInfo, "Image rewriting and cache extension not enabled, " |
| "so not rewriting images in CSS in %s", |
| hierarchy->css_base_url().spec_c_str()); |
| } |
| |
| return is_enabled; |
| } |
| |
| } // namespace net_instaweb |