ASP.NET Core Library – ImageSharp
前言
2021 年就写过一篇了, Asp.net core 学习笔记 Image processing (ImageSharp), 只是那时还是旧的写法, 这篇作为翻新和以后继续增加新功能的介绍.
ImageSharp 是 .NET 平台开源的图片处理 Library. 完全用 C# 来写, 从 0 开始写. 写了很多年, 目前算是比较 ok 用了. 2022 年开始也支持 webp 了哦.
参考
安装
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, width: 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,
backgroundLocation: 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(width: 500, height: 500 * tifa.Height / tifa.Width); // 按比例缩小
});
newImage.Mutate(imageProcessing =>
{
// 画到中间去
imageProcessing.DrawImage(tifa, backgroundLocation: 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(width: 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 只是随便举个例子而已.
写字 DrawText
参考:
How to add text to an image with C# in dotnet
写字功能目前还处于 beta version, 需要用到 2 个的插件, SixLabors.Fonts 和 SixLabors.ImageSharp.Drawing
dotnet add package SixLabors.Fonts --version 1.0.0-beta19
dotnet add package SixLabors.ImageSharp.Drawing --version 1.0.0-beta15
下载字体
.ttf 肯定可以, .otf 我没有测试. 我是用 Google Font 做测试.
Load Image
public class Program
{
public static void Main()
{
var rootPath = Path.Combine(AppContext.BaseDirectory, @"..\..\..\");
var yangmi = new FileInfo(Path.Combine(rootPath, "yangmi.jpg"));
using var image = Image.Load(yangmi.FullName);
}
}
Setup Font
var collection = new FontCollection();
var family = collection.Add(Path.Combine(rootPath, "Caveat-Bold.ttf"));
var font = family.CreateFont(128, FontStyle.Bold);
导入字体, 并且设置 font style
{
font-family: 'Caveat';
font-size: 128px;
font-weight: 700;
}
不想导入, 也可以用 system font
var font = SystemFonts.CreateFont("Times New Roman", 128, FontStyle.Bold);
Setup Font Drawing Options
var textOptions = new TextOptions(font)
{
Origin = new PointF(64, 64),
WrappingLength = 3840f * 0.25f,
HorizontalAlignment = HorizontalAlignment.Left,
};
Origin 是要 draw 的 coordinate (x,y)
WrappingLength 是 frame 的 width. 多少之后要 wrap (字掉去下一行)
HorizontalAlignment 是从左写到右
Setup Brush
var brush = Brushes.Solid(Color.White);
Brush 负责调颜色
DrawText
string text = "Yang Mi is a Chinese actress, singer, and model known for her roles in popular Chinese TV dramas and films.";
using var newImage = image.Clone(ctx =>
{
ctx.DrawText(options, text, brush);
});
await newImage.SaveAsJpegAsync(Path.Combine(rootPath, "yangmi-name.jpg"));
把字和 setting 丢进去就可以了.
效果

漂亮吧
Text Background Color
继续上一 part 的例子. 我们要添加 overlay background 到 text 上.
首先得画出 overlay.
overlay 的 width, height depend on text 内容. 所以需要计算.
Calc Text Szie
var font = SystemFonts.CreateFont("Times New Roman", 128, FontStyle.Bold);
var text = "Yang Mi is a Chinese actress, singer, and model known for her roles in popular Chinese TV dramas and films.";
var textOptions = new TextOptions(font)
{
Origin = new PointF(16, 16),
WrappingLength = 3840f * 0.25f,
HorizontalAlignment = HorizontalAlignment.Left,
};
var textSize = TextMeasurer.MeasureSize(text, textOptions);
Console.Write(textSize.Width + "x" + textSize.Height);
Draw Text Overlay
using var overlay = new Image<Rgba32>((int)textSize.Width + 64, (int)textSize.Height + 64);
overlay.Mutate(ctx =>
{
ctx.Fill(Color.Red.WithAlpha(0.5f));
var textBrush = Brushes.Solid(Color.White);
ctx.DrawText(textOptions, text, textBrush);
});
创建一张图, 通过 fill 设置 background color, 再把字上进去.
注: background 是有 opacity 的哦, 但字没有.
Draw into Image
最后把 overlay 图片换进原图. 也就是上面做 watermark 的方法.
using var newImage = image.Clone(ctx =>
{
ctx.DrawImage(overlay, location: new Point(64, 64), opacity: 1);
});
await newImage.SaveAsJpegAsync(Path.Combine(rootPath, "yangmi-name.jpg"));
效果

注
我们不可以直接在原图上 Fill. 它会覆盖掉原图, 而且没有 opacity 效果.
using var newImage = image.Clone(ctx =>
{
ctx.Fill(Color.Red.WithAlpha(0.5f));
var textBrush = Brushes.Solid(Color.White);
ctx.DrawText(textOptions, text, textBrush);
});
上面这样是不 ok 的. 必须用 DrawImage 的方式来做. (我不清楚原因...)
ASP.NET Core Library – ImageSharp的更多相关文章
- 《ASP.NET Core 高性能系列》致敬伟大的.NET斗士甲骨文!
写在开始 三年前,曾写过一篇文章:从.NET和Java之争谈IT这个行业,当时遭到某些自认为懂得java就了不起的Javaer抨击, 现在可以致敬伟大的.NET斗士甲骨文了 (JDK8以上都需要收费, ...
- ASP.NET Core and .NET Core Library Support
ASP.NET Core and .NET Core Library Support 详情参见:https://github.com/linezero/NETCoreLibrary/blob/mast ...
- ASP.NET CORE MVC 2.0 项目中引用第三方DLL报错的解决办法 - InvalidOperationException: Cannot find compilation library location for package
目前在学习ASP.NET CORE MVC中,今天看到微软在ASP.NET CORE MVC 2.0中又恢复了允许开发人员引用第三方DLL程序集的功能,感到甚是高兴!于是我急忙写了个Demo想试试,我 ...
- 基于 ASP.NET Core 2.1 的 Razor Class Library 实现自定义错误页面的公用类库
注意:文中使用的是 razor pages ,建议使用 razor views ,使用 razor pages 有一个小坑,razor pages 会用到 {page} 路由参数,如果应用中也用到了这 ...
- ASP.NET Core 1.0 开发记录
官方资料: https://github.com/dotnet/core https://docs.microsoft.com/en-us/aspnet/core https://docs.micro ...
- ASP.NET Core: You must add a reference to assembly mscorlib, version=4.0.0.0
ASP.NET Core 引用外部程序包的时候,有时会出现下面的错误: The type 'Object' is defined in an assembly that is not referenc ...
- ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...
- ASP.NET Core 1.0中实现文件上传的两种方式(提交表单和采用AJAX)
Bipin Joshi (http://www.binaryintellect.net/articles/f1cee257-378a-42c1-9f2f-075a3aed1d98.aspx) Uplo ...
- Asp.Net Core 项目实战之权限管理系统(3) 通过EntityFramework Core使用PostgreSQL
0 Asp.Net Core 项目实战之权限管理系统(0) 无中生有 1 Asp.Net Core 项目实战之权限管理系统(1) 使用AdminLTE搭建前端 2 Asp.Net Core 项目实战之 ...
- 来份ASP.NET Core尝尝
0x01.前言 学习ASP.NET Core也有一段时间了,虽说很多内容知识点还是处于一知半解的状态,但是基本的,还是 略懂一二.如果有错误,还望见谅. 本文还是和之前一样,Demo+在Linux下运 ...
随机推荐
- Go微服务开发指南
在这篇深入探讨Go语言在微服务架构中的应用的文章中,我们介绍了选择Go构建微服务的优势.详细分析了主要的Go微服务框架,并探讨了服务发现与注册和API网关的实现及应用. 关注TechLead,复旦博士 ...
- win10配置 sshkey
win10配置 sshkey 一:生成sshkey 打开powershell 或者 cmd 输入命令: ssh-keygen -t rsa 1 提示输入文件名 [不输入,使用默认文件名,回车] 2 提 ...
- 火山引擎ByteHouse发布高性能全文检索引擎
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群. 随着数字时代的发展,数据的来源和生成方式越来越广泛,数据形态也愈加丰富. 以某电商平台的数据情况举例.该电 ...
- [oeasy]python0086_ASCII_出现背景_1963年_DEC_PDP系列主机_VT系列终端
编码进化 回忆上次内容 上次 回顾了 字符编码的新陈代谢 ibm 曾经的EBCDIC 由于 字符不连续 导致 后续 出现无数问题 随着 网络的发展 数据交换的 需要 原来的小隐患 现在 产生了 巨大问 ...
- [oeasy]python0078_设置索引颜色_index_color_ansi_控制终端颜色
更多颜色 回忆上次内容 上次 了解了 高亮颜色 91-97 是 高亮 前景色 101-107是 高亮 背景色 颜色种类 在原来基础上 增加了一些 但也非常有限 还想要 更精细的颜色 有可能吗?? 更多 ...
- 题解:AT_abc360_c [ABC360C] Move It
背景 机房大佬掉大分了,乐悲. 题意 给你几个箱子和每个箱子里装有的东西 \(a\) 和对应的重量 \(w\),现在要让每个箱子里都装有一个东西,每次可以移动任意一个箱子中的任意一个东西,代价为它的重 ...
- 题解:P10781 【MX-J1-T1】『FLA - III』Spectral
本题的主要思路就是数学. 首先,让我们先来打一个表. \(i\) \(1\) \(2\) \(3\) \(4\) \(\dots\) \(T_{i}\) \(k\) \(1.5k\) \(1.5k\) ...
- CF916C 题解
CF916C 题解 思路 思考发现,如果我们让很多边的边权变得非常大,而故意留下 \(1\) 到 \(n\) 的某一条路径,使整条路径之和甚至还没有剩下一条边的权值大,这条路径显然就是最短路了. 更重 ...
- Jmeter强制结束线程
例子:正常的线程是执行2次请求 1.需要实现结果 执行请求1后,判断test1=100,强制结束线程 执行请求1后,判断test1 != 100,继续执行请求2 2. 线程组改造 在请求1后面增加[i ...
- 【MySQL】MGR高可用搭建
MySQL8.0.27如何安装 https://www.cnblogs.com/mindzone/p/15450312.html 部署过程中各种问题可参考的解决方案 我遇见的搭建问题,解决方案参考下面 ...