Rss

Working with Images in MonoTouch using CGBitmapContext

I needed to do some work combining images on-the-fly for both 1x and 2x pixel densities on an iPad using MonoTouch, and I needed to be able to do it on a background thread.

UIGraphics-related routines are pretty easy to use, but the work ends up being done on the main thread, which drags down the GUI various levels of severely. So I put in the time to get the same stuff done on a background thread. The goal was a DialogViewController-based tree view (sometimes a TableView hierarchy just won’t work), which now works pretty well with image manipulation happening in the background. Hopefully I’ll have some time in the near future to share that with the world as well.

The code below exposes three “do something” methods: MergeImages, SuperimposeImage, and RepeatImage. These methods take UIImages and manipulate them in ways that the names imply. This allows me to quickly manipulate images loaded using UIImage.FromBundle (so they’ll be appropriate image for the platform).

There are also some extension methods for UIImage that make it even easier: AddImageOnRight, Superimpose, and Repeat. It’s important to know what goes on under the hood, and the methods are easy enough already, but well-implemented syntactical sugar can really improve code’s readability.

The heart of the work, though, is the PerformContextOperations method. It sets up the canvas on which everything is drawn, extracts the image resulting from the various operations, and corrects the output for the scaling used by the platform. Isolating this into a single method and injecting the actual drawing steps using an Action makes all of the image operations rather simple. Image operations can be completely agnostic to screen scaling with this, so they become just a few lines of computing image locations and the couple lines of drawing that, and the rest just happens. Just happens, at least, now that I’ve written the hardest part.

Here’s the code, and thanks for reading!

using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System.Drawing;
using MonoTouch.CoreGraphics;

namespace Utils
{
    public static class Image
    {
        /// <summary>
        /// Takes two images and combines them into one image with the second image placed to the right of the first.
        /// </summary>
        /// <param name="leftImage">Image that will appear on the left</param>
        /// <param name="rightImage">Image that will appear on the right</param>
        /// <returns>Merged image</returns>
        public static UIImage MergeImages(UIImage leftImage, UIImage rightImage)
        {
            var lSize = leftImage.Size;
            var rSize = rightImage.Size;
            var width = lSize.Width + rSize.Width;
            var height = Math.Max(lSize.Height, rSize.Height);
            var lRect = new RectangleF(new Point(0, 0), lSize);
            var rRect = new RectangleF(new Point((int)lSize.Width, 0), rSize);

            var image = PerformContextOperations(width, height, (context) =>
            {
                context.DrawImage(lRect, leftImage.CGImage);
                context.DrawImage(rRect, rightImage.CGImage);
            });

            return image;
        }

        /// <summary>
        /// Takes two images and draws the first image over the second, allowing the bottom image to show through transparent areas in the first
        /// </summary>
        /// <param name="topImage">Image to be drawn on top</param>
        /// <param name="bottomImage">Image to drawn on bottom</param>
        /// <returns>Image resulting from the superimpose operation</returns>
        public static UIImage SuperimposeImage(UIImage topImage, UIImage bottomImage)
        {
            var scaleFactor = ScaleFactor;
            var tSize = topImage.Size;
            var bSize = bottomImage.Size;
            var width = Math.Max(tSize.Width, bSize.Width);
            var height = Math.Max(tSize.Height, bSize.Height);
            var tRect = new RectangleF(new Point(0, 0), tSize);
            var bRect = new RectangleF(new Point(0, 0), bSize);

            var image = PerformContextOperations(width, height, (context) =>
            {
                context.DrawImage(bRect, bottomImage.CGImage);
                context.DrawImage(tRect, topImage.CGImage);
            });

            return image;
        }

        /// <summary>
        /// Repeat an image horizontally.
        /// </summary>
        /// <param name="image">Image to repeat</param>
        /// <param name="count">the number of times to perform the repeat operation (result will contain count + 1 images) </param>
        /// <returns></returns>
        public static UIImage RepeatImage(UIImage image, int count = 1)
        {
            var repeatedImage = image;
            for (int i = 0; i < count; i++)
            {
                repeatedImage = MergeImages(repeatedImage, image);
            }
            return repeatedImage;
        }

