Click here to Skip to main content
15,881,852 members
Articles / Web Development / HTML

Generate CSS sprites and thumbnail images on the fly in ASP.NET sites

Rate me:
Please Sign up or sign in to vote.
4.82/5 (40 votes)
9 Jun 2012CPOL64 min read 116.9K   2.8K   85  
Reduces page load times of ASP.NET web sites by combining page images and CSS background images into CSS sprites. Compresses and physically resizes images to make thumbnails. Caters for repeating background images.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Text.RegularExpressions;

namespace CssSpriteGenerator
{
    public class HtmlUtils
    {

        /// <summary>
        /// Analyses a string with html, finds all image tags, and returns
        /// a collection of ImageTags, one for each image in the html.
        /// </summary>
        /// <param name="html"></param>
        /// <returns></returns>
        public static IList<ImageTag> ImagesInHtml(string html)
        {
            List<ImageTag> result = new List<ImageTag>();

            string regexpImgGroup =
                RegexTagWithAttributes("img"); // just an img tag

            Regex r = new Regex(regexpImgGroup, RegexOptions.IgnoreCase);
            Match m = r.Match(html);

            while (m.Success)
            {
                string tagText = m.Value;

                ImageAttributeDictionary imgAttributes = TagAttributes("img", m);

                result.Add(new ImageTag { ImgAttributes = imgAttributes, TagText = tagText });

                m = m.NextMatch();
            }

            return result;
        }

        /// <summary>
        /// Generates the regular expression that matches an html tag and its attributes.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        private static string RegexTagWithAttributes(string tag)
        {
            const string regexpTagGroup =
                @"<{0}" +
                @"(?:" + // start attributes definition
                @"\s*" + // starts with zero more more white spaces
                @"(?<{0}attrname>\w*)" + // attribute name, must consist of letter, digits or underscore
                @"\s*=\s*" + // followed by zero or more space, then an equals sign, then zero or more spaces
                @"(?<{0}quote>""|')" + // followed by quote (" or ')
                @"(?<{0}attrvalue>.*?)" + // followed by value. Using a non greedy match on zero or more characters .*?
                @"\k<{0}quote>" + // followed by the same quote as we saw before. The \k<{0}quote> refers to the first quote.
                @")*" + // zero or more attributes of the form attr="value"
                @"(?:[^>]*)" + // followed by zero or more non-> characters
                @">"; // followed by closing >

            string result = string.Format(regexpTagGroup, tag);
            return result;
        }

        /// <summary>
        /// Generates the regular expression that matches an html end tag and its attributes.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        private static string RegexEndTag(string tag)
        {
            const string regexpEndTagGroup =
                @"</{0}>";

            string result = string.Format(regexpEndTagGroup, tag);
            return result;
        }

        /// <summary>
        /// After the regex generated by RegexTagWithAttributes has been matched,
        /// use this method to retrieve the attributes of the tag and return them in a
        /// ImageAttributeDictionary.
        /// </summary>
        /// <param name="tag">
        /// Tag that was passed to RegexTagWithAttributes to generate the regular expression
        /// that has now been matched
        /// </param>
        /// <param name="m">
        /// Match of the regular expression.
        /// </param>
        /// <returns></returns>
        private static ImageAttributeDictionary TagAttributes(string tag, Match m)
        {
            CaptureCollection attrNames = m.Groups[tag + "attrname"].Captures;
            CaptureCollection attrValues = m.Groups[tag + "attrvalue"].Captures;

            int nbrNames = attrNames.Count;
            int nbrValues = attrValues.Count;

            if (nbrNames != nbrValues)
            {
                throw new Exception(
                    string.Format("Image tag {0} in {1} has {2} attribute names, but {3} attribute values", tag, m.Value, nbrNames, nbrValues));
            }

            // All attributes will be stored in this dictionary (attribute name = key, attribute value = value)
            ImageAttributeDictionary attributes = new ImageAttributeDictionary();

            // If an image tag has the same attribute multiple times, the browser uses the first occurrance (tested on IE, Firefox, Chrome).
            // So, go backwards through the list of attributes. That way, if there are duplicates, the first one will
            // remain in the dictionary.
            for (int i = nbrNames - 1; i >= 0; i--)
            {
                attributes[attrNames[i].Value.ToLower()] = attrValues[i].Value;
            }

            return attributes;
        }

        
        /// <summary>
        /// Produces the CSS to be used with a div tag, in order to make that div tag show a sprite.
        /// </summary>
        /// <param name="spriteUrl">
        /// Url of the sprite image.
        /// </param>
        /// <param name="xOffset">
        /// X offset within the sprite where the original image starts.
        /// </param>
        /// <param name="yOffset">
        /// Y offset within the sprite where the original image starts.
        /// </param>
        /// <param name="imageWidth">
        /// Width in px of the original image.
        /// </param>
        /// <param name="imageHeight">
        /// Height in px of the original image.
        /// </param>
        /// <returns></returns>
        public static string PageSpriteCss(
            string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight)
        {
            string css =
                string.Format(
                    "width: {0}px; height: {1}px; background: url({2}) -{3}px -{4}px;",
                    imageWidth, imageHeight, UrlUtils.EscapedUrl(spriteUrl), xOffset, yOffset);

            return css;
        }

