最近在开发一个项目,需要对图片进行处理,比如生成缩略图、生成图片验证码、图片添加水印等功能,项目使用.netcore6.0开发,开发系统使用的云桌面(win10系统),由于是云桌面系统,无法在开发时使用docker进行调试,docker desktop无法启动,原因是云桌面系统禁止了系统更新,导致安装了docker desktop需要更新系统某些功能失败,所以docker desktop无法启动,不知道大家有没有遇到过这个问题,有没有解决办法,可以告诉我一下。由于docker desktop无法启动,所以也无法在本地模拟docker运行,本地开发一直都是使用的vs2022自带的IISExpress进行的调试和开发,此处对系统进行了详细说明就是为了说明为什么在开发过程中为什么没有遇到图片处理的问题,因为使用的windows系统进行的调试,一直没有出现问题,直到发布了测试版本到docker(liunx)上面,才发现了问题,图片上传不了,一直报错。错误信息如下:
System.TypeInitializationException: The type initializer for ‘Gdip’ threw an exception.
—> System.DllNotFoundException: Unable to load shared library ‘libgdiplus’ or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: liblibgdiplus: cannot open shared object file: No such file or directory
错误大致意思就是加载libgdiplus组件失败,确实我刚部署上去时没有安装这个组件,Liunx系统是需要单独安装这个组件,于是我按百度搜索到的方法安装了组件,但是安装完成后,问题仍然还在,报的错误还是一样。于是继续查询解决办法,但是各种方法都试了,问题仍然没有解决。最后我找到一篇文章,内容如下 :
.NET 6之前
在Linux服务器上安装 libgdiplus 即可解决,安装方法可参考原文或者在百度搜索,由于本人使用的是.net6,没有成功,具体方法不再说明
NET 6及以后
由于官方不再支持在非Windows环境使用libgdiplus,需要单独开启运行时环境支持
处理步骤
- 按照.NET 6之前的方案安装 libgdiplus
- runtimeconfig.json配置文件中新增“System.Drawing.EnableUnixSupport": true,这句代码加在runtimeOptions结点下面的,configProperties结点下面
- 构建项目时,会在输出目录中生成[appname].runtimeconfig.json文件,只需要修改该配置文件即可
按这个方法处理了之后,项目运行仍然还是报错,报的错误和之前一样,所以问题仍然没有解决
原文地址:https://www.cnblogs.com/yswenli/archive/2022/02/15/15895485.html
最后我也没有办法了,只好寻找新的解决方案,采用ImageSharp组件重写,还有其他两个组件可以选择SkiaSharp和Microsoft.Maui.Graphics,这三种方法都是跨平台支持的,不需要另外安装单独的组件。
需要安装这三个包:SixLabors.Fonts,SixLabors.ImageSharp,SixLabors.ImageSharp.Drawing
下面简单的贴几个常用用图片处理方法的代码,注意:以下几个方法,除了保存图片,生成缩略图以及生成验证码的方法验证过,其他方法暂时还没有用到也没有去验证,如果遇到问题欢迎指正
1.保存图片并按要求压缩图片,并且按要求生成缩略图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | /// <summary> /// 保存图片 /// </summary> /// <param name="stream">文件流</param> /// <param name="path">保存路径,相对路径 </param> /// <param name="isCompress">是否压缩,如果图片超过限制大小,是否压缩图片</param> /// <param name="size">限制大小</param> /// <param name="width">图片宽度</param> /// <param name="height">图片高度</param> /// <param name="flag">质量</param> public static bool CreateImage(Stream stream, string path, bool isCompress, decimal size, int width, int height, int flag, List< int > thumSizes, out string msg) { msg = "" ; byte [] filebytes = stream.ToByteArray(); string savepath = FileHelper.GetMapPath(path); FileHelper.CreatePath(savepath); stream.Position = 0; //如果是第一次调用,原始图像的大小小于要压缩的大小,则直接复制文件,并且返回true bool needCompress = false ; using (Image iSource = Image.Load(stream)) { try { iSource.Save(savepath); if (isCompress && ((iSource.Width > width && iSource.Height > height) || stream.Length > size * 1024 * 1024)) needCompress = true ; } catch (Exception ex) { msg = ex.Message; return false ; } finally { iSource.Dispose(); } } if (needCompress) { CompressImage(filebytes, savepath, width, height, flag, (size * 1024).ToInt()); } if (thumSizes != null && thumSizes.Count > 0) { System.Threading.Tasks.Task.Factory.StartNew(() => { foreach ( int thumsize in thumSizes) { CreateThumbnail(filebytes, thumsize, thumsize, savepath); } }); } return true ; } |
2.生成缩略图,其中参数bytes是由stream生成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /// <summary> /// 生成缩略图 /// </summary> /// <param name="filebytes">图片文件保存的字节数组</param> /// <param name="thumbnailPath">缩略图保存的绝对路径及文件名</param> /// <param name="width">缩略图长度</param> /// <param name="height">缩略图宽度</param> /// <param name="path">保存路径</param> public static void CreateThumbnail( byte [] filebytes, int width, int height, string path) { using (MemoryStream ms = new MemoryStream(filebytes)) { Image imageFrom = Image.Load(ms); //var original = bytes.ToStream(); var fileExt = FileHelper.GetFileExt(path); var newpath = path.Replace(fileExt, "_" + width + fileExt); if (imageFrom.Width <= width && imageFrom.Height <= height) { // 如果源图的大小没有超过缩略图指定的大小,则直接把源图复制到目标文件 imageFrom.Save(newpath); imageFrom.Dispose(); return ; } // 源图宽度及高度 int imageFromWidth = imageFrom.Width; int imageFromHeight = imageFrom.Height; float scale = height / ( float )imageFromHeight; if ((width / ( float )imageFromWidth) < scale) scale = width / ( float )imageFromWidth; width = ( int )(imageFromWidth * scale); height = ( int )(imageFromHeight * scale); imageFrom.Mutate(x => x.Resize(width, height)); imageFrom.Save(newpath); imageFrom.Dispose(); } } |
3. 压缩图片 暂时不支持通过压缩质量的方法来压缩图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | /// <summary> /// 无损压缩图片 /// </summary> /// <param name="sFile">原图片地址</param> /// <param name="dFile">压缩后保存图片地址</param> /// <param name="flag">压缩质量(数字越小压缩率越高)1-100</param> /// <param name="size">压缩后图片的最大大小</param> /// <param name="isCreateSmallImage">是否生成有损的质量较小的图</param> /// <param name="sfsc">是否是第一次调用</param> /// <returns></returns> public static bool CompressImage( byte [] bytes, string dFile, int width, int height, int flag = 90, int size = 300, bool isCreateSmallImage = false , bool sfsc = true ) { Stream stream = bytes.ToStream(); //如果是第一次调用,原始图像的大小小于要压缩的大小,则直接复制文件,并且返回true using (Image image = Image.Load(stream)) { if (sfsc == true && ((image.Width < width && image.Height < height && isCreateSmallImage) || stream.Length < size * 1024)) { // 如果源图的大小没有超过缩略图指定的大小,直接返回 return true ; } var imageFromWidth = image.Width; var imageFromHeight = image.Height; if (isCreateSmallImage) { var scale = height / ( float )imageFromHeight; if ((width / ( float )imageFromWidth) < scale) scale = width / ( float )imageFromWidth; width = ( int )(imageFromWidth * scale); height = ( int )(imageFromHeight * scale); } else { height = image.Height; width = image.Width; } // Resize the image in place and return it for chaining. // 'x' signifies the current image processing context. image.Mutate(x => x.Resize(width, height)); // The library automatically picks an encoder based on the file extensions then encodes and write the data to disk. image.Save(dFile); return true ; } } |
4.添加文字水印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | /// <summary> /// 生成文字水印 /// </summary> /// <param name="originalPath">源图路径</param> /// <param name="targetPath">保存路径</param> /// <param name="text">水印文字</param> /// <param name="textSize">文字大小</param> /// <param name="textFont">文字字体</param> /// <param name="position">位置0 居中 1 左上 2 右上 3 左下 4右下</param> public static void GenerateTextWatermark( string originalPath, string targetPath, string text, int textSize, EnumPosition position, string textFont = "" , float px = 0, float py = 0) { var image = Image.Load(originalPath); // Clone会返回一个经过处理的深度拷贝的image对象. //直接处理用Mutate=>Action var newImage = image.Clone(x => { //获取该文件绘制所需的大小 var size = TextMeasurer.Measure(text, new TextOptions(font)); //绘制.这里是右下角, 也可以加入参数动态处理左上/右下/居中等... //绘制图片等也是类似 //计算文字位置,默认居中 var pointX = (image.Width - size.Width) / 2; var pointY = (image.Height - size.Height) / 2; (pointX, pointY) = GetPosition(image, size.Width, size.Height, position, px, py); x.DrawText(text, font, Color.WhiteSmoke, new PointF(pointX, pointY)); }); newImage.Save(targetPath); } /// <summary> /// 获取图片位置点 /// </summary> /// <param name="image"></param> /// <param name="width"></param> /// <param name="height"></param> /// <param name="position"></param> /// <returns></returns> private static ( float , float ) GetPosition(Image image, float width, float height, EnumPosition position, float x = 0, float y = 0) { var pointX = (image.Width - width) / 2; var pointY = (image.Height - height) / 2; switch (position) { case EnumPosition.LeftTop: pointX = 2; pointY = 2; break ; case EnumPosition.RightTop: pointX = image.Width - width - 2; pointY = 2; break ; case EnumPosition.LeftMiddle: pointX = 2; pointY = (image.Height - height) / 2; break ; case EnumPosition.LeftBottom: pointX = 2; pointY = image.Height - height - 2; break ; case EnumPosition.RightMiddle: pointX = image.Width - width - 2; pointY = (image.Height - height) / 2; break ; case EnumPosition.RightBottom: pointX = image.Width - width - 2; pointY = image.Height - height - 2; break ; case EnumPosition.TopCenter: pointX = (image.Width - width) / 2; pointY = 2; break ; case EnumPosition.BottomCenter: pointX = (image.Width - width) / 2; pointY = image.Height - height - 2; break ; case EnumPosition.Custom: pointX = x; pointY = y; break ; } return (pointX, pointY); } |
// 获取系统默认字体
Font font = null;
FontCollection fonts = new FontCollection();
if(!textFont.IsEmptyString())
{
//装载字体(ttf)
FontFamily fontfamily = fonts.Add(textFont);
if(fontfamily != null)
{
font = new Font(fontfamily, textSize, FontStyle.Bold);
}
}//没有指定字体则加载系统默认字体
else if(SystemFonts.Families != null && SystemFonts.Families.Count() > 0)
{
font = SystemFonts.CreateFont(SystemFonts.Families.First().Name, textSize, FontStyle.Bold);
}
else //如果系统默认字体加载失败,则指定字体,请注意要将simhei.ttf字体放在根目录,字体文件可以更换
{
FontFamily fontfamily = fonts.Add("simhei.ttf");//字体的路径,也就是可以使用配置文件来指定字体
font = new Font(fontfamily, textSize, FontStyle.Bold);
}
5. 添加图片水印以及合并图片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /// <summary> /// 添加图片水印 /// </summary> /// <param name="originalPath"></param> /// <param name="watermarkPath"></param> /// <param name="targetPath"></param> /// <param name="position"></param> /// <param name="width"></param> /// <param name="heihgt"></param> /// <param name="x"></param> /// <param name="y"></param> public static void GenerateImageWatermark( string originalPath, string watermarkPath, string targetPath, EnumPosition position, int width, int heihgt, float x = 0, float y = 0) { var image = Image.Load(originalPath); var waterimage = Image.Load(watermarkPath); if (width > image.Width) { width = image.Width; } if (heihgt > image.Height) { heihgt = image.Height; } float pointX = (image.Width - width) / 2; float pointY = (image.Height - heihgt) / 2; (pointX, pointY) = GetPosition(image, waterimage.Width, waterimage.Height, position, x, y); var newImage = MergeImage(image, waterimage, pointX.ToInt(), pointY.ToInt(), width, heihgt); newImage.Save(targetPath); } /// <summary> /// 合并图片 /// </summary> /// <param name="templateImage"></param> /// <param name="mergeImagePath">合并图片</param> /// <param name="x">X坐标</param> /// <param name="y">y坐标</param> /// <param name="width">宽度</param> /// <param name="height">高度</param> /// <returns></returns> public static Image MergeImage(Image templateImage, Image mergeImage, int x, int y, int width, int height) { mergeImage.Mutate(m => { m.Resize( new Size(width, height)); }); templateImage.Mutate(o => { o.DrawImage(mergeImage, new Point(x, y), 1); }); return templateImage; } |
6. 生成图片验证码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | /// <summary> /// 获取图片验证码 /// </summary> /// <param name="code"></param> /// <param name="width"></param> /// <param name="height"></param> /// <param name="lineNum">干扰线条数</param> /// <param name="pointNum">噪点个数</param> /// <returns></returns> public static MemoryStream GenerateCheckCode( string code, int fontsize = 25, int width = 140, int height = 50, int lineNum = 4, int pointNum = 60) { #region 颜色集合,去掉了一些和白色底色接近的颜色 List<Color> colors = new List<Color>(); colors.Add(Color.PaleGreen); colors.Add(Color.PaleGoldenrod); colors.Add(Color.Orchid); colors.Add(Color.OrangeRed); colors.Add(Color.Orange); colors.Add(Color.OliveDrab); colors.Add(Color.Olive); colors.Add(Color.Navy); colors.Add(Color.Moccasin); colors.Add(Color.MidnightBlue); colors.Add(Color.MediumVioletRed); colors.Add(Color.MediumTurquoise); colors.Add(Color.MediumSpringGreen); colors.Add(Color.LightSteelBlue); colors.Add(Color.Lime); colors.Add(Color.PaleTurquoise); colors.Add(Color.Magenta); colors.Add(Color.MediumAquamarine); colors.Add(Color.MediumBlue); colors.Add(Color.MediumOrchid); colors.Add(Color.MediumPurple); colors.Add(Color.MediumSeaGreen); colors.Add(Color.MediumSlateBlue); colors.Add(Color.Maroon); colors.Add(Color.PaleVioletRed); colors.Add(Color.PeachPuff); colors.Add(Color.SpringGreen); colors.Add(Color.SteelBlue); colors.Add(Color.Tan); colors.Add(Color.Teal); colors.Add(Color.Thistle); colors.Add(Color.Tomato); colors.Add(Color.Violet); colors.Add(Color.Wheat); colors.Add(Color.Yellow); colors.Add(Color.YellowGreen); colors.Add(Color.Turquoise); colors.Add(Color.LightSkyBlue); colors.Add(Color.SlateBlue); colors.Add(Color.Silver); colors.Add(Color.Peru); colors.Add(Color.Pink); colors.Add(Color.Plum); colors.Add(Color.PowderBlue); colors.Add(Color.Purple); colors.Add(Color.Red); colors.Add(Color.SkyBlue); colors.Add(Color.RosyBrown); colors.Add(Color.SaddleBrown); colors.Add(Color.Salmon); colors.Add(Color.SandyBrown); colors.Add(Color.SeaGreen); colors.Add(Color.Sienna); colors.Add(Color.RoyalBlue); colors.Add(Color.LightSeaGreen); colors.Add(Color.LightSalmon); colors.Add(Color.LightPink); colors.Add(Color.Crimson); colors.Add(Color.Cyan); colors.Add(Color.DarkBlue); colors.Add(Color.DarkCyan); colors.Add(Color.DarkGoldenrod); colors.Add(Color.DarkGray); colors.Add(Color.Cornsilk); colors.Add(Color.DarkGreen); colors.Add(Color.DarkMagenta); colors.Add(Color.DarkOliveGreen); colors.Add(Color.DarkOrange); colors.Add(Color.DarkOrchid); colors.Add(Color.DarkRed); colors.Add(Color.DarkSalmon); colors.Add(Color.DarkKhaki); colors.Add(Color.DarkSeaGreen); colors.Add(Color.CornflowerBlue); colors.Add(Color.Chocolate); colors.Add(Color.AntiqueWhite); colors.Add(Color.Aqua); colors.Add(Color.Aquamarine); colors.Add(Color.Bisque); colors.Add(Color.Coral); colors.Add(Color.Black); colors.Add(Color.Blue); colors.Add(Color.BlueViolet); colors.Add(Color.Brown); colors.Add(Color.BurlyWood); colors.Add(Color.CadetBlue); colors.Add(Color.Chartreuse); colors.Add(Color.BlanchedAlmond); colors.Add(Color.DarkSlateBlue); colors.Add(Color.DarkTurquoise); colors.Add(Color.IndianRed); colors.Add(Color.Indigo); colors.Add(Color.Khaki); colors.Add(Color.HotPink); colors.Add(Color.LawnGreen); colors.Add(Color.LightBlue); colors.Add(Color.LightCoral); colors.Add(Color.LightCyan); colors.Add(Color.LightGreen); colors.Add(Color.Green); colors.Add(Color.DarkViolet); colors.Add(Color.DeepSkyBlue); colors.Add(Color.DimGray); colors.Add(Color.DodgerBlue); colors.Add(Color.Firebrick); colors.Add(Color.GreenYellow); colors.Add(Color.Fuchsia); colors.Add(Color.Gainsboro); colors.Add(Color.Gold); colors.Add(Color.Goldenrod); colors.Add(Color.ForestGreen); #endregion using var image = new Image<Rgba32>(width, height); var r = new Random(); image.Mutate(ctx => { // 白底背景 ctx.Fill(Color.White); // 画验证码 for ( int i = 0; i < code.Length; i++) { ctx.DrawText(code[i].ToString() , font , colors[r.Next(colors.Count)] , new PointF(20 * i + 10, r.Next(2, 12))); } // 画干扰线 for ( int i = 0; i < lineNum; i++) { var pen = new Pen(colors[r.Next(colors.Count)], 1); var p1 = new PointF(r.Next(width), r.Next(height)); var p2 = new PointF(r.Next(width), r.Next(height)); ctx.DrawLines(pen, p1, p2); } // 画噪点 for ( int i = 0; i < pointNum; i++) { var pen = new Pen(colors[r.Next(colors.Count)], 1); var p1 = new PointF(r.Next(width), r.Next(height)); var p2 = new PointF(p1.X + 1f, p1.Y + 1f); ctx.DrawLines(pen, p1, p2); } }); using var ms = new System.IO.MemoryStream(); // gif 格式 image.SaveAsGif(ms); return ms; }<em id= "__mceDel" > { checkCode = string .Empty; //验证码的字符集,去掉了一些容易混淆的字符 比如1和L,0和O char [] character = { '2' , '3' , '4' , '5' , '6' , '8' , '9' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'J' , 'K' , 'L' , 'M' , 'N' , 'P' , 'R' , 'S' , 'T' , 'W' , 'X' , 'Y' }; var rnd = new Random(); //生成验证码字符串 for ( var i = 0; i < codelen; i++) { checkCode += character[rnd.Next(character.Length)]; } if (width == 0) { width = checkCode.Length * (fontsize + 2); } if (height == 0) { height = fontsize + 4; } return GenerateCheckCode(checkCode, fontsize, width, height, lineNum, pointNum); } </em> |
// 字体
Font font = null;
FontCollection fonts = new FontCollection();
if(SystemFonts.Families != null && SystemFonts.Families.Count() > 0)
{
font = SystemFonts.CreateFont(SystemFonts.Families.First().Name, fontsize, FontStyle.Bold);
}
else
{
FontFamily fontfamily = fonts.Add("simhei.ttf");//字体的路径和文件名,默认放在根目录
font = new Font(fontfamily, fontsize, FontStyle.Bold);
}
/// <summary>
/// 生成验证码,并且返回验证码和验证码的图片流
/// </summary>
/// <param name="checkCode">返回的验证码</param>
/// <param name="codelen">验证码长度,默认为4位</param>
/// <param name="fontsize">字体大小,默认为25</param>
/// <param name="width">图片宽度,默认为0,根据字体大小和验证码长度计算</param>
/// <param name="height">图片宽度,默认为0,根据字体大小来计算</param>
/// <param name="lineNum">干扰线的数量</param>
/// <param name="pointNum">噪点的数量</param>
/// <returns></returns>
public static MemoryStream GenerateCheckCode(out string checkCode, int codelen = 4, int fontsize = 25, int width = 0, int height = 0, int lineNum = 4, int pointNum = 60)
7.扩展类 字节数组和Stream之间的相互转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public static class StreamExtensions { /// <summary> /// 数据流转换为字节流 /// </summary> /// <param name="stream"></param> /// <returns></returns> public static byte [] ToByteArray( this Stream stream) { var data = new byte [stream.Length]; stream.Read(data, 0, data.Length); return data; } /// <summary> /// 字节流转换为数据流 /// </summary> /// <param name="bytes"></param> /// <returns></returns> public static Stream ToStream( this byte [] bytes) { Stream stream = new MemoryStream(bytes); return stream; } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南