Reputation: 4948
This is the problem:
I save a Bitmap in .png
with colors say ARGB(50,210,102,70)
with dimension 1 x 1 pixel
.
I retrieve the same image again and use the GetPixel(0,0)
method, what I get is ARGB(50,209,102,70)
.
There is a slight variation in the retrieved value, the RGB
values slightly differ but the A
value remains same.
However when i use 255
for A
value, the correct RGB
values are returned.
So,.. Using a value less than 255
for A
results in the problem mentioned above.
Here is the code which saves the Bitmap.
Bitmap bmpPut = new Bitmap(1, 1); //Also tried with 'PixelFormat.Format32bppArgb'
bmpPut.SetPixel(0, 0, Color.FromArgb(254, 220, 210, 70));
bmpPut.Save("1.png"); //Also tried with using 'ImageFormat.Png'
Here is the code which gets the pixel color
Bitmap bit = new Bitmap(Image.FromFile("1.png"));
MessageBox.Show("R:" + bit.GetPixel(0, 0).R.ToString() +
"| G: " + bit.GetPixel(0, 0).G.ToString() +
"| B: " + bit.GetPixel(0, 0).B.ToString() +
"| A: " + bit.GetPixel(0, 0).A.ToString());
What i get is ARGB(254,219,209,70)
P.S.: I read a few similar questions, they were'nt addressing this exact issue and I din't find a solution yet.
Upvotes: 3
Views: 2438
Reputation: 244991
mammago has found a workaround, namely using the class constructor to construct a Bitmap
object directly from a file, rather than constructing a Bitmap
object indirectly via the Image
object returned by Image.FromFile()
.
The purpose of this answer is to explain why that works, and in particular, what the actual difference is between the two approaches that causes different per-pixel color values to be obtained.
One proposal for the difference was color management. However, this appears to be a non-starter, as neither invocation is asking for color-management (ICM) support.
You can, however, tell a lot by inspecting the source code for the .NET BCL. In a comment, mammago posted links to the code for the Image
and Bitmap
class implementations, but wasn't able to discern the relevant differences.
Let's start with the Bitmap
class constructor that creates a Bitmap
object directly from a file, since that's the simplest:
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);
}
Lots of stuff going on there, but most of it is not relevant. The first bits of code simply obtain and validate the path. After that is the important bit: a call to the native GDI+ function, GdipCreateBitmapFromFile
, one of the many Bitmap-related functions provided by the GDI+ flat API. It does exactly what you would think, it creates a Bitmap
object from a path to an image file without using color matching (ICM). This is the function that does the heavy lifting. The .NET wrapper then checks for errors and validates the resulting object again. If validation fails, it cleans up and throws an exception. If validation succeeds, it saves the handle in a member variable (the call to SetNativeImage
), and then calls a function (EnsureSave
) that does nothing unless the image if a GIF. Since this one isn't, we'll ignore that completely.
Okay, so conceptually, this is just a big, expensive wrapper around GdipCreateBitmapFromFile
that performs a bunch of redundant validation.
What about Image.FromFile()
? Well, the overload you're actually calling is just a stub that forwards to the other overload, passing false
to indicate that color matching (ICM) is not desired. The code for the interesting overload is as follows:
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;
}
This looks very much the same. It validates the file name in a slightly different way, but that isn't failing here, so we can ignore these differences. If embedded color management was not requested, it delegates to another native GDI+ flat API function to do the heavy lifting: GdipLoadImageFromFile
.
Others have speculated that the difference may be a result of these two different native functions. It's a good theory, but I disassembled these functions, and though they have distinct implementations, there are no salient differences that would account for the behavior observed here. GdipCreateBitmapFromFile
will perform validation, attempt to load a metafile if possible, and then call down to the constructor for an internal GpBitmap
class to do the actual loading. GdipLoadImageFromFile
is implemented similarly, except that it arrives at the GpBitmap
class constructor indirectly via the internal GpImage::LoadImage
function. Furthermore, I was unable to reproduce the behavior you described by calling these native functions directly in C++, so that eliminates them as candidates for an explanation.
Interestingly, I was also unable to reproduce the behavior you describe by casting the result of Image.FromFile
to a Bitmap
, e.g.:
Bitmap bit = (Bitmap)(Image.FromFile("1.png"));
Although not a good idea to rely on it, you can see that this is actually legal if you go back to the source code for Image.FromFile
. It calls the internal CreateImageObject
function, which delegates either to Bitmap.FromGDIplus
to Metafile.FromGDIplus
according to the actual type of the image being loaded. The Bitmap.FromGDIplus
function just constructs a Bitmap
object, calls the SetNativeImage
function we have already seen to set its underlying handle, and returns that Bitmap
object. Therefore, when you load a bitmap image from a file, Image.FromFile
actually returns a Bitmap
object. And this Bitmap
object behaves identically to one created using the Bitmap
class constructor.
The key to reproducing the behavior is to create a new Bitmap
object based on the result of Image.FromFile
, which is what exactly your original code did:
Bitmap bit = new Bitmap(Image.FromFile("1.png"));
This will call the Bitmap
class constructor that takes an Image
object, which delegates internally to 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();
}
}
}
And here is where we finally find an explanation for the behavior you describe in the question! You can see that it creates a temporary Graphics
object from the specified Image
object, fills the Graphics
object with a transparent color, and finally draws a copy of the specified Image
into that Graphics
context. At this point, it is not the same image you're working with, but a copy of that image. This is where color matching can kick in, as well as a variety of other things that potentially affect the image.
In fact, aside from the unexpected behavior described in the question, the code you had written hid a bug: it fails to dispose the temporary Image
object created by Image.FromFile
!
Mystery solved. Apologies for the long, indirect answer, but hopefully it has taught you something about debugging! Do continue to use the solution recommended by mammago, as it is both simple and correct.
Upvotes: 7
Reputation: 247
Replacing
Bitmap bit = new Bitmap(Image.FromFile("1.png"));
with
Bitmap bit = new Bitmap("1.png");
Should do the trick.
It seems like Image.FromFile()
isn't as precise as the Bitmap
constructor.
Upvotes: 1