在.NET Core中加载和处理图像

在本文中,我想谈谈我在实现.NET Core应用程序中加载图像的机制,然后调整其大小并将其保存到文件系统中的经验。对于图像处理,我使用了Six Labors 的跨平台ImageSharp有许多用于处理图像的库,但是因为 我正在开发一个跨平台应用程序,我想找到一个跨平台库。在撰写本文时,它们仍处于候选发布阶段,但社区表示一切正常,可以安全使用。
任务是从前端下载图像,将其裁剪为一定的宽高比并调整大小,以使保存的图像不会占用大量磁盘空间,因为云中的每兆字节都是金钱。

在前端,实现了一个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并保存。

除了一瞬间,这几乎就是一切。 service方法将返回文件的路径,但它不是URL,而是PATH,看起来像这样:logos\\yourFile.jpg如果将此PATH提供给浏览器,它将成功地找出它并向您显示图像,但是如果将其提供给移动应用程序,则不会看到该图像。尽管如此,我还是决定将PATH存储在数据库中以便能够访问文件(如果需要),并存储在前排的DTO中,我通过将其转换为URL来映射此字段。为此,我需要以下扩展方法

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

感谢您的关注,编写简洁的代码,请不要生病!

All Articles