        /// <summary>
        /// Generates the html for a sprite that lives on the page (rather than a sprite used with the css).
        /// Essentially, this will be a div tag that is styled via a class or via inline style.
        /// However, if the original img was enclosed in an anchor, you get a span enclosed in an anchor.
        /// 
        /// Example for simple img translation:
        /// 
        /// <div style="width: 50px; height: 50px; background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px; display:inline-block;
        ///             text-indent:-9999px;">the alt text</div>
        ///
        /// Example for img enclosed with anchor:
        /// 
        /// <a href="http://google.com" target="_blank"
        ///    style="text-decoration: none;
        ///           width: 50px; height: 50px;background: url(___spritegen/2-0-0-B3-45-8A-FF-3E-C3-78-19-4B-89-7E-E0-91-04-8B-70.png) -50px -0px;display:inline-block;
        ///           >
        ///     <span style="display:inline-block;text-indent:-9999px; ">the alt text</span>
        /// </a>
        /// 
        /// </summary>
        /// <param name="pageSpriteCss">
        /// CSS to be used with the div tag.
        /// </param>
        /// <param name="imageAttributes">
        /// Attributes of the original image.
        /// </param>
        /// <param name="inlineSpriteStyles">
        /// True: add the css to the div tag as inline style.
        /// False: add the css via a css class.
        /// </param>
        /// <param name="altCopyOptions">
        /// Determines how the alt attribute of the img is treated. See description of altCopyOptions in ConfigSection.cs.
        /// </param>
        /// <param name="classPostfix">
        /// If you need to create a new css class, give its name this postfix.
        /// </param>
        /// <param name="additionalCss">
        /// If you need to add CSS to a stylesheet, return it here.
        /// The caller is reponsible for creating a stylesheet that contains this CSS.
        /// </param>
        /// <returns>
        /// Complete html representing the sprite.
        /// </returns>
        public static string PageSpriteHtml(
            string spriteUrl, int xOffset, int yOffset, int imageWidth, int imageHeight, 
            ImageAttributeDictionary imageAttributes, 
            bool inlineSpriteStyles, 
            Stylesheet additionalCss)
        {
            string spriteCss = PageSpriteCss(spriteUrl, xOffset, yOffset, imageWidth, imageHeight);

            // -----------------
            // create opening tag

            List<string> imgExcludes = new List<string>
                                           {
                                                "src",
                                                "width",
                                                "height",
                                                "class",
                                                "style"
                                           };

            // -----------------
            // copy over the attributes from the original img tag

            string tagAttributesString = imageAttributes.ToString(imgExcludes, null);

            // ----------------

            string styleAttributeValue = imageAttributes.AttributeValue("style");
            string classAttributeValue = imageAttributes.AttributeValue("class");

            if (inlineSpriteStyles)
            {
                styleAttributeValue = CombinedCss(styleAttributeValue, spriteCss);
            }
            else
            {
                string additionalClass = additionalCss.AddDeclaration(spriteCss);
                classAttributeValue = CombinedClasses(classAttributeValue, additionalClass);
            }

            // ---------------
            // assemble final html

            string html =
                "<img src=\"" + UrlUtils.UrlTransparent1x1Png() + "\"" +
                tagAttributesString +
                NonEmptyHtmlAttribute("style", styleAttributeValue) +
                NonEmptyHtmlAttribute("class", classAttributeValue) +
                @" />";

            return html;
        }

        private static string NonEmptyHtmlAttribute(string attributeName, string attributeValue)
        {
            string htmlAttribute = "";
            if (!string.IsNullOrWhiteSpace(attributeValue))
            {
                htmlAttribute = string.Format(@" {0}=""{1}""", attributeName, attributeValue);
            }

            return htmlAttribute;
        }

