Originally published December 2002 on DoItIn.net using VB7.0/2002. Updated for VB7.1 February 2005
Links for compiled demo versions, all required resources and source code are included at the end of this article.
Plus, get the complete eBook in Adobe Acrobat 7 format ... all here.
There are a number of ways to make a pixel for pixel copy of a raster graphic. As we've seen, applying RotateFlip is one way but it doesn't help anyone deal with multipage tifs. Some of the other techniques include:
- Create a new bitmap object and loop through each individual pixel in the source graphic using GetPixel to read the pixel data and SetPixel to copy it to the same X/Y location of the destination bitmap. Even on a fast computer this can be very slow process and is best used when you have to perform some manipulation to each pixel such as altering the RGB value for a color adjustment.
- P/Invoke ("Platform Invoke", a fancy term for working directly with the Win32 API) using GDI32.dll's GetPixel/SetPixel or the slightly faster GetPixelV/SetPixelV. This is faster because it cuts right through the .Net Runtime overhead but it forces you to do a lot of Marshaling in and out of managed memory so you have to be very careful and it's still doing a loop that can take noticeable time on even average sized images. (Though most .Net code will forever be run on Windows based systems, if there ever comes a time when your project will be expected to run on Linux via MONO P/Invoke is out for obvious reasons)
- "BitBlast" the graphic's data to a new bitmap all at once using the GDI32.dll's bitblit function. Again, this is P/Invoke so it has both the upside of speed and the downsides of not being very portable and putting you in unsafe territory, marshalling the bits in and out of the CLR's process with non-OOP code that can quickly turn ugly. BitBlasting from a source image currently displayed unobstructed on the screen is pretty simple hence it still being the defacto standard for screen capturing, but once you get into loading images from files into memory bitmaps you have to deal with creating DeviceContexts which gets complicated quickly, and trying to get at the individual page/frames of a multipage tif with the API just starts turning your code into confusing and unstable spaghetti. Google for "bitblt" if you want to get into it but keep in mind that there's a reason even VB5/6 graphics gurus took unsupported chances with automating the Wang/Kodak image applet interfaces or did their fax work with expensive 3rd party components.
- Use GDI+'s DrawImage/DrawImageUnscaled. These give you pretty darned fast bitblast style results and the upsides are that they are relatively simple ("less code = less chance of bugs"), have a number of overloaded versions to fit most all needs, and your code stays in the managed realm letting the runtime take care of *most* of the memory concerns. This is our winner.
Now that you're ready for your code review with the reasons why, right click on the UC's project node and "Add" a "New Module", name it "ImageUtilities.vb". Add an "Imports System.Drawing.Drawing2D" and a DrawImage function wrapper named CopyImage:
Imports System.Drawing.Drawing2D
Module ImageUtilities
Friend Function CopyImage(ByVal SourceImage As Image) As Image
Dim bmpDest As Bitmap
Dim gfx As Graphics
Try
'make bitmap for result
bmpDest = New Bitmap(SourceImage.Width, SourceImage.Height)
'create Graphics object for final bitmap
gfx = Graphics.FromImage(bmpDest)
'changing Interpolation Modes from Default affects quality and speed
'with higher quality taking the most time. Start with the highest
'setting - HighQualityBicubic - and if it drags down the systems then
'start dropping down to other levels. You could even make the mode
'an argument of the function and expose it as a control property
'so the dev-users could deal with it as they see fit
gfx.InterpolationMode = InterpolationMode.HighQualityBicubic
'blast the source image into the dest
gfx.DrawImage(SourceImage, 0, 0, bmpDest.Width, bmpDest.Height)
Return DirectCast(bmpDest.Clone, Image)
Catch ex As OutOfMemoryException
'this is GDI+'s generic error
Throw ex
Catch ex As Exception
Throw ex
'Todo: remove after complete testing
Finally
If Not (SourceImage Is Nothing) Then
SourceImage.Dispose() ' get rid of the old one if it exists.
End If
If Not (bmpDest Is Nothing) Then
bmpDest.Dispose() ' get rid of the old one if it exists.
End If
If Not gfx Is Nothing Then
gfx.Dispose()
End If
End Try
End Function
End Module
Update the LoadImage function to slice out the individual images from the original file using CopyImage. Because DrawImage is processor-intensive (GDI+ 1x doesn't take advantage of hardware acceleration), we'll only do it for multipage images:
...
If pageCount > 1 Then
For i As Integer = 0 To UBound(m_ImagePages)
tmpFileImage.SelectActiveFrame(FrameDimension.Page, i)
tmpFrameImage = DirectCast(tmpFileImage.Clone, Image)
tmpFrameImage = CopyImage(tmpFrameImage)
m_ImagePages(i).OriginalImage = tmpFrameImage
m_ImagePages(i).WorkingImage = DirectCast(tmpFrameImage.Clone, Image)
m_ImagePages(i).CurrentZoom = 100
Next
Else
m_ImagePages(0).OriginalImage = tmpFileImage
m_ImagePages(0).WorkingImage = DirectCast(tmpFileImage.Clone, Image)
m_ImagePages(0).CurrentZoom = 100
End If
...
Rebuild, Save and F5. That's better :)
Next: Zooming
Robert Smith
Kirkland, WA
added to smithvoice march 2005