public void ReplaceCssImagesWithSprite(CombinatorResource resource) { Func <RuleSet, Term, bool> noSprite = (ruleSet, url) => { if (url.Value.Contains("no-sprite") || ruleSet.Selectors.Any(selector => selector.SimpleSelectors.Any(simpleSelector => simpleSelector.Class == "no-sprite"))) { return(true); } // Images with a background position are not suitable for sprite generation if they aren't top-left if (ruleSet.Declarations.Any(declaration => declaration.Name == "background-position" && (declaration.Expression.Terms.Count() != 2 || !declaration.Expression.Terms.Any(term => term.Value == "top") || !declaration.Expression.Terms.Any(term => term.Value == "left")))) { return(true); } // Images with a background size are not suitable for sprite generation if (ruleSet.Declarations.Any(declaration => declaration.Name == "background-size")) { return(true); } // Images with a background repeat are not suitable for sprite generation if (ruleSet.Declarations.Any(declaration => declaration.Name == "background-repeat" && (declaration.Expression.Terms.Count() != 1 || declaration.Expression.Terms.First().Value != "no-repeat"))) { return(true); } var backgroundTerms = ruleSet.Declarations .Where(declaration => declaration.Name == "background") .SelectMany(declaration => declaration.Expression.Terms); // Sized backgrounds are not suitable either // LineHeightTerm is filled with a value when if in the shorthand background declaration there's the background size specified, e.g.: // background: url(url-to-img) 300px 400px / 500px 600px no-repeat; // Now there will be a term for 400px, having LineHeightTerm specified for 500px. if (backgroundTerms.Any(term => term.LineHeightTerm != null)) { return(true); } var backgroundTermValues = backgroundTerms.Select(term => term.Value); // Positioned backgrounds are not suitable either, except top-left ones if (backgroundTerms .Any(term => term.Value == "center" || term.Value == "top" || term.Value == "right" || term.Value == "bottom" || term.Unit != null) && !(backgroundTermValues.Contains("top") && backgroundTermValues.Contains("left"))) { return(true); } if (backgroundTermValues.Any(value => value == "repeat-x" || value == "repeat-y" || value == "repeat")) { return(true); } return(false); }; var images = new Dictionary <string, CssImage>(); var stylesheet = new StylesheetParser().Parse(resource.Content); ProcessImageUrls( resource, stylesheet, (ruleSet, urlTerm) => { var url = urlTerm.Value; if (noSprite(ruleSet, urlTerm)) { return; } var imageContent = _resourceFileService.GetImageContent(InlineUriFactory(resource, url)); if (imageContent.Length / 1024 <= 5000) { images[url] = new CssImage { Content = imageContent }; } }); if (images.Count == 0) { return; } _cacheFileService.WriteSpriteStream( resource.Content.GetHashCode() + ".jpg", (stream, publicUrl) => { using (var sprite = new Sprite(images.Values.Select(image => image.Content))) { var imageEnumerator = images.Values.GetEnumerator(); foreach (var backgroundImage in sprite.Generate(stream, ImageFormat.Jpeg)) { imageEnumerator.MoveNext(); imageEnumerator.Current.BackgroundImage = backgroundImage; imageEnumerator.Current.BackgroundImage.Url = InlineUriFactory(resource, publicUrl); } } }); ProcessImageUrls( resource, stylesheet, (ruleSet, urlTerm) => { var url = urlTerm.Value; if (images.ContainsKey(url)) { var backgroundImage = images[url].BackgroundImage; var imageDeclaration = new Declaration { Name = "background-image", Expression = new Expression { Terms = new List <Term> { new Term { Type = TermType.Url, Value = backgroundImage.Url.ToStringWithoutScheme() } } } }; ruleSet.Declarations.Add(imageDeclaration); var bgPosition = backgroundImage.Position; var positionDeclaration = new Declaration { Name = "background-position", Expression = new Expression { Terms = new List <Term> { new Term { Type = TermType.Number, Value = bgPosition.X.ToString(), Unit = Unit.Px }, new Term { Type = TermType.Number, Value = bgPosition.Y.ToString(), Unit = Unit.Px } } } }; ruleSet.Declarations.Add(positionDeclaration); } }); resource.Content = stylesheet.ToString(); }