        /// <summary>
        /// Creates an image tag out of the passed in imageAttributes.
        /// If anchorAttributes contains attributes, than an anchor tag is created around the img.
        /// </summary>
        /// <param name="imageUrl">
        /// Overrides the src attribute
        /// </param>
        /// <param name="imageAttributes">
        /// </param>
        /// <param name="imageWidth">
        /// Overrides the width attribute
        /// </param>
        /// <param name="imageHeight">
        /// Overrides the height attribute
        /// </param>
        /// <param name="overrideImgTagDimensionProperties">
        /// If this is true, than any width and height properties of the img tag will be
        /// removed, and new width and height properties created based on the imageWidth and imageHeight.
        /// </param>
        /// <returns></returns>
        public static string ImgHtml(
            string imageUrl, ImageAttributeDictionary imageAttributes, 
            int imageWidth, int imageHeight, bool overrideImgTagDimensionProperties)
        {
            // ------------
            // Create the html for the image.

            List<string> imgExcludes = new List<string>();
            imgExcludes.Add("src");
            if (overrideImgTagDimensionProperties)
            {
                imgExcludes.Add("width");
                imgExcludes.Add("height");
            }

            string imgAttributesString = imageAttributes.ToString(imgExcludes, null);

            string imgHtml = "<img";
            
            if (overrideImgTagDimensionProperties)
            {
                // Make sure width and height are added
                imgHtml += string.Format(@" width=""{0}""", imageWidth);
                imgHtml += string.Format(@" height=""{0}""", imageHeight);
            }

            imgHtml +=
                string.Format(@" src=""{0}""", UrlUtils.EscapedUrl(imageUrl)) +
                imgAttributesString +
                @" />";

            return imgHtml;
        }

        private static string CombinedClasses(string class1, string class2)
        {
            if (string.IsNullOrWhiteSpace(class1))
            {
                return class2;
            }

            string trimmedClass1 = class1.Trim(new char[] { ' ' });
            return trimmedClass1 + " " + class2;
        }

        private static string CombinedCss(string css1, string css2)
        {
            if (string.IsNullOrWhiteSpace(css1))
            {
                return css2;
            }

            string trimmedCss1 = css1.Trim(new char[] { ' ', ';' });
            return trimmedCss1 + ";" + css2;
        }

        /// <summary>
        /// Produces the CSS to be used with a sprite that will be used for CSS background images.
        /// </summary>
        /// <param name="spriteUrl">
        /// Url of the sprite image.
        /// </param>
        /// <param name="xOffset">
        /// X offset within the sprite where the original image starts.
        /// </param>
        /// <param name="yOffset">
        /// Y offset within the sprite where the original image starts.
        /// </param>
        /// <param name="alignment">
        /// Required alignment of the background image.
        /// </param>
        /// <returns></returns>
        public static string CssSpriteCss(
            string spriteUrl, int xOffset, int yOffset, Alignment alignment)
        {
            string xPos = posString(xOffset, "xOffset", alignment, Alignment.Left, Alignment.Right);
            string yPos = posString(yOffset, "yOffset", alignment, Alignment.Top, Alignment.Bottom);

            string result = 
                string.Format(
                    "background-image: url({0}); background-position: {1} {2};",
                    UrlUtils.EscapedUrl(spriteUrl), xPos, yPos);

            return result;
        }

        private static string posString(int offset, string offsetName, Alignment alignment, Alignment checkAlignment1, Alignment checkAlignment2)
        {
            string result = null;
            if ((alignment == checkAlignment1) || (alignment == checkAlignment2))
            {
                if (offset != 0)
                {
                    throw new Exception(
                        string.Format("Alignment is {0} but {1} is {2} while it should be 0", alignment, offsetName, offset));
                }

                result = alignment.ToString();
            }
            else
            {
                result = (-1 * offset).ToString() + "px";
            }

            return result;
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect
Australia Australia
Twitter: @MattPerdeck
LinkedIn: au.linkedin.com/in/mattperdeck
Current project: JSNLog JavaScript Logging Package

Matt has over 9 years .NET and SQL Server development experience. Before getting into .Net, he worked on a number of systems, ranging from the largest ATM network in The Netherlands to embedded software in advanced Wide Area Networks and the largest ticketing web site in Australia. He has lived and worked in Australia, The Netherlands, Slovakia and Thailand.

He is the author of the book ASP.NET Performance Secrets (www.amazon.com/ASP-NET-Site-Performance-Secrets-Perdeck/dp/1849690685) in which he shows in clear and practical terms how to quickly find the biggest bottlenecks holding back the performance of your web site, and how to then remove those bottlenecks. The book deals with all environments affecting a web site - the web server, the database server and the browser.

Matt currently lives in Sydney, Australia. He recently worked at Readify and the global professional services company PwC. He now works at SP Health, a global provider of weight loss web sites such at CSIRO's TotalWellBeingDiet.com and BiggestLoserClub.com.

Comments and Discussions