Carga y procesamiento de imágenes en .NET Core

En este artículo quiero hablar sobre mi experiencia en la implementación del mecanismo para cargar imágenes en una aplicación .NET Core, seguido de su cambio de tamaño y guardado en el sistema de archivos. Para el procesamiento de imágenes, utilicé la biblioteca ImageSharp multiplataforma de Six Labors. Hay muchas bibliotecas diferentes para trabajar con imágenes, pero porque Estoy desarrollando una aplicación multiplataforma, quería encontrar una biblioteca multiplataforma. Al momento de escribir, todavía están en la etapa de candidato de liberación, pero la comunidad dice que todo funciona bien y puede usarse de manera segura.
La tarea consistía en descargar la imagen desde el frente, recortarla a una determinada relación de aspecto y cambiar su tamaño para que la imagen guardada no ocupe mucho espacio en disco, porque cada megabyte en la nube es dinero.

En el frente, se implementó un componente angular, que inmediatamente después de seleccionar un archivo lo envía al método API para guardarlo. La API, a su vez, devuelve la ruta a la imagen ya guardada, que luego se guarda en la base de datos. Porque Como este artículo no trata sobre Angular, omitimos la implementación del componente y vamos directamente al 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);
    }
}

Y para ser más precisos, entonces a dos métodos. Mi tarea consistía en cargar imágenes para dos propósitos diferentes, que pueden ser de diferentes tamaños y deben almacenarse en diferentes lugares. Los tipos de imágenes se enumeran en Enum ImageType. Y para describir el tipo de imagen, creé una interfaz IImageProfiley dos implementaciones para cada tipo de imagen, que contiene información sobre cómo se debe procesar la imagen.

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

A continuación, inyectaré estos perfiles de imagen en el método de servicio, para esto debe registrarlos en el contenedor DI.

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

Después de eso, puede inyectar una colección en el controlador o servicio IImageProfile

...
private readonly IEnumerable<IImageProfile> _imageProfiles;

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

Ahora veamos el método de servicio, donde usaremos todo esto. Permítame recordarle que para el procesamiento de imágenes, utilicé la biblioteca ImageSharp, que se puede encontrar en NuGet. En el siguiente método, usaré el tipo Imagey sus métodos para trabajar con la imagen. La documentación detallada se puede leer aquí.

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

Y algunos métodos privados que lo sirven

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

Algunas palabras sobre el método Resize. En ResizeOptionsSolía ResizeMode.Min, este es el modo cuando la imagen cambia de tamaño hasta que se alcanza la longitud deseada del lado más pequeño de la imagen. Por ejemplo, si la imagen original es 1000x2000 y mi objetivo es obtener 500x500, luego de cambiar el tamaño se convertirá en 500x1000, luego se recortará a 500x500 y se guardará.

Esto es casi todo, excepto por un momento. El método de servicio devolverá la ruta al archivo, pero no será una URL, sino una RUTA y se verá así:logos\\yourFile.jpg. Si alimenta esta RUTA al navegador, la resolverá con éxito y le mostrará la imagen, pero si la entrega, por ejemplo, a una aplicación móvil, no verá la imagen. A pesar de esto, decidí almacenar la RUTA en la base de datos para poder acceder al archivo, si es necesario, y en el DTO, que vuela al frente, mapeo este campo convirtiéndolo en una URL. Para hacer esto, necesito el siguiente método de extensión

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

Gracias por su atención, escriba un código limpio y no se enferme.

All Articles