Click here to Skip to main content
15,884,986 members
Articles / Programming Languages / C#

Silverlight Database Deep Zoom

Rate me:
Please Sign up or sign in to vote.
4.96/5 (40 votes)
26 Mar 2009CPOL11 min read 122.5K   1.7K   84  
The article describes how to create a Deep Zoom image and store the tiles in a database, and how to read the image from the database and display it in the browser.
// This code has been changed by J�rg Lang (lang.joerg@gmail.com) from a class 
// called Decos.DeepZoom that was made 2008 by Berend Engelbrecht, b.engelbrecht@gmail.com
// The original code can be found at www.codeproject.com
//
// The changes made are mainly taking out all stuff that seemed to be product
// specific to a product by the original creator of the code and that was not
// needed to create tiles for a single image.
// The main change is of course, that the images get stored in a database.
//
// This source code is licensed for commercial and non-commercial use under the 
// Code Project Open License (CPOL) 1.02  http://www.codeproject.com/info/cpol10.aspx
//  
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

namespace DbDzComposer
{
    /// <summary>
    /// DeepZoom encapsulates code used to generate a DeepZoom image from a
    /// single image. This class does not contain any Windows Forms related or user
    /// interface code. 
    /// </summary>
    public class DeepZoomGenerator
    {

        /// <summary>
        /// Occurs when the creation of the deep zoom image progresses.
        /// </summary>
        public event EventHandler<DeepZoomCreationProgressEventArgs> CreationProgress;

        /// <summary>Overlap in pixels for DeepZoom image tiles</summary>
        internal const int tileOverlap = 1;
        internal const int maxThumbnailWidth = 125;

        private ImageCodecInfo jpegCodec;
        
        /// <summary>
        /// Gets or sets the database persister.
        /// </summary>
        /// <value>The database persister.</value>
        public IDzDbPersistance DatabasePersister { get; set; }

        /// <summary>JPEG quality used for jpg image tiles, must be between 1 and 100</summary>
        public int JpegQuality { get; set; }

        /// <summary>PixelFormat used in memory bitmaps</summary>
        public PixelFormat ColorPixelFormat { get; set; }

        /// <summary>PixelFormat used in memory bitmaps</summary>
        public int TileSize { get; set; }

 
        /// <summary>
        /// Initializes a new instance of the <see cref="DeepZoomGenerator"/> class with 
        /// default values for jpeg quality (90), tile size (256) and a color format
        /// of 24bppRgb.
        /// </summary>
        public DeepZoomGenerator(): this(90, 256, System.Drawing.Imaging.PixelFormat.Format24bppRgb)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DeepZoomGenerator"/> class.
        /// </summary>
        /// <param name="jpegQuality">The JPEG quality. Integer values from 0 to 100</param>
        /// <param name="tileSize">Size of the tiles.</param>
        /// <param name="colorPixelFormat">The pixel format.</param>
        public DeepZoomGenerator(int jpegQuality, int tileSize, PixelFormat colorPixelFormat)
        {
            JpegQuality = jpegQuality;
            TileSize = tileSize;
            ColorPixelFormat = colorPixelFormat;
        }


        /// <summary>
        /// Generates a deepzoom image from a file
        /// The image file can be anything
        /// that System.Drawing.Bitmap allows in its constructor, usually tiff or jpg.
        /// </summary>
        /// <param name="sourceFile">Full path to the file</param>
        /// <param name="imageName">Name of the image.</param>
        /// <param name="useJpeg">if set to <c>true</c> [use JPEG].</param>
        /// <param name="useOverlap">if set to <c>true</c> the tiles will be created with a one pixel overlap.</param>
        /// <returns>the id of the image in the database</returns>
        public int GenerateFromFile(string sourceFile, string imageName, bool useJpeg, bool useOverlap)
        {
            Bitmap sourceImage;
            if (File.Exists(sourceFile))
                sourceImage = new Bitmap(sourceFile);
            else
                throw new FileNotFoundException("File not found!", sourceFile);

            // Generate full deepzoom image 
            int id = CreateSingleDeepZoomImage(imageName, sourceImage, useJpeg, useOverlap);

            sourceImage.Dispose();

            return id;
        }

        /// <summary>
        /// Creates the DeepZoom image tile set for one image.
        /// </summary>
        /// <param name="imageName">Name of the image.</param>
        /// <param name="bitmap">The bitmap.</param>
        /// <param name="useJpeg">true if color images (jpg) should be written</param>
        /// <param name="useOverlap">if set to <c>true</c> tiles will be created a one pixel overlap.</param>
        /// <returns>The id of the image in the database</returns>
        public int CreateSingleDeepZoomImage(string imageName, Bitmap bitmap, bool useJpeg, bool useOverlap)
        {
            int maxLevel = CalcMaxLevel(bitmap.Width, bitmap.Height);
            int width = bitmap.Width;
            int height = bitmap.Height;
            double progressStep = (double) 100 / maxLevel;
            double progress = 0;
            int overlap = useOverlap ? tileOverlap : 0;

            // Create a thumbnail to store in the db as a preview
            Bitmap thumbnail = new Bitmap(bitmap, maxThumbnailWidth, bitmap.Height / (bitmap.Width / maxThumbnailWidth));

            // Persist the image info in the database
            int imageId = DatabasePersister.SaveImageInfo(imageName, width, height, TileSize, overlap, GetMimeType(useJpeg), thumbnail );

            for (int level = maxLevel; level >= 0; level--)
            {
                CreateTiles(bitmap, imageId, level, width, height, useJpeg, useOverlap);
                width = (width / 2);
                height = (height / 2);
                progress += progressStep;

                OnDeepZoomCreationProgress(new DeepZoomCreationProgressEventArgs((int) progress));
            }

            return imageId;
        }

        /// <summary>
        /// Raises the DeepZoomCreationProgress event.
        /// </summary>
        /// <param name="e">The <see cref="DbDzComposer.DeepZoomCreationProgressEventArgs"/> instance containing the event data.</param>
        private void OnDeepZoomCreationProgress(DeepZoomCreationProgressEventArgs e)
        {
            // To prevent race conditions assign it to a variable and raise the event from there
            EventHandler<DeepZoomCreationProgressEventArgs> handler = CreationProgress;
            if (handler != null)
            {
                handler(this, e);
            }
        }


        /// <summary>
        /// Creates a tile set for the specified bitmap and level. The caller should calculate
        /// and pass on the zoom width and height for for the level.
        /// </summary>
        /// <param name="bitmap">Original bitmap</param>
        /// <param name="imageId">The image id.</param>
        /// <param name="level">Level</param>
        /// <param name="width">overall width of the image to be used for specified zoom level</param>
        /// <param name="height">overall height of the image to be used for specified zoom level</param>
        /// <param name="useJpeg">true if color image (should generate jpg)</param>
        /// <param name="useOverlap">true to generate overlapped tiles (deepzoom images), false for fixed 256x256 tiles (collection thumbnails)</param>
        /// <returns>Count of generated tiles</returns>
        internal int CreateTiles(Bitmap bitmap, int imageId, int level, int width, int height, bool useJpeg, bool useOverlap)
        {
            int tilesCount = 0;

            // Make sure we have valid height and width
            if (width < 1) width = 1;
            if (height < 1) height = 1;

            bool useSmoothScaling = useOverlap && ((width < bitmap.Width) || (height < bitmap.Height));
            using (var scaledBitmap = new EditableBitmap(bitmap, ColorPixelFormat, width, height, useSmoothScaling))
            {
                for (int x = 0, iX = 0; x < width; x += TileSize, iX++)
                {
                    int left;
                    int tileWidth = GetTileSize(x, width, out left, useOverlap);
                    for (int y = 0, iY = 0; y < height; y += TileSize, iY++)
                    {
                        int top;
                        int tileHeight = GetTileSize(y, height, out top, useOverlap);
                        var rectTile = new Rectangle(left, top, tileWidth, tileHeight);
                        string outputFile = iX + "_" + iY + (useJpeg ? ".jpg" : ".png");
                        using (EditableBitmap tileBitmap = scaledBitmap.CreateView(rectTile))
                        {
                            if (!useOverlap && ((tileWidth < TileSize) || (tileHeight < TileSize)))
                            {
                                // Collection thumbnail tiles are always the tilesize in minimum, even if the image content
                                // is much smaller. Draw a smaller image on top of a black TileSize x TileSize image.
                                using (var bmExtended = new Bitmap(TileSize, TileSize, tileBitmap.Bitmap.PixelFormat))
                                {
                                    using (Graphics gfx = Graphics.FromImage(bmExtended))
                                    {
                                        gfx.FillRectangle(Brushes.Black, 0, 0, TileSize, TileSize);
                                        gfx.DrawImage(tileBitmap.Bitmap, 0, 0);
                                    }
                                    SaveTile(bmExtended, imageId, level, iX, iY, JpegQuality, useJpeg);
                                }
                            }
                            else
                                SaveTile(tileBitmap.Bitmap, imageId, level, iX, iY, JpegQuality, useJpeg);
                        }
                        tilesCount++;
                    }
                }
            }
            return tilesCount;
        }


        /// <summary>
        /// Returns the maximum tile level for the given image dimensions.
        /// </summary>
        /// <param name="width">Image width in pixels</param>
        /// <param name="height">Image height in pixels</param>
        /// <returns>Maximum DeepZoom tile level for the image</returns>
        internal static int CalcMaxLevel(int width, int height)
        {
            int iDimension = Math.Max(width, height);
            return Convert.ToInt32(Math.Ceiling(Math.Log(iDimension) / Math.Log(2)));
        }

