使用 Image.Save 和 EncoderParameters 的 JPG 图像无损旋转失败

Lossless rotation of a JPG image with Image.Save and EncoderParameters fails

我必须在 .net 中无损旋转 JPG 图片 (90°|180°|270°)。以下文章展示了如何操作:

这些例子看起来很简单;但是,我没有运气让这个工作。我的源数据以数组的形式出现(各种 JPG 文件,来自互联网的相机等),所以我想 return 旋转的图像也作为字节数组。这是(简化的)代码:

Image image;
using (var ms = new MemoryStream(originalImageData)) {
    image = System.Drawing.Image.FromStream(ms);
}

// If I don't copy the image into a new bitmap, every try to save the image fails with a general GDI+ exception. This seems to be another bug of GDI+.
var bmp = new Bitmap(image);    

// Creating the parameters for saving
var encParameters = new EncoderParameters(1);            
encParameters.Param[0] = new EncoderParameter(Encoder.Transformation, (long)EncoderValue.TransformRotate90);              
using (var ms = new MemoryStream()) {                
    // Now saving the image, what fails always with an ArgumentException from GDI+
    // There is no difference, if I try to save to a file or to a stream.
    bmp.Save(ms, GetJpgEncoderInfo(), encParameters);
    return ms.ToArray();
}

我总是从 GDI+ 得到一个 ArgumentException 而没有任何有用的信息:

The operation failed with the final exception [ArgumentException].
Source: System.Drawing

我尝试了很多东西,但从来没有成功过。 主要代码似乎是正确的,因为如果我将 EncoderParameter 更改为 Encoder.Quality,代码工作正常:

encParameters.Param[0] = new EncoderParameter(Encoder.Quality, 50L);

我在互联网上发现了一些关于这个问题的有趣 post,但是没有真正的解决方案。一个特别包含 Hans Passant 的声明,这似乎真的是一个错误,来自 MS 员工的回复,我不明白或者可能也很奇怪:

https://social.msdn.microsoft.com/Forums/vstudio/en-US/de74ec2e-643d-41c7-9d04-254642a9775c/imagesave-quotparameter-is-not-validquot-in-windows-7?forum=netfxbcl

然而,这个 post 已有 10 年历史了,我无法相信,这还没有修复,尤其是因为转换在 MSDN 文档中有一个明确的示例。

有没有人提示我做错了什么,或者,如果这真的是一个错误,我该如何规避它?

请注意,我必须使转换无损(只要像素大小允许)。因此,Image.RotateFlip 不是一个选项。

Windows版本为10.0.17763,.Net为4.7.2

using (var ms = new MemoryStream(originalImageData)) {
    image = System.Drawing.Image.FromStream(ms);
}

这是万恶之源,让第一次尝试失败了。它违反了 the documentation 的备注部分中规定的规则,您必须在图像的生命周期内保持流打开 。违反规则不会导致一致的麻烦,请注意 Save() 调用是如何失败但 Bitmap(image) 构造函数成功的。 GDI+ 有点懒惰,你有很好的证据表明 JPEG 编解码器确实试图避免重新压缩图像。但这是行不通的,流中的原始数据在流被处理后无法再访问。这个异常很糟糕,因为本机 GDI+ 代码不知道有关 MemoryStream 的 bean。修复很简单,只需在调用 Save() 之后移动右括号即可。

从那里它以另一种方式出错,主要由新的 bmp 对象触发。 imagebmp 对象都没有被释放。这会很快消耗地址 space,GC 不能 运行 足够频繁地让你远离麻烦,因为位图的数据存储在非托管内存中。现在,当 MemoryStream 无法再分配内存时,Save() 调用将失败。

必须对这些对象使用using语句,这样就不会发生这种情况。

应该解决这些问题,请摆脱位图解决方法,因为这会强制重新压缩 JPEG。从技术上讲,当图像很大时,您仍然会遇到麻烦,在 32 位进程中遭受地址 space 碎片的困扰。密切关注进程的 "Private bytes" 内存计数器,理想情况下它保持在 1 GB 以下。如果不是,则使用 Project > Properties > Build 选项卡,取消勾选 "Prefer 32-bit".