mammago has found a workaround,即使用类构造函数直接从文件构造Bitmap对象,而不是通过Image.FromFile()返回的Image对象间接构造Bitmap对象。
此答案的目的是解释 为什么 有效,特别是,导致不同的每像素颜色值的两种方法之间的实际差异是什么获得。
一个关于差异的建议是色彩管理。然而,这似乎不是一个开始,因为这两个调用都没有要求颜色管理 (ICM) 支持。
但是,您可以通过检查 .NET BCL 的源代码来了解很多信息。在 a comment 中,mammago 发布了指向 Image 和 Bitmap 类实现的代码的链接,但无法辨别相关差异。
让我们从Bitmap class constructor that creates a Bitmap object directly from a file 开始,因为这是最简单的:
public Bitmap(String filename) {
IntSecurity.DemandReadFileIO(filename);
// GDI+ will read this file multiple times. Get the fully qualified path
// so if our app changes default directory we won't get an error
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,the many Bitmap-related functions provided by the GDI+ flat API 之一。它完全按照您的想法进行,它从图像文件的路径创建Bitmap 对象,而不使用颜色匹配 (ICM)。这是完成繁重工作的功能。 .NET 包装器然后检查错误并再次验证生成的对象。如果验证失败,它会清理并抛出异常。如果验证成功,它将句柄保存在成员变量中(对SetNativeImage 的调用),然后调用一个函数(EnsureSave),除非图像是 GIF,否则该函数什么都不做。由于这不是,我们将完全忽略它。
好的,所以从概念上讲,这只是 GdipCreateBitmapFromFile 的一个大而昂贵的包装器,它执行一堆冗余验证。
Image.FromFile() 呢?好吧,the overload you're actually calling 只是一个转发到the other overload 的存根,通过false 表示不需要颜色匹配(ICM)。有趣的重载代码如下:
public static Image FromFile(String filename,
bool useEmbeddedColorManagement) {
if (!File.Exists(filename)) {
IntSecurity.DemandReadFileIO(filename);
throw new FileNotFoundException(filename);
}
// GDI+ will read this file multiple times. Get the fully qualified path
// so if our app changes default directory we won't get an error
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。
Others have speculated 表示差异可能是这两个不同的原生函数造成的。这是一个很好的理论,但我分解了这些函数,尽管它们有不同的实现,但没有显着差异可以解释这里观察到的行为。 GdipCreateBitmapFromFile 将执行验证,如果可能,尝试加载元文件,然后调用内部GpBitmap 类的构造函数来进行实际加载。 GdipLoadImageFromFile 实现类似,只是它通过内部GpImage::LoadImage 函数间接到达GpBitmap 类构造函数。此外,我无法通过直接在 C++ 中调用这些本机函数来重现您所描述的行为,因此将它们排除为解释的候选对象。
有趣的是,我也无法通过将Image.FromFile 的结果转换为Bitmap 来重现您描述的行为,例如:
Bitmap bit = (Bitmap)(Image.FromFile("1.png"));
虽然依赖它不是一个好主意,但如果你回到Image.FromFile 的源代码,你会发现这实际上是合法的。它调用the internal CreateImageObject function,它根据正在加载的图像的实际类型将Bitmap.FromGDIplus 委托给Metafile.FromGDIplus。 The Bitmap.FromGDIplus function 只是构造了一个Bitmap 对象,调用我们已经看到的SetNativeImage 函数来设置它的底层句柄,然后返回那个Bitmap 对象。因此,当您从文件加载位图图像时,Image.FromFile 实际上会返回一个Bitmap 对象。这个Bitmap 对象的行为与使用Bitmap 类构造函数创建的对象相同。
重现该行为的关键是根据Image.FromFile 的结果创建一个新 Bitmap 对象,这正是您的原始代码所做的:
Bitmap bit = new Bitmap(Image.FromFile("1.png"));
这将调用the Bitmap class constructor that takes an Image object,它在内部委托给one that takes explicit dimensions:
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();
}
}
}
here 是我们最终为您在问题中描述的行为找到解释的地方!可以看到 它从指定的Image 对象创建一个临时的Graphics 对象,用透明颜色填充Graphics 对象,最后将指定Image 的副本绘制到那个Graphics 中语境。此时,它不是您正在使用的同一图像,而是该图像的副本。这就是颜色匹配可以发挥作用的地方,以及可能影响图像的各种其他因素。
事实上,除了问题中描述的意外行为之外,您编写的代码还隐藏了一个错误:它无法释放由Image.FromFile 创建的临时Image 对象! p>
谜团解开了。为冗长的间接答案道歉,但希望它教会了你一些关于调试的知识!请继续使用 mammago 推荐的解决方案,因为它既简单又正确。