Задачей было загружать изображение с фронта, обрезать его до определённого соотношения сторон и ресайзить, чтобы сохранённое изображение не съедало много дискового пространства, ведь каждый мегабайт в облаке — это деньги.
На фронте был реализован 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("\\", "/");
}
Спасибо за внимание, пишите чистый код и не болейте!
Katsuko
Ваше продукт не противоречит лицензии ImageSharp?
slavontkn Автор
В данный момент нет