mammago已经找到了一种解决方法, 即使用类构造函数直接从文件构造Bitmap
对象,而不是通过Image.FromFile()
间接构造Bitmap
对象。
这个答案的目的是解释为什么这样做可行,并特别说明两种方法之间实际上的区别是什么,导致获得不同的每像素颜色值。
有人提出可能是颜色管理的问题。然而,这似乎行不通,因为两种调用都没有要求颜色管理(ICM)支持。
您可以通过检查.NET BCL的源代码来了解很多信息。在一条评论中,mammago发布了Image
和Bitmap
类实现的代码链接,但无法确定相关差异。
让我们从直接从文件创建Bitmap
对象的Bitmap
类构造函数开始,因为这是最简单的:
public Bitmap(String filename) {
IntSecurity.DemandReadFileIO(filename);
filename = Path.GetFullPath(filename);
IntPtr bitmap = IntPtr.Zero;
int status = SafeNativeMethods.Gdip.GdipCreateBitmapFromFile(filename, out bitmap);
if (status != SafeNativeMethods.Gdip.Ok)
throw SafeNativeMethods.Gdip.StatusException(status);
status = SafeNativeMethods.Gdip.GdipImageForceValidation(new HandleRef(null, bitmap));
if (status != SafeNativeMethods.Gdip.Ok) {
SafeNativeMethods.Gdip.GdipDisposeImage(new HandleRef(null, bitmap));
throw SafeNativeMethods.Gdip.StatusException(status);
}
SetNativeImage(bitmap);
EnsureSave(this, filename, null);
}
有很多东西在那里发生,但大部分都不相关。代码的前几部分只是获取和验证路径。之后是重要的部分:调用本地GDI+函数
GdipCreateBitmapFromFile
,其中之一是
GDI+ flat API提供的许多与位图相关的函数。它正是你所想的那样,它从指向图像文件的路径创建一个
Bitmap
对象,而不使用颜色匹配(ICM)。这个函数承担了大部分工作。然后.NET包装器检查错误并再次验证结果对象。如果验证失败,则清理并抛出异常。如果验证成功,则将句柄保存在成员变量中(调用
SetNativeImage
),然后调用一个函数(
EnsureSave
),该函数除非图片是GIF,否则什么都不做。由于这个不是,我们将完全忽略它。
好的,概念上,这只是一个围绕
GdipCreateBitmapFromFile
的大而昂贵的包装器,执行一堆冗余验证。
关于Image.FromFile()
呢?实际上,你正在调用的重载只是一个桩函数,它会转发到另一个重载,传递false
来表示不需要颜色匹配(ICM)。有趣的重载代码如下:
public static Image FromFile(String filename,
bool useEmbeddedColorManagement) {
if (!File.Exists(filename)) {
IntSecurity.DemandReadFileIO(filename);
throw new FileNotFoundException(filename);
}
filename = Path.GetFullPath(filename);
IntPtr image = IntPtr.Zero;
int status;
if (useEmbeddedColorManagement) {
status = SafeNativeMethods.Gdip.GdipLoadImageFromFileICM(filename, out image);
}
else {
status = SafeNativeMethods.Gdip.GdipLoadImageFromFile(filename, out image);
}
if (status != SafeNativeMethods.Gdip.Ok)
throw SafeNativeMethods.Gdip.StatusException(status);
status = SafeNativeMethods.Gdip.GdipImageForceValidation(new HandleRef(null, image));
if (status != SafeNativeMethods.Gdip.Ok) {
SafeNativeMethods.Gdip.GdipDisposeImage(new HandleRef(null, image));
throw SafeNativeMethods.Gdip.StatusException(status);
}
Image img = CreateImageObject(image);
EnsureSave(img, filename, null);
return img;
}
这看起来非常相似。它以稍微不同的方式验证文件名,但这里没有失败,所以我们可以忽略这些差异。如果未请求嵌入式颜色管理,则委托给另一个本地GDI+平面API函数来完成繁重的工作:
GdipLoadImageFromFile
。
其他人推测这种差异可能是这两个不同的本地函数的结果。这是一个好理论,但我反汇编了这些函数,尽管它们具有不同的实现,但没有明显的差异可以解释在此处观察到的行为。
GdipCreateBitmapFromFile
将执行验证,尝试加载元文件(如果可能),然后调用内部
GpBitmap
类的构造函数来进行实际加载。
GdipLoadImageFromFile
的实现类似,只是通过内部
GpImage::LoadImage
函数间接到达
GpBitmap
类构造函数。此外,我无法通过在C++中直接调用这些本地函数来复现您所描述的行为,因此将它们排除为解释的候选者。
有趣的是,我也无法通过将
Image.FromFile
的结果转换为
Bitmap
来重现您所描述的行为,例如:
Bitmap bit = (Bitmap)(Image.FromFile("1.png"));
虽然依赖它并不是一个好主意,但如果您回到
Image.FromFile
的源代码,您会发现这实际上是合法的。它调用
内部的CreateImageObject
函数,该函数根据正在加载的图像的实际类型委托给
Bitmap.FromGDIplus
或
Metafile.FromGDIplus
。
Bitmap.FromGDIplus
函数只是构造了一个
Bitmap
对象,并调用我们已经看过的
SetNativeImage
函数来设置其底层句柄,然后返回该
Bitmap
对象。因此,当您从文件中加载位图图像时,
Image.FromFile
实际上返回一个
Bitmap
对象。而且,这个
Bitmap
对象的行为与使用
Bitmap
类构造函数创建的对象完全相同。
关键在于创建一个基于
Image.FromFile
的结果的
新Bitmap
对象,这正是您原先的代码所做的:
Bitmap bit = new Bitmap(Image.FromFile("1.png"));
这将调用
接受Image
对象的Bitmap
类构造函数, 该构造函数在内部委托给
接受显式尺寸的构造函数:
public Bitmap(Image original, int width, int height) : this(width, height) {
Graphics g = null;
try {
g = Graphics.FromImage(this);
g.Clear(Color.Transparent);
g.DrawImage(original, 0, 0, width, height);
}
finally {
if (g != null) {
g.Dispose();
}
}
}
这里我们终于找到了你在问题中描述的行为的解释!你可以看到,它从指定的Image
对象创建一个临时的Graphics
对象,用透明颜色填充Graphics
对象,最后将指定的Image
的副本绘制到该Graphics
上下文中。此时,它不是你正在使用的同一张图片,而是那张图片的一个副本。这就是可能引发颜色匹配以及其他多种可能影响图像的因素的地方。
实际上,除了问题中描述的意外行为之外,你编写的代码还隐藏了一个错误:它未能处理由Image.FromFile
创建的临时Image
对象!
谜团解开了。很抱歉回答过程有些冗长,但希望它能教给你一些关于调试的知识!请继续使用mammago推荐的解决方案,因为它既简单又正确。
Image.FromFile()
部分,像这样:Bitmap bit = new Bitmap("1.png");
。对我有效。 - mammago