c#中“压缩图片大小”实为缩放尺寸,需用highqualitybicubic等参数保清晰度和alpha;等比缩放须校验双轴限制;大图应流式解码;web后端禁用graphics实时缩图,须预生成或改用imagesharp。

直接说结论:C# 里“压缩图片大小” ≠ “缩放尺寸”,但绝大多数人真正要的其实是后者——用高质量重采样把大图变成小图,同时保住清晰度和 Alpha 通道。靠 Graphics.DrawImage 默认行为只会得到模糊、发灰、带摩尔纹的图,这不是代码写错了,是没设对渲染参数。
为什么 Graphics.DrawImage 缩放后图片发虚或变暗
默认用的是 InterpolationMode.Low 和 SmoothingMode.Default,底层走快速双线性插值,不抗锯齿,也不处理预乘 Alpha。PNG 透明像素缩放后变灰,八成是因为没设 PixelFormat.Format32bppPArgb;JPEG 边缘糊成一片,大概率是漏了 PixelOffsetMode.Half。
-
InterpolationMode.HighQualityBicubic是安全下限,别用NearestNeighbor(块状失真)或Low(细节崩坏) -
SmoothingMode.HighQuality影响曲线和文字边缘,非可选 -
CompositingQuality.HighQuality对含透明区域的图必须开,否则半透像素混合出错 - 记得调用
g.Clear(Color.Transparent)或Color.White,避免背景色污染 Alpha
等比例缩放的尺寸计算不能只比大小
常见错误是写一堆 if-else 判断“宽超没、高超没”,结果在 1920×1080 → 800×600 这种场景下算出 800×450,却没检查是否超出目标高度上限——最终图被强行拉伸填满,比例全毁。
- 正确做法是先按宽度算出等比高度:
int calcHeight = (int)(srcHeight * targetWidth / (double)srcWidth) - 再跟目标高度比:
if (calcHeight > targetHeight) { targetWidth = (int)(srcWidth * targetHeight / (double)srcHeight); } - 或者更稳妥:用
Math.Min(targetWidth, (int)(srcWidth * targetHeight / (double)srcHeight))直接得目标宽 - 别忘了返回新尺寸时做整数截断校验,
Math.Max(1, width)防止除零或负值
大图(>3000px)直接 Bitmap.Load 会爆内存
一张 8000×6000 的 PNG 解码后占约 192MB 内存(32bpp × 像素数),OutOfMemoryException 不是异常,是必然。你不需要整张图进内存,只需要缩略图。
-
Image.GetThumbnailImage()快但有硬限制:仅支持 JPEG,且输出尺寸建议 ≤ 1200×1200 - WPF 的
BitmapDecoder+BitmapFrame.Create(decoder, new BitmapCreateOptions(), new BitmapCacheOption())可指定PixelWidth/PixelHeight按需解码,不加载全图 - 生产环境强烈建议换
ImageSharp:它用流式解码,decoder.Options.PreferContiguousBuffer = false能控住峰值内存,ResizeOptions.Mode = ResizeMode.Max自动保比例
Web 后端千万别实时调用 Graphics 缩图
每个 HTTP 请求都 new 一个 Graphics,GDI+ 句柄池会迅速耗尽,GdiplusException 和 CPU 拉满是并发 20+ 就触发的常态。这不是性能问题,是架构错误。
- 静态图必须预生成多尺寸文件,比如
photo_1200.jpg、photo_400.jpg,Nginx/Apache 直接 serve - 若真要动态生成(如用户上传头像),用
ImageSharp替代System.Drawing,它不依赖 GDI+,无句柄泄漏风险 - Windows Server 上禁用
System.Drawing.Common的 GDI+ 后端(通过DOTNET_SYSTEM_DRAWING_USEGDIPLUS=0环境变量),强制走 SkiaSharp 或纯托管路径
最易被忽略的一点:所有 Bitmap、Graphics、Image 实例都必须显式 .Dispose(),哪怕用了 using,也要确认嵌套资源(比如从 Image.FromFile 得到的对象)没被外部引用残留——GDI+ 句柄不会自动回收,卡死常源于此。











