Loading and processing images in .NET Core

In this article I want to talk about my experience in implementing the mechanism for loading images in a .NET Core application, followed by their resize and save to the file system. For image processing, I used the cross-platform ImageSharp library from Six Labors. There are many different libraries for working with images, but because I’m developing a cross-platform application, I wanted to find a cross-platform library. At the time of writing, they are still at the release candidate stage, but the community says that everything works fine and can be safely used.
The task was to download the image from the front, crop it to a certain aspect ratio and resize so that the saved image does not eat up a lot of disk space, because every megabyte in the cloud is money.

At the front, an Angular component was implemented, which immediately after selecting a file sends it to the API method for saving. The API, in turn, returns the path to the already saved image, which is then saved in the database. Because Since this article is not about Angular, we omit the implementation of the component and go straight to the API method.

[HttpPost]
[Route("upload/box")]
public IActionResult UploadBoxImage(IFormFile file)
{
    return UploadImage(file, ImageType.Box);
}

[HttpPost]
[Route("upload/logo")]
public IActionResult UploadLogoImage(IFormFile file)
{
    return UploadImage(file, ImageType.Logo);
}

private IActionResult UploadImage(IFormFile file, ImageType type)
{
    if (file.Length == 0)
        return BadRequest(new ApiResponse(ErrorCodes.EmptyFile, Strings.EmptyFile));

    try
    {
        var filePath = _imageService.SaveImage(file, type);
        return Ok(new ApiResponse<string>(filePath));
    }
    catch (ImageProcessingException ex)
    {
        var response = new ApiResponse(ErrorCodes.ImageProcessing, ex.Message);
        return BadRequest(response);
    }
    catch (Exception ex)
    {
        var response = new ApiResponse(ErrorCodes.Unknown, ex.Message);
        return BadRequest(response);
    }
}

And to be more precise, then to two methods. My task was to upload images for two different purposes, which can be of different sizes and must be stored in different places. Image types are listed in Enum ImageType. And to describe the type of image, I created an interface IImageProfileand two implementations for each type of image, which contains information about how the image should be processed.

public interface IImageProfile
{
    ImageType ImageType { get; }

    string Folder { get; }

    int Width { get; }

    int Height { get; }

    int MaxSizeBytes { get; }

    IEnumerable<string> AllowedExtensions { get; }
}

public class BoxImageProfile : IImageProfile
{
    private const int mb = 1048576;

    public BoxImageProfile()
    {
        AllowedExtensions = new List<string> { ".jpg", ".jpeg", ".png", ".gif" };
    }

    public ImageType ImageType => ImageType.Box;

    public string Folder => "boxes";

    public int Width => 500;

    public int Height => 500;

    public int MaxSizeBytes => 10 * mb;

    public IEnumerable<string> AllowedExtensions { get; }
}

public class LogoImageProfile : IImageProfile
{
    private const int mb = 1048576;

    public LogoImageProfile()
    {
        AllowedExtensions = new List<string> { ".jpg", ".jpeg", ".png", ".gif" };
    }

    public ImageType ImageType => ImageType.Logo;

    public string Folder => "logos";

    public int Width => 300;

    public int Height => 300;

    public int MaxSizeBytes => 5 * mb;

    public IEnumerable<string> AllowedExtensions { get; }
}

Next, I will inject these image profiles into the service method, for this you need to register them in the DI container.

...
services.AddTransient<IImageProfile, BoxImageProfile>();
services.AddTransient<IImageProfile, LogoImageProfile>();
...

After that, you can inject a collection into the controller or service IImageProfile

...
private readonly IEnumerable<IImageProfile> _imageProfiles;

public ImageService(IEnumerable<IImageProfile> imageProfiles)
{
    ...
    _imageProfiles = imageProfiles;
}

Now let's look at the service method, where we will use all this. Let me remind you that for image processing, I used the library ImageSharp, which can be found in NuGet. In the next method, I will use the type Imageand its methods for working with the image. Detailed documentation can be read here.

