smithvoice.com
 Y'herd thisun? 

“Seen the funny and GREAT movie "the Big Dish?" Heading to Cali for a vaca? You have to visit the Goldstone array complex. Check this to see why”

from Visit to Goldstone by Smith

Your own image control and App part 8

TaggedCoding, VB, Imaging

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.


 

8) Feature 2: Rotating image displays

There are two main ways to rotate graphics with .Net, [Graphics].RotateTransform and [Image].RotateFlip.

[Graphics].RotateTransform is very powerful and with it you can rotate an image by any amount of degrees. With its power, of course, comes extra work and having to add support to eliminate assumptions (such as: when the resulting bitmap exposes areas that aren't filled in by the original image data, how does the user want those areas filled in?). If we were creating a full image editing program to go up against PaintShopPro then we would be very happy to have this big gun at our fingertips.

In our situation a simple way to transform images by 90 degree increments is really all that's needed and that need is quickly and easily taken care of using [Image].RotateFlip set to one of the many built-in System.Drawing.Imaging.RotateFlipType enumeration values.

Open the UC's codeview and add a private module level variable named "m_CurrentRotation" of the type RotateFlipType (you don't have to do all of the dots if you Imported System.Drawing.Imaging):

Private m_CurrentRotation As RotateFlipType = RotateFlipType.RotateNoneFlipNone

Now add a public property named "Rotation" that gets/sets that same type... Hmm, wait a second, do you see a problem here? If we apply the rotation to the image in the picturebox then it's fine for single-image files but multipage TIFs might need different rotations for each page. We could make the setting apply the transformation only to the currently displayed page/image without holding it in a variable ... nope, that's not good enough because if we apply a 180 degree rotation to page 1 then move to page 2 which isn't rotated and then go back to page 1 our rotation for that page would be lost and that's going to confuse the users. Looking ahead we see that the situation is going to hit us with Zooms too.

Looks like we're not out of the infrastructure woods yet after all.

We have to have a rotation (and zoom) variable for each image in the file. Further, we need to either apply the variable to the correct page of the Original image as it's called up to be displayed OR as the property is set on a current page we could create a new image with the rotation applied and hold that new image for later displays (and optional later added rotations and zooms).

Remove that m_CurrentRotation variable and replace it with an array of the structure type "svImagePage".

 

#Region "Private Variables"
Private m_OriginalImage As Image = Nothing
Private m_ImagePages() As svImagePage
  
Private m_CurrentPageNumber As Integer = 0
  
#End Region

Obviously we'll have to define the structure somewhere and for easy code maintenance we'll follow the style we used for Events. Right click on the project node in Solution Explorer and add either a module or a class (it doesn't matter which, we just need a *.vb file). In the wizard, name the new file svTypes.vb and press OK. When the file is displayed in codeview select all of the default text and delete it. Enter the svImagePage structure including a member for Zooms because we know we'll need that shortly (note the use of Friend so that it won't be visible outside of the UC's assembly):

Friend Structure svImagePage
Dim CurrentRotation As RotateFlipType
Dim CurrentZoom As Integer
End Structure

Instead of using an array of structures, we could make svImagePage a class and use a typed collection. If the svImagePage were going to throw events or have more than basic internal methods or if we were going to expose the pages to our dev-user individually or as a group so that they could work with them outside of our control then objects and collections would be the route I'd take, but because all of the processing is going to stay inside the UC and only the results are going to be exposed there's no need for the overhead. The only real pain in the neck of using an array is having to check both its length and whether it is currently Nothing every time you ping it, but them's the breaks :).

One more thing. The way we have it now we're going to be hitting the m_OriginalImage variable every time we go to an element of the array and vice versa. To keep things more straight-forward, we might as well dump the m_OriginalImage and just put the individual page images into a member of the svImagePage structure. Add that member to the structure:

Friend Structure svImagePage
      Dim OriginalImage as Image
Dim CurrentRotation As RotateFlipType
Dim CurrentZoom As Integer
End Structure

Then replace the LoadImage function with this updated version:

Public Function LoadImage(ByVal img As Image) As Boolean
Dim pageCount As Integer
Dim tmpFileImage As Image
Dim tmpFrameImage As Image
Try
m_CurrentPageNumber = 0
tmpFileImage = DirectCast(img.Clone, Image)
pageCount = SourceFilePageCount(tmpFileImage)
''reset the array to the number of pages
ReDim m_ImagePages(pageCount - 1)
For i As Integer = 0 To UBound(m_ImagePages)
 tmpFileImage.SelectActiveFrame(FrameDimension.Page, i)
 tmpFrameImage = DirectCast(tmpFileImage.Clone, Image)
 m_ImagePages(i).OriginalImage = tmpFrameImage
 m_ImagePages(i).CurrentRotation = RotateFlipType.RotateNoneFlipNone
 m_ImagePages(i).CurrentZoom = 100
  
Next
ResetImage()
RaiseEvent ImageLoaded(Me, New svImageLoadEventArgs(pageCount))
Catch ex As OutOfMemoryException
Throw New svImageInvalidImageFileException(ex)
Catch ex As Exception
'todo:  test to figure errors
'       and remove catch-all
Throw ex
Finally
If Not tmpFrameImage Is Nothing Then
 tmpFrameImage = Nothing
End If
If Not tmpFileImage Is Nothing Then
 tmpFileImage = Nothing
End If
End Try
 End Function

And add this helper function SourceFilePageCount to the "Private Methods" region:

 Private Function SourceFilePageCount(ByVal sourceImage As Image) As Integer
Try
Return sourceImage.GetFrameCount(FrameDimension.Page)
Catch ex As Runtime.InteropServices.ExternalException
'some formats such as Gifs don't support
'Page framedimensions.
'we'll just eat this error
Return 1
Catch ex As Exception
'ToDo: remove this catch-all for release
Throw ex
End Try
 End Function

SourceFilePageCount replaces the similar functionality in the original TotalPageCount property, this new function will only be called once when an image is originally loaded into the control and from then on we'll get the TotalPageCount by reading the length of the array. Update The TotalPageCount property as follows:

Public ReadOnly Property TotalPageCount() As Integer
Get
If Not m_ImagePages Is Nothing Then
 Return m_ImagePages.Length
Else
 Return 0
End If
End Get
End Property

Now comes some tedium ;-). If you don't have the IDE Task List window open press CTL+ALT+K to load it and then delete the m_OriginalImage variable declaration and watch the Task List fill with Build Errors. Click on each error to jump to the location and replace the references to m_OriginalImage with hits on the array. In the code below I've highlighted the changes that are required.

 