        /// <summary>
        /// Gets the main screen scale factor (for Retina support).
        /// Value is cached for use on background threads, as UIScreen.MainScreen.Scale must be peformed on the main thread. If the property is accessed for the first
        /// time on the background thread, it will call on the main thread and block the background thread until the value is populated.
        /// </summary>
        public static float ScaleFactor
        {
            get
            {
                if (_scaleFactor > 0.0f)
                    return _scaleFactor;

                if (NSRunLoop.Current == NSRunLoop.Main)
                {
                    _scaleFactor = UIScreen.MainScreen.Scale;
                    return _scaleFactor;
                }
                else
                {
                    NSRunLoop.Main.InvokeOnMainThread(() =>
                    {
                        if (_scaleFactor == 0.0f)
                        {
                            var x = ScaleFactor;
                        }
                    });

                    while (_scaleFactor == 0.0f)
                        System.Threading.Thread.Sleep(50);

                    return _scaleFactor;
                }
            }
        }
        /// <summary>
        /// Backing field for ScaleFactor property
        /// </summary>
        private static float _scaleFactor = 0.0f;

        /// <summary>
        /// Performs a delegate-supplied set of operations on a CGBitmapContext
        /// </summary>
        /// <param name="canvasWidth">Width of context in display-scaled units (scaling will be applied to go from display units to pixels for Retina support)</param>
        /// <param name="canvasHeight">Height of context in display-scaled units (scaling will be applied to go from display units to pixels for Retina support)</param>
        /// <param name="operations">delegate that will perform context operations</param>
        /// <returns>UIImage containing result of context operations</returns>
        public static UIImage PerformContextOperations(float canvasWidth, float canvasHeight, Action<CGBitmapContext> operations)
        {
            var scaleFactor = ScaleFactor;
            var contextWidth = (int)(canvasWidth * scaleFactor);
            var contextHeight = (int)(canvasHeight * scaleFactor);

            var colorSpace = MonoTouch.CoreGraphics.CGColorSpace.CreateDeviceRGB();
            var context = new CGBitmapContext(null, contextWidth, contextHeight, 8, 0, colorSpace, CGBitmapFlags.PremultipliedFirst);
            context.ScaleCTM(scaleFactor, scaleFactor);

            operations(context);

            var image = new UIImage(context.ToImage(), scaleFactor, UIImageOrientation.Up);

            context.Dispose();
            colorSpace.Dispose();

            return image;
        }

        /// <summary>
        /// Performs a delegate-supplied set of operations on a CGBitmapContext
        /// </summary>
        /// <param name="canvasSize">Size of context in display units (scaling will be applied to go from display units to pixels for Retina support)</param>
        /// <param name="operations">delegate that will perform context operations</param>
        /// <returns>UIImage containing result of context operations</returns>
        public static UIImage PerformContextOperations(SizeF canvasSize, Action<CGBitmapContext> operations)
        {
            return PerformContextOperations(canvasSize.Width, canvasSize.Height, operations);
        }

        /// <summary>
        /// Create a new image with another image drawn to the right of this image
        /// This is a convenience extension method to UIImage.
        /// </summary>
        /// <param name="leftImage">Left image</param>
        /// <param name="rightImage">Right image</param>
        /// <returns>Merged image</returns>
        public static UIImage AddImageOnRight(this UIImage leftImage, UIImage rightImage)
        {
            return MergeImages(leftImage, rightImage);
        }

        /// <summary>
        /// Create a new image with another image drawn on top of this image
        /// This is a convenience extention method to UIImage
        /// </summary>
        /// <param name="bottomImage">Bottom image</param>
        /// <param name="topImage">Top image</param>
        /// <returns>Merged image</returns>
        public static UIImage Superimpose(this UIImage bottomImage, UIImage topImage)
        {
            return SuperimposeImage(topImage, bottomImage);
        }

        /// <summary>
        /// Create a new image containing one or more repetitions of this image.
        /// </summary>
        /// <param name="image">Image to repeat</param>
        /// <param name="count">Number of times to perform the repeat operation (result will contain count + 1 copies)</param>
        /// <returns>Repetition result</returns>
        public static UIImage Repeat(this UIImage image, int count = 1)
        {
            return RepeatImage(image, count);
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *