Various utility methods for working with images in C# .NET.

#region License
/*
 **************************************************************
 *  Author: Rick Strahl
 *          (c) West Wind Technologies, 2009
 *          http://www.west-wind.com/
 *
 * Created: 09/12/2009
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 **************************************************************
*/
#endregion

#if NETFULL
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
#endif

using System;
using System.Collections.Generic;
using System.IO;

namespace Westwind.Utilities
{
    /// <summary>
    /// Summary description for wwImaging.
    /// </summary>
    public static class ImageUtils
    {

#if NETFULL
        /// <summary>
        /// Creates a resized bitmap from an existing image on disk. Resizes the image by
        /// creating an aspect ratio safe image. Image is sized to the larger size of width
        /// height and then smaller size is adjusted by aspect ratio.
        ///
        /// Image is returned as Bitmap - call Dispose() on the returned Bitmap object
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns>Bitmap or null</returns>
        public static Bitmap ResizeImage(string filename, int width, int height, InterpolationMode mode = InterpolationMode.HighQualityBicubic)
        {
            try
            {
                using (Bitmap bmp = new Bitmap(filename))
                {
                    return ResizeImage(bmp, width, height, mode);
                }
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Resizes an image from byte array and returns a Bitmap.
        /// Make sure you Dispose() the bitmap after you're done
        /// with it!
        /// </summary>
        /// <param name="data"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        public static Bitmap ResizeImage(byte[] data, int width, int height, InterpolationMode mode = InterpolationMode.HighQualityBicubic)
        {
            try
            {
                using (Bitmap bmp = new Bitmap(new MemoryStream(data)))
                {
                    return ResizeImage(bmp, width, height, mode);
                }
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Resizes an image and saves the image to a file
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="outputFilename"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="mode"></param>
        /// <param name="jpegCompressionMode">
        /// If using a jpeg image
        /// </param>
        /// <returns></returns>
        public static bool ResizeImage(string filename, string outputFilename,
                                        int width, int height,
                                        InterpolationMode mode = InterpolationMode.HighQualityBicubic,
                                        int jpegCompressionMode = 85)
        {

            using (var bmpOut = ResizeImage(filename, width, height, mode))
            {
                var imageFormat = GetImageFormatFromFilename(filename);
                if (imageFormat == ImageFormat.Emf)
                    imageFormat = bmpOut.RawFormat;

                if(imageFormat == ImageFormat.Jpeg)
                    SaveJpeg(bmpOut, outputFilename, jpegCompressionMode);
                else
                    bmpOut.Save(outputFilename, imageFormat);
            }

            return true;
        }


        /// <summary>
        /// Resizes an image from a bitmap.
        /// Note image will resize to the larger of the two sides
        /// </summary>
        /// <param name="bmp">Bitmap to resize</param>
        /// <param name="width">new width</param>
        /// <param name="height">new height</param>
        /// <returns>resized or original bitmap. Be sure to Dispose this bitmap</returns>
        public static Bitmap ResizeImage(Bitmap bmp, int width, int height,
                                         InterpolationMode mode = InterpolationMode.HighQualityBicubic)
        {
            Bitmap bmpOut = null;

            try
            {
                decimal ratio;
                int newWidth = 0;
                int newHeight = 0;

                // If the image is smaller than a thumbnail just return original size
                if (bmp.Width < width && bmp.Height < height)
                {
                    newWidth = bmp.Width;
                    newHeight = bmp.Height;
                }
                else
                {
                    if (bmp.Width == bmp.Height)
                    {
                        if (height > width)
                        {
                            newHeight = height;
                            newWidth = height;
                        }
                        else
                        {
                            newHeight = width;
                            newWidth = width;
                        }
                    }
                    else if (bmp.Width >= bmp.Height)
                    {
                        ratio = (decimal)width / bmp.Width;
                        newWidth = width;
                        decimal lnTemp = bmp.Height * ratio;
                        newHeight = (int)lnTemp;
                    }
                    else
                    {
                        ratio = (decimal)height / bmp.Height;
                        newHeight = height;
                        decimal lnTemp = bmp.Width * ratio;
                        newWidth = (int)lnTemp;
                    }
                }

                bmpOut = new Bitmap(newWidth, newHeight);
                bmpOut.SetResolution(bmp.HorizontalResolution, bmp.VerticalResolution);

                using (Graphics g = Graphics.FromImage(bmpOut))
                {
                    g.InterpolationMode = mode;
                    g.SmoothingMode = SmoothingMode.HighQuality;
                    g.PixelOffsetMode = PixelOffsetMode.HighQuality;

                    g.FillRectangle(Brushes.White, 0, 0, newWidth, newHeight);
                    g.DrawImage(bmp, 0, 0, newWidth, newHeight);
                }
            }
            catch
            {
                return null;
            }

            return bmpOut;
        }

        /// <summary>
        /// Adjusts an image to a specific aspect ratio by clipping
        /// from the center outward - essentially capturing the center
        /// to fit the width/height of the aspect ratio.
        /// </summary>
        /// <param name="imageStream">Stream to an image</param>
        /// <param name="ratio">Aspect ratio default is 16:9</param>
        /// <param name="resizeWidth">Optionally resize with to this width (if larger than height)</param>
        /// <param name="resizeHeight">Optionally resize to this height (if larger than width)</param>
        /// <returns>Bitmap image - make sure to dispose this image</returns>
        public static Bitmap AdjustImageToRatio(Stream imageStream, decimal ratio = 16M / 9M, int resizeWidth = 0,
            int resizeHeight = 0)
        {
            if (imageStream == null)
                return null;

            decimal width = 0;
            decimal height = 0;


            Bitmap bmpOut = null;
            Bitmap bitmap = null;

            try
            {
                bitmap = new Bitmap(imageStream);

                height = bitmap.Height;
                width = bitmap.Width;

                if (width >= height * ratio)
                {
                    // clip width
                    decimal clipWidth = height * ratio;
                    decimal clipX = (width - clipWidth) / 2;

                    bmpOut = new Bitmap((int) clipWidth, (int) height);
                    bmpOut.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);

                    using (Graphics g = Graphics.FromImage(bmpOut))
                    {
                        g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                        g.SmoothingMode = SmoothingMode.HighQuality;
                        g.PixelOffsetMode = PixelOffsetMode.HighQuality;


                        var sourceRect = new Rectangle((int) clipX, 0, (int) clipWidth, (int) height);
                        var targetRect = new Rectangle(0, 0, (int) clipWidth, (int) height);

                        g.DrawImage(bitmap, targetRect, sourceRect, GraphicsUnit.Pixel);
                    }
                }
                else if (width < height * ratio)
                {
                    // clip height
                    decimal clipHeight = width / ratio;
                    decimal clipY = (height - clipHeight) / 2;

                    bmpOut = new Bitmap((int) width, (int) clipHeight);
                    bmpOut.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution);

                    using (Graphics g = Graphics.FromImage(bmpOut))
                    {
                        g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                        g.SmoothingMode = SmoothingMode.HighQuality;
                        g.PixelOffsetMode = PixelOffsetMode.HighQuality;

                        var sourceRect = new Rectangle(0, (int) clipY, (int) width, (int) clipHeight);
                        var targetRect = new Rectangle(0, 0, (int) width, (int) clipHeight);

                        g.DrawImage(bitmap, targetRect, sourceRect, GraphicsUnit.Pixel);
                    }
                }
                else
                    bmpOut = bitmap;

                if (resizeWidth == 0 || resizeWidth == 0)
                    return bmpOut;

                var resizedImage = ResizeImage(bmpOut, resizeWidth, resizeHeight);
                return resizedImage;
            }
            finally
            {
                bitmap?.Dispose();
                bmpOut?.Dispose();
            }
        }


        /// <summary>
        /// Adjusts an image to a specific aspect ratio by clipping
        /// from the center outward - essentially capturing the center
        /// to fit the width/height of the aspect ratio.
        /// </summary>
        /// <param name="imageContent"></param>
        /// <param name="ratio"></param>
        /// <param name="resizeWidth"></param>
        /// <param name="resizeHeight"></param>
        /// <returns></returns>
        public static Bitmap AdjustImageToRatio(byte[] imageContent, decimal ratio = 16M / 9M, int resizeWidth = 0,
            int resizeHeight = 0)
        {
            using (var ms = new MemoryStream(imageContent))
            {
                return AdjustImageToRatio(ms, ratio, resizeWidth, resizeHeight);
            }
        }


        /// <summary>
        /// Saves a jpeg BitMap  to disk with a jpeg quality setting.
        /// Does not dispose the bitmap.
        /// </summary>
        /// <param name="bmp">Bitmap to save</param>
        /// <param name="outputFileName">file to write it to</param>
        /// <param name="jpegQuality"></param>
        /// <returns></returns>
        public static bool SaveJpeg(Bitmap bmp, string outputFileName, long jpegQuality = 90)
        {
            try
            {
                //get the jpeg codec
                ImageCodecInfo jpegCodec = null;
                if (Encoders.ContainsKey("image/jpeg"))
                    jpegCodec = Encoders["image/jpeg"];

                EncoderParameters encoderParams = null;
                if (jpegCodec != null)
                {
                    //create an encoder parameter for the image quality
                    EncoderParameter qualityParam = new EncoderParameter(Encoder.Quality, jpegQuality);

                    //create a collection of all parameters that we will pass to the encoder
                    encoderParams = new EncoderParameters(1);
                    encoderParams.Param[0] = qualityParam;
                }
                bmp.Save(outputFileName, jpegCodec, encoderParams);
            }
            catch
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Saves a jpeg BitMap  to disk with a jpeg quality setting.
        /// Does not dispose the bitmap.
        /// </summary>
        /// <param name="bmp">Bitmap to save</param>
        /// <param name="outputStream">Binary stream to write image data to</param>
        /// <param name="jpegQuality"></param>
        /// <returns></returns>
        public static bool SaveJpeg(Bitmap bmp, Stream imageStream, long jpegQuality = 90)
        {
            try
            {
                //get the jpeg codec
                ImageCodecInfo jpegCodec = null;
                if (Encoders.ContainsKey("image/jpeg"))
                    jpegCodec = Encoders["image/jpeg"];

                EncoderParameters encoderParams = null;
                if (jpegCodec != null)
                {
                    //create an encoder parameter for the image quality
                    EncoderParameter qualityParam = new EncoderParameter(Encoder.Quality, jpegQuality);

                    //create a collection of all parameters that we will pass to the encoder
                    encoderParams = new EncoderParameters(1);
                    encoderParams.Param[0] = qualityParam;
                }
                bmp.Save(imageStream, jpegCodec, encoderParams);
            }
            catch
            {
                return false;
            }

            return true;
        }


        /// <summary>
        /// Rotates an image and writes out the rotated image to a file.
        /// </summary>
        /// <param name="filename">The original image to roatate</param>
        /// <param name="outputFilename">The output file of the rotated image file. If not passed the original file is overwritten</param>
        /// <param name="type">Type of rotation to perform</param>
        /// <returns></returns>
        public static bool RoateImage(string filename, string outputFilename = null,
                                      RotateFlipType type = RotateFlipType.Rotate90FlipNone,
                                      int jpegCompressionMode = 85)
        {
            Bitmap bmpOut = null;

            if (string.IsNullOrEmpty(outputFilename))
                outputFilename = filename;

            try
            {
                ImageFormat imageFormat;
                using (Bitmap bmp = new Bitmap(filename))
                {
                    imageFormat = GetImageFormatFromFilename(filename);
                    if (imageFormat == ImageFormat.Emf)
                        imageFormat = bmp.RawFormat;

                    bmp.RotateFlip(type);

                    using (bmpOut = new Bitmap(bmp.Width, bmp.Height))
                    {
                        bmpOut.SetResolution(bmp.HorizontalResolution, bmp.VerticalResolution);

                        Graphics g = Graphics.FromImage(bmpOut);
                        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                        g.DrawImage(bmp, 0, 0, bmpOut.Width, bmpOut.Height);

                        if (imageFormat == ImageFormat.Jpeg)
                            SaveJpeg(bmpOut, outputFilename, jpegCompressionMode);
                        else
                            bmpOut.Save(outputFilename, imageFormat);
                    }
                }

            }
            catch (Exception ex)
            {
                var msg = ex.GetBaseException();
                return false;
            }

            return true;
        }

        public static byte[] RoateImage(byte[] data, RotateFlipType type = RotateFlipType.Rotate90FlipNone)
        {
            Bitmap bmpOut = null;

            try
            {
                Bitmap bmp = new Bitmap(new MemoryStream(data));

                ImageFormat imageFormat;
                imageFormat = bmp.RawFormat;
                bmp.RotateFlip(type);

                bmpOut = new Bitmap(bmp.Width, bmp.Height);
                bmpOut.SetResolution(bmp.HorizontalResolution, bmp.VerticalResolution);

                Graphics g = Graphics.FromImage(bmpOut);
                g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                g.DrawImage(bmp, 0, 0, bmpOut.Width, bmpOut.Height);

                bmp.Dispose();

                using (var ms = new MemoryStream())
                {
                    bmpOut.Save(ms, imageFormat);
                    bmpOut.Dispose();

                    ms.Flush();
                    return ms.ToArray();
                }
            }
            catch (Exception ex)
            {
                var msg = ex.GetBaseException();
                return null;
            }

        }


        /// <summary>
        /// Opens the image and writes it back out, stripping any Exif data
        /// </summary>
        /// <param name="imageFile">Image to remove exif data from</param>
        /// <param name="imageQuality">image quality 0-100 (100 no compression)</param>
        public static void StripJpgExifData(string imageFile, int imageQuality = 90)
        {
            using (var bmp = new Bitmap(imageFile))
            {
                using (var bmp2 = new Bitmap(bmp, bmp.Width, bmp.Height))
                {
                    bmp.Dispose();
                    SaveJpeg(bmp2, imageFile, imageQuality);
                }
            }
        }


        /// <summary>
        /// If the image contains image rotation Exif data, apply the image rotation and
        /// remove the Exif data. Optionally also allows for image resizing in the same
        /// operation.
        /// </summary>
        /// <param name="imageFile">Image file to work on</param>
        /// <param name="imageQuality">Jpg</param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public static void NormalizeJpgImageRotation(string imageFile, int imageQuality = 90, int width = -1, int height = -1)
        {
            using (var bmp = new Bitmap(imageFile))
            {
                Bitmap bmp2;
                using (bmp2 = new Bitmap(bmp, bmp.Width, bmp.Height))
                {
                    if (bmp.PropertyItems != null)
                    {
                        foreach (var item in bmp.PropertyItems)
                        {
                            if (item.Id == 0x112)
                            {
                                int orientation = item.Value[0];
                                if (orientation == 6)
                                    bmp2.RotateFlip(RotateFlipType.Rotate90FlipNone);
                                if (orientation == 8)
                                    bmp2.RotateFlip(RotateFlipType.Rotate270FlipNone);
                            }
                        }
                    }

                    bmp.Dispose();

                    if (width > 0 || height > 0)
                        bmp2 = ResizeImage(bmp2, width, height);

                    SaveJpeg(bmp2, imageFile, imageQuality);
                }
            }
        }



        /// <summary>
        /// A quick lookup for getting image encoders
        /// </summary>
        public static Dictionary<string, ImageCodecInfo> Encoders
        {
            //get accessor that creates the dictionary on demand
            get
            {
                //if the quick lookup isn't initialised, initialise it
                if (_encoders != null)
                    return _encoders;

                _encoders = new Dictionary<string, ImageCodecInfo>();

                //get all the codecs
                foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders())
                {
                    //add each codec to the quick lookup
                    _encoders.Add(codec.MimeType.ToLower(), codec);
                }

                //return the lookup
                return _encoders;
            }
        }
        private static Dictionary<string, ImageCodecInfo> _encoders = null;


        /// <summary>
        /// Tries to return an image format
        /// </summary>
        /// <param name="filename"></param>
        /// <returns>Image format or ImageFormat.Emf if no match was found</returns>
        public static ImageFormat GetImageFormatFromFilename(string filename)
        {
            string ext = Path.GetExtension(filename).ToLower();

            ImageFormat imageFormat;

            if (ext == ".jpg" || ext == ".jpeg")
                imageFormat = ImageFormat.Jpeg;
            else if (ext == ".png")
                imageFormat = ImageFormat.Png;
            else if (ext == ".gif")
                imageFormat = ImageFormat.Gif;
            else if (ext == ".bmp")
                imageFormat = ImageFormat.Bmp;
            else
                imageFormat = ImageFormat.Emf;

            return imageFormat;
        }
#endif

        /// <summary>
        /// Returns the image media type for a give file extension based
        /// on a filename or url passed in.
        /// </summary>
        /// <param name="file"></param>
        /// <returns></returns>
        public static string GetImageMediaTypeFromFilename(string file)
        {
            if (string.IsNullOrEmpty(file))
                return file;

            string ext = Path.GetExtension(file).ToLower();
            if (ext == ".jpg" || ext == ".jpeg")
                return "image/jpeg";
            if (ext == ".png")
                return "image/png";
            if (ext == ".gif")
                return "image/gif";
            if (ext == ".bmp")
                return "image/bmp";
            if (ext == ".tif" || ext == ".tiff")
                return "image/tiff";

            return "application/image";
        }
    }
}