Click here to Skip to main content
15,896,201 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 117.8K   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.Drawing;
using System.Drawing.Imaging;
using System.Web;
using System.Drawing.Drawing2D;
using SimplePaletteQuantizer.Quantizers;

namespace CssSpriteGenerator
{
    public class ImageUtils
    {
        private static ImageCodecInfo _jpgCodec = null;
        private static bool? _hasFullTrust = null;

        /// <summary>
        /// Saves a jpg image with the given quality
        /// </summary>
        /// <param name="bitmap">
        /// Bitmap to be saved to disk
        /// </param>
        /// <param name="fileSystemPath">
        /// Location where the image to be saved.
        /// </param>
        /// <param name="jpegQuality">
        /// Quality reduction of the image. 25 means that jpg image will be written with its quality reduced to 25%.
        /// </param>
        public static void SaveJpgImage(Bitmap bitmap, string fileSystemPath, int jpegQuality)
        {
            ImageCodecInfo jpgCodec = GetJpgCodec();

            if ((jpegQuality < 0) || (jpegQuality > 100) || (jpgCodec == null))
            {
                // Something went wrong. If we're in release mode, recover. Otherwise throw an exception.

                if (HttpContext.Current.IsDebuggingEnabled)
                {
                    throw new ArgumentOutOfRangeException(
                        string.Format("SaveJpgImage error - jpgEncoder={0}, jpegQuality={1}", 
                            ((jpgCodec == null) ? "null" : "not null"), jpegQuality));
                }
                else
                {
                    bitmap.Save(fileSystemPath, ImageFormat.Jpeg);
                    return;
                }
            }

            // Encoder parameter for image quality 

            EncoderParameter qualityParam = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, jpegQuality);

            EncoderParameters encoderParams = new EncoderParameters(1);
            encoderParams.Param[0] = qualityParam;

            // Save the image

            bitmap.Save(fileSystemPath, jpgCodec, encoderParams);
        }

        /// <summary> 
        /// Returns the image codec for jpg images
        /// </summary> 
        private static ImageCodecInfo GetJpgCodec()
        {
            if (_jpgCodec != null) { return _jpgCodec; }

            // Get image codecs for all image formats 
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();

            // Find the correct image codec 
            for (int i = 0; i < codecs.Length; i++)
            {
                if (codecs[i].MimeType == "image/jpeg")
                {
                    _jpgCodec = codecs[i];
                    return _jpgCodec;
                }
            }
                
            return null;
        }

        /// <summary>
        /// Resizes a bitmap to a required size.
        /// 
        /// Resizing is an involved subject. See:
        /// 
        ///http://nathanaeljones.com/163/20-image-resizing-pitfalls/
        ///http://msdn.microsoft.com/en-us/library/k0fsyd4e.aspx]
        ///http://glennjones.net/2005/10/high-quality-dynamically-resized-images-with-dot-net/
        ///http://www.peterprovost.org/blog/post/Resize-Image-in-C.aspx
        ///http://www.switchonthecode.com/tutorials/csharp-tutorial-image-editing-saving-cropping-and-resizing
        ///http://stackoverflow.com/questions/30569/resize-transparent-images-using-c
        /// 
        /// </summary>
        /// <param name="bitmap">
        /// Pass the bitmap to be resized here. This parameter will be updated to a bitmap with the new size.
        /// </param>
        /// <param name="newSize"></param>
        public static void ResizeBitmap(ref Bitmap bitmap, Size newSize)
        {
            if (bitmap.Size == newSize) { return; }

            // Note that if you try to create a Graphics object from a bitmap that has an indexed PixelFormat,
            // you get an exception. So, give newBitmap an indexed PixelFormat, unless bitmap has a higher
            // PixelFormat than the default.
            PixelFormat newPixelFormat = PixelFormatUtils.HigherPixelFormat(PixelFormat.Format24bppRgb, bitmap.PixelFormat);

            Bitmap newBitmap = new Bitmap(newSize.Width, newSize.Height, newPixelFormat);
            newBitmap.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);

            Graphics graphics = Graphics.FromImage(newBitmap);

            graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            graphics.SmoothingMode = SmoothingMode.HighQuality;
            graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
            graphics.CompositingQuality = CompositingQuality.HighQuality;

            graphics.DrawImage(bitmap, 0, 0, newSize.Width, newSize.Height);

            // At this point, you could set the PixelFormat of the bitmap back to what it used to be, with something like:
            // newBitmap.Clone(newBitmapRect, bitmap.PixelFormat);
            // However, when you resize an image, it may need additional colors for anti aliasing. If you give it a low 
            // bits per pixel (such as one of the indexed formats), you get a lot of pixellation and a very ugly 
            // resized image. So, keep the higher PixelFormat you've got now and let the user reduce it (via group property
            // PixelFormat) if they want to.
            // Dispose the old bitmap

            bitmap.Dispose();

