Chargement et traitement d'images dans .NET Core

Dans cet article, je veux parler de mon expérience dans la mise en œuvre du mécanisme de chargement d'images dans une application .NET Core, suivi de leur redimensionnement et de leur enregistrement dans le système de fichiers. Pour le traitement d'image, j'ai utilisé la bibliothèque multiplateforme ImageSharp de Six Labors. Il existe de nombreuses bibliothèques différentes pour travailler avec des images, mais parce que Je développe une application multiplateforme, je voulais trouver une bibliothèque multiplateforme. Au moment d'écrire ces lignes, ils sont encore au stade de la version candidate, mais la communauté dit que tout fonctionne bien et peut être utilisé en toute sécurité.
La tâche consistait à télécharger l'image de face, à la recadrer dans un certain rapport d'aspect et à la redimensionner afin que l'image enregistrée ne consomme pas beaucoup d'espace disque, car chaque mégaoctet dans le cloud est de l'argent.

À l'avant, un composant angulaire a été implémenté, qui immédiatement après la sélection d'un fichier l'envoie à la méthode API pour l'enregistrement. L'API, à son tour, renvoie le chemin d'accès à l'image déjà enregistrée, qui est ensuite enregistrée dans la base de données. Parce que Étant donné que cet article ne concerne pas Angular, nous omettons l'implémentation du composant et passons directement à la méthode API.

[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);
    }
}

Et pour être plus précis, alors à deux méthodes. Ma tâche consistait à télécharger des images à deux fins différentes, qui peuvent être de tailles différentes et doivent être stockées à différents endroits. Les types d'images sont répertoriés dans Enum ImageType. Et pour décrire le type d'image, j'ai créé une interface IImageProfileet deux implémentations pour chaque type d'image, qui contiennent des informations sur la façon dont l'image doit être traitée.

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; }
}

De plus, j'injecterai ces profils d'image dans la méthode de service, pour cela, vous devez les enregistrer dans le conteneur DI.

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

Après cela, vous pouvez injecter une collection dans le contrôleur ou le service IImageProfile

...
private readonly IEnumerable<IImageProfile> _imageProfiles;

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

Voyons maintenant la méthode de service, où nous utiliserons tout cela. Permettez-moi de vous rappeler que pour le traitement d'images, j'ai utilisé la bibliothèque ImageSharp, qui se trouve dans NuGet. Dans la méthode suivante, j'utiliserai le type Imageet ses méthodes pour travailler avec l'image. Une documentation détaillée peut être lue ici.

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);
}

Et quelques méthodes privées qui le servent

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);
}

Quelques mots sur la méthode Resize. Dans ResizeOptionsJ'ai utilisé ResizeMode.Min, c'est le mode lorsque l'image est redimensionnée jusqu'à ce que la longueur souhaitée du plus petit côté de l'image soit atteinte. Par exemple, si l'image d'origine est 1000x2000, et mon objectif est d'obtenir 500x500, puis après le redimensionnement, elle deviendra 500x1000, puis elle sera rognée à 500x500 et enregistrée.

C'est presque tout, sauf un instant. La méthode de service renverra le chemin d'accès au fichier, mais ce ne sera pas une URL, mais un CHEMIN et cela ressemblera à ceci:logos\\yourFile.jpg. Si vous introduisez ce CHEMIN dans le navigateur, il réussira à le comprendre et à vous montrer l'image, mais si vous le donnez, par exemple, à une application mobile, vous ne verrez pas l'image. Malgré cela, j'ai décidé de stocker le PATH dans la base de données afin de pouvoir accéder au fichier si nécessaire, et dans le DTO, qui vole vers l'avant, je mappe ce champ en le convertissant en URL. Pour ce faire, j'ai besoin de la méthode d'extension suivante

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

Merci de votre attention, écrivez du code propre et ne soyez pas malade!

All Articles