• ASP.NET Core Library – ImageSharp


    前言

    2021 年就写过一篇了, Asp.net core 学习笔记 Image processing (ImageSharp), 只是那时还是旧的写法, 这篇作为翻新和以后继续增加新功能的介绍.

    ImageSharp 是 .NET 平台开源的图片处理 Library. 完全用 C# 来写, 从 0 开始写. 写了很多年, 目前算是比较 ok 用了. 2022 年开始也支持 webp 了哦.

    参考

    官网 Docs

    安装

    dotnet add package SixLabors.ImageSharp

    查看图片信息

    var fileFullPath = @"C:\keatkeat\projects\stooges-lib\stooges-aspnetcore\Project\wwwroot\uploaded-files\vertical-huawei.jpg";
    using var image = Image.Load(fileFullPath);
    var w = image.Width;
    var h = image.Height;
    var exifOrientation = image.Metadata.ExifProfile.GetValue(ExifTag.Orientation);

    手机图片经常会出现反转问题, 可以查看 Exif Orientation 信息. 关于这个问题可以看我之前写的: Image Exif Orientation 图片方向信息

    修改图片

    using var newImage = image.Clone(imageProcessing =>
    {
        imageProcessing.AutoOrient();
        imageProcessing.Resize((int)Decimal.Divide(image.Width, 2), (int)Decimal.Divide(image.Width, 2));
        imageProcessing.Crop(new Rectangle(x: 150, y: 150,  150, height: 150));
        imageProcessing.Rotate(RotateMode.Rotate90);
        imageProcessing.Flip(FlipMode.Vertical);
    });
    newImage.SaveAsJpeg(
        @"C:\keatkeat\projects\stooges-lib\stooges-aspnetcore\Project\wwwroot\uploaded-files\vertical-huawei-croped.jpg", new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder
        {
            Quality = 85
        }
    );

    Clone() 是复制一张来做修改, 如果想改原图拿就用 .Mutate(). 注: clone 记得使用 using 哦

    AutoOrient 就是依据 Exif Orientation 来旋转和 flip 图片, 超方便的. 不需要自己搞, 转换之后 Exif Orientation 会变成 1 (哪怕之前是 0, 比如华为手机是 0)

    resize, crop, rotate, flip 也是常见的图片操作, 还有一个 crop avatar 的 Github Sample.

    修改 background color

    png to jpg 默认 background color 是黑色的. 参考: Converting Png to Jpg give a color background and not white (Asp.Net Core)

    var clonedImage = image.Clone(imageProcessing =>
    {
        imageProcessing.BackgroundColor(Color.White);
    });

    想保存为 webp, 调用 SaveAsWebp 就可以了哦.

    await image.SaveAsWebpAsync(Path.Combine(rootPath, "tifa2.webp"));

    注: SaveAs... 是可以多次调用的. 它内部有处理好 stream reading 了.

    水印 Watermark

    水印的做法是在一张图上面, 添加上另一张图, 同时第二张图需要带有点 opacity 的效果.

    using var tifa = await Image.LoadAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\tifa.jpg"); // 原图
    using var logo = await Image.LoadAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\Stooges Logo.png"); // 水印
    // Clone 做出 new image
    using var newImage = tifa.Clone(imageProcessing =>
    {
        logo.Mutate(x => x.StgResizeWidth(200)); // 缩小 logo (这个不是必须的, 刚巧我的图片比较大而已)
        imageProcessing.DrawImage(
            logo,
            opacity: 0.5f,
            location: new Point(100, 100)
        );
        // imageProcessing 就是原图
        // DrawImage 就是在图上画画
        // logo 就是把水印画上去的意思
        // opacity 给 logo 加上透明度
        // location 是 x,y 坐标, 看你想把水印打到哪里
    });
    await newImage.SaveAsJpegAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\new-image.jpg"); // 保存

    效果

    New Image for object-fit: content 效果

    CSS object-fit 效果, 把一张大图按比例缩小到框框里, 并且保留所有图片信息 (留白)

    using var tifa = await Image.LoadAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\tifa.jpg");
    using var newImage = new Image<Rgba32>(500, 500, backgroundColor: Rgba32.ParseHex("#fff")); // hex 或者 Color.White 都可以
    tifa.Mutate(imageProcessing =>
    {
        imageProcessing.Resize( 500, height: 500 * tifa.Height / tifa.Width); // 按比例缩小
    });
    newImage.Mutate(imageProcessing =>
    {
        // 画到中间去
        imageProcessing.DrawImage(tifa, location: new Point(0, (500 / 2) - (tifa.Height / 2)), opacity: 1);
    });
    await newImage.SaveAsJpegAsync(@"C:\Users\keatk\Desktop\temp\review-avatar\tifa-contain.jpg");

    效果

    扩展 ImageProcessing

    虽然 imageProcessing 已经有很多好用的接口了, 但是不够上层.

    resize width keep aspect ratio

    比如我有一张图, dimension 是 1349 x 761

    我想把它 resize to width 500, aspect ratio 保持

    那可以这样写

    var clonedImage = image.Clone(imageProcessing =>
    {
        imageProcessing.Resize( 500, height: 500 * image.Height / image.Width);
    });

    它的缺点就是要写计算. 这就是所谓的不够上层. 那我们自己封装一下

    封装 extensions

    调用

    var clonedImage = image.Clone(imageProcessing =>
    {
        imageProcessing.MyResizeWidth(500);
    });
    extensions
    public static class ImageProcessingExtensions
    {
        public static IImageProcessingContext MyResizeWidth(this IImageProcessingContext imageProcessing, int width)
        {
            imageProcessing.Resize(width, height: width * originalImage.Height / originalImage.Width);
            return imageProcessing;
        }
    }

    这里遇到了一个问题, 算法需要 original image 的 dimension. 上面我们用了闭包才拿到的. 封装以后就拿不到了.

    难不成要通过 parameter 传进来 (这样接口调用就扣分了丫). 还是可以从 IImageProcessingContext 里获取呢? 

    在 IImageProcessingContext 如何获取 Image

    源码追踪

    首先去看看文档, 没发现类似的案例. 也不奇怪啦. 大部分文档都对扩展不友好的. 还是翻源码看看呗.

    GetCurrentSize 最接近, 嗯... 通常 current size 已经足够我们用了, 但既然来了, 就再看看能不能拿到 original image 呗.

    我们继续翻 Clone 的源码

    AcceptVisitor 调用了 Accept

    Accept 又调用了 visitor 的 Visit 并把 Image 自己传进去. 

     继续看 Visitor 的 visit 

    到这里还算顺利, 我们要的 Image 最终有传入到 ImageProcessing 里头. 那样我们就可以通过 ImageProcessing 找回 Image 了.

    这个 DefaultImageProcessorContext 就是我们最终使用的 ProcessingImage 了, 反射来确认一下

    最后就是看这个 ProcessingImage 初始化, 看它把 Image 藏去哪里了.

    很遗憾, 我们要的 Image 被放到了 private field. 没有任何接口可以拿到.

    黑科技 – 反射获取 private field

    虽然没有接口没有公开, 但可以通过反射, 强制去获取 private field

    public static class ImageProcessingExtensions
    {
        public static Image MyGetOriginalImage(this IImageProcessingContext imageProcessing)
        {var sourceField = imageProcessing.GetType().GetField("source", BindingFlags.NonPublic | BindingFlags.Instance)!;
            return (sourceField.GetValue(imageProcessing) as Image)!;
        }
        public static IImageProcessingContext ResizeWidth(this IImageProcessingContext imageProcessing, int width)
        {
            var originalImage = imageProcessing.MyGetOriginalImage();
            imageProcessing.Resize(width, height: width * originalImage.Height / originalImage.Width);
            return imageProcessing;
        }
    }

    这样就可以了. 但是要记得哦, 这个是 hacking way, 每次升级都有可能引发 unknown breaking change. 一定要特别注意哦.

    p.s. rezie 用 current size 通常才是正确的需求, 上面拿 original 只是随便举个例子而已.

  • 相关阅读:
    Redis 的基本操作、Key的操作及命名规范
    python离线安装外部库(第三方库)
    STL之deque
    STL之list
    STL学习之vector
    STL三种标准容器
    Lua系统库
    Lua输入输出库
    Lua字符串库
    Lua面向对象
  • 原文地址:https://www.cnblogs.com/keatkeat/p/15918624.html
Copyright © 2020-2023  润新知