            bitmap = newBitmap;
        }

        /// <summary>
        /// Returns true if the image with the given bitmap is animated.
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        public static bool IsAnimated(Bitmap bitmap)
        {
            int nbrFrames = bitmap.GetFrameCount(System.Drawing.Imaging.FrameDimension.Time);
            return (nbrFrames > 1);
        }

        /// <summary>
        /// Changes the pixel format of a bitmap and returns the new bitmap.
        /// Does not change its dimensions.
        /// </summary>
        /// <param name="bitmap">
        /// Input bitmap.
        /// </param>
        /// <param name="newPixelFormat">
        /// New pixel format.
        /// 
        /// If we're running with medium trust level, if new pixel format is indexed, use
        /// the lowest guaranteed non-indexed pixel format: Format24bppRgb.
        /// 
        /// This because if you run under a version of Windows before Windows 7,
        /// you get an OutOfMemory exception if you try to use the CloneBitmap or the Bitmap constructor
        /// to create a version of the bitmap with the new pixel format. And this uses the Windows palette
        /// anyway, which is really shitty.
        /// 
        /// This means you have to use your own code to create a new Bitmap with the new indexed pixel format,
        /// using eg. the Octree algorithm to find an optimal palette. But to do this quickly, you need
        /// Bitmap.Lockbits to access the actual bytes making up the Bitmap - and this give you a security exception
        /// if you're not in full trust.
        /// 
        /// TODO 1: If the source image has transparancy (either on/off or full alpha),
        /// try to ensure that the output image has transparancy too.
        /// 
        /// TODO 2: Implement the latest release of
        /// http://www.codeproject.com/KB/recipes/SimplePaletteQuantizer.aspx
        /// </param>
        /// <param name="paletteAlgorithm">
        /// Only relevant if the new pixel format is an index format. Used to calculate the palette.
        /// This can be PaletteAlgorithm.DontCare, in which case the method makes its own choice.
        /// </param>
        /// <returns>
        /// Bitmap with the changed pixel format.
        /// </returns>
        public static Bitmap ChangePixelFormat(Bitmap bitmap, PixelFormat newPixelFormat, PaletteAlgorithm paletteAlgorithm)
        {
            // If bitmap already has the desired pixel format, just return a clone
            if (bitmap.PixelFormat == newPixelFormat)
            {
                Bitmap bitmapClone = new Bitmap(bitmap);
                return bitmapClone;
            }

            // If the nex pixel format is not indexed, simply use the .Net functionality
            if (!PixelFormatUtils.IsIndexedPixelFormat(newPixelFormat))
            {
                return ChangePixelFormatWithDotNet(bitmap, newPixelFormat);
            }

            // At this stage, you know newPixelFormat is indexed.

            // If we don't have full trust (that is, running in Medium trust shared hosting account),
            // use Format24bppRgb instead of indexed format.
            if (!HasFullTrust())
            {
                return ChangePixelFormatWithDotNet(bitmap, PixelFormat.Format24bppRgb);
            }

            // When not running under Windows 7, you consistently get an OutOfMemory exception when trying
            // to clone a bitmap changing the pixel format to an indexed pixel format.
            // This is a known issue:
            // http://social.msdn.microsoft.com/Forums/en/vbgeneral/thread/90f6f014-26e1-46d4-bb0b-ec0632dda321
            //
            // So if new pixel format is indexed, don't rely on .Net. 

            // -----------------------------
            // Adjust the algorithm if needed

            PaletteAlgorithm paletteAlgorithmToUse = paletteAlgorithm;

            if (newPixelFormat == PixelFormat.Format1bppIndexed)
            {
                paletteAlgorithmToUse = PaletteAlgorithm.Windows;
            }
            else if (newPixelFormat == PixelFormat.Format4bppIndexed)
            {
                // Uniform quantizer only supports 8bpp
                if (paletteAlgorithm == PaletteAlgorithm.Uniform)
                {
                    paletteAlgorithmToUse = PaletteAlgorithm.HSB;
                }
            }

            if (paletteAlgorithm == PaletteAlgorithm.DontCare)
            {
                paletteAlgorithmToUse = PaletteAlgorithm.HSB;
            }

            // ------------------------
            // If we're using the Windows algorithm, use CopyToBpp which is nice and fast.

            if (paletteAlgorithmToUse == PaletteAlgorithm.Windows)
            {
                int bpp = PixelFormatUtils.BitsPerPixel(newPixelFormat);
                Bitmap clonedSpriteBitmap = CopyToBpp.IndexedBitmap(bitmap, bpp);
                return clonedSpriteBitmap;
            }

            // ----------------------------
            // Otherwise use a Quantizer algo

            IColorQuantizer colorQuantizer = null;

            switch (paletteAlgorithmToUse)
            {
                case PaletteAlgorithm.HSB:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.HSB.PaletteQuantizer();
                    break;

                case PaletteAlgorithm.MedianCut:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Median.MedianCutQuantizer();
                    break;

                case PaletteAlgorithm.Octree:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Octree.OctreeQuantizer();
                    break;

                case PaletteAlgorithm.Popularity:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Popularity.PopularityQuantizer();
                    break;

                case PaletteAlgorithm.Uniform:
                    colorQuantizer = new SimplePaletteQuantizer.Quantizers.Uniform.UniformQuantizer();
                    break;

                default:
                    throw new Exception(
                        string.Format("ChangePixelFormat - unexpected paletteAlgorithmToUse: {0}", paletteAlgorithmToUse));
            }

            Bitmap newBitmap =
                SimplePaletteQuantizer.Helpers.QuantizationImageHelper.GetQuantizedImage(
                    bitmap, colorQuantizer, newPixelFormat);

            return newBitmap;
        }

        /// <summary>
        /// Version of ChangePixelFormat that uses .Net functionality.
        /// Don't use to change to an indexed pixel format.
        /// </summary>
        /// <param name="bitmap"></param>
        /// <param name="newPixelFormat"></param>
        /// <returns></returns>
        private static Bitmap ChangePixelFormatWithDotNet(Bitmap bitmap, PixelFormat newPixelFormat)
        {
            // Bitmap.Clone does not reliably give the clonedSpriteBitmap the newPixelFormat. (bug in GDI+)
            // Need to do the following little dance (from http://stackoverflow.com/questions/2016406/converting-bitmap-pixelformats-in-c)

            Bitmap clonedSpriteBitmap = new Bitmap(bitmap.Width, bitmap.Height, newPixelFormat);

            Rectangle imageRect = new Rectangle(0, 0, clonedSpriteBitmap.Width, clonedSpriteBitmap.Height);
            using (Graphics gr = Graphics.FromImage(clonedSpriteBitmap))
            {
                gr.DrawImage(bitmap, imageRect);
            }

            return clonedSpriteBitmap;
        }

        public static bool DenotesSpecificNumberOfBitsPerPixel(PixelFormat? pixelFormat)
        {
            return
                ((pixelFormat == PixelFormat.Alpha) ||
                 (pixelFormat == PixelFormat.Extended) ||
                 (pixelFormat == PixelFormat.Gdi) ||
                 (pixelFormat == PixelFormat.Indexed) ||
                 (pixelFormat == PixelFormat.Max) ||
                 (pixelFormat == PixelFormat.PAlpha) ||
                 (pixelFormat == PixelFormat.Canonical));
        }

        /// <summary>
        /// Returns true if the code runs in a full trust environment.
        /// Returns false otherwise - this normally indicates a medium trust environment.
        /// 
        /// TODO: If you don't make ImageUtils.cs into a separate project, move the trust stuff to
        /// a separate TrustUtils.cs file.
        /// </summary>
        /// <returns></returns>
        private static bool HasFullTrust()
        {
            // If another task in the same app pool has already figured out the trust level,
            // return that.
            if (_hasFullTrust != null) { return _hasFullTrust ?? false; }

            // Try to get it from cache
            _hasFullTrust = CacheUtils.Entry<bool?>(CacheUtils.CacheEntryId.hasFullTrust, "");

            if (_hasFullTrust == null)
            {
                AspNetHostingPermissionLevel currentTrustLevel = GetCurrentTrustLevel();

                // Note that a "High" permission level (which is between Unrestricted and Medium)
                // is not enough to run the image manipulation code in QuantizationImageHelper.
                _hasFullTrust =
                    (currentTrustLevel == AspNetHostingPermissionLevel.Unrestricted);

                CacheUtils.InsertEntry<bool?>(CacheUtils.CacheEntryId.hasFullTrust, "", _hasFullTrust, null);
            }

            return _hasFullTrust ?? false;
        }

        /// <summary>
        /// Returns the current trust level, assuming this code (or the calling code up the stack), 
        /// is not loaded from GAC.
        /// 
        /// Based on 
        /// http://blogs.msdn.com/b/dmitryr/archive/2007/01/23/finding-out-the-current-trust-level-in-asp-net.aspx
        /// </summary>
        /// <returns></returns>
        private static AspNetHostingPermissionLevel GetCurrentTrustLevel()
        {
            foreach (AspNetHostingPermissionLevel trustLevel in
                    new AspNetHostingPermissionLevel[] {
                    AspNetHostingPermissionLevel.Unrestricted,
                    AspNetHostingPermissionLevel.High,
                    AspNetHostingPermissionLevel.Medium,
                    AspNetHostingPermissionLevel.Low,
                    AspNetHostingPermissionLevel.Minimal 
                })
            {
                try
                {
                    new AspNetHostingPermission(trustLevel).Demand();
                }
                catch (System.Security.SecurityException)
                {
                    continue;
                }

                return trustLevel;
            }

            return AspNetHostingPermissionLevel.None;
        }
    }
}

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