Public Property CurrentPageNumber() As Integer
'current page is held internally as zero-based
'we expose as one-based
Get
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
Return m_CurrentPageNumber + 1
 
Else
Return 0
 
End If
 
End Get
 
Set(ByVal Value As Integer)
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
If Value <= TotalPageCount And Value > 0 Then
 Dim initialPage As Integer = m_CurrentPageNumber + 1
 m_CurrentPageNumber = Value - 1
 
 pbDisplay.Image = m_ImagePages(m_CurrentPageNumber).OriginalImage
 
 'tell the host
 RaiseEvent PageChanged(Me, _
 New svImagePageChangeEventArgs(TotalPageCount, _
 initialPage, m_CurrentPageNumber + 1))
 
Else
 Throw New ArgumentOutOfRangeException("Page " & Value.ToString & _
 " does not exist (file contains " & _
 TotalPageCount.ToString & " pages)")
 
End If
 
End If
 
End Set
End Property
 
'....
 
Private Sub ResetImage()
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
pbDisplay.Image = DirectCast(m_ImagePages (m_CurrentPageNumber).OriginalImage.Clone, Image)
 
End If
 
End Sub

It's worth mentioning that the LoadImage code that sets the individual page frames to the svImagePage OriginalImage member is not saving you any memory. If you pull up one of the structures and use GetFrameCount you'll see that each of the four structures has an image object that contains all of the four page/frames. All you've done is set a pointer so that when you display the image the right page will be used but in memory you've now got the equivalent of 16 images. We'll take care of that in due time.

Save and Rebuild the solution then F5 to see that even though we've done some pretty major work it's all internal to the UC so the client didn't need any changes. Now let's get back to those rotations.

Feature 2: Rotating image displays - Take 2 :)

Using [Image].RotateFlipType is as easy as cloning the current image and choosing a value from the type's enumeration. This is a great chance to save ourselves a lot of time because we can simply expose the setting as that same CLR type and the dev-user can use VB.Net's ability to bind GUI controls directly to enums like this:

 

With cboPickARotation
.Items.AddRange([Enum].GetNames(GetType(System.Drawing.RotateFlipType)))
'Or
.DataSource = [Enum].GetValues(GetType(System.Drawing.RotateFlipType))
End with
  
'convert back to type with this:
'Return CType([Enum].Parse(GetType(RotateFlipType), cboPickARotation.Text), RotateFlipType)

This is a valid option so long as you're aware of a little weirdness of RotateFlip. Unlike most other enums that we think could use added values, the RotateFlipType has "too many". If you look at the result of the above GetValues and compare it to the return of the GetNames you see that the number of elements is the same but the texts are different and GetValues returns duplicates:

While it's confusing that the typecode's Name property (GetNames) is different from its ToString result (GetValues), the duplication of the ToString values makes sense. Rotate90FlipNone does a 90degree clockwise rotation and Rotate270FlipXY does a 270degree clockwise rotation (the reverse of the 90), then applies the Flip on the X (Horizontal) axis and then applies to that a Y (vertical) axis flip. The result is "the same", as shown in the demonstration below:

If you had to take a second to wrap your head around that then you see the potential problem, users could also be confused if we expose the RotateFlipType enum and the dev-user takes the easy path of exposing the options in a combo with simple binding. Should we instead create our own enum that eliminates the duplicates and map our values to the RotateFlipTypes internally? It's a tough call. I've created versions of this control with both styles over the years and my dev-users questioned both. You just can't win.

For this version of the control we're going to follow both the RAD rule: "less code means less chance of bugs", and the spirit of MS's guidelines for .Net Exceptions: "If the Runtime provides an exception that covers the issue then use that instead of designing your own" and we'll expose the .Net RotateFlipType enumeration as-is. At least this way we have a better chance of passing the blame off on Microsoft <g>.

That decided, we immediately hit an implementation issue. Our svImagePage structure definition fits our decision with its CurrentRotation member of type RotateFlipType. The logic was that every time an image was to be displayed we'd apply the rotation to the OriginalImage object. This logic doesn't really work because users will likely want to apply multiple rotations to some images (a 90degree followed by another 90, rather than a 90 followed by a 180 to get a final 180). Had we decided to expose our own simplified enumeration we could hold the rotation degrees in an integer and on each change add the previous to the new taking into account the 360degree rollover and figure out what RotateFlipType to apply.

Luckily we're not the first ones to ever come across this quandary.

A cool idea that's often used in professional editing products that allow multiple levels of undo - and that have high hardware requirements - is to hold a collection of change definitions and as each image is displayed the app starts with the original image and applies all of the remembered steps in turn. If you later add color adjustments to this project then holding an undo collection is definitely the most user-friendly way to go, but in our case of only offering rotations it's a lot of code for little payback.

Another traditional method is taken to the extreme by Microsoft XP/2003S's "Windows Picture and Fax Viewer": As each rotation is applied the original image is destructively overwritten. This is simple and if you test it out with repeated rotations you'll find that RotateFlip doesn't degrade the image on multiple calls. The only real drawback is that with it we don't get the option of resetting to the original image and we like that option, so we'll hybridize the idea by adding another Image object member to the svImagePage structure to hold the "working image" Oh, and notice that we're using the structure's in-memory image objects rather than harming the actual files like Microsoft does (they probably paid Alan Cooper for that brilliant idea.).

Here's the new structure definition. Notice that we got rid of the CurrentRotation member because it's no longer needed.

 

Friend Structure svImagePage
Dim OriginalImage as Image
Dim WorkingImage as Image
Dim CurrentZoom As Integer
End Structure

Make the adjustment to the LoadImage sub:

...
''reset the array to the number of pages
ReDim m_ImagePages(pageCount - 1)
  
For i As Integer = 0 To UBound(m_ImagePages)
tmpFileImage.SelectActiveFrame(FrameDimension.Page, i)
tmpFrameImage = DirectCast(tmpFileImage.Clone, Image)
m_ImagePages(i).OriginalImage = tmpFrameImage
m_ImagePages(i).WorkingImage = DirectCast(tmpFileImage.Clone, Image)
m_ImagePages(i).CurrentZoom = 100
  
Next
  
ResetImage()
...

And a change to the CurrentPageNumber property's Set:

...
  
If Value <= TotalPageCount And Value > 0 Then
Dim initialPage As Integer = m_CurrentPageNumber + 1
m_CurrentPageNumber = Value - 1
  
pbDisplay.Image = m_ImagePages(m_CurrentPageNumber).WorkingImage
  
...

And lastly, a change to the ResetImage method including a new optional argument for resetting all of the images in the array:

Private Sub ResetImage(Optional ByVal resetAll As Boolean = False)
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
If resetAll Then
 For i As Integer = 0 To UBound(m_ImagePages)
 m_ImagePages(i).WorkingImage = _
 DirectCast(m_ImagePages(i).OriginalImage.Clone, Image)
 Next
Else
 m_ImagePages(m_CurrentPageNumber).WorkingImage = _
 DirectCast(m_ImagePages(m_CurrentPageNumber).OriginalImage.Clone, Image)
End If
pbDisplay.Image = m_ImagePages(m_CurrentPageNumber).WorkingImage
End If
End Sub

Now we're set to apply the rotations. Add a new public method to the UC called "RotateImage" giving it an optional boolean argument to allow all of the image pages to be rotated en-mass as we did with ResetImage:

Public Sub RotateDisplay(ByVal rotation As RotateFlipType, _
Optional ByVal rotateAll As Boolean = False)
Dim tmpFrameImage As Image
If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
If rotateAll Then
 For i As Integer = 0 To UBound(m_ImagePages)
 tmpFrameImage = m_ImagePages(i).WorkingImage
 tmpFrameImage.RotateFlip(rotation)
 m_ImagePages(i).WorkingImage = tmpFrameImage
 Next
Else
 tmpFrameImage = m_ImagePages(m_CurrentPageNumber).WorkingImage
 tmpFrameImage.RotateFlip(rotation)
 m_ImagePages(m_CurrentPageNumber).WorkingImage = _
 DirectCast(tmpFrameImage.Clone, Image)
End If
pbDisplay.Image = m_ImagePages(m_CurrentPageNumber).WorkingImage
End If
End Sub

Speaking of ResetImage, it's now going to be useful to the dev-user so change the declaration from Private to Public and move it into the "Public Methods" region.

Now switch to the testHarness project and add a checkbox (Name="chkRotateAll"), a combobox for the rotation options (Name="cboPickARotation" DropdownStyle=Dropdownlist), a button to apply the setting (Name="butSetRotation" Text="set rotation") and a button to reset the working images to the originals (Name="butResetImage" Text="Reset").

Double click on butSetRotation to bring its click stub into codeview and fill it in:

Private Sub butSetRotation_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles butSetRotation.Click
Dim RotateType As RotateFlipType = CType([Enum].Parse(GetType(RotateFlipType), _
cboPickARotation.Text), RotateFlipType)
SvImageEditor1.RotateDisplay(RotateType, chkRotateAll.Checked)
End Sub

Then fill in the click stub for butReset:

Private Sub butResetRotation_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles butResetImages.Click
SvImageEditor1.ResetImage(chkRotateAll.Checked)
End Sub

Still in codeview, move up to the InitForm() sub and add a call to "InitRotationCombo()" then create that new routine:

 

Private Sub InitRotationCombo()
With cboPickARotation
.Items.AddRange([Enum].GetNames(GetType(System.Drawing.RotateFlipType)))
 
'force the first index to display
.SelectedIndex = 0
End With
End Sub

 

Rebuild and save the solution and hit F5. Remembering that the first two options "RotateNoneFlipNone" and "Rotate180FlipXY" result in no apparent change, test out the other options with a gif, bmp and jpg. After confirming those easy ones load up the demo tif. Everything looks great... until you move off of the first page. When you apply a rotation to any but the first page the display appears to jump back to page one, and, even worse, if you manually move off of that page then back again it looks like you've lost your SelectActiveFrame setting and now all you get is the first page. What's going on?

We've just hit a side effect of RotateFlip.

When you apply a RotateFlip, even one that appears to do nothing, to a multipage image object it does its work with the first frame/page in the image object, ignoring any previous SelectActiveFrame pointer. Even more important, RotateFlip performs a destructive overwrite. We knew that and counted on it when we chose to add a WorkingImage member to the structure but didn't take into account that the overwrite (which creates a new image and puts that result into the variable) would throw away all but the first page of the original.

What we really need is a way to make our own true copy of the selected page frame image. If we could do that it would not only fix this issue but also take care of the memory problem we've got with each svImagePage OriginalImage and WorkingImage holding onto all of the original frame/pages (adding up to the equivalent of 32 images for our four frame/page demo file). Luckily .Net gives us a pretty fast way of doing this.

Next: GDI+ and the power of DrawImage

Robert Smith
Kirkland, WA

added to smithvoice march 2005


jump to:

  • 1) The spec
  • 2) Setting up the workspace
  • 3) Feature 1: Loading an image
  • 4) Custom Exceptions
  • 5) "Fax images" and Multipage TIFFs
  • 6) Custom events
  • 7) Selecting specific fax pages
  • 8) Feature 2: Rotating image displays
  • 9) The most useful tool in GDI+: DrawImage
  • 10) Feature 3: Zooming
  • 11) Handling the unhandleable exception
  • 12) Fixing the squish
  • 13) Zooming to fit the control
  • 14) You're already beating the Pros
  • 15) Feature 4: Cropping
  • 16) Bonus Feature: StickyMouse
  • 17a) Final Cleanup
  • 17b) Passing the current display image
  • 18) Making the application
  • 19) Source and result viewports
  • 20) A better toolbar
  • 21) Hooking the toolbar to the project
  • 22) Adding ImageEditors
  • 23) The toolbar ZoomCombo
  • 24) The final solution
  • 25) Saving to image files
  • 26) An integer-only textbox
  • 27) Passing save options between forms
  • 28) Dealing with that last exception
  • 29) Offer more options with menus
  • 30 The downloads and ebook


  • home     who is smith    contact smith     rss feed π
    Since 1997 a place for my stuff, and it if helps you too then all the better smithvoice.com