I came across SkiaSharpImageMerger project in Github and was inspired to try out a Xamarin.Forms app that leverages Skiasharp to generate a simple collage image. Link to Github repo. Here is the result.
Collaged image generation is done by the ImageMergerHelper. Combine ( List<string> files)
method in ImageMergerHelper static class. Let us analyse on a higher-level what is happening here. This method accepts a list of strings
(paths of image files to be collaged) and returns the path to the collaged image.
ImageMergerHelper: Helper class to generate collaged image
public static string Combine(List<string> files)
{
//read all images into memory
List<SKBitmap> images = new List<SKBitmap>();
SKImage finalImage = null;
List<SKRect> outputRects = new List<SKRect>();
List<SKRect> croppingRects = new List<SKRect>();
SKImageInfo newCanvasInfo;
List<SKSizeI> inputSizes = new List<SKSizeI>();
try
{
foreach (string image in files)
{
//create a bitmap from the file and add it to the list
SKBitmap bitmap = SKBitmap.Decode(image);
inputSizes.Add(bitmap.Info.Size);
images.Add(bitmap);
}
CalculateDimesnions(inputSizes, out outputRects, out croppingRects, out newCanvasInfo);
//get a surface so we can draw an image
using (var tempSurface = SKSurface.Create(newCanvasInfo))
{
//get the drawing canvas of the surface
var canvas = tempSurface.Canvas;
//set background color
canvas.Clear(borderColor);
int position = 0;
foreach (SKBitmap image in images)
{
SKRect rect = outputRects[position];
SKRect cropRect = croppingRects[position];
position++;
canvas.DrawBitmap(image, cropRect, rect);
}
// return the surface as a manageable image
finalImage = tempSurface.Snapshot();
}
//return the image that was just drawn
string documentBasePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string filePath = documentBasePath + "/collageImage.png";
//save the new image
using (SKData encoded = finalImage.Encode(SKEncodedImageFormat.Png, 100))
using (Stream outFile = File.OpenWrite(filePath))
{
encoded.SaveTo(outFile);
}
return filePath;
}
finally
{
//clean up memory
foreach (SKBitmap image in images)
{
image.Dispose();
}
}
}
This method is pretty simple. It reads all the input images and creates SKBitmap
s with them. Then it calculates some dimensions in CalculateDimesnions
method.
outputRects
:SKRect
s of each image in theSKCanvas
of new image collage.croppingRects
:SKRect
s which defines what is the cropping rectangle for each image. This is done to respect the aspect ratios of the original images. The result will be the same asAspectFill
in Xamarin.FormsImage
s.newCanvasInfo
:SKImageInfo
of the new canvas, mainly to specify what should be the size of the new canvas to be created.
We will see how they are calculated in the next code snippet. A SKCanvas
is created with newCanvasInfo
and is painted with borderColor
. This is the colour of the collage frame. Then we draw the cropped portion of each image to the canvas at corresponding outputRects
. Voila, our collage canvas is ready to be exported. It’s exported as a png image and the path to this file is returned by the method. Let’s explore CalculateDimesnions()
now
private static void CalculateDimesnions(List<SKSizeI> inputSizes, out List<SKRect> outputRects, out List<SKRect> croppingRects, out SKImageInfo newCanvasInfo)
{
outputRects = new List<SKRect>();
croppingRects = new List<SKRect>();
float screenWidth = (float)DeviceDisplay.MainDisplayInfo.Width;
float singleImageDimension = screenWidth - (2 * borderWidth);
float doubleImageDimension = (screenWidth / 2) - ((float)(1.5) * borderWidth);
float tripleImageDimension = (screenWidth / 3) - ((4f / 3f) * borderWidth);
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)screenWidth);
//If selected images are more than one, they are cropped so as to have square aspect ratio
//as each slot in the photogrid has square aspect ratio. If it is a single image, original aspect ratio is respectd.
if (inputSizes.Count > 1)
{
foreach (SKSize inputSize in inputSizes)
{
float cropRectDimensionSize = Math.Min(inputSize.Width, inputSize.Height);
float cropRectX = 0, cropRectY = 0;
if (cropRectDimensionSize < inputSize.Height)
{
cropRectY = (inputSize.Height - cropRectDimensionSize) / 2f;
}
if (cropRectDimensionSize < inputSize.Width)
{
cropRectX = (inputSize.Width - cropRectDimensionSize) / 2f;
}
croppingRects.Add(SKRect.Create(cropRectX, cropRectY, cropRectDimensionSize, cropRectDimensionSize));
}
}
switch (inputSizes.Count)
{
case 1:
float scale = inputSizes[0].Width / singleImageDimension;
float firstImageHeight = inputSizes[0].Height / scale;
outputRects.Add(SKRect.Create(borderWidth, borderWidth, singleImageDimension, firstImageHeight));
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)(firstImageHeight + (2 * borderWidth)));
croppingRects.Add(SKRect.Create(0, 0, inputSizes[0].Width, inputSizes[0].Height));
break;
case 2:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, doubleImageDimension, doubleImageDimension));
float secondImageXPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, doubleImageDimension, doubleImageDimension));
newCanvasInfo = new SKImageInfo((int)screenWidth, ((int)borderWidth * 2) + (int)doubleImageDimension);
break;
case 3:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageXPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, doubleImageDimension, doubleImageDimension));
float secondImageYPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, singleImageDimension, singleImageDimension));
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)secondImageYPosition + (int)singleImageDimension + (int)borderWidth);
break;
case 4:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageXPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageYPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, doubleImageDimension, doubleImageDimension));
outputRects.Add(SKRect.Create(secondImageXPosition, secondImageYPosition, doubleImageDimension, doubleImageDimension));
break;
case 5:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageXPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageYPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, tripleImageDimension, tripleImageDimension));
float secondImageInSecondRowXPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageInSecondRowXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
float thirdImageInSecondRowXPosition = (2 * tripleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(thirdImageInSecondRowXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)secondImageYPosition + (int)tripleImageDimension + (int)borderWidth);
break;
case 6:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, tripleImageDimension, tripleImageDimension));
secondImageXPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, tripleImageDimension, tripleImageDimension));
float thirdImageXPosition = (2 * tripleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(thirdImageXPosition, borderWidth, tripleImageDimension, tripleImageDimension));
secondImageYPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(secondImageXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(thirdImageXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)secondImageYPosition + (int)tripleImageDimension + (int)borderWidth);
break;
case 7:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageXPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageYPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, doubleImageDimension, doubleImageDimension));
outputRects.Add(SKRect.Create(secondImageXPosition, secondImageYPosition, doubleImageDimension, doubleImageDimension));
float thirdImageYPosition = (2 * doubleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
float secondImageInThirdRowXPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageInThirdRowXPosition, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
thirdImageXPosition = (2 * tripleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(thirdImageXPosition, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)thirdImageYPosition + (int)tripleImageDimension + (int)borderWidth);
break;
case 8:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageXPosition = doubleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, doubleImageDimension, doubleImageDimension));
secondImageYPosition = doubleImageDimension + (2 * borderWidth);
secondImageInSecondRowXPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(secondImageInSecondRowXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
thirdImageInSecondRowXPosition = (2 * tripleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(thirdImageInSecondRowXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
thirdImageYPosition = (tripleImageDimension + doubleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(secondImageInSecondRowXPosition, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(thirdImageInSecondRowXPosition, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
newCanvasInfo = new SKImageInfo((int)screenWidth, (int)thirdImageYPosition + (int)tripleImageDimension + (int)borderWidth);
break;
case 9:
outputRects.Add(SKRect.Create(borderWidth, borderWidth, tripleImageDimension, tripleImageDimension));
secondImageXPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(secondImageXPosition, borderWidth, tripleImageDimension, tripleImageDimension));
thirdImageXPosition = (2 * tripleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(thirdImageXPosition, borderWidth, tripleImageDimension, tripleImageDimension));
secondImageYPosition = tripleImageDimension + (2 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, secondImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(secondImageXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(thirdImageXPosition, secondImageYPosition, tripleImageDimension, tripleImageDimension));
thirdImageYPosition = (2 * tripleImageDimension) + (3 * borderWidth);
outputRects.Add(SKRect.Create(borderWidth, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(secondImageXPosition, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
outputRects.Add(SKRect.Create(thirdImageXPosition, thirdImageYPosition, tripleImageDimension, tripleImageDimension));
break;
default:
throw (new Exception("Cannot merge more than 9 images."));
}
}
Here, we first calculate the cropping rectangles for each image. Since we want AspectFill like feature, we take the smaller dimension from the height and width of the original image as the edge size of the cropping rectangle. Next, we calculate the outputrects for each image. Dimensions of each image is calculated so that the total width of the collaged image will match the device screen width. We have a unique layout defined for the photocard based on the number of input images in that loooong switch-case.
In the sample application, Xamarin.Essentials has been used for handling permissions, sharing the image to other apps and Xam.Plugin.Media for picking up the image. Please refer to my previous post on Xamarin.Forms.VideoTrimmer on how I have used them.
Happy Coding :-)