public string SaveImage(IFormFile file, ImageType imageType)
{
    var imageProfile = _imageProfiles.FirstOrDefault(profile => 
                       profile.ImageType == imageType);

    if (imageProfile == null)
        throw new ImageProcessingException("Image profile has not found");

    ValidateExtension(file, imageProfile);
    ValidateFileSize(file, imageProfile);

    var image = Image.Load(file.OpenReadStream());

    ValidateImageSize(image, imageProfile);

    var folderPath = Path.Combine(_hostingEnvironment.WebRootPath, imageProfile.Folder);

    if (!Directory.Exists(folderPath))
        Directory.CreateDirectory(folderPath);

    string filePath;
    string fileName;

    do
    {
        fileName = GenerateFileName(file);
        filePath = Path.Combine(folderPath, fileName);
    } while (File.Exists(filePath));

    Resize(image, imageProfile);
    Crop(image, imageProfile);
    image.Save(filePath, new JpegEncoder { Quality = 75 });

    return Path.Combine(imageProfile.Folder, fileName);
}

And a few private methods that serve it

private void ValidateExtension(IFormFile file, IImageProfile imageProfile)
{
    var fileExtension = Path.GetExtension(file.FileName);

    if (imageProfile.AllowedExtensions.Any(ext => ext == fileExtension.ToLower()))
        return;

    throw new ImageProcessingException(Strings.WrongImageFormat);
}

private void ValidateFileSize(IFormFile file, IImageProfile imageProfile)
{
    if (file.Length > imageProfile.MaxSizeBytes)
        throw new ImageProcessingException(Strings.ImageTooLarge);
}

private void ValidateImageSize(Image image, IImageProfile imageProfile)
{
    if (image.Width < imageProfile.Width || image.Height < imageProfile.Height)
        throw new ImageProcessingException(Strings.ImageTooSmall);
}

private string GenerateFileName(IFormFile file)
{
    var fileExtension = Path.GetExtension(file.FileName);
    var fileName = Path.GetFileNameWithoutExtension(Path.GetRandomFileName());

    return $"{fileName}{fileExtension}";
}

private void Resize(Image image, IImageProfile imageProfile)
{
    var resizeOptions = new ResizeOptions
    {
        Mode = ResizeMode.Min,
        Size = new Size(imageProfile.Width)
    };

    image.Mutate(action => action.Resize(resizeOptions));
}

private void Crop(Image image, IImageProfile imageProfile)
{
    var rectangle = GetCropRectangle(image, imageProfile);
    image.Mutate(action => action.Crop(rectangle));
}

private Rectangle GetCropRectangle(IImageInfo image, IImageProfile imageProfile)
{
    var widthDifference = image.Width - imageProfile.Width;
    var heightDifference = image.Height - imageProfile.Height;
    var x = widthDifference / 2;
    var y = heightDifference / 2;

    return new Rectangle(x, y, imageProfile.Width, imageProfile.Height);
}

A few words about the method Resize. In ResizeOptionsI used ResizeMode.Min, this is the mode when the image is resized until the desired length of the smaller side of the image is reached. For example, if the original image is 1000x2000, and my goal is to get 500x500, then after resizing it will become 500x1000, then it is cropped to 500x500 and saved.

This is almost everything, except for one moment. The service method will return the path to the file, but it will not be a URL, but a PATH and it will look something like this:logos\\yourFile.jpg. If you feed this PATH to the browser, then it will successfully figure it out and show you the image, but if you give it away, for example, to a mobile application, you will not see the image. Despite this, I decided to store the PATH in the database in order to be able to access the file if necessary, and in the DTO, which flies to the front, I map this field by converting it to a URL. To do this, I need the following extension method

public static string PathToUrl(this string path)
{
    return path?.Replace("\\", "/");
}

Thank you for your attention, write clean code and don’t be sick!

All Articles