В этой статье я хочу рассказать о своём опыте реализации механизма загрузки изображений в .NET Core приложении с последующим их ресайзом и сохранением в файловой системе. Для обработки изображений я использовал кроссплатформенную библиотеку ImageSharp от Six Labors. Существует множество различных библиотек для работы с изображениями, но т.к. я разрабатываю именно кроссплатформенное приложение, хотелось найти и кроссплатформенную библиотеку. На момент написания статьи они до сих пор находятся на стадии релиз-кандидата, но сообщество уверяет, что всё работает отлично и можно смело использовать.
Задачей было загружать изображение с фронта, обрезать его до определённого соотношения сторон и ресайзить, чтобы сохранённое изображение не съедало много дискового пространства, ведь каждый мегабайт в облаке — это деньги.
На фронте был реализован Angular-компонент, который сразу после выбора файла отправляет его на API метод для сохранения. API в свою очередь возвращает путь к уже сохранённому изображению, который потом сохраняется в БД. Т.к. эта статья не о Angular, то реализацию компонента опустим и перейдём сразу к API методу.
А если быть точнее, то к двум методам. Моей задачей было загружать изображения для двух различных назначений, которые могут быть разного размера и должны храниться в разных местах. Типы изображений указаны в Enum
Далее эти профайлы изображений я буду инжектить в метод сервиса, для этого нужно их зарегистрировать в DI-контейнере.
После этого в контроллер или сервис можно инжектить коллекцию
Теперь заглянем в метод сервиса, где мы всё это будем использовать. Напомню, что для процессинга изображения я использовал библиотеку
И несколько приватных методов, которые его обслуживают
Пару слов о методе
Это почти всё, кроме одного момента. Метод сервиса будет возвращать путь к файлу, но это будет не URL, а PATH и выглядеть это будет примерно так:
Спасибо за внимание, пишите чистый код и не болейте!
Задачей было загружать изображение с фронта, обрезать его до определённого соотношения сторон и ресайзить, чтобы сохранённое изображение не съедало много дискового пространства, ведь каждый мегабайт в облаке — это деньги.
На фронте был реализован 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, это режим, когда изображение ресайзится до достижения нужной длинны меньшей стороны изображения. Например, если исходное изображение 1000х2000, а моя цель получить 500х500, то после ресайза оно станет 500х1000, затем оно обрезается до 500х500 и сохраняется.Это почти всё, кроме одного момента. Метод сервиса будет возвращать путь к файлу, но это будет не URL, а PATH и выглядеть это будет примерно так:
logos\\yourFile.jpg. Если такой PATH скормить браузеру, то он с этим удачно разберётся и покажет вам изображение, но если вы это отдадите, например, мобильному приложению, то изображение вы не увидите. Не смотря на это я принял решение в БД хранить именно PATH, чтобы иметь возможность получить доступ к файлу, если потребуется, а в DTO, которое улетает на фронт я маплю это поле, конвертируя его в URL. Для этого мне нужен следующий extension метод public static string PathToUrl(this string path) { return path?.Replace("\\", "/"); }
Спасибо за внимание, пишите чистый код и не болейте!
