Resizing Graphics for iOS 12 Loading Image using DotNetCore 2.0

Posted in software by Christopher R. Wirz on Fri Dec 28 2018



In building an iPhone app, XCode requires you use a loading storyboard or a loading image. As there are still some major bugs with the storyboard mechanism, safe area padding, using an image asset is a much more supported approach. The problem is that the developer has to support it. In order to do that, a correct asset has to be made in the Assets.xcassets sub-folder along with a Contents.json file that looks like this:


{
  "images" : [
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "2688h",
      "filename" : "LoadingX_1242w2688h.png",
      "minimum-system-version" : "12.0",
      "orientation" : "portrait",
      "scale" : "3x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "2688h",
      "filename" : "LoadingX_2688w1242h.png",
      "minimum-system-version" : "12.0",
      "orientation" : "landscape",
      "scale" : "3x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "1792h",
      "filename" : "LoadingX_828w1792h.png",
      "minimum-system-version" : "12.0",
      "orientation" : "portrait",
      "scale" : "2x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "1792h",
      "filename" : "LoadingX_1792w828h.png",
      "minimum-system-version" : "12.0",
      "orientation" : "landscape",
      "scale" : "2x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "2436h",
      "filename" : "LoadingX_1125w2436h.png",
      "minimum-system-version" : "11.0",
      "orientation" : "portrait",
      "scale" : "3x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "2436h",
      "filename" : "LoadingX_2436w1125h.png",
      "minimum-system-version" : "11.0",
      "orientation" : "landscape",
      "scale" : "3x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "736h",
      "filename" : "LoadingX_1242w2208h.png",
      "minimum-system-version" : "8.0",
      "orientation" : "portrait",
      "scale" : "3x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "736h",
      "filename" : "LoadingX_2208w1242h.png",
      "minimum-system-version" : "8.0",
      "orientation" : "landscape",
      "scale" : "3x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "667h",
      "filename" : "LoadingX_750w1334h.png",
      "minimum-system-version" : "8.0",
      "orientation" : "portrait",
      "scale" : "2x"
    },
    {
      "orientation" : "portrait",
      "idiom" : "iphone",
      "filename" : "LoadingX_640w960h.png",
      "extent" : "full-screen",
      "minimum-system-version" : "7.0",
      "scale" : "2x"
    },
    {
      "extent" : "full-screen",
      "idiom" : "iphone",
      "subtype" : "retina4",
      "filename" : "LoadingX_640w1136h.png",
      "minimum-system-version" : "7.0",
      "orientation" : "portrait",
      "scale" : "2x"
    },
    {
      "orientation" : "portrait",
      "idiom" : "iphone",
      "filename" : "LoadingX_320w480h.png",
      "extent" : "full-screen",
      "scale" : "1x"
    },
    {
      "orientation" : "portrait",
      "idiom" : "iphone",
      "filename" : "LoadingX_640w960h.png",
      "extent" : "full-screen",
      "scale" : "2x"
    },
    {
      "orientation" : "portrait",
      "idiom" : "iphone",
      "filename" : "LoadingX_640w1136h.png",
      "extent" : "full-screen",
      "subtype" : "retina4",
      "scale" : "2x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

Note: In this example, I have made the image names depict the correct resolution.

Let's define a structure in C# that can provide this information...


using System;
using System.IO;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

struct ContentAsset
{
	public System.Drawing.Size Size;
	public int Scale;
	public string Extent;
	public string Orientation;
	public string MinimumSystemVersion;
	public string SubType;
	public string Device;
}


The goal is not to take an arbitrary image and stretch it to cover the required area without changing aspect ratio. Again, we turn to C# code...


public static partial class SystemDrawingFunctions
{
	/// <summary>
	///     Scales an image filling a size
	/// </summary>
	/// <param name="image">The image.</param>
	/// <param name="size">The size.</param>
	/// <param name="fitAspect">if set to <c>true</c> [fit aspect].</param>
	/// <returns>A new image of the desired size</returns>
	public static System.Drawing.Image ScaledToFill(
		this System.Drawing.Image image,
		System.Drawing.Size size,
		bool fitAspect = true)
	{
		double destX = 0;
		double destY = 0;
		double scaleRatio = 1;
		double scaleX = ((double)size.Width / (double)image.Width);
		double scaleY = ((double)size.Height / (double)image.Height);
		if (!fitAspect)
		{
			scaleRatio = Math.Min(scaleY, scaleX);
		}
		else
		{
			scaleRatio = Math.Max(scaleY, scaleX);
			destY = (size.Height - image.Height * scaleRatio) / 2;
			destX = (size.Width - image.Width * scaleRatio) / 2;
		}

		int destWidth = (int)Math.Round(image.Width * scaleRatio);
		int destHeight = (int)Math.Round(image.Height * scaleRatio);

		System.Drawing.Bitmap returnImage = new System.Drawing.Bitmap(size.Width, size.Height);
		using (System.Drawing.Graphics graphic = System.Drawing.Graphics.FromImage(returnImage))
		{
			graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
			graphic.CompositingQuality = CompositingQuality.HighQuality;
			graphic.SmoothingMode = SmoothingMode.HighQuality;

			Rectangle to = new System.Drawing.Rectangle(
				(int)Math.Round(destX), (int)Math.Round(destY),
				destWidth, destHeight);
			Rectangle from = new System.Drawing.Rectangle(0, 0, image.Width, image.Height);
			graphic.DrawImage(image, to, from, System.Drawing.GraphicsUnit.Pixel);

			return returnImage;
		}
	}

	/// <summary>
	///     Converts  System.Drawing.Image to a byte array.
	/// </summary>
	/// <param name="image">The image.</param>
	/// <returns>a raw byte array</returns>
	public static byte[] ToByteArray(this System.Drawing.Image image)
	{
		using (var ms = new MemoryStream())
		{
			image.Save(ms, image.RawFormat);
			return ms.ToArray();
		}
	}

	/// <summary>
	///     Scales an image byte array to fill.
	/// </summary>
	/// <param name="image">The image.</param>
	/// <param name="width">The width.</param>
	/// <param name="height">The height.</param>
	/// <param name="fitAspect">if set to <c>true</c> [fit aspect].</param>
	/// <returns>a raw byte array of the scaled image</returns>
	public static byte[] ScaledToFill(
		this byte[] image,
		int width = 196,
		int height = 196,
		bool fitAspect = true)
	{
		using (var ms = new MemoryStream(image))
		{
			return Image.FromStream(ms).ScaledToFill(new Size(width, height)).ToByteArray();
		}
	}
}

Now we can build out the list of images to generate, and get to work!


// Pick a sample image off the desktop
var img = @"C:\Users\admin\Desktop\imageToResize\LaunchImage.png";

var fi = new FileInfo(img);
using (var image = System.Drawing.Image.FromFile(img, true))
{
	var sizes = new ContentAsset[] {
	new ContentAsset(){ Size = new System.Drawing.Size(1242, 2688),
		Orientation="portrait", Extent="full-screen", Scale=3, SubType="2688h",
		MinimumSystemVersion ="12.0", Device ="iPhone Xs Max" },
	new ContentAsset(){ Size = new System.Drawing.Size(2688, 1242),
		Orientation="landscape", Extent="full-screen", Scale=3, SubType="2688h",
		MinimumSystemVersion ="12.0", Device ="iPhone Xs Max" },

	new ContentAsset(){ Size = new System.Drawing.Size(828, 1792),
		Orientation="portrait", Extent="full-screen", Scale=2, SubType="1792h",
		MinimumSystemVersion ="12.0", Device ="iPhone Xr Max" },
	new ContentAsset(){ Size = new System.Drawing.Size(1792, 828),
		Orientation ="landscape", Extent="full-screen", Scale=2, SubType="1792h",
		MinimumSystemVersion ="12.0", Device ="iPhone Xr Max" },

	new ContentAsset(){ Size = new System.Drawing.Size(1125, 2436),
		Orientation="portrait", Extent="full-screen", Scale=3, SubType="2436h",
		MinimumSystemVersion ="11.0", Device ="iPhone X" },
	new ContentAsset(){ Size = new System.Drawing.Size(2436, 1125),
		Orientation="landscape", Extent="full-screen", Scale=3, SubType="2436h",
		MinimumSystemVersion ="11.0", Device ="iPhone X Landscape" },

	new ContentAsset(){ Size = new System.Drawing.Size(1242, 2208),
		Orientation="portrait", Extent="full-screen", Scale=3, SubType="736h",
		MinimumSystemVersion ="8.0", Device ="iPhone 6s Plus - 8 Plus" },
	new ContentAsset(){ Size = new System.Drawing.Size(2208, 1242),
		Orientation="landscape", Extent="full-screen", Scale=3, SubType="736h",
		MinimumSystemVersion ="8.0", Device ="Retina HD 5.5" },

	new ContentAsset(){ Size = new System.Drawing.Size(750,1334), Extent="full-screen",
		Scale =2, SubType="667h", MinimumSystemVersion="8.0", Device ="iPhone 6s - 8" },

	new ContentAsset(){ Size = new System.Drawing.Size(640,960),
		Orientation="portrait", Extent="full-screen", Scale=2,
		MinimumSystemVersion ="7.0",  Device ="iPhone 4, 4s" },
	new ContentAsset(){ Size = new System.Drawing.Size(640,1136),
		Orientation="portrait", Extent="full-screen", Scale=2, SubType="retina4",
		MinimumSystemVersion ="7.0", Device ="iPhone 5, 5c, 5s" },
	new ContentAsset(){ Size = new System.Drawing.Size(320,480),
		Orientation="portrait", Extent="full-screen", Scale=1, Device ="iPhone 1g-3Gs" },
	new ContentAsset(){ Size = new System.Drawing.Size(640,960),
		Orientation="portrait", Extent="full-screen", Scale=2, Device ="iPhone 4, 4s" },
	new ContentAsset(){ Size = new System.Drawing.Size(640, 1136),
		Orientation="portrait", Extent="full-screen", Scale=2, SubType="retina4", Device ="Retina 4" }

		/// No longer used
		/*
		new ContentAsset(){ Size = new System.Drawing.Size(768,1024), Device ="iPad, iPad 2, Mini" },
		new ContentAsset(){ Size = new System.Drawing.Size(1024,768), Device ="iPad Landscape" },
		new ContentAsset(){ Size = new System.Drawing.Size(1536,2048), Device ="iPad Retina" },
		new ContentAsset(){ Size = new System.Drawing.Size(2048,1536), Device ="12.9\" iPad Pro" },
		*/
	};

	foreach (var size in sizes)
	{
		var newImage = ScaledToFill(image, size.Size);
		try
		{
			newImage.Save(Path.Combine(fi.DirectoryName,
				"LoadingX_" + size.Size.Width + "w" + size.Size.Height + "h" + fi.Extension));
		}
		catch { }
	}
	// This matches the Contents.json file generated above
}

The entire output directory can be zipped, then unzipped into the appropriate .loadingxcassets folder. XCode will recognize it as a loading image - with every component populated correctly.