Carregando e processando imagens no .NET Core

Neste artigo, quero falar sobre minha experiência na implementação do mecanismo de carregamento de imagens em um aplicativo .NET Core, seguido pelo redimensionamento e salvamento no sistema de arquivos. Para o processamento de imagens, usei a biblioteca ImageSharp de plataforma cruzada da Six Labors. Existem muitas bibliotecas diferentes para trabalhar com imagens, mas porque Estou desenvolvendo um aplicativo de plataforma cruzada, queria encontrar uma biblioteca de plataforma cruzada. No momento da redação, eles ainda estão no estágio de candidato a lançamento, mas a comunidade diz que tudo funciona bem e pode ser usado com segurança.
A tarefa era fazer o download da imagem de frente, cortá-la para uma determinada proporção e redimensionar para que a imagem salva não ocupasse muito espaço em disco, porque cada megabyte na nuvem é dinheiro.

Na frente, um componente Angular foi implementado, o qual imediatamente após a seleção de um arquivo o envia ao método API para salvar. A API, por sua vez, retorna o caminho para a imagem já salva, que é salva no banco de dados. Porque Como este artigo não é sobre Angular, omitimos a implementação do componente e vamos diretamente para o método 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);
    }
}

E para ser mais preciso, depois para dois métodos. Minha tarefa era fazer upload de imagens para dois propósitos diferentes, que podem ter tamanhos diferentes e devem ser armazenados em locais diferentes. Os tipos de imagem estão listados em Enum ImageType. E para descrever o tipo de imagem, criei uma interface IImageProfilee duas implementações para cada tipo de imagem, que contêm informações sobre como a imagem deve ser processada.

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

Em seguida, injetarei esses perfis de imagem no método de serviço, para isso você precisará registrá-los no contêiner de DI.

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

Depois disso, você pode injetar uma coleção no controlador ou serviço IImageProfile

...
private readonly IEnumerable<IImageProfile> _imageProfiles;

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

Agora, vejamos o método de serviço, onde usaremos tudo isso. Deixe-me lembrá-lo que, para o processamento de imagens, usei a biblioteca ImageSharp, que pode ser encontrada no NuGet. No próximo método, usarei o tipo Imagee seus métodos para trabalhar com a imagem. A documentação detalhada pode ser lida aqui.

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

E alguns métodos particulares que o servem

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

Algumas palavras sobre o método Resize. No que ResizeOptionseu usei ResizeMode.Min, esse é o modo em que a imagem é redimensionada até que o comprimento desejado do lado menor da imagem seja atingido. Por exemplo, se a imagem original é 1000x2000 e meu objetivo é obter 500x500, depois de redimensioná-la, ela se tornará 500x1000, e será cortada para 500x500 e salva.

Isso é quase tudo, exceto por um momento. O método de serviço retornará o caminho para o arquivo, mas não será uma URL, mas um PATH e será algo parecido com isto:logos\\yourFile.jpg. Se você alimentar esse PATH no navegador, ele descobrirá e exibirá a imagem com êxito, mas se você o distribuir, por exemplo, para um aplicativo móvel, você não verá a imagem. Apesar disso, decidi armazenar o PATH no banco de dados para poder acessar o arquivo, se necessário, e no DTO, que voa para a frente, mapeio esse campo convertendo-o em um URL. Para fazer isso, preciso do seguinte método de extensão

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

Obrigado por sua atenção, escreva um código limpo e não fique doente!

All Articles