        /// <summary>
        /// Helper function to get tile coordinates. DeepZoom uses tiles that have a net
        /// size of 256 x 256, but have an overlap of 1 pixel on all sides. Tiles at the 
        /// border of the image are slightly smaller (e.g., 257 x 258) than tiles in the 
        /// middle (258 x 258).
        /// 
        /// Collection thumbnails do not use overlap but have tiles that are always exactly
        /// 256 x 256. For the non-overlap case Getc_tileSize will still truncate to the 
        /// image border, it returns the exact rectangle of the source image to be copied.
        /// </summary>
        /// <param name="start">Net start coordinate</param>
        /// <param name="max">Maximum coordinate in image</param>
        /// <param name="actualStart">OUT: Actual start coordinate can be 1 pixel below iStart</param>
        /// <param name="useOverlap">true to use overlap</param>
        /// <returns>Actual tile size</returns>
        internal int GetTileSize(int start, int max, out int actualStart, bool useOverlap)
        {
            int ic_tileSize;
            if (useOverlap)
            {
                ic_tileSize = TileSize + tileOverlap;
                actualStart = start;
                if (start > 0)
                {
                    ic_tileSize += tileOverlap;
                    actualStart -= tileOverlap;
                }
            }
            else
            {
                actualStart = start;
                ic_tileSize = TileSize;
            }
            if (actualStart + ic_tileSize > max)
                ic_tileSize = (max - actualStart);
            if (ic_tileSize < 1)
                ic_tileSize = 1;
            return ic_tileSize;
        }

        /// <summary>
        /// SaveTile is used to save a tile bitmap, either as jpg (bUseJpeg == true) or
        /// as png (bUseJpeg == false).
        /// </summary>
        /// <param name="bitmap">Bitmap to be saved</param>
        /// <param name="level">The level.</param>
        /// <param name="imageId">The image id.</param>
        /// <param name="x">The x coordinates of the image</param>
        /// <param name="y">The y coordinates of the image</param>
        /// <param name="quality">Quality (only used for jpg)</param>
        /// <param name="useJpeg">true: save jpg format; false: save png format</param>
        internal void SaveTile(Bitmap bitmap, int imageId, int level, int x, int y, long quality, bool useJpeg)
        {
            MemoryStream memStream = new MemoryStream();
            if (useJpeg)
            {
                // Encoder parameter for image quality
                var qualityParam = new EncoderParameter(Encoder.Quality, quality);

                // Jpeg image codec
                if (jpegCodec == null)
                    jpegCodec = GetEncoderInfo(GetMimeType(true));

                if (jpegCodec == null)
                    return;

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

                // Create a new bitmap according to the users quality settings
                bitmap.Save(memStream, jpegCodec, encoderParams);
                Bitmap bmp = new Bitmap(memStream);

                // Save the jpge to the database
                DatabasePersister.SaveImageTile(imageId, level, x, y, bmp);
            }
            else
            {
                bitmap.Save(memStream, ImageFormat.Png);
                Bitmap bmp = new Bitmap(memStream);

                // Save the png to the database
                DatabasePersister.SaveImageTile(imageId, level, x, y, bmp);
            }
        }

        /// <summary>
        /// Helper function that returns the mime type for either jpeg or png
        /// </summary>
        /// <param name="useJpeg">if set to <c>true</c> [use JPEG].</param>
        /// <returns></returns>
        private string GetMimeType(bool useJpeg)
        {
            return useJpeg ? "image/jpeg" : "image/png";
        }

        /// <summary>
        /// Helper function that is used to locate the jpeg codec used in GDI+.
        /// </summary>
        /// <param name="mimeType">Mime type for which codec must be located</param>
        /// <returns></returns>
        private static ImageCodecInfo GetEncoderInfo(string mimeType)
        {
            // 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 == mimeType)
                    return codecs[i];
            return null;
        }
    }

    /// <summary>
    /// Provides data for the DeepZoomCreationProgress. 
    /// </summary>
    public class DeepZoomCreationProgressEventArgs: EventArgs
    {
        private readonly int m_creationProgress;
        public int CreationProgress
        {
            get { return m_creationProgress; }
        }

        public DeepZoomCreationProgressEventArgs(int percentage)
        {
            if (percentage > 100)
                percentage = 100;
            m_creationProgress = percentage;
        }
    }
}

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
CEO
Switzerland Switzerland
I have my own software company called Evelix (www.evelix.ch). The company is located in Liestal, Switzerland. I develop software for the web and the desktop. Every now and then I give computer classes in a learning institution.

I was born in 1966, am married and have one kid. Hobbies are Fasnacht (www.bmg.bs), skiing and of course computers.

Actually I studied mechanical engineering and have a bachelors degree in it, but computers interested me since I had a Commodore C128. In the meantime my mobile has a thousand times more memory than my computers back then. First I started programming in Basic. After that I did use Pascal for a while, but the real (commercial) programming started with VB3. Now I do programming in C# and sometimes still in VB6 when I have to support an older application.

Currently I'm working towards my Microsoft Certified Trainer status.

Comments and Discussions