Quality of Picture when Resized in Paint Event verses Method

Recently there was some great posts about how to resize and center images. Which involved using the Graphics class directly in the Paint event of a Canvas, as well as returning a new resized Picture from a Method. Thank you @Javier Menendez, @Thom McGrath and @David Cox.

Because the bulk of my programming experience is SQL Server and web apps, there are still some holes in my knowledge when it comes to Desktop apps, like working with images dynamically.

So, with the above posts in mind, I was playing with how to resize some icons when the ScaleFactor is not 1.00 or 2.00, such as 1.25, 1.50 or 1.75 which is common on Windows.

The following screen-shot shows the same icon “ImageSet” (includes x1, x2 & x3 sizes), where the top Canvas resizes the icon from x2 down to fit ScaleFactor 1.25 (rendered using the Graphics class directly in the Canvas Paint Event) and the bottom one is resized to the same proportions using a Method that returns a new resized Picture.

Both are using more-or-less the same resizing logic, but the bottom image (returned from a method) does not have the same level of quality. To me it looks blurry. Why is that?

The logic for resizing directly in the Paint Event is basically the following:

Var pic As Picture If Self.mPolarBear <> Nil Then pic = Self.mPolarBear.BestRepresentation(Me.Width, Me.Height, Me.ScaleFactor) End If If pic <> Nil Then Var ratio As Double = Min(Me.Width / pic.Width, Me.Height / pic.Height, 1.0) Var w As Int32 = Floor(pic.Width * ratio) Var h As Int32 = Floor(pic.Height * ratio) Var x As Int32 = Floor((Me.Width - w) / 2) Var y As Int32 = Floor((Me.Height - h) / 2) g.DrawPicture(pic, x, y, w, h, 0, 0, pic.Width, pic.Height) End If
The logic for resizing in a separate Method and returning a new Picture is basically the following:

// Method called like the following If Self.mPolarBear <> Nil Then g.DrawPicture(Self.ImageResize(Me.Width, Me.Height, Me.ScaleFactor, Self.mPolarBear), 0, 0) End If

// Method definition Private Function ImageResize(targetWidth As Double, targetHeight As Double, scale As Double, img As Picture) as Picture If img <> Nil Then Var best As Picture = img.BestRepresentation(targetWidth, targetHeight, scale) If best <> Nil Then If best.Width = targetWidth And best.Height = targetHeight Then // selected image does not need resizing Return best Else // resize image Var pic As Picture Var ratio As Double = Min(targetWidth / best.Width, targetHeight / best.Height, 1.0) Var w As Int32 = Floor(best.Width * ratio) Var h As Int32 = Floor(best.Height * ratio) // resized image fits given target size, so place in top-left corner and fill width & height Var x As Int32 = 0 Var y As Int32 = 0 pic = New Picture(targetWidth, targetHeight) Var g As Graphics = pic.Graphics g.AntiAliasMode = Graphics.AntiAliasModes.HighQuality g.DrawPicture(best, x, y, w, h, 0, 0, best.Width, best.Height) Return pic End If End If Return Nil Else Return Nil End If End Function
But why should using a Method to return the same resized image be blurrier? Is there something about returning dynamic Pictures like this that I’m missing? Or is this a known consequence of doing things this way?

My Ultimate Goal

The reason for exploring the Method approach is to fix how Icons are rendered in pull-down Menus on Windows.

Because as I’ve found, on Windows when the ScaleFactor is a fraction of a whole number, e.g., 1.25, there is sometimes some weird upscaling or sizing problems that make the icons look non-standard. The results get even weirder if you have multiple monitors, where not all screens are using the same scaling size and you move your app from screen-to-screen.

For example:

The good news, is the Method I made appears to fix the Menu size issue on Windows, but I’m still concerned about the quality of the resized icon.

The following is how I’m assigning the Icon to the MenuItem:

Var w As Double = 16 * Me.ScaleFactor Var h As Double = 16 * Me.ScaleFactor EditPaste.Icon = Me.ImageResize(w, h, Me.ScaleFactor, Me.mPasteIcon)

If anyone is interested, here is a link to my test project (Xojo 2019r3.1)

Any insight would be appreciated. Thank you.

Well well well, it just so happens I put up a blog post about this one also: https://thezaz.com/blog/precise_drawing_with_doubles

Edit: This could also be due to scaling up instead. The problem with offloading to another method. Your ImageResize function produces a 1x picture, and you’re wanting to draw it at 1.25. You will need to set the Picture.Graphics.ScaleX, Picture.Graphics.ScaleY, Picture.HorizontalResolution, and Picture.VerticalResolution values according to the desired scale factor.

For a 1.25 scale, you would use

Pic.Graphics.ScaleX = 1.25
Pic.Graphics.ScaleY = 1.25
Pic.HorizontalResolution = 72 * Pic.Graphics.ScaleX
Pic.VerticalResolution = 72 * Pic.Graphics.ScaleY

But to complicate life, you also need to consider that your source (img) may be a different scale factor, so you’ll need to compensate for that too. Needless to say, this is tricky stuff. I’ll try to put together a blog post for this one too, because I don’t want to put information here that may be incorrect. These details are so hard to get correct that I don’t know them perfectly off the top of my head and need some time to get my thoughts together.

Thank you Thom, I did also see this other posting as well. I was using your ideas there to help fix a different problem (drawing in a ListBox row), that I’ll come back to later.

In the source of my test/example project there are commented out lines where I was playing with the *Resolution and Scale* properties trying to improve the output. But I obviously I didn’t take that far enough. Although I did find that assigning the ratio variable to ScaleX & ScaleY can resize an image without using all the parameters of Graphics.DrawPicture method, which I thought was interesting.

I appreciate your feedback Thom. Thank you.

Ok, so the work is simpler than I remember. Still, here’s a blog post on the issue: https://thezaz.com/blog/creating_pictures_in_hidpi

The meat of the topic is this simpler-than-expected method though:

[code]Public Function IconAtSize(Icon As Picture, Width As Integer, Height As Integer, Factor As Double) As Picture
Var ScaledIcon As Picture = Icon.BestRepresentation(Width, Height, Factor)

Var Pic As New Picture(Width * Factor, Height * Factor)
Pic.VerticalResolution = 72 * Factor
Pic.HorizontalResolution = 72 * Factor
Pic.Graphics.DrawPicture(ScaledIcon, 0, 0, Pic.Width, Pic.Height, 0, 0, ScaledIcon.Width, ScaledIcon.Height)

Return Pic
End Function[/code]

Awwwe-SOME! Thank you for your time Thom, this really fills in my lack of knowledge here.

I had starting playing with your earlier suggestions (i.e., ScaleX & ScaleY) and was getting good results when your new post came through, and yes, your new logic is much less verbose.

After reworking my example project (updated version here), my two images (one resized directly in the Canvas, the other returned from a method) now appear identical.

For my purposes, for resizing MenuItem icons, my implementation looks like slightly. But at it’s heart is your code. Here’s my new resize Method:

Private Function ImageResize(targetWidth As Double, targetHeight As Double, scale As Double, img As Picture, Optional higherQuality As Boolean) as Picture If img <> Nil Then Var best As Picture // if this is a single image or part of ImageSet // deterimine the best version to start the scaling If img.ImageCount = 0 Then best = img Else best = img.BestRepresentation(targetWidth, targetHeight, scale) End If If best <> Nil Then If best.Width = targetWidth And best.Height = targetHeight Then // selected image does not need resizing Return best Else // resize image where relevant sizing properties are re-scaled Var pic As New Picture(targetWidth * scale, targetHeight * scale) pic.HorizontalResolution = best.HorizontalResolution * scale pic.VerticalResolution = best.VerticalResolution * scale If higherQuality Then // optional pic.Graphics.AntiAliasMode = Graphics.AntiAliasModes.HighQuality End If pic.Graphics.DrawPicture(best, 0, 0, pic.Width, pic.Height, 0, 0, best.Width, best.Height) Return pic End If End If Return Nil Else Return Nil End If End Function

Your new logic also helped to simplify how I assign the MenuItem icon in the EnableMenuItems Event as well:

Var iconSize As Double = 16.0 // expected pixel size of MenuItem icons EditPaste.Icon = Me.ImageResize(iconSize, iconSize, Me.ScaleFactor, Me.mPasteIcon)

Thank you again Thom, you’re a hero of the Xojo community.

You’re very welcome. I remember having to figure this out a while ago, so I’m happy to share.