تحميل ومعالجة الصور في .NET Core

في هذه المقالة ، أريد أن أتحدث عن تجربتي في تنفيذ آلية تحميل الصور في تطبيق .NET Core ، متبوعًا بتغيير حجمها وحفظها في نظام الملفات. لمعالجة الصور ، استخدمت مكتبة ImageSharp عبر الأنظمة الأساسية من Six Labors. هناك العديد من المكتبات المختلفة للعمل مع الصور ، ولكن بسبب أقوم بتطوير تطبيق عبر الأنظمة الأساسية ، أردت أن أجد مكتبة عبر الأنظمة الأساسية. في وقت كتابة هذا التقرير ، كانوا لا يزالون في مرحلة مرشح الإصدار ، لكن المجتمع يقول أن كل شيء يعمل بشكل جيد ويمكن استخدامه بأمان.
كانت المهمة تنزيل الصورة من الأمام ، واقتصاصها إلى نسبة عرض إلى ارتفاع معينة وتغيير حجمها حتى لا تستهلك الصورة المحفوظة مساحة كبيرة من القرص ، لأن كل ميجابايت في السحابة هو نقود.

في المقدمة ، تم تنفيذ مكون Angular ، والذي يرسله مباشرة بعد تحديد ملف إلى طريقة API للحفظ. بدوره ، يعيد API المسار إلى الصورة المحفوظة بالفعل ، والتي يتم حفظها بعد ذلك في قاعدة البيانات. لان نظرًا لأن هذه المقالة ليست حول Angular ، فإننا نحذف تنفيذ المكون وننتقل مباشرةً إلى طريقة 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);
    }
}

وأن نكون أكثر دقة ، ثم إلى طريقتين. كانت مهمتي هي تحميل الصور لغرضين مختلفين ، يمكن أن يكونا بأحجام مختلفة ويجب تخزينهما في أماكن مختلفة. يتم سرد أنواع الصور في Enum ImageType. ولوصف نوع الصورة ، قمت بإنشاء واجهة IImageProfileوتنفيذين لكل نوع من الصور ، والتي تحتوي على معلومات حول كيفية معالجة الصورة.

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

علاوة على ذلك ، سأقوم بحقن ملفات تعريف الصور هذه في طريقة الخدمة ، لذلك تحتاج إلى تسجيلها في حاوية DI.

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

بعد ذلك ، يمكنك حقن مجموعة في وحدة التحكم أو الخدمة IImageProfile

...
private readonly IEnumerable<IImageProfile> _imageProfiles;

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

الآن دعونا نلقي نظرة على طريقة الخدمة ، حيث سنستخدم كل هذا. دعني أذكرك بأنني لمعالجة المكتبة ImageSharp، استخدمت المكتبة التي يمكن العثور عليها في NuGet. في الطريقة التالية ، سأستخدم النوع Imageوطرقه للعمل مع الصورة. يمكن قراءة وثائق مفصلة هنا.

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

وبعض الطرق الخاصة التي تخدمها

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

بضع كلمات حول الطريقة Resize. في الوضع ResizeOptionsالمستخدم ResizeMode.Min، هذا هو الوضع عندما يتم تغيير حجم الصورة حتى يتم الوصول إلى الطول المطلوب للجانب الأصغر من الصورة. على سبيل المثال ، إذا كانت الصورة الأصلية هي 1000x2000 ، وهدفي هو الحصول على 500x500 ، فبعد تغيير حجمها ستصبح 500x1000 ، ثم يتم اقتصاصها إلى 500x500 وحفظها.

هذا هو كل شيء تقريبًا ، باستثناء لحظة واحدة. ستُرجع طريقة الخدمة المسار إلى الملف ، لكنها لن تكون عنوان URL ، بل مسارًا وستبدو كالتالي:logos\\yourFile.jpg. إذا قمت بإدخال PATH هذا إلى المستعرض ، فسوف يكتشفه بنجاح ويعرض لك الصورة ، ولكن إذا تركته ، على سبيل المثال ، إلى تطبيق محمول ، فلن ترى الصورة. على الرغم من ذلك ، قررت تخزين PATH في قاعدة البيانات حتى أتمكن من الوصول إلى الملف ، إذا لزم الأمر ، وفي DTO ، الذي يطير إلى الأمام ، أقوم بتعيين هذا الحقل عن طريق تحويله إلى عنوان URL. للقيام بذلك ، أحتاج إلى طريقة التمديد التالية

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

شكرًا لك على اهتمامك ، اكتب رمزًا نظيفًا ولا تمرض!

All Articles