Image.GetThumbnailImage and beyond
Posted by Dan Byström on January 5, 2009
Once I tried to use Image.GetThumbnailImage because I wanted to fire up small thumbnails as fast as possible. So I tried:
// totally and completely, utterly useless private Bitmap getThumbnailImage( string filename, int width, int height ) { using ( Image img = Image.FromFile( filename ) ) return (Bitmap)img.GetThumbnailImage( width, height, null, IntPtr.Zero ); }
This turned out to be completely useless, since Image.FromFile first loads the full image so that no performance is gained whatsoever. Trying to google after a solution only resulted in tons of articles saying that Image.GetThumbnailImage is pretty useless and shouldn’t be used. So I dropped it and solved my problem in a completely different way. Now I just stumbled across an overloaded version Image.FromStream which I haven’t noticed before:
Image.FromStream( Stream stream, bool useEmbeddedColorManagement, bool validateImageData )
This opens up for some interesting usages. For example it means that we really can get a thumbnail fast:
// way, way faster, but still pretty useless private Bitmap getThumbnailImage( string filename, int width, int height ) { using ( FileStream fs = new FileStream( filename, FileMode.Open ) ) using ( Image img = Image.FromStream( fs, true, false ) ) return (Bitmap)img.GetThumbnailImage( width, height, null, IntPtr.Zero ); }
This is still pretty useless, since this way we really don’t know how to get a proportional thumbnail. This is something that seems to be lacking in GDI+: an easy way to rescale images proportionally. Quite frankly: how often are we interested in non-proportional rescales? Not that often, I’d say! Here’s a better version:
// actually works... private Bitmap getThumbnailImage( string filename, int width ) { using ( FileStream fs = new FileStream( filename, FileMode.Open ) ) using ( Image img = Image.FromStream( fs, true, false ) ) return (Bitmap)img.GetThumbnailImage( width, width * img.Height / img.Width, null, IntPtr.Zero ); }
But if we arm ourselves with a way to rescale images proportionally, something Microsoft apparently decided to leave as an exercise for each and every programmer who wants to do even the simplest things with images in GDI+:
public static Size adaptProportionalSize( Size szMax, Size szReal ) { int nWidth; int nHeight; double sMaxRatio; double sRealRatio; if ( szMax.Width < 1 || szMax.Height < 1 || szReal.Width < 1 || szReal.Height < 1 ) return Size.Empty; sMaxRatio = (double)szMax.Width / (double)szMax.Height; sRealRatio = (double)szReal.Width / (double)szReal.Height; if ( sMaxRatio < sRealRatio ) { nWidth = Math.Min( szMax.Width, szReal.Width ); nHeight = (int)Math.Round( nWidth / sRealRatio ); } else { nHeight = Math.Min( szMax.Height, szReal.Height ); nWidth = (int)Math.Round( nHeight * sRealRatio ); } return new Size( nWidth, nHeight ); } [/sourcecode] With that, we can fire up a thumbnail image fast, with a given maximum allowed size while still proportional: [sourcecode language='csharp'] // even better... private Bitmap getThumbnailImage( string filename, Size szMax ) { using ( FileStream fs = new FileStream( filename, FileMode.Open ) ) using ( Image img = Image.FromStream( fs, true, false ) ) { Size sz = adaptProportionalSize( szMax, img.Size ); return (Bitmap)img.GetThumbnailImage( sz.Width, sz.Height, null, IntPtr.Zero ); } } [/sourcecode] So, it appears that <strong>Image.GetThumbnailImage</strong> had its use after all! But there's more we can do with this. Even though we managed to load thumbnail images fast and proportionally, the quality isn't particularly good. That's seems to be the main concern among those who advocate not using Image.GetThumbnailImage at all. If a thumbnail is found in the image file it has already been resized once, and resizing a resized image once more certainly won't improve the quality, especially if a crappy resizing algorithm is being used. Let's see what we can do about this. If we're working with JPG images coming from a digital camera, we can most probably find the "real" thumbnail image like this: private Bitmap getExifThumbnail( string filename ) { using ( FileStream fs = new FileStream( filename, FileMode.Open ) ) using ( Image img = Image.FromStream( fs, true, false ) ) { foreach ( PropertyItem pi in img.PropertyItems ) if ( pi.Id == 20507 ) return (Bitmap)Image.FromStream( new MemoryStream( pi.Value ) ); } return null; }
If we can retrieve a thumbnail this way, it will be in its original size and so we can skip an implicit resize. If we want to rescale it anyway, we can do it in a high quality fashion. I don’t know if this is something everybody knows – I had worked with GDI+ for quite some time before I found it – but fact is that resizing an image like this give good performance but crappy result:
// poor image quality Bitmap bmpResized = new Bitmap( bmpOriginal, newWidth, newHeight );
Instead, try the following:
// superior image quality Bitmap bmpResized = new Bitmap( newWidth, newHeight ); using ( Graphics g = Graphics.FromImage( bmpResized ) ) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.DrawImage( bmpOriginal, new Rectangle( Point.Empty, bmpResized.Size ), new Rectangle( Point.Empty, bmpOriginal.Size ), GraphicsUnit.Pixel ); }
With these code pieces glued together I can now get a thumbnail from a JPG image both faster and with better quality than with my original Image.ImageFromThumbnail attempt!
UPDATE: This technique is used “live” in the demo source code accompanying this post: Thumbnails with glass table reflection in GDI+.
Finally, one more thing that I just come to think of while I was typing this. I have complained in earlier posts that Image.FromFile for some obscure reason keeps the file locked until disposed of. I just realized that there is an easy way around his:
using ( FileStream fs = new FileStream( filename, FileMode.Open ) ) bmp = (Bitmap)Image.FromStream( fs );
Behold – now the image is loaded and the file is NOT locked! 🙂 Ekeforshus
rion said
Hello
Just want to ask, if we can retrieve the original image width and height in item.properties, is it faster than adapting Proportional Size or rescaling the image?
danbystrom said
Sorry, I really can’t figure out what you mean… of course retrieving the original image width and height is faster than rescaling any image. But since that is probably not really what you’re asking: why not just benchmark it for yourself? Good luck!
Rob said
I keep getting a “A generic GDI+ error occured”. Would you know what could be causing that?
I am using Vista … not sure if it has to do with permissions or not.
Thanks.
danbystrom said
That depends on what you were trying to do when you got the error.
Justin said
Hi,
I’m getting the same error message also when I tried to run the project.
ExternalException was unhandled
A generic error occurred in GDI+.
protected virtual Bitmap createFramedBitmap( Bitmap bmpSource, Size szFull )
{
Bitmap bmp = new Bitmap( szFull.Width, szFull.Height );
using ( Graphics g = Graphics.FromImage( bmp ) )
{
g.FillRectangle( FrameBrush, 0, 0, szFull.Width, szFull.Height );
g.DrawRectangle( BorderPen, 0, 0, szFull.Width – 1, szFull.Height – 1 );
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(
bmpSource,
new Rectangle(FrameWidth, FrameWidth, szFull.Width – FrameWidth * 2, szFull.Height – FrameWidth * 2),
new Rectangle(Point.Empty, bmpSource.Size),
GraphicsUnit.Pixel);
}
return bmp;
}
The error stops at the g.DrawImage function.
danbystrom said
You should investigate the source image file and see what’s wrong with it.
Zvonimir said
Can you send me source code or upload some example of that code. Thanks in advance.
Adnan said
I got the culprit. When I load the image using the filestream method that you have shown it gives a Generic GDI+ error whenever the image if being saved. The error goes away if I use Image.FromFile()
A Generic error occured GDI+ and Create High Quality Thumbnail - Across Boundaries - ( Cipto ) said
[…] The solution comes from this Guy […]
Arizankoski said
Before u do anything, just turn on the Garbage Collector, GC.Collect();
That should fix the problem.
Philippe said
Thanks a lot, real quick, real smooth, really useful !
venkat said
nice article..
A generic GDI+ error occured
reason: 1. source file
2. destination file
3. permissions on the folder .
How to resize images and upload ? said
[…] […]
Roman said
Very appreciated! I think this article saved me couple of days of google’ing.
GdipGetImageThumbnail caused errors in my Lazarus project, but this method of resizing is stable and quick.
Alain said
Great !
I was stuck for while with a performance issue when loading images.
Thanks to you, I could fix it.
Ala'a Alnajjar said
Great article!
Hugo Andrés said
Thank youuuu so much!