This project is a teaser to get one's feet wet. For a fully functional usercontrol that is based almost entirely on GDI+, see my alpha image control project.
If you have questions on how to do something with GDI+ that the attached project does not do, please post your question in the Graphics portion of the forum. I do not want, nor intend that this posting become a proxy "GDI+ Forum"
If you have questions about this project then by all means ask away. There may be a few logic errors in the project; as we find those I'll continue to update. Jump to bottom to determine when last updated.
GDI+ is a very handy tool and many coders stay away from it because it appears difficult, at first. So, the classes in this project are aimed at easing you along a bit. GDI+ has 100's of functions and the project herein only touches a small percentage of them. But should be enough for you to get your feet wet without being overwhelmed.
I do plan on updating this project from time to time. Things I have knowledge on and have not yet added to the project include GDI+ paths, strings, and regions. The project does include a very complete image processing class, a rendering class, and a pens/brushes class to get you started.
If you want the absolute minimal code to read and display an image at actual size, see post #8 below
Some advantages of using GDI+
:: Load/Save PNGs and TIFFs, even multi-page TIFFs
:: Load and display animated GIFs
:: Alphablending - images, lines, shapes, everything. GDI+ does not use standard RGB colors. All colors are ARGB which includes level of alphablending.
:: Built-in niffty options like rotation, mirroring, on-the-fly pixel transformations and more
:: No manual premultiplication of alpha values
Some disadvantages of GDI+
:: Crashes easy if not used properly. Objects created around other objects (i.e., images) are not actually stand-alone. GDI+ requires the source data to remain present until the GDI+ object is destroyed.
-- Here is the MS KB Article link: http://support.microsoft.com/kb/814675
-- The workaround is to create a DIBSection, then create a GDI+ bitmap from it and destroy the DIBSection
-- But that workaround won't allow access to other images in a multi-image formats (GIFs, TIFFs) and 32bpp become pARGB.
:: In IDE, can crash easily too -- like subclassing, you cannot just hit End or terminate your app from the debug window while GDI+ is running.
:: As mentioned above, a little complicated for new users
:: Does not support all older GDI functions. For example, you can't create an XOR pen with GDI+
:: Requires extra steps to setup for drawing; but like anything new to you, just takes a little getting used to
___________________________________________________________________________ Last Edited: Jan 21, 2010 8:50 PM GMT/Zulu
:: Modified primarily for TIFF saving
-- Can now save multiple images with/without attributes applied
-- TIFF images can be saved in 1,4,8,24,32 bit depths - dependent upon source image depth
:: Added another optional rendering quality setting when drawing: PixelOffsetMode
:: Removed requirement of cGDIpRenderer to be passed token class during its function calls.
-- Now entire class can be passed it one time via its AttachTokenClass method
:: Single page TIFFs no longer cache their original source data; more memory savings
Jan 19, 2010 2:20 AM GMT/Zulu
:: Revamped quite a bit
-- To reduce memory usage, PNG, BMP, & JPG no longer cache original data
-- CloneImage routine still had some shortcomings when trying to clone without applying image attributes; fixed
-- Fixed logic error reported by Jonney when cloning rotated images
:: Added additional image types: imageIconPNG & imageCursorPNG to distinguish between PNGs loaded from ico/cur resources
Jan 14, 2010 19:10 PM GMT/Zulu
:: Found 2 memory leaks, one each in 2 different functions; fixed
:: Changed logic when loading new images
-- If new image then all attributes are reset automatically (were not before). Optional load flag prevents this.
-- If multi-image format displays another image within same source; attributes remain (no change from previous logic)
:: CreateCloneWithAttributes renamed to CreateClone. WithAttributes now an optional parameter
:: ImageType property distinguishes between icons and cursors now
:: Reorganized and cleaned up some routines
:: Demo reworked a tad
-- You can now apply multiple attributes to the same image and see results
-- Included sample of saving a multi-page TIFF
Jan 12, 2010 01:10 AM GMT/Zulu
:: Added a little more functionality to the cGDIpImage class
-- GetPixel, SetPixel, ExtraTransparentColor (similar functionality of TransparentBLT)
:: Mirroring was off, had my constants swapped around, fixed
:: When SaveAsOriginalFormat was applied for 32bpp alphablended bitmaps, was saved as 24bpp vs 32pp, fixed
:: Updated demo a bit
-- included SaveAsOriginalFormat, TransparentBLT examples
-- included option to view any image in a multiple image format
-- included option to view image info: size, depth, type
Jan 11, 2010 01:30 AM GMT/Zulu
:: Jonney helped identify a logic error when deciding whether 32bpp images use the Alpha channel and how it is used. Thanx Jonney
...
Last edited by LaVolpe; Oct 26th, 2020 at 12:23 PM.
Reason: updated comments
Insomnia is just a byproduct of, "It can't be done"
Another place in LoadPicture_DIBSection
"If ProcessAlphaBitmap(bData(), 14&, BHI.bmiHeader.biWidth, BHI.bmiHeader.biHeight) = True Then"
should be "If ProcessAlphaBitmap(bData(), 0&, BHI.bmiHeader.biWidth, BHI.bmiHeader.biHeight) = True Then"
Another place in LoadPicture_DIBSection
"If ProcessAlphaBitmap(bData(), 14&, BHI.bmiHeader.biWidth, BHI.bmiHeader.biHeight) = True Then"
should be "If ProcessAlphaBitmap(bData(), 0&, BHI.bmiHeader.biWidth, BHI.bmiHeader.biHeight) = True Then"
Jonney, you'd have to send me a sample bitmap that it doesn't work with. I suspect you were sending the arrays that contain BitmapInfo + PixelData not a proper bitmap file format. Note my final comments in last paragraph below.
Regarding your 2 comments
1. When passing offset as 14 vs 0, this is required because the array being passed to that function has a BITMAPFILEHEADER attached, where the 1st 14 bytes are an additional header in front of the typical BITMAPINFO. We are passing bitmap data in a file format, not DIBSECTION format. If you noticed a couple lines before that call, you will see the pixel data is being added 54 bytes + color table, into the array. The 54 bytes are the 40 for the bitmapinfo and 14 for fileheader. So 14, is correct, not 0.
Code:
If GetDIBits(dDC, DIBhandle, 0&, BHI.bmiHeader.biHeight, bData(54& + clrUsed * 4&), BHI, 0&) Then
2. And in your previous post, again, for same reasons, offset cannot be LBound(Array) because the routine should be being passed an array that contains a bitmap/DIB with the BITMAPFILEHEADER in play.
If anything I may have misled you in the function name. It probably would be better named as LoadPicture_DIBHandle vs LoadPicture_DIBSection. The ProcessAlphaBitmap routine was not designed to pass some VB array that contains just the bitmapinfoheader and pixel data, it was designed to process bitmaps file formats. If you are using it for a custom purpose, simply add an additional 14 bytes in front of your VB array before sending it to be parsed/loaded. But the 1st 2 bytes of that 14 byte buffer you may add must contain this value: &H4D42. That is the magic number for a bitmap file. You should also populate bytes the final 8 bytes with real values and there are examples on what those values are suppose to be in several of the routines. Bottom line: All loading functions in the classes assume proper file format of whatever was passed, not portions of that file format.
Last edited by LaVolpe; Jan 10th, 2010 at 04:52 PM.
Insomnia is just a byproduct of, "It can't be done"
Jonney, thanx for the bitmap. The issue isn't with offsets, the ProcessAlphaBitmap is identifying yours as 32bpp ARGB bitmap but GDI+ says it is 32bpp RGB (no alpha channel used). I'll have to re-examine my ARGB, pARGB loop to see what happened and why.
Thanx again for the bug report.
Edited: Fixed and updated zip. Note that logic was flawed twice, not only did it fail when all alpha bytes were zero, it would have failed if all alpha bytes were 255. I threw that part of the project in as a final thought. I was always annoyed that GDI+ doesn't seem to want to process 32bpp alphablended bitmaps; though in the real-world, they are hard to find..
FYI. Why your tweaks worked. Because my routine was flawed it told GDI+ that it used the alpha channel. Since the alpha channel was all zeros, GDI+ interpretted it as a 100% transparent bitmap. The reason why changing the offsets worked for you is that the routine failed on purpose because the information retrieved was offset by -14 bytes and basically retrieved garbage. Because it failed, it simply passed it off to GDI+ to see if it could load it, which it did because the image was fine, just my logic was flawed. However, those tweaks would have prevented an alphablended bitmap from loading correctly.
Last edited by LaVolpe; Jan 10th, 2010 at 09:26 PM.
Insomnia is just a byproduct of, "It can't be done"
A little bonus code. If you only want to render a PNG, TIFF to a DC of your choosing and render it full size, this is the minimal code you need. You could also render jpg, bmp, wmf, emf and first frame of a gif. Not all icons will be loaded by GDI+. To render at a size other than full size, you will need more GDI+ API calls.
Edited: See post #19 & post #21 below for potentially weird behavior using GdipDrawImage.
Code:
Private Declare Function GdiplusStartup Lib "gdiplus" (Token As Long, inputbuf As GdiplusStartupInput, Optional ByVal outputbuf As Long = 0) As Long
Private Declare Function GdipLoadImageFromFile Lib "GdiPlus.dll" (ByVal mFilename As Long, ByRef mImage As Long) As Long
Private Declare Function GdipDeleteGraphics Lib "GdiPlus.dll" (ByVal mGraphics As Long) As Long
Private Declare Function GdipCreateFromHDC Lib "gdiplus" (ByVal hDC As Long, hGraphics As Long) As Long
Private Declare Function GdipDrawImage Lib "GdiPlus.dll" (ByVal mGraphics As Long, ByVal mImage As Long, ByVal mX As Single, ByVal mY As Single) As Long
Private Declare Function GdipDisposeImage Lib "gdiplus" (ByVal Image As Long) As Long
Private Declare Sub GdiplusShutdown Lib "gdiplus" (ByVal Token As Long)
Private Type GdiplusStartupInput
GdiplusVersion As Long
DebugEventCallback As Long
SuppressBackgroundThread As Long
SuppressExternalCodecs As Long
End Type
Private Function RenderPNG(FileName As String, hDC As Long, X As Long, Y As Long) As Boolean
On Error Resume Next
Dim GDIsi As GdiplusStartupInput, gToken As Long, hGraphics As Long, hBitmap As Long
GDIsi.GdiplusVersion = 1&
GdiplusStartup gToken, GDIsi
If Err Then
Err.Clear
Exit Function
ElseIf gToken = 0& Then
Exit Function
End If
On Error Goto 0
Call GdipCreateFromHDC(hDC, hGraphics)
If hGraphics Then
Call GdipLoadImageFromFile(StrPtr(FileName), hBitmap)
If hBitmap Then
GdipDrawImage hGraphics, hBitmap, X, Y
GdipDisposeImage hBitmap
RenderPNG = True
End If
GdipDeleteGraphics hGraphics
End If
GdiplusShutdown gToken
End Function
Last edited by LaVolpe; Feb 6th, 2010 at 01:49 PM.
Insomnia is just a byproduct of, "It can't be done"
Keith, thanks for sharing this fine work. Over the years I've
learned a lot from your PSC projects and this is no exception.
This really takes a lot of the mystery out of GDIP.
Just gotta know how
Look at the PowerBasic declarations, not the C decs. They are far easier, DWORD is Long. Also use declarations in the project as a guide to help.
Insomnia is just a byproduct of, "It can't be done"
I didn't see Attributes being changed with a Rotation (m_Angle) in pvModifyAttributes. Does a Rotation cause Image Attribute being modified? It seems yes. A Rotation could cause changes of Image Width or Height.
Code:
' Return the GDI+ image angle
' Attribute permanent until Destroy is called
Public Property Get Rotation() As Single
Rotation = m_Angle
End Property
Public Property Let Rotation(newVal As Single)
If m_Angle <> newVal Then
m_Angle = newVal
pvModifyAttributes 'Called,but no Rotation code inside
End If
End Property
No, attributes are not modified when rotation is in play or not. What handles rotation is the RenderImageClassToDC function in cGDIpRenderer class. Rotation is done via a world transformation on the DC.
So, the call to pvModifyAttributes is useless in that one spot & should be removed from that property Let statement & will do next update (a result of copy & pasting one property to the next and to the next). Will re-think a bit to see if having Rotation property non-zero has any ill effects elsewhere in the routines. If it does, then I will update sooner than later. I am thinking the answer is no, otherwise I would have seen those ill effects by now; but will think about it in my sleep tonight.
Edited: I didn't answer both of your questions. The one regarding the size, you are very correct in that it can affect the size.
When rendering to a DC, it isn't the code's responsibility to ensure the rotated image fits the DC area, much like any rendering routine. A simpler example is if you have a 64x64 drawing surface and load a 126x126 image and render it, it will not fit the surface. It is the coder's responsibility to dictate what dimensions the rendering should be done at.
Now in the CloneImage routine where the image is actually rotated and/or resized, it matters very much because I am taking the responsibility of ensuring the end result is large enough for the rotated image. I admit I did take a shortcut where I resized the image to SQR(W*W + H*H) which is the maximum size any dimension can have and be able to rotate the image at any angle without chopping it off. Because that size may be larger than the actual rotated image, a fixer-upper section in that routine trims excess transparency. Now, I could have taken the time to calculate the true bounds of the non-alpha pixels and then calculated their rotated dimensions to create the end result exactly sized without the fixer-upper stuff and wouldn't require a 2nd temp image because the 1st one would have been correct from the start. But 1) I really hate trig, 2) pretty much the same amount of code to determine opacity bounds, and 3) I just wanted to draw the line on how advanced some of the functions were going to be. I took out hundreds of lines of more advanced code from this project that no-one has seen, simply because, in my opinion, it was too advanced for a "get your feet wet" type of project.
Last edited by LaVolpe; Jan 14th, 2010 at 10:11 PM.
Insomnia is just a byproduct of, "It can't be done"
Since What I indicated previously,rotation don't change attributes, so I can't figure out how to save a rotated picture directly using one of the cGDIpImage functions.
Here is a way how to save a rotated picture based on your demo:
1. Change DoRotationExample:
Code:
Private Sub DoRotationExample()
If cImage.Handle = 0& Then LoadResourceImage "Spider"
cImage.Rotation = cboSample.ListIndex * 5
RenderTheImage
cImage.ExtraTransparentColor = cImage.GetPixel(0, 0)
RenderTheImage 'Apply m_TransColor so that we can modify m_Attr so that we can call below codes
'(
'If m_Attr Then ' any attributes to apply?
'Set cNewImage = New cGDIpImage
'CloneImage cNewImage, , , True
'hImage = cNewImage.Handle
'Else
'hImage = m_Image(m_SourceIndex)
'End If
')
End Sub
2. Save the rotated picture using DoSaveExample.
eg.Case 0: bOk = cImage.SaveAsBMP(bData(), True) to save as bmp file.
3.If the original Boundary is 'bigger' than Boundary of after-rotated picture,then the picture will save 'rightly' else some of part will cut off.
4.Your extra routine will need to cope with Saving the rotated picture.
Last edited by Jonney; Jan 15th, 2010 at 07:37 AM.
Jonney, thanx for the bug report.
Regarding your previous post and rotation, there is something I need to update in multiple routines. The code is already present & suitable
1. In the SaveAs.... routines, there is a statement that needs to be changed in each of them (except SaveAsOriginalFormat).
From: If m_Attr Then
To: If m_Attr <> 0& Or m_Angle <> 0! Then
or depending on taste: If Not (m_Attr = 0& And m_Angle = 0!) Then
2. In CloneImage, need to offset the rendering so it doesn't get chopped off; oversight.
From: cRenderer.RenderImageClassToDC ....
To:
Jonney, thanx for the bug report.
Regarding your previous post and rotation, there is something I need to update in multiple routines. The code is already present & suitable
1. In the SaveAs.... routines, there is a statement that needs to be changed in each of them (except SaveAsOriginalFormat).
From: If m_Attr Then
To: If m_Attr <> 0& Or m_Angle <> 0! Then
or depending on taste: If Not (m_Attr = 0& And m_Angle = 0!) Then
2. In CloneImage, need to offset the rendering so it doesn't get chopped off; oversight.
From: cRenderer.RenderImageClassToDC ....
To:
Jonney brought up a good example where the classes I supplied may not be ideal as is.
Scenario: Loading some massive compressed image like a TIFF or JPG or PNG, for example, that is 5-10x screen size.
Issue: Takes quite awhile for the image to eventually be displayed.
Reasons: The classes create stand-alone images to prevent accidental GDI+ crashes if the source image is removed while the GDI+ image is bound to that source. See this link that describes the issue and workarounds by MSDN.
1. Because of this the image file is processed (bytes read from file - potential bottleneck for massive images). The image is not processed directly from file because GDI+ has issues with many types of image formats. Therefore the bytes are sent to another routine that will handle those issues.
2. In order to eventually create the source image, an IStream interface is created and the bytes copied to that interface so that GDIpLoadFromStream API can be used by GDI+ to load image from an array. Another potential bottleneck for massive images.
3. The stand-alone image is a bitmap, same size and bit depth of the original, then the source is rendered into the stand-alone or a data-copy from source to stand-alone is performed. For typical sized image, this process is quick but not for massive images.
4. Now, if scaling or attributes are applied (i.e., flipping, grayscaling, rotation, etc), GDI+ must process that massive image pixel by pixel and this will take time, no matter what.
5. Also, if one were to use one or more of the high-quality rendering options, that time is increased yet again.
Possible Solutions.
1. Since these massive image sources will undoubtedly be files vs byte arrays... Create a new procedure or rework existing ones that create a GDI+ image directly from file (GDIpLoadImageFromFile API) and do not move, rename or delete the file while the GDI+ image is active. This should speed things up dramatically.
2. Create a new procedure or rework existing ones that will create a destination sized or a maximum of screen-sized GDI+ 32bpp bitmap, then load the file using GDIpLoadImageFromFile API and render that image into the 32bpp image, releasing the image created from the file and using the 32bpp GDI+ image. This will be a bit slower than option 1 initially, but since the image used by GDI+ is now no larger than the screen, any future rendering and applied attributes will be far faster.
3. Any tweaks to existing procedures may require tweaks to supporting procedures within the classes; so before tweaking ensure you fully understand how the classes really work.
Note: GDIpLoadFromFile should be used for formats GDI+ handles easily: TIFF, PNG for example, not all bitmaps, icons, and not cursors.
Just some ideas. The classes were designed for every day use and ideally handling relatively normal-sized images, say screen-size or smaller. Because the classes are designed that way, they may not be optimal for special situations and that is where you may wish to modify/tweak the code for those circumstances.
Last edited by LaVolpe; Jan 29th, 2010 at 10:30 AM.
Insomnia is just a byproduct of, "It can't be done"
First I want to thank you for sharing this knowledge.
I just want to render a full size PNG file to my Form DC using your 'Bonus Code'
But the image displayed in my form is the mini version the original image.
My image resolution is 787x765, the displayed one is 252x245... how come?
I just simply copy paste your code, nothing changed.
I haven't a clue. Obviously GDI+ sees the image as 252x245. Maybe the image may have multiple resolutions/pages? Try loading it with the main classes in post #1, double click on the picturebox and get the image details; they may explain it and may not.
Edited: See post #22 below for the reason
Last edited by LaVolpe; Jan 16th, 2012 at 11:54 AM.
Insomnia is just a byproduct of, "It can't be done"
Well, I'm glad I never use that function in real life. Always have used gdipDrawImageRectRectI since it allows much more flexibility.
Edited: Just another thing to chalk up to GDI+'s sometimes strange behavior. There is probably a reason why it did what it did, would require a bit of research for the answer.
Edited again. I modified post #8 to reference your #19 & #21 should someone else have a similar experience.
Follow up: I believe the issue is with GDI+ auto-DPI scaling. Per MSDN:
Another feature of GDI+ is that images carry around the DPI they were designed for (as in Image:GetPhysicalDimension and Bitmap:SetResolution, for example). You can use this information to scale images properly, or you can let GDI+ do it. If you don’t specify a height and width when you call DrawImage, GDI+ calculates them based on the screen’s DPI and the image’s DPI.
GdipDrawImage does not have width/height parameters. Per reply #19 above the image reports being 787x765 but rendered at 252x245. Well, if the image's DPI property/value reported 300 DPI, then using DPI scaling formula:
logicalSize = (physicalSize * screenDPI) / imageDPI
Width: (787 * 96) / 300 = 251.84 rounded to 252
Height: (765 * 96) / 300 = 244.8 rounded to 245
So, 787x765 = 252x245 @ source image DPI of 300
Last edited by LaVolpe; Jan 17th, 2012 at 10:44 AM.
Insomnia is just a byproduct of, "It can't be done"
This all looks very interesting. I'm working on a old old vb app which I need to get transparency working and we always resorted to using GIF files before and always wondered whether we could get true alpha channels working, this will be quite exciting to test out, especially as we can change brightness as well!
Does GDI+ allow for blending modes? Multiply/add/screen/overlay?
GDI+ does allow this via matrices if I am understanding your question correctly. If so, it can be a little complex but not too much. In post #1, I provided a link that describes GDI+ API functions. At the link are also some examples. They are in PowerBasic syntax, but I found them fairly easy to translate to VB.
May be worth looking at... Some functions you may want to look at (Graphics section of link): GdipMultiplyWorldTransform, GdipScaleWorldTransform, GdipSetWorldTrfansform, GdipTranslateWorldTransform. Also in the Matrix section of the link are the Matrix modification routines.
As far as overlay goes? If you are talking about transaprency, then the classes already have that built in.
Edited: Additional questions regarding GDI+ capabilities and usage of the functions I mentioned above should be posted in the Graphics portion of the forum.
Last edited by LaVolpe; Feb 22nd, 2010 at 09:07 AM.
Insomnia is just a byproduct of, "It can't be done"
Just one small question on your post 8 code, I'm trying to get it to load the png file into a Picturebox and don't seem to be able to find the correct syntax... (sorry it's been a while since I've dabbled in vb, moved on to more graphics based things)
Just one small question on your post 8 code, I'm trying to get it to load the png file into a Picturebox and don't seem to be able to find the correct syntax... (sorry it's been a while since I've dabbled in vb, moved on to more graphics based things)
You might want to make the picturebox's AutoRedraw=True. Then refresh it after the call. Note the remarks in that posting too; it is bare-bones code and offers no flexibility. You can't even get the image dimensions with it. However, you can modify the code to include more functionality. Review the main classes in post #1 and pull what you need from there, as appropriate.
1. MetaFiles (nope, I don't deal with them and IMO they are rarely used (creation) vs PNGs, Bmps, etc)
2. The Effect branch is something I'm not familiar with and haven't played with in great detail. Those effects are only available with GDI+ v2 (I believe on Vista and above). Whether it works on XP or not, I don't know.
3. With the exception of strings, regions & paths, the other stuff is provided in various degrees of completeness. I do have a semi-complete paths class which I know you are a bit familiar with. When I find the time, I'll add it to this project. Right now, I am focused on another project at the moment.
Note: My intention, as stated in post #1, is to get one's feet wet; not to attempt a comprehensive "how to" tutorial.
Insomnia is just a byproduct of, "It can't be done"
Regarding GetScaledImageSizes function, if Rotation is being set,e.g. 45 degree,How to calculate the BestFit size? GetScaledImageSizes doesn't consider Rotation. In you demo,can you try to Rotation Example?part of picture will cut visually.
How can we scale the picture to best fit the picture1.width and picture1.height with any angles rotation?
Ok, here is a routine you can tweak/add to your routines.
First the remarks.
1. Rotating and scaling an image relative to a destination rectangle can produce undesirable effects. For example rotating a square within the same square without having the rotated shape escape the original square's bounds, produces a rubber-band effect on the rotated shape -- changing sizes as angle changes.
2. In most cases, I would think rubber-banding would not be desirable. For example, a clock hand rotating.
Anyway, here is the algo that will rubberband. Adjust as needed.
a. New form, add 2 shape controls, 1 command button
b. Leave all controls at default names, properties, positions
c. Copy & paste the following into the form and run it
Code:
Option Explicit
Private Declare Sub Sleep Lib "kernel32.dll" (ByVal dwMilliseconds As Long)
Private Type POINTAPI
X As Long
Y As Long
End Type
Private Sub Form_Load()
Me.ForeColor = vbBlue
With Shape1
.Move 135, 135, 2550, 1485
End With
With Shape2
.Move 3105, 135, 1485, 1485
End With
With Command1
.Move 2250, 1710, 1215, 495
.Caption = "Do It"
End With
Me.Move Me.Left, Me.Top, 4875, 2805
End Sub
Private Sub Command1_Click()
Dim Angle As Single, A As Single, d2r As Double
Dim sinT As Double, cosT As Double
Dim h1 As Long, h2 As Long, hh As Long, ww As Long
d2r = (4& * Atn(1)) / 180 ' conversion factor for degree>radian
For A = 0 To 360
Angle = Abs(A) ' keep range between -90 to 90 for bounds calcs
Select Case Angle
Case Is < 91
Case Is < 181
Angle = 180 - Angle
Case Is < 271
Angle = Angle - 180
Case Else
Angle = 360 - Angle
End Select
sinT = Sin(Angle * d2r)
cosT = Cos(Angle * d2r)
With Shape1
h1 = .Height * .Height / (.Width * sinT + .Height * cosT)
h2 = .Width * .Width / (.Width * cosT + .Height * sinT)
If h1 < h2 Then hh = h1 Else hh = h2
ww = hh * .Width / .Height
End With
Me.Cls
Call ShowResults(A, ww, hh, Shape1)
With Shape2
h1 = .Height * .Height / (.Width * sinT + .Height * cosT)
h2 = .Width * .Width / (.Width * cosT + .Height * sinT)
If h1 < h2 Then hh = h1 Else hh = h2
ww = hh * .Width / .Height
End With
Call ShowResults(A, ww, hh, Shape2)
Me.Caption = A & " Degree Rotation"
DoEvents
Sleep 10
Next
Me.Caption = "Done"
End Sub
Private Sub ShowResults(Angle As Single, ScaledCx As Long, ScaledCy As Long, sampleShape As Shape)
' following is not part of the rotation/resizing routine
Dim tPTs(1 To 4) As POINTAPI, X As Long
Dim ctrSrc As POINTAPI, ctrDest As POINTAPI
Dim d2r As Double
Dim sinT As Double, cosT As Double
ctrSrc.X = ScaledCx \ 2
ctrSrc.Y = ScaledCy \ 2
With sampleShape
ctrDest.X = .Width \ 2 + .Left
ctrDest.Y = .Height \ 2 + .Top
End With
d2r = (4& * Atn(1)) / 180
sinT = Sin((Angle Mod 360) * d2r)
cosT = Cos((Angle Mod 360) * d2r)
tPTs(1).X = (-ctrSrc.X * cosT) - (-ctrSrc.Y * sinT) + ctrDest.X
tPTs(1).Y = (-ctrSrc.X * sinT) + (-ctrSrc.Y * cosT) + ctrDest.Y
tPTs(2).X = (ScaledCx - ctrSrc.X) * cosT - (-ctrSrc.Y * sinT) + ctrDest.X
tPTs(2).Y = (ScaledCx - ctrSrc.X) * sinT + (-ctrSrc.Y * cosT) + ctrDest.Y
tPTs(3).X = (ScaledCx - ctrSrc.X) * cosT - (ScaledCy - ctrSrc.Y) * sinT + ctrDest.X
tPTs(3).Y = (ScaledCx - ctrSrc.X) * sinT + (ScaledCy - ctrSrc.Y) * cosT + ctrDest.Y
tPTs(4).X = (-ctrSrc.X * cosT) - (ScaledCy - ctrSrc.Y) * sinT + ctrDest.X
tPTs(4).Y = (-ctrSrc.X * sinT) + (ScaledCy - ctrSrc.Y) * cosT + ctrDest.Y
For X = 1 To 3
Me.Line (tPTs(X).X, tPTs(X).Y)-(tPTs(X + 1).X, tPTs(X + 1).Y)
Next
Me.Line (tPTs(X).X, tPTs(X).Y)-(tPTs(1).X, tPTs(1).Y)
End Sub
Edited: ww & hh would be the scaled width/height in relation to the target rectangle and angle of rotation. Rendering must be done from center of target rectangle to prevent image drawing over target bounds.
Edited yet again. Now if you wanted to know what would be the scaled size of an image in a target rectangle so that the image can be rotated at any angle without rubberbanding, you can use this formula (refering to the GetScaledImageSizes code):
Code:
Dim rotSize As Long
rotSize = Sqr(m_Size.nWidth *m_Size.nWidth + m_Size.nHeight * m_Size.nHeight)
xRatio = destWidth / rotSize
yRatio = destHeight / rotSize
If xRatio > yRatio Then xRatio = yRatio
.... rest of code
Last edited by LaVolpe; May 8th, 2010 at 01:30 PM.
Insomnia is just a byproduct of, "It can't be done"
Edited yet again. Now if you wanted to know what would be the scaled size of an image in a target rectangle so that the image can be rotated at any angle without rubberbanding, you can use this formula (refering to the GetScaledImageSizes code):
Code:
Dim rotSize As Long
rotSize = Sqr(m_Size.nWidth *m_Size.nWidth + m_Size.nHeight * m_Size.nHeight)
xRatio = destWidth / rotSize
yRatio = destHeight / rotSize
If xRatio > yRatio Then xRatio = yRatio
.... rest of code
You missed considering the rotation angle? e.g. Rotate 5 degree,the image boundary is much less than picturebox size. We have to calculate the boundary of 4 corner of image with a rotation. See screenshot.
The first option I provided accounts for dynamic sizes and if you run the code in a test form, you will see that.
The second option is a shortcut, its size is static. The minimum size is determined that will allow the image to be rotated at any angle. This means that the size can be larger, than needed, for some angles but will ensure all angles can be rotated within that size if rendered from the center out. In other words, the scaled size ensures the image can be rotated at any angle without rubberbanding the image.
Edited: Here is something you can play with:
Try the following, remarks follow the code
1. Replace: GetScaledImageSizes
Code:
Public Function GetScaledImageSizes(ByVal destWidth As Long, ByVal destHeight As Long, ScaledWidth As Long, ScaledHeight As Long, _
Optional ByVal CanScaleUp As Boolean = True, Optional ByVal ClipRotation As Boolean = True) As Boolean
' Function returns scaled (maintaining scale ratio) for passed destination width/height
' The CanScaleUp when set to false will never return scaled sizes > than 1:1
' The ClipRotation parameter, if False, will ensure rotated image does not exceed destination width/height
If m_Image(m_SourceIndex) = 0& Then Exit Function
Dim xRatio As Single, yRatio As Single
Dim sinT As Double, cosT As Double, d2r As Double
Dim h1 As Long, h2 As Long, A As Single
xRatio = destWidth / m_Size.nWidth
yRatio = destHeight / m_Size.nHeight
If xRatio > yRatio Then xRatio = yRatio
If Abs(Int(m_Angle + 0.99!) Mod 360) = 0! Or ClipRotation = True Then
If xRatio >= 1! And CanScaleUp = False Then
ScaledWidth = m_Size.nWidth
ScaledHeight = m_Size.nHeight
Else
ScaledWidth = m_Size.nWidth * xRatio
ScaledHeight = m_Size.nHeight * xRatio
End If
Else
ScaledWidth = m_Size.nWidth * xRatio
ScaledHeight = m_Size.nHeight * xRatio
A = Abs(m_Angle)
Select Case A
Case Is < 91
Case Is < 181: A = 180 - A
Case Is < 271: A = A - 180
Case Else: A = 360 - A
End Select
d2r = (4& * Atn(1)) / 180 ' conversion factor for degree>radian
sinT = Sin(A * d2r)
cosT = Cos(A * d2r)
h1 = destHeight * destHeight / (ScaledWidth * sinT + ScaledHeight * cosT)
h2 = destWidth * destHeight / (ScaledWidth * cosT + ScaledHeight * sinT)
If h1 < h2 Then h2 = h1
h1 = h2 * destWidth / destHeight
If xRatio >= 1& And CanScaleUp = False Then
If Not (h1 < m_Size.nWidth Or h2 < m_Size.nHeight) Then
ScaledWidth = m_Size.nWidth
ScaledHeight = m_Size.nHeight
GetScaledImageSizes = True
Exit Function
End If
End If
xRatio = h1 / m_Size.nWidth
yRatio = h2 / m_Size.nHeight
If xRatio > yRatio Then xRatio = yRatio
ScaledWidth = m_Size.nWidth * xRatio
ScaledHeight = m_Size.nHeight * xRatio
End If
GetScaledImageSizes = True
End Function
Note remarks at top of new function above.
2. Test it by hardcoding the final two parameters in the form's RenderTheImage function. Try different combinations.
I believe he algorithm is the first release in GDI+ world and VB world. (You always create marvels in/for VB world!!!)
On best fit mode (rotated picture will fit the canvas,will re-size the rotated picture),it works seamless.How about non-bestfit mode (picturebox force to fit rotated picture, will auto re-size the picture box)?
scenario:
Auto-resize canvas (e.g.picturebox) when rotating a raw size picture or fixed ratio picture. What is the algorithm to calculate new size of canvas (in this case,picture is always on center of canvas or with a fixed offset X/Y) with a rotation?
I talks incessantly.
Last edited by Jonney; May 11th, 2010 at 01:59 AM.
...How about non-bestfit mode (picturebox force to fit rotated picture, will auto re-size the picture box)?
Good question, try this.
1. Add this as new function in cGDIpImage class
Code:
Public Sub GetScaledCanvasSize(ByVal imgWidth As Long, ByVal imgHeight As Long, ByRef CanvasWidth As Long, ByRef CanvasHeight As Long)
Dim sinT As Double, cosT As Double
Dim A As Double, d2r As Double
Dim ctrX As Double, ctrY As Double
A = Abs(Int(m_Angle) Mod 360)
Select Case A
Case Is < 91#
Case Is < 181#: A = 180# - A
Case Is < 271#: A = A - 180#
Case Else: A = 360# - A
End Select
A = A + m_Angle - Int(m_Angle)
d2r = (4# * Atn(1)) / 180# ' conversion factor for degree>radian
sinT = Sin(A * d2r)
cosT = Cos(A * d2r)
ctrX = imgWidth / 2#
ctrY = imgHeight / 2#
A = (-ctrX * sinT) + (-ctrY * cosT)
CanvasHeight = (imgWidth - ctrX) * sinT + (imgHeight - ctrY) * cosT - A
A = ((-ctrX * cosT) - (imgHeight - ctrY) * sinT)
CanvasWidth = (imgWidth - ctrX) * cosT - (-ctrY * sinT) - A
End Sub
2. In the form's RenderThePicture routine, tweak to show bounding canvas size. Add this before Picture1.Refresh:
Code:
Dim canvasCx As Long, canvasCy As Long
cImage.GetScaledCanvasSize Cx, Cy, canvasCx, canvasCy
Picture1.Line (X + (Cx - canvasCx) \ 2, Y + (Cy - canvasCy) \ 2)-(X + (Cx + canvasCx) \ 2, Y + (Cy + canvasCy) \ 2), vbBlue, B
3. You will see the canvas rectangle adjust when you rotate the image.
Suggest selecting a rectangular, non-transparent image < 256x256 and pass CanScaleUp=False to the render function. This way you will easily see the canvas rectangle.
Edited: The canvas rectangle would be your picturebox (question in your previous reply). However, remember that the CanvasWidth/CanvasHeight is returned in pixels and you should convert to needed scalemode to resize picturebox. Also note that you need to include any extra pixels for borders if the picturebox has borders.
Also remember that images with transparency often have buffers of transparent pixels. This gives the illusion that the canvas rectangle is much larger than required. No algo is going to correct for that and you really only have 3 choices that I can think of: 1) accept it, 2) run a trimming routine to transfer only non-transparent pixels to another appropriately-sized image & use that image, 3) process the image to determine the tightest rectangle that excludes all fully-transparent pixels and use that size to pass to your algos (complicated because rendering offsets will most likely be necessary).
Last edited by LaVolpe; May 11th, 2010 at 04:04 PM.
Insomnia is just a byproduct of, "It can't be done"
Hi, I would like to make a large number of multiple shapes with the same parameters, except location (left and top) and color. Is there a way to easily make multiple rectangles, say?
Tom, I am not sure I understand your question. If this is not related to my posted project, you should post your question in the appropriate language forum: VB6 & earlier, .Net, VBA, C++, etc, etc.
Insomnia is just a byproduct of, "It can't be done"
Private Sub Label1_Click()
'
cImage.LoadPicture_stdPicture Clipboard.GetData(vbCFBitmap), cGDIplus, 1
Call RenderTheImage
End Sub
The picture look weird with <Black> transparent color. The Return of lColorFormat in pvProcessAlphaBitmap is PixelFormat32bppARGB instead of PixelFormat32bppRGB.
I don't know how you created the clipboard bitmap.
Here's the only way pvProcessAlphaBitmap will return ARGB vs pARGB or RGB.
-- One of the R,G,B values was > the alpha channel value & the alpha value is non-zero
In that routine, you can place a break on this line: lColorFormat = PixelFormat32bppARGB
That should help indicate that what I said above is correct.
If it is correct and you are creating your own DIBs with CreateDIBSection API, don't assume the DIB is created with all bytes zeroed out. I have seen similar issues before in other parsing engines I've written in the past. As a matter of habit, it might be wise to call FillMemory and zero out the DIB.
Let's say DIB was 256x256 & 32 bit.
Code:
Private Declare Sub FillMemory Lib "kernel32.dll" Alias "RtlFillMemory" (ByRef Destination As Any, ByVal Length As Long, ByVal Fill As Byte)
Dim lSize As Long
lSize = 256& * 256& * 4&
FillMemory ByVal DibPtr, lSize, 0
P.S. In that routine, RGB vs. pARGB/ARGB is only returned if ALL alpha values are either 0 or 255. It does appear the bitmap you passed is using the alpha channel, whether on purpose, accidentally, or dirty when initialized, I can't tell you.
Last edited by LaVolpe; Jun 4th, 2010 at 01:12 PM.
Insomnia is just a byproduct of, "It can't be done"