Why does the Bitmap constructor not retain the frame information? In the example below using imagePath
as a path to a 3 page tiff image, the first test works fine showing the different sizes of three pages. However, the second test only shows the first page's size.
/* Returns the following...
Page:1 W/H: 7650/9900
Page:2 W/H: 3600/2025
Page:3 W/H: 3600/2025
Page:1 W/H: 7650/9900
(missing page 2 and 3)
*/
// Test opening using the Image.FromFile method
using (Image img = Image.FromFile(imagePath))
{
int p = img.GetFrameCount(FrameDimension.Page);
for (int i = 0; i < p; i++)
{
img.SelectActiveFrame(FrameDimension.Page, i);
Debug.WriteLine($"Page:{i + 1} W/H: {img.Width}/{img.Height}");
}
}
// Testing opening using the Bitmap constructor
using (Image orgImg = Image.FromFile(imagePath))
{
using (Image img = new Bitmap(orgImg))
{
int p = img.GetFrameCount(FrameDimension.Page);
for (int i = 0; i < p; i++)
{
img.SelectActiveFrame(FrameDimension.Page, i);
Debug.WriteLine($"Page:{i + 1} W/H: {img.Width}/{img.Height}");
}
}
}
As a follow up question, doing the following I can get the same results as the first test. But is there a better way of gaining access to the entire frame collection again like the first test without having to create another Bitmap/Image object?
/* Returns the following...
Page:1 W/H: 7650/9900
Page:2 W/H: 3600/2025
Page:3 W/H: 3600/2025
*/
// Testing opening by creating a new Image object
using (Image orgImg = Image.FromFile(imagePath))
{
using (Image img = (Image)orgImg.Clone())
{
int p = img.GetFrameCount(FrameDimension.Page);
for (int i = 0; i < p; i++)
{
img.SelectActiveFrame(FrameDimension.Page, i);
Debug.WriteLine($"Page:{i + 1} W/H: {img.Width}/{img.Height}");
}
}
}
As a preface:
System.Drawing
is a surprisingly thin wrapper over GDI+ (aka gdiplus
).System.Drawing.Image
is an abstract class
(and the supertype of Bitmap
); corresponds to GDI+'s C++ class Image
. It has only two subclasses:
System.Drawing.Bitmap
which corresponds to GDI+'s C++ class Bitmap
.System.Drawing.Imaging.Metafile
corresponds to GDI+'s C++ class Metafile
- these are (scalable) non-raster images which are basically a linear sequence of GDI drawing ops (so a biiit like PostScript) but in a binary format, and might be thought of as an ancient-ancestor to today's SVG. Metafiles are out of the scope of this answer, however.Bitmap
class is surprisingly multifunctional, as it can represent any of:
.bmp
- which is essentially just a serialized DDB bitmap with extra file headers..ico
/.cur
file-format is also a multi-image raster-image file, GDI+ will only load the first image with no way of loading the other images contained in the file. To work-around this, .NET includes the Icon
and Cursor
classes which provide some (rudimentary) support for getting to the other images in the icon-directory structure contained within the .ico
/.cur
file, but this is also out-of-scope.When you use Image.FromFile(String filename)
to load a single-image raster file (like .bmp
, .jpeg
or .png
), then the image will be returned as a new Bitmap
object, with any supported metadata (e.g. EXIF tags in a JPEG file) loaded into the PropertyItems
collection.
When you use Image.FromFile(String fileName)
to load a multi-image raster file (like an animated .gif
or multi-page TIFF) then it will also return a Bitmap
object, which represents (and contains) the first image in the file - while any other frames/pages/image in the GIF or TIFF file can only be "swapped in" by using SelectActiveFrame
on that specific Bitmap
object - the SelectActiveFrame
method significantly mutates the Bitmap
object, including updating properties most people would reasonably assume were fixed and immutable once-loaded (like the .PixelFormat
, .Width
and .Height
, crazy!).
When you use new Bitmap(Image orignal)
or new Bitmap(Image original, Int32 width, Int32 height)
take note that this does not create any kind of state-copy or clone of the Image source
object. Instead, it simply draws it onto the new Bitmap
via a temporary Graphics
context.
Bitmap
class has a lot of these convenience/helper constructors, including many which don't have equivalents in GDI+ - some might even call it a mess - I believe this is because System.Drawing
's was part of .NET Framework 1.x back in 2001/2002 when Microsoft's .NET library API designers were still throwing-around different ideas/experiments for how an OOP library should be like - hence why there's a confusing mess of constructors, static-factories and clone methods that all seem to do the same thing (until you realise they don't - as you found out).So if you do want to create a proper clone of a Bitmap
object which was loaded from a multi-image file on-disk then you need to use Image.Clone()
which is inherited by Bitmap
, but its return-type is Object
(because this predates .NET's support for covariant return types), you'll need to add a cast yourself (maybe define an extension-method to reduce hassle in future?)
But is there a better way of gaining access to the entire frame collection again like the first test without having to create another Bitmap/Image object?
If you simply want to iterate over the images contained in the file (and you aren't making any changes to the image data: it's all read-only), then there's no need to clone anything, you should be able to re-iterate over the frames (i.e. 0 < i < Image.FrameCount
).
If you want to be able to directly access the raster data for multiple GIF frames (or TIFF pages) simultaneously then unfortunately it looks like you'll have to .Clone()
the Bitmap
for each concurrently-accessible raster-frame you want open - or copy each frame within a loop over FrameCount
between LockBits+UnlockBits
calls, as GDI+ throws an exception if you call .SelectActiveFrame
without first calling UnlockBits
.
...so this doesn't work:
using( Bitmap gif = (Bitmap)Image.FromFile( @"dancingBaby.gif" ) ) // https://en.wikipedia.org/wiki/Dancing_baby
{
List<BitmapData> lockedBits = new List<BitmapData>();
Int32 cnt = gif.GetFrameCount( FrameDimension.Time );
for( Int32 i = 0; i < cnt; i++ )
{
#warning The `SelectActiveFrame` call below will fail when it tries to lock the second image/frame/page in a GIF or TIFF file:
gif.SelectActiveFrame( FrameDimension.Time, frameIndex: i );// <-- When `i == 1` then this line throws: InvalidOperationException: "Bitmap region is already locked."
Rectangle frameRect = new Rectangle( x: 0, y: 0, width: gif.Width, height: gif.Height );
BitmapData frameLock = gif.LockBits( frameRect, ImageLockMode.ReadOnly, gif.PixelFormat );
lockedBits.Add( frameLock );
}
try
{
foreach( BitmapData bd in lockedBits )
{
// Do stuff
}
}
finally
{
foreach( BitmapData bd in lockedBits )
{
gif.UnlockBits( bd );
}
}
}
Instead you'll have to do this:
using( Bitmap gif = (Bitmap)Image.FromFile( @"underConstruction-best-viewed-with-netscapeNavigator-3.0.gif" ) )
{
List<Byte[]> buffers = new List<Byte[]>();
Int32 cnt = gif.GetFrameCount( FrameDimension.Time );
for( Int32 i = 0; i < cnt; i++ )
{
gif.SelectActiveFrame( FrameDimension.Time, frameIndex: i );
Rectangle frameRect = new Rectangle( x: 0, y: 0, width: gif.Width, height: gif.Height );
BitmapData frameLock = gif.LockBits( frameRect, ImageLockMode.ReadOnly, gif.PixelFormat );
try
{
Byte[] frameBuffer = new Byte[ /* width * height * pixel-format-byte-length */ ]
buffers.Add( frameBuffer );
// TODO: Copy from `frameLock.Scan0` into `frameBuffer`.
// Do stuff
}
finally
{
gif.UnlockBits( frameLock );
}
}
}
I spent some time digging in System.Drawing
and GDI+'s internals to see if there was any way to test/check if an Image
or Bitmap
was loaded-from-file (with all other frames, if available) or is a single-frame copy (with information-loss). Unfortunately I don't think it's possible to differentiate between (for example) a correctly-loaded TIFF file containing 1 image and the scenario where a Bitmap
object was incorrectly cloned from a multi-image TIFF file such that it only contains 1 image.