| /* |
| * 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: abliss@google.com (Adam Bliss) |
| |
| #include "net/instaweb/rewriter/public/image_combine_filter.h" |
| |
| #include <cstddef> // for size_t |
| #include <iterator> |
| #include <map> |
| #include <set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/logging.h" |
| #include "net/instaweb/rewriter/cached_result.pb.h" |
| #include "net/instaweb/rewriter/public/css_resource_slot.h" |
| #include "net/instaweb/rewriter/public/css_util.h" |
| #include "net/instaweb/rewriter/public/image.h" |
| #include "net/instaweb/rewriter/public/output_resource.h" |
| #include "net/instaweb/rewriter/public/output_resource_kind.h" |
| #include "net/instaweb/rewriter/public/resource.h" |
| #include "net/instaweb/rewriter/public/resource_combiner.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 "net/instaweb/rewriter/public/rewrite_result.h" |
| #include "net/instaweb/rewriter/public/server_context.h" |
| #include "net/instaweb/spriter/image_library_interface.h" |
| #include "net/instaweb/spriter/public/image_spriter.h" |
| #include "net/instaweb/spriter/public/image_spriter.pb.h" |
| #include "pagespeed/kernel/base/basictypes.h" |
| #include "pagespeed/kernel/base/function.h" |
| #include "pagespeed/kernel/base/md5_hasher.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/stl_util.h" |
| #include "pagespeed/kernel/base/string.h" |
| #include "pagespeed/kernel/base/string_util.h" |
| #include "pagespeed/kernel/base/timer.h" |
| #include "pagespeed/kernel/http/content_type.h" |
| #include "pagespeed/kernel/http/google_url.h" |
| #include "pagespeed/kernel/http/image_types.pb.h" |
| #include "util/utf8/public/unicodetext.h" |
| #include "webutil/css/identifier.h" |
| #include "webutil/css/parser.h" |
| #include "webutil/css/property.h" |
| #include "webutil/css/value.h" |
| |
| namespace net_instaweb { |
| |
| typedef std::map<GoogleString, const spriter::Rect*> RectMap; |
| namespace { |
| |
| // names for Statistics variables. |
| const char kImageFileCountReduction[] = "image_file_count_reduction"; |
| |
| } // namespace |
| |
| // Unfortunately SpriteFuture can't be inside an anonymous namespace due to |
| // http://gcc.gnu.org/bugzilla/show_bug.cgi?id=29365, which is relevant to |
| // some of our supported open-source platforms. |
| namespace spriter_binding { |
| |
| // A SpriteFuture keeps track of a single image that is to be sprited. When |
| // constructed, it is in an invalid state and merely serves as a token for the |
| // partnership. |
| class SpriteFuture { |
| public: |
| // old_url is the original URL which will be replaced with the sprite. We |
| // keep track of it so that we can avoid putting the same image in the sprite |
| // twice. |
| // Initialize declarations for future use. |
| // Used for async flow where we can't check image against decls |
| // when the future is created. |
| explicit SpriteFuture(const StringPiece& old_url, int width, int height, |
| Css::Declarations* decls) |
| : url_value_(NULL), |
| x_value_(NULL), |
| y_value_(NULL), |
| declarations_(decls), |
| declaration_to_push_(NULL), |
| div_width_(width), |
| div_height_(height), |
| has_position_(false) { |
| old_url.CopyToString(&old_url_); |
| x_offset_ = 0; |
| y_offset_ = 0; |
| } |
| |
| ~SpriteFuture() {} |
| |
| // Bind this Future to a particular image. Owns nothing; the inputs must |
| // outlive this future. |
| void Initialize(Css::Value* url_value) { |
| url_value_ = url_value; |
| } |
| |
| const GoogleString& old_url() { return old_url_; } |
| |
| Css::Declarations* decls() { return declarations_; } |
| |
| // Set x_px and y_px to the alignment for this image/div combination |
| // before spriting. |
| bool SetAlignmentValues(Css::Value* x_value, Css::Value* y_value, |
| int image_width, int image_height, |
| int* x_px, int* y_px) { |
| bool ret = true; |
| if (x_value->GetLexicalUnitType() == Css::Value::NUMBER) { |
| if (IsValidNumberPosition(*x_value)) { |
| int int_value = x_value->GetIntegerValue(); |
| *x_px = int_value; |
| } else { |
| ret = false; |
| } |
| } else if (x_value->GetLexicalUnitType() == Css::Value::IDENT) { |
| switch (x_value->GetIdentifier().ident()) { |
| case Css::Identifier::LEFT: |
| *x_px = 0; |
| break; |
| case Css::Identifier::RIGHT: |
| *x_px = div_width_ - image_width; |
| break; |
| case Css::Identifier::CENTER: |
| *x_px = (div_width_ - image_width) / 2; |
| break; |
| default: |
| ret = false; |
| break; |
| } |
| } |
| if (y_value->GetLexicalUnitType() == Css::Value::NUMBER) { |
| if (ret && IsValidNumberPosition(*y_value)) { |
| int int_value = y_value->GetIntegerValue(); |
| *y_px = int_value; |
| } else { |
| ret = false; |
| } |
| } else if (ret && y_value->GetLexicalUnitType() == Css::Value::IDENT) { |
| switch (y_value->GetIdentifier().ident()) { |
| case Css::Identifier::TOP: |
| *y_px = 0; |
| break; |
| case Css::Identifier::BOTTOM: |
| *y_px = div_height_ - image_height; |
| break; |
| case Css::Identifier::CENTER: |
| *y_px = (div_height_ - image_height) / 2; |
| break; |
| default: |
| ret = false; |
| break; |
| } |
| } |
| return ret; |
| } |
| |
| // (1) Figure out what position declaration we have. |
| // (2) If we have x, create y, and vice versa. |
| // (3) Insert the new value into the values vector. |
| bool ReadSingleValue(Css::Values* values, int values_offset, |
| Css::Value** x_value, Css::Value** y_value) { |
| Css::Value* extra_value = new Css::Value(Css::Identifier::CENTER); |
| Css::Value* value = values->at(values_offset); |
| if (value->GetLexicalUnitType() == Css::Value::IDENT) { |
| switch (value->GetIdentifier().ident()) { |
| case Css::Identifier::LEFT: |
| case Css::Identifier::RIGHT: |
| case Css::Identifier::CENTER: |
| *x_value = value; |
| *y_value = extra_value; |
| break; |
| case Css::Identifier::TOP: |
| case Css::Identifier::BOTTOM: |
| *y_value = value; |
| *x_value = extra_value; |
| break; |
| default: |
| delete extra_value; |
| return false; |
| } |
| } else { |
| return false; |
| } |
| values->insert(values->begin() + values_offset + 1, extra_value); |
| return true; |
| } |
| |
| // (1) Figure out what position declaration we have first. |
| // (2) If horizontal, other is vertical, and vice versa. |
| // (3) If first value is a number, second value is vertical, |
| // first is horizontal. |
| bool ReadTwoValues(Css::Values* values, int values_offset, |
| Css::Value**x_value, Css::Value** y_value) { |
| Css::Value* value = values->at(values_offset); |
| Css::Value* other_value = values->at(values_offset + 1); |
| if (value->GetLexicalUnitType() == Css::Value::IDENT) { |
| switch (value->GetIdentifier().ident()) { |
| case Css::Identifier::LEFT: |
| case Css::Identifier::RIGHT: |
| *x_value = value; |
| *y_value = other_value; |
| break; |
| case Css::Identifier::TOP: |
| case Css::Identifier::BOTTOM: |
| *x_value = other_value; |
| *y_value = value; |
| break; |
| case Css::Identifier::CENTER: |
| if (other_value->GetLexicalUnitType() == Css::Value::IDENT) { |
| switch (other_value->GetIdentifier().ident()) { |
| case Css::Identifier::LEFT: |
| case Css::Identifier::RIGHT: |
| *x_value = other_value; |
| *y_value = value; |
| break; |
| case Css::Identifier::TOP: |
| case Css::Identifier::BOTTOM: |
| case Css::Identifier::CENTER: |
| *x_value = value; |
| *y_value = other_value; |
| break; |
| default: |
| return false; |
| } |
| } else { |
| // TODO(nforman): Allow for mixing of alignment types, |
| // i.e. left 2px. |
| return false; |
| } |
| break; |
| default: |
| return false; |
| } |
| } else { |
| // If there are two values and neither is an identifier, x comes |
| // first: e.g. "5px 6px" means x=5, y=6. |
| // TODO(nforman): support % values. |
| for (int i = 0; i < 2; ++i) { |
| Css::Value* val = values->at(values_offset + i); |
| if (val->GetLexicalUnitType() == Css::Value::NUMBER && |
| IsValidNumberPosition(*val)) { |
| continue; |
| } |
| return false; |
| } |
| *x_value = values->at(values_offset); |
| *y_value = values->at(values_offset + 1); |
| } |
| return true; |
| } |
| |
| // Attempts to read the x and y values of the background position. *values |
| // is a value array which includes the background-position at values_offset. |
| // new_x and new_y are the coordinates of the image in the sprite. Returns |
| // true, and sets up {x,y}_{value,offset}_ if successful. |
| bool ReadBackgroundPosition(Css::Values* values, int values_offset, |
| int image_width, int image_height) { |
| // Parsing these values is trickier than you might think. If either |
| // of the two values is a non-center identifier, it determines which |
| // is x and which is y. So for example, "5px left" means x=0, y=5 but |
| // "5px top" means x=5, y=0. |
| // See: http://www.w3.org/TR/CSS21/colors.html#propdef-background-position |
| // TODO(abliss): move this to webutil/css? |
| Css::Value* x_value = NULL; |
| Css::Value* y_value = NULL; |
| if ((static_cast<int>(values->size()) - values_offset == 1) || |
| !IsPositionValue(*(values->at(values_offset + 1)))) { |
| if (!ReadSingleValue(values, values_offset, &x_value, &y_value)) { |
| return false; |
| } |
| } else if (!ReadTwoValues(values, values_offset, &x_value, &y_value)) { |
| return false; |
| } |
| // Now that we know which value is which dimension, we can extract the |
| // values in px. |
| int x_px = 0; |
| int y_px = 0; |
| if (!SetAlignmentValues(x_value, y_value, image_width, image_height, |
| &x_px, &y_px)) { |
| return false; |
| } |
| // When sprited, these x_value_ and y_value_ will both be replaced |
| // with absolute pixel values (i.e. not center or left), so they need |
| // to be in x-first, y-second order. |
| x_value_ = values->at(values_offset); |
| x_offset_ = x_px; |
| y_value_ = values->at(values_offset + 1); |
| y_offset_ = y_px; |
| return true; |
| } |
| |
| // Returns whether or not this is a number value we can handle. |
| static bool IsValidNumberPosition(const Css::Value& value) { |
| CHECK(value.GetLexicalUnitType() == Css::Value::NUMBER); |
| int int_value = value.GetIntegerValue(); |
| // If the aligment is specified in pixels, or is 0, we can just use it. |
| if ((value.GetDimension() == Css::Value::PX) || (int_value == 0)) { |
| return true; |
| } |
| return false; |
| } |
| |
| // Tries to guess whether this value is an x- or y- position value in the |
| // background shorthand value list. |
| static bool IsPositionValue(const Css::Value& value) { |
| if (value.GetLexicalUnitType() == Css::Value::NUMBER) { |
| return true; |
| } else if (value.GetLexicalUnitType() == Css::Value::IDENT) { |
| switch (value.GetIdentifier().ident()) { |
| case Css::Identifier::LEFT: |
| case Css::Identifier::RIGHT: |
| case Css::Identifier::TOP: |
| case Css::Identifier::BOTTOM: |
| case Css::Identifier::CENTER: |
| return true; |
| default: |
| return false; |
| } |
| } |
| return false; |
| } |
| |
| // Attempt to actually perform the url substitution. Initialize must have |
| // been called first, and must have returned true. |
| void Realize(const char* url, int x, int y) { |
| if (!has_position_) { |
| // If no position was specified, it defaults to "0% 0%", which is the same |
| // as "0px 0px". |
| Css::Values* values = new Css::Values(); |
| x_value_ = new Css::Value(0, Css::Value::PX); |
| values->push_back(x_value_); |
| y_value_ = new Css::Value(0, Css::Value::PX); |
| values->push_back(y_value_); |
| declaration_to_push_ = new Css::Declaration( |
| Css::Property::BACKGROUND_POSITION, values, false); |
| } |
| CHECK(x_value_ != NULL); |
| *url_value_ = Css::Value(Css::Value::URI, UTF8ToUnicodeText(url)); |
| *x_value_ = Css::Value(x_offset_ - x, Css::Value::PX); |
| *y_value_ = Css::Value(y_offset_ - y, Css::Value::PX); |
| |
| if ((declarations_ != NULL) && (declaration_to_push_ != NULL)) { |
| declarations_->push_back(declaration_to_push_); |
| } |
| } |
| |
| int width() { |
| return div_width_; |
| } |
| |
| int height() { |
| return div_height_; |
| } |
| |
| // Attempt to find the background position values, or create them if |
| // necessary. If we return true, we should be all set for a call to |
| // Realize(). If we return false, Realize() must never be called. |
| // set has_position_ to true if there is already a position declaration. |
| // If has_position_ is false, we will create a new declaration when |
| // rendering. |
| // Returns true if this is a viable sprite-future. If |
| // we return false, Realize must not be called. |
| bool FindBackgroundPositionValues(int image_width, int image_height) { |
| // Find the original background offsets (if any) so we can add to them. |
| has_position_ = false; |
| for (Css::Declarations::iterator decl_iter = declarations_->begin(); |
| !has_position_ && (decl_iter != declarations_->end()); |
| ++decl_iter) { |
| Css::Declaration* decl = *decl_iter; |
| switch (decl->prop()) { |
| case Css::Property::BACKGROUND_POSITION: { |
| Css::Values* decl_values = decl->mutable_values(); |
| if (decl_values->size() > 2 || decl_values->size() < 1) { |
| return false; |
| } |
| if (ReadBackgroundPosition(decl_values, 0, |
| image_width, image_height)) { |
| has_position_ = true; |
| } else { |
| // Upon failure here, we abort the sprite. |
| return false; |
| } |
| break; |
| } |
| case Css::Property::BACKGROUND_POSITION_X: |
| case Css::Property::BACKGROUND_POSITION_Y: |
| // These are non-standard, though supported in IE and Chrome. |
| // TODO(abliss): handle these. |
| return false; |
| case Css::Property::BACKGROUND: { |
| Css::Values* decl_values = decl->mutable_values(); |
| // The background shorthand can include many values in any order. |
| // We'll look for two consecutive position values. (If only one |
| // position value is present, the other is considered to be CENTER |
| // which we don't support.) |
| for (int i = 0, n = decl_values->size(); i < n; ++i) { |
| if (IsPositionValue(*(decl_values->at(i)))) { |
| if (ReadBackgroundPosition(decl_values, i, |
| image_width, image_height)) { |
| has_position_ = true; |
| break; |
| } else { |
| // Upon failure here, we abort the sprite. |
| return false; |
| } |
| } |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| } |
| // TODO(abliss): consider specifying width and height. Currently we are |
| // assuming the node is already sized correctly. |
| return true; |
| } |
| |
| private: |
| GoogleString old_url_; |
| // Pointer to the value where the url of the image is stored. |
| Css::Value* url_value_; |
| // Pointer to the value where the background position x coordinate is stored. |
| Css::Value* x_value_; |
| // Pointer to the value where the background position y coordinate is stored. |
| Css::Value* y_value_; |
| // Optional pointer to a declarations object where a new declaration will be |
| // pushed. |
| Css::Declarations* declarations_; |
| // Optional pointer to a declaration object to be pushed onto declarations_. |
| Css::Declaration* declaration_to_push_; |
| int x_offset_; |
| int y_offset_; |
| // Width and height of original div. We use these to check against |
| // the image's dimensions once the image is loaded, so we can determine |
| // if the image can be sprited in this context. |
| int div_width_; |
| int div_height_; |
| bool has_position_; |
| DISALLOW_COPY_AND_ASSIGN(SpriteFuture); |
| }; |
| |
| // An implementation of the Spriter's ImageLibraryInterface on top of our own |
| // Image class. Instead of using the filesystem, we keep an in-memory map, |
| // which owns pointers to images. |
| class Library : public spriter::ImageLibraryInterface { |
| public: |
| // A thin layer of glue around an Image as input to the Spriter. |
| class SpriterImage : public spriter::ImageLibraryInterface::Image { |
| public: |
| // Owns nothing. Image may not be null. The library is expected to |
| // maintain ownership of the image pointer. |
| SpriterImage(net_instaweb::Image* image, |
| spriter::ImageLibraryInterface* lib) : |
| Image(lib), image_(image) { |
| DCHECK(image_ != NULL) << "null image not allowed."; |
| } |
| |
| virtual ~SpriterImage() {} |
| |
| virtual bool GetDimensions(int* out_width, int* out_height) const { |
| ImageDim dim; |
| image_->Dimensions(&dim); |
| *out_width = dim.width(); |
| *out_height = dim.height(); |
| return (dim.width() >= 0) && (dim.height() >= 0); |
| } |
| |
| // TODO(abliss): This should really be returning const Image*. |
| net_instaweb::Image* image() const { return image_; } |
| |
| private: |
| net_instaweb::Image* image_; |
| DISALLOW_COPY_AND_ASSIGN(SpriterImage); |
| }; |
| |
| // A thin layer of glue around an Image as output from the Spriter. |
| // Owns its own mutable image. |
| class Canvas : public spriter::ImageLibraryInterface::Canvas { |
| public: |
| Canvas(int width, int height, Library* lib, |
| const StringPiece& tmp_dir, Timer* timer, MessageHandler* handler) : |
| spriter::ImageLibraryInterface::Canvas(lib), |
| lib_(lib) { |
| DCHECK(lib != NULL); |
| net_instaweb::Image::CompressionOptions* options = |
| new net_instaweb::Image::CompressionOptions(); |
| options->recompress_png = true; |
| image_.reset(BlankImageWithOptions(width, height, |
| net_instaweb::IMAGE_PNG, |
| tmp_dir, timer, handler, options)); |
| } |
| |
| virtual ~Canvas() { } |
| |
| virtual bool DrawImage(const Image* image, int x, int y) { |
| const SpriterImage* spriter_image |
| = static_cast<const SpriterImage*>(image); |
| return image_->DrawImage(spriter_image->image(), x, y); |
| } |
| |
| // On successfully writing, we release our image. |
| virtual bool WriteToFile( |
| const FilePath& write_path, spriter::ImageFormat format) { |
| if (format != spriter::PNG) { |
| return false; |
| } |
| lib_->RegisterImage(write_path, image_.release()); |
| return true; |
| } |
| |
| private: |
| scoped_ptr<net_instaweb::Image> image_; |
| Library* lib_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Canvas); |
| }; |
| |
| Library(Delegate* delegate, const StringPiece& tmp_dir, |
| Timer* timer, MessageHandler* handler) |
| : spriter::ImageLibraryInterface(delegate), timer_(timer), |
| handler_(handler) { |
| tmp_dir.CopyToString(&tmp_dir_); |
| } |
| |
| ~Library() { |
| STLDeleteValues(&fake_fs_); |
| } |
| |
| // Read an image from disk. Return NULL (after calling delegate method) on |
| // error. Caller owns the returned pointer, which must not outlive this |
| // library. |
| virtual SpriterImage* ReadFromFile(const FilePath& path) { |
| net_instaweb::Image* image = fake_fs_[path]; |
| if (image == NULL) { |
| return NULL; |
| } |
| return new SpriterImage(image, this); |
| } |
| |
| virtual Canvas* CreateCanvas(int width, int height) { |
| return new Canvas(width, height, this, tmp_dir_, timer_, handler_); |
| } |
| |
| // Does not take ownership of the resource. Returns true if the image could |
| // be detected as a valid format, in which case we'll keep our own pointer to |
| // the image backed by the resource, meaning that resource must not be |
| // destroyed before the next call to Clear(). |
| bool Register(Resource* resource, MessageHandler* handler) { |
| net_instaweb::Image* prev_image = fake_fs_[resource->url()]; |
| if (prev_image != NULL) { |
| // Already registered |
| return true; |
| } |
| |
| net_instaweb::Image::CompressionOptions* image_options = |
| new net_instaweb::Image::CompressionOptions(); |
| // TODO(satyanarayana): Use appropriate quality param for spriting. |
| image_options->jpeg_quality = |
| RewriteOptions::kDefaultImageJpegRecompressQuality; |
| // TODO(nikhilmadan): Use appropriate progressive setting for spriting. |
| image_options->progressive_jpeg = false; |
| |
| scoped_ptr<net_instaweb::Image> image( |
| NewImage(resource->ExtractUncompressedContents(), resource->url(), |
| tmp_dir_, image_options, timer_, handler_)); |
| |
| // We only handle PNGs and GIFs (which are converted to PNGs) for now. |
| net_instaweb::ImageType image_type = image->image_type(); |
| if ((image_type != net_instaweb::IMAGE_PNG) && |
| (image_type != net_instaweb::IMAGE_GIF)) { |
| handler->Message(kInfo, "Cannot sprite: not PNG or GIF, %s", |
| resource->url().c_str()); |
| return false; |
| } |
| RegisterImage(resource->url(), image.release()); |
| return true; |
| } |
| |
| void Clear() { |
| STLDeleteValues(&fake_fs_); |
| fake_fs_.clear(); |
| } |
| |
| private: |
| typedef std::map<const GoogleString, net_instaweb::Image*> ImageMap; |
| void RegisterImage(const StringPiece& key, net_instaweb::Image* image) { |
| std::pair<ImageMap::iterator, bool> result( |
| fake_fs_.insert(std::make_pair(key.as_string(), image))); |
| if (!result.second) { |
| // Already existed. |
| ImageMap::iterator iter = result.first; |
| if (iter->second != image) { |
| delete iter->second; |
| iter->second = image; |
| } |
| } |
| } |
| |
| // The spriter expects a filesystem interface for accessing images, but we |
| // don't want to hit the disk excessively. We keep here an in-memory map from |
| // a "pathname" to its Image (which contains both the encoded input and the |
| // decoded raster) for quick access. Owns the Image objects. |
| ImageMap fake_fs_; |
| GoogleString tmp_dir_; |
| Timer* timer_; |
| MessageHandler* handler_; |
| }; |
| |
| } // namespace spriter_binding |
| |
| using spriter_binding::Library; |
| using spriter_binding::SpriteFuture; |
| |
| // The Combiner does all the work of spriting. Each combiner takes a set of |
| // images and produces a single sprite as a combination. |
| class ImageCombineFilter::Combiner : public ResourceCombiner { |
| public: |
| Combiner(ImageCombineFilter* filter, Library* library) |
| // TODO(jmaessen): The addition of 1 below avoids the leading "."; |
| // make this convention consistent and fix all code. |
| : ResourceCombiner(filter->driver(), kContentTypePng.file_extension() + 1, |
| filter), |
| library_(library) { } |
| |
| virtual ~Combiner() { |
| // Note that the superclass's dtor will not call our overridden Clear. |
| // Fortunately there's no harm in calling Clear() several times. |
| Clear(); |
| } |
| |
| virtual bool WriteCombination( |
| const ResourceVector& combine_resources, |
| const OutputResourcePtr& combination, |
| MessageHandler* handler) { |
| spriter::ImageSpriter spriter(library_); |
| |
| spriter::SpriterInput input; |
| input.set_id(0); |
| spriter::SpriteOptions* options = input.mutable_options(); |
| options->set_output_base_path(""); |
| options->set_output_image_path("sprite"); |
| options->set_placement_method(spriter::VERTICAL_STRIP); |
| |
| for (int i = 0, n = combine_resources.size(); i < n; ++i) { |
| const ResourcePtr& resource = combine_resources[i]; |
| input.add_input_image_set()->set_path(resource->url()); |
| } |
| |
| scoped_ptr<spriter::SpriterResult> result(spriter.Sprite(input)); |
| if (result.get() == NULL) { |
| handler->Error(UrlSafeId().c_str(), 0, |
| "Could not sprite."); |
| return false; |
| } |
| scoped_ptr<Library::SpriterImage> result_image( |
| library_->ReadFromFile(result->output_image_path())); |
| if (result_image.get() == NULL) { |
| handler->Error(UrlSafeId().c_str(), 0, |
| "Could not read sprited image."); |
| return false; |
| } |
| |
| combination->EnsureCachedResultCreated()->mutable_spriter_result()-> |
| CopyFrom(*result); |
| if (!rewrite_driver_->Write(combine_resources, |
| result_image->image()->Contents(), |
| &kContentTypePng, |
| StringPiece(), // no charset on images. |
| combination.get())) { |
| handler->Error(UrlSafeId().c_str(), 0, |
| "Could not write sprited resource."); |
| return false; |
| } |
| return true; |
| } |
| |
| OutputResourcePtr MakeOutput() { |
| return Combine(rewrite_driver_->message_handler()); |
| } |
| |
| virtual void Clear() { |
| ResourceCombiner::Clear(); |
| added_urls_.clear(); |
| } |
| |
| bool Write(const ResourceVector& in, const OutputResourcePtr& out) { |
| return WriteCombination(in, out, rewrite_driver_->message_handler()); |
| } |
| |
| private: |
| virtual const ContentType* CombinationContentType() { |
| return &kContentTypePng; |
| } |
| |
| StringSet added_urls_; |
| Library* library_; |
| }; |
| |
| // Special resource slot that has a future_ pointer. |
| class SpriteFutureSlot : public CssResourceSlot { |
| public: |
| SpriteFutureSlot(const ResourcePtr& resource, |
| const GoogleUrl& base_url, const RewriteOptions* options, |
| Css::Values* values, size_t value_index, |
| SpriteFuture* future) |
| : CssResourceSlot(resource, base_url, options, values, value_index), |
| future_(future), |
| may_sprite_(false) { |
| } |
| |
| SpriteFuture* future() { return future_.get(); } |
| |
| virtual void Render() { |
| // If we couldn't sprite this slot, try to apply other filters. |
| if (!may_sprite_) { |
| CssResourceSlot::Render(); |
| } |
| } |
| |
| void set_may_sprite(bool x) { may_sprite_ = x; } |
| |
| bool may_sprite() const { return may_sprite_; } |
| |
| protected: |
| REFCOUNT_FRIEND_DECLARATION(SpriteFutureSlot); |
| virtual ~SpriteFutureSlot() { |
| } |
| |
| private: |
| scoped_ptr<SpriteFuture> future_; |
| bool may_sprite_; |
| DISALLOW_COPY_AND_ASSIGN(SpriteFutureSlot); |
| }; |
| |
| typedef RefCountedPtr<SpriteFutureSlot> SpriteFutureSlotPtr; |
| |
| class ImageCombineFilter::Context : public RewriteContext { |
| public: |
| Context(ImageCombineFilter* filter, RewriteContext* parent, |
| const GoogleUrl& css_url, const StringPiece& css_text) |
| : RewriteContext(NULL, parent, NULL), |
| library_(NULL, |
| filter->driver()->server_context()->filename_prefix(), |
| filter->driver()->timer(), |
| filter->driver()->message_handler()), |
| filter_(filter) { |
| css_base_url_.Reset(css_url); |
| MD5Hasher hasher; |
| key_suffix_ = StrCat("css-key=", hasher.Hash(css_text), |
| "_", hasher.Hash(css_url.AllExceptLeaf())); |
| } |
| |
| Context(RewriteDriver* driver, ImageCombineFilter* filter) |
| : RewriteContext(driver, NULL, NULL), |
| library_(NULL, |
| filter->driver()->server_context()->filename_prefix(), |
| filter->driver()->timer(), |
| filter->driver()->message_handler()), |
| filter_(filter) { |
| } |
| |
| virtual ~Context() {} |
| |
| // This cache key will no longer match the partition key generated for |
| // fetches in RewriteContext. This may or may not cause cache misses |
| // when we could have had hits. It should not cause any functional |
| // errors. |
| // We hash the usual cache key, which is a list of the urls in the |
| // combination, in order to keep it short so it doesn't run up against |
| // filename length limits on apache. |
| // TODO(nforman): Figure out a way to test cache keys in general. |
| virtual GoogleString CacheKeySuffix() const { |
| return key_suffix_; |
| } |
| |
| bool AddFuture(CssResourceSlotPtr slot) { |
| SpriteFutureSlot* future_slot = static_cast<SpriteFutureSlot*>(slot.get()); |
| StringPiece url(future_slot->future()->old_url()); |
| AddSlot(ResourceSlotPtr(slot)); |
| return true; |
| } |
| |
| virtual const UrlSegmentEncoder* encoder() const { |
| return filter_->encoder(); |
| } |
| virtual const char* id() const { return filter_->id(); } |
| virtual OutputResourceKind kind() const { return kRewrittenResource; } |
| |
| void Reset() { |
| library_.Clear(); |
| } |
| |
| protected: |
| // Write the combination out. |
| virtual void Rewrite(int partition_index, CachedResult* partition, |
| const OutputResourcePtr& output) { |
| RewriteResult result = kRewriteOk; |
| if (!output->IsWritten()) { |
| // Note that this method expects to do something for only the fetch path, |
| // when only one partition should be in use --- in the rewrite path |
| // we should have already written everything out in Partition(). |
| DCHECK_EQ(0, partition_index); |
| ImageCombineFilter::Combiner combiner(filter_, &library_); |
| |
| ResourceVector resources; |
| bool ok = true; |
| for (int i = 0, n = num_slots(); (i < n) && ok; ++i) { |
| ResourcePtr resource(slot(i)->resource()); |
| resources.push_back(resource); |
| RegisterResource(resource.get()); |
| ok = EnsureLoaded(resource->url()); |
| } |
| if (!ok || !combiner.Write(resources, output)) { |
| result = kRewriteFailed; |
| } |
| } |
| RewriteDone(result, partition_index); |
| } |
| |
| // Finalize the declarations for the sprited slots. |
| // TODO(nforman): be smarter about when to sprite and when not. |
| // e.g. if it turns out all the divs are too big to use the sprite |
| // except for one, don't use it. |
| virtual void Render() { |
| for (int p = 0, np = num_output_partitions(); p < np; ++p) { |
| CachedResult* partition = output_partition(p); |
| int num_inputs = partition->input_size(); |
| if (num_inputs > 1) { |
| if (!partition->has_spriter_result()) { |
| // TODO(nforman): some error handling here. |
| LOG(DFATAL) << "spriting failed during Render"; |
| break; |
| } |
| const spriter::SpriterResult& spriter_result = |
| partition->spriter_result(); |
| RectMap url_to_clip_rect; |
| // Now gather up the positions for each of the original urls. |
| for (int i = spriter_result.image_position_size() - 1; i >= 0; i--) { |
| const spriter::ImagePosition& image_position = |
| spriter_result.image_position(i); |
| // Where the spriter expects file paths, we are using urls. |
| url_to_clip_rect[image_position.path()] = &image_position.clip_rect(); |
| } |
| |
| StringSet replaced_urls; // for stats purposes |
| for (int i = 0; i < num_inputs; ++i) { |
| int slot_index = partition->input(i).index(); |
| SpriteFutureSlot* sprite_slot = |
| static_cast<SpriteFutureSlot*>(slot(slot_index).get()); |
| SpriteFuture* future = sprite_slot->future(); |
| const spriter::Rect* clip_rect = url_to_clip_rect[future->old_url()]; |
| // Check against original image dimensions. |
| // If these are smaller than the div we're putting the image |
| // into then we can't sprite this declaraion |
| if (clip_rect->width() < future->width() || |
| clip_rect->height() < future->height()) { |
| continue; |
| } |
| if (clip_rect != NULL) { |
| DCHECK(css_base_url_.IsAnyValid()); |
| GoogleString new_url; |
| if (css_base_url_.IsAnyValid()) { |
| new_url = ResourceSlot::RelativizeOrPassthrough( |
| filter_->driver()->options(), partition->url(), |
| sprite_slot->url_relativity(), css_base_url_); |
| } else { |
| new_url = partition->url(); |
| } |
| |
| future->Realize(new_url.c_str(), clip_rect->x_pos(), |
| clip_rect->y_pos()); |
| MessageHandler* handler = filter_->driver()->message_handler(); |
| handler->Message(kInfo, "Inserted sprite, url: %s\n", |
| new_url.c_str()); |
| replaced_urls.insert(future->old_url()); |
| sprite_slot->set_may_sprite(true); |
| } |
| } |
| int sprited = replaced_urls.size(); |
| filter_->AddFilesReducedStat(sprited - 1); |
| } |
| } |
| Reset(); |
| } |
| |
| // Partition the slots by what can get sprited and what can't. |
| // Currently, we greedily combine everything that can be combined |
| // in as few partitions as possible. We skip over slots that point to images |
| // that are too small for the context they're in. |
| // TODO(nforman): Consider separating by color map to group things smarter. |
| virtual void PartitionAsync(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| // Partitioning here requires image decompression, so we want to |
| // move it to a different thread. |
| Driver()->AddLowPriorityRewriteTask(MakeFunction( |
| this, &Context::PartitionImpl, &Context::PartitionCancel, |
| partitions, outputs)); |
| } |
| |
| void PartitionImpl(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| StringSet no_sprite; |
| FindUnspritable(&no_sprite); |
| CollectSlots(partitions, outputs, &no_sprite); |
| CrossThreadPartitionDone(partitions->partition_size() != 0 ? |
| kRewriteOk : kRewriteFailed); |
| } |
| |
| void PartitionCancel(OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| CrossThreadPartitionDone(kTooBusy); |
| } |
| |
| private: |
| // Class that associates a list of urls and a partition with a combiner. |
| class ImageCombination : public ImageCombineFilter::Combiner { |
| public: |
| ImageCombination(ImageCombineFilter* filter, Library* library) |
| : ImageCombineFilter::Combiner(filter, library), |
| partition_(NULL) { } |
| |
| virtual ~ImageCombination() { } |
| |
| void AddResourceToPartition(Resource* resource, int index) { |
| resource->AddInputInfoToPartition( |
| Resource::kIncludeInputHash, index, partition_); |
| } |
| |
| void set_partition(CachedResult* partition) { partition_ = partition; } |
| |
| CachedResult* partition() { return partition_; } |
| |
| private: |
| CachedResult* partition_; // Does not own memory. |
| DISALLOW_COPY_AND_ASSIGN(ImageCombination); |
| }; |
| |
| typedef std::vector<ImageCombination*> ImageCombinationVector; |
| |
| // Put this resource in the library. |
| bool RegisterResource(Resource* resource) { |
| return library_.Register(resource, filter_->driver()->message_handler()); |
| } |
| |
| bool EnsureLoaded(const GoogleString& url) { |
| scoped_ptr<Library::SpriterImage> spriter_image(library_.ReadFromFile(url)); |
| if (spriter_image.get() == NULL) { |
| return false; |
| } |
| |
| return spriter_image->image()->EnsureLoaded(false); |
| } |
| |
| // Returns true if the image at url has already been added to the collection |
| // and is at least as large as the given dimensions. |
| bool GetImageDimensions(const GoogleString& url, int* width, int* height) { |
| scoped_ptr<Library::SpriterImage> image(library_.ReadFromFile(url)); |
| if (image.get() == NULL) { |
| return false; |
| } |
| return image->GetDimensions(width, height); |
| } |
| |
| // Returns true iff declarations were setup properly, and the image |
| // are smaller than the specified div dimensions. |
| bool SetupSpriteDimensions(SpriteFuture* future) { |
| int image_width, image_height; |
| if (!GetImageDimensions(future->old_url(), &image_width, &image_height)) { |
| return false; |
| } |
| if (image_width < future->width() || image_height < future->height()) { |
| return false; |
| } |
| return future->FindBackgroundPositionValues(image_width, image_height); |
| } |
| |
| // Walk through and find any resources that won't be able to be |
| // sprited. If we can't sprite them, add the url to the no-sprite |
| // set. |
| // |
| // TODO(abliss) We exhibit zero intelligence about which image files to |
| // combine; we combine whatever is possible. This can reduce cache |
| // effectiveness by combining highly cacheable shared resources with |
| // transient ones. |
| void FindUnspritable(StringSet* no_sprite) { |
| StringSet seen_urls; |
| for (int i = 0, n = num_slots(); i < n; ++i) { |
| ResourcePtr resource(slot(i)->resource()); |
| SpriteFutureSlot* sprite_slot = |
| static_cast<SpriteFutureSlot*>(slot(i).get()); |
| SpriteFuture* future = sprite_slot->future(); |
| GoogleString resource_url = resource->url(); |
| if (no_sprite->find(resource_url) == no_sprite->end()) { |
| if (!resource->IsSafeToRewrite(rewrite_uncacheable())) { |
| no_sprite->insert(resource_url); |
| } else { |
| // Register the resource with the library and then check |
| // its dimensions against those of the declaration to make |
| // sure we can sprite here. |
| // TODO(nforman): cheaper image dimension checking. |
| if (seen_urls.find(resource_url) == seen_urls.end()) { |
| RegisterResource(resource.get()); |
| seen_urls.insert(resource_url); |
| } |
| if (!SetupSpriteDimensions(future) || !EnsureLoaded(resource_url)) { |
| no_sprite->insert(resource_url); |
| } |
| } |
| } |
| } |
| } |
| |
| // For each slot, try to add its resource to the current partition. |
| // If we can't, then finalize the last combination, and then |
| // move on to the next slot. |
| void CollectSlots(OutputPartitions* partitions, |
| OutputResourceVector* outputs, |
| StringSet* no_sprite) { |
| ImageCombinationVector combinations; |
| MessageHandler* handler = filter_->driver()->message_handler(); |
| std::map<GoogleString, ImageCombination*> urls_to_combos; |
| |
| for (int i = 0, n = num_slots(); i < n; ++i) { |
| ResourcePtr resource(slot(i)->resource()); |
| SpriteFutureSlot* sprite_slot = |
| static_cast<SpriteFutureSlot*>(slot(i).get()); |
| SpriteFuture* future = sprite_slot->future(); |
| GoogleString resource_url = future->old_url(); |
| if (no_sprite->find(resource_url) != no_sprite->end()) { |
| continue; |
| } |
| bool added = false; |
| // Don't add the same url to a combination twice. |
| |
| std::map<GoogleString, ImageCombination*>::iterator it; |
| it = urls_to_combos.find(resource_url); |
| if (it != urls_to_combos.end()) { |
| ImageCombination* combo = it->second; |
| DCHECK(combo != NULL) << "Combination points to NULL partition."; |
| combo->AddResourceToPartition(resource.get(), i); |
| added = true; |
| } |
| // If it wasn't already in the combination, see if we can add it. |
| // Even if the image is spritable, it may not be able to be sprited |
| // in a particular combination due to domain lawyer restrictions, |
| // or (perhaps in the future) image type. |
| if (!added) { |
| for (int j = 0, m = combinations.size(); j < m; ++j) { |
| ImageCombination* combo = combinations[j]; |
| if (combo->AddResourceNoFetch(resource, handler).value) { |
| combo->AddResourceToPartition(resource.get(), i); |
| urls_to_combos[resource_url] = combo; |
| added = true; |
| break; |
| } |
| } |
| // If we couldn't add this resource to any of the existing partitions, |
| // try making a new one and adding it there. We may still not be able |
| // to do that if the resource isn't spritable at all. |
| if (!added) { |
| scoped_ptr<ImageCombination> combo(new ImageCombination( |
| filter_, &library_)); |
| if (combo->AddResourceNoFetch(resource, handler).value) { |
| combo->set_partition(partitions->add_partition()); |
| combo->AddResourceToPartition(resource.get(), i); |
| urls_to_combos[resource_url] = combo.get(); |
| combinations.push_back(combo.release()); |
| } else { |
| no_sprite->insert(resource_url); |
| } |
| } |
| } |
| } |
| FinalizePartitions(combinations, partitions, outputs); |
| STLDeleteElements(&combinations); |
| Reset(); |
| } |
| |
| // Write the output for the combinations. If a combination can not be |
| // written (e.g. it has only one element), then remove its partition. |
| void FinalizePartitions(const ImageCombinationVector& combinations, |
| OutputPartitions* partitions, |
| OutputResourceVector* outputs) { |
| std::set<int> remove_indices; |
| for (int i = 0, n = combinations.size(); i < n; ++i) { |
| ImageCombination* combination = combinations[i]; |
| CachedResult* partition = combination->partition(); |
| if (partition != NULL) { |
| OutputResourcePtr combination_output(combination->MakeOutput()); |
| if (combination_output.get() == NULL) { |
| remove_indices.insert(i); |
| } else { |
| combination_output->UpdateCachedResultPreservingInputInfo(partition); |
| outputs->push_back(combination_output); |
| } |
| } |
| } |
| // We can re-arrange the partitions at this point only because we are |
| // about to delete the ImageCombinations (and with them, their pointers |
| // to those partitions). |
| std::set<int>::reverse_iterator rit; |
| for (rit = remove_indices.rbegin(); rit != remove_indices.rend(); rit++) { |
| int last_partition = partitions->partition_size() - 1; |
| if (*rit != last_partition) { |
| partitions->mutable_partition()->SwapElements(*rit, last_partition); |
| } |
| partitions->mutable_partition()->RemoveLast(); |
| } |
| } |
| |
| Library library_; |
| ImageCombineFilter* filter_; |
| GoogleString key_suffix_; |
| // Only stored when actually rewriting CSS. Not for reconstruction requests |
| // for the image .pagespeed. sprite itself. |
| GoogleUrl css_base_url_; |
| }; |
| |
| ImageCombineFilter::ImageCombineFilter(RewriteDriver* driver) |
| : RewriteFilter(driver), |
| context_(NULL) { |
| Statistics* stats = driver->server_context()->statistics(); |
| image_file_count_reduction_ = stats->GetVariable(kImageFileCountReduction); |
| } |
| |
| ImageCombineFilter::~ImageCombineFilter() { |
| } |
| |
| void ImageCombineFilter::InitStats(Statistics* statistics) { |
| statistics->AddVariable(kImageFileCountReduction); |
| } |
| |
| // Get the dimensions of the declaration. This is tricky. |
| // If the element is larger than the image, spriting will not work correctly. |
| // TODO(abliss): support same-sized vertically-repeating backgrounds in a |
| // horizontal sprite, and horizontal ones in a vertical sprite. |
| bool ImageCombineFilter::GetDeclarationDimensions( |
| Css::Declarations* declarations, int* width, int* height) { |
| css_util::DimensionState state = |
| css_util::GetDimensions(declarations, width, height); |
| return (state == css_util::kHasBothDimensions); |
| } |
| |
| // Must initialize context_ with appropriate parent before hand. |
| // parent passed here because it's private. |
| bool ImageCombineFilter::AddCssBackgroundContext( |
| const GoogleUrl& original_url, const GoogleUrl& base_url, |
| Css::Values* values, int value_index, |
| CssFilter::Context* parent, Css::Declarations* decls, |
| bool* is_authorized, MessageHandler* handler) { |
| CHECK(context_ != NULL); |
| *is_authorized = true; // Only false if original_url isn't authorized. |
| int width, height; |
| if (!GetDeclarationDimensions(decls, &width, &height)) { |
| return false; |
| } |
| StringPiece url_piece(original_url.Spec()); |
| scoped_ptr<SpriteFuture> future(new SpriteFuture(url_piece, width, height, |
| decls)); |
| future->Initialize(values->at(value_index)); |
| |
| ResourcePtr resource = CreateInputResource(url_piece, is_authorized); |
| if (resource.get() == NULL) { |
| return false; |
| } |
| |
| // transfers ownership of future to slot_obj |
| SpriteFutureSlot* slot_obj = new SpriteFutureSlot( |
| resource, base_url, driver()->options(), values, value_index, |
| future.release()); |
| CssResourceSlotPtr slot(slot_obj); |
| parent->slot_factory()->UniquifySlot(slot); |
| // Spriting must run before all other filters so that the slot for the |
| // resource a SpriteFutureSlot |
| if (slot.get() != slot_obj) { |
| return false; |
| } |
| context_->AddFuture(slot); |
| return true; |
| } |
| |
| void ImageCombineFilter::Reset(RewriteContext* parent, |
| const GoogleUrl& css_url, |
| const StringPiece& css_text) { |
| context_ = MakeNestedContext(parent, css_url, css_text); |
| } |
| |
| void ImageCombineFilter::RegisterOrReleaseContext() { |
| if ((context_ != NULL) && (context_->num_slots() != 0)) { |
| context_->parent()->AddNestedContext(context_); |
| } else { |
| delete context_; |
| context_ = NULL; |
| } |
| } |
| |
| // Make a new context that is nested under parent. |
| ImageCombineFilter::Context* ImageCombineFilter::MakeNestedContext( |
| RewriteContext* parent, const GoogleUrl& css_url, |
| const StringPiece& css_text) { |
| Context* context = new Context(this, parent, css_url, css_text); |
| return context; |
| } |
| |
| RewriteContext* ImageCombineFilter::MakeRewriteContext() { |
| return new Context(driver(), this); |
| } |
| |
| void ImageCombineFilter::AddFilesReducedStat(int reduced) { |
| image_file_count_reduction_->Add(reduced); |
| } |
| |
| } // namespace net_instaweb |