Laden und Verarbeiten von Bildern in .NET Core

In diesem Artikel möchte ich über meine Erfahrungen bei der Implementierung des Mechanismus zum Laden von Bildern in einer .NET Core-Anwendung sprechen, gefolgt von deren Größenänderung und Speichern im Dateisystem. Für die Bildverarbeitung habe ich die plattformübergreifende ImageSharp- Bibliothek von Six Labors verwendet. Es gibt viele verschiedene Bibliotheken für die Arbeit mit Bildern, aber weil Ich entwickle eine plattformübergreifende Anwendung und wollte eine plattformübergreifende Bibliothek finden. Zum Zeitpunkt des Schreibens befinden sie sich noch im Stadium der Release-Kandidaten, aber die Community sagt, dass alles gut funktioniert und sicher verwendet werden kann.
Die Aufgabe bestand darin, das Bild von vorne herunterzuladen, auf ein bestimmtes Seitenverhältnis zuzuschneiden und die Größe zu ändern, damit das gespeicherte Bild nicht viel Speicherplatz beansprucht, da jedes Megabyte in der Cloud Geld ist.

Im Vordergrund wurde eine Angular-Komponente implementiert, die unmittelbar nach Auswahl einer Datei zum Speichern an die API-Methode gesendet wird. Die API gibt wiederum den Pfad zum bereits gespeicherten Bild zurück, das dann in der Datenbank gespeichert wird. weil Da es in diesem Artikel nicht um Angular geht, lassen wir die Implementierung der Komponente weg und gehen direkt zur API-Methode.

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

Genauer gesagt zu zwei Methoden. Meine Aufgabe war es, Bilder für zwei verschiedene Zwecke hochzuladen, die unterschiedlich groß sein können und an verschiedenen Orten gespeichert werden müssen. Bildtypen sind in Enum aufgeführt ImageType. IImageProfileUm den Bildtyp zu beschreiben, habe ich für jeden Bildtyp eine Schnittstelle und zwei Implementierungen erstellt, die Informationen darüber enthalten, wie das Bild verarbeitet werden soll.

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

Außerdem werde ich diese Bildprofile in die Servicemethode einfügen. Dazu müssen Sie sie im DI-Container registrieren.

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

Danach können Sie eine Sammlung in den Controller oder Service einfügen IImageProfile

...
private readonly IEnumerable<IImageProfile> _imageProfiles;

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

Schauen wir uns nun die Servicemethode an, bei der wir all dies verwenden werden. Ich möchte Sie daran erinnern, dass ich für die Bildverarbeitung die Bibliothek verwendet habe ImageSharp, die in NuGet zu finden ist. In der nächsten Methode werde ich den Typ Imageund seine Methoden für die Arbeit mit dem Bild verwenden. Eine ausführliche Dokumentation finden Sie hier.

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

Und ein paar private Methoden, die ihm dienen

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

Ein paar Worte zur Methode Resize. In ResizeOptionsI verwendet ResizeMode.Min, ist dies der Modus, in dem die Größe des Bildes geändert wird, bis die gewünschte Länge der kleineren Seite des Bildes erreicht ist. Wenn das Originalbild beispielsweise 1000 x 2000 ist und mein Ziel darin besteht, 500 x 500 zu erhalten, wird es nach der Größenänderung 500 x 1000, dann wird es auf 500 x 500 zugeschnitten und gespeichert.

Das ist fast alles, bis auf einen Moment. Die Dienstmethode gibt den Pfad zur Datei zurück, es handelt sich jedoch nicht um eine URL, sondern um einen PFAD, und es sieht ungefähr so ​​aus:logos\\yourFile.jpg. Wenn Sie diesen Pfad dem Browser zuführen, wird er erfolgreich ermittelt und das Bild angezeigt. Wenn Sie ihn jedoch beispielsweise an eine mobile Anwendung weitergeben, wird das Bild nicht angezeigt. Trotzdem habe ich beschlossen, den PATH in der Datenbank zu speichern, um bei Bedarf auf die Datei zugreifen zu können, und im DTO, das nach vorne fliegt, ordne ich dieses Feld zu, indem ich es in eine URL konvertiere. Dazu benötige ich die folgende Erweiterungsmethode

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

Vielen Dank für Ihre Aufmerksamkeit, schreiben Sie sauberen Code und seien Sie nicht krank!

All Articles