-
Notifications
You must be signed in to change notification settings - Fork 946
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix ToolboxBitmapAttribute
to improve error tolerance and better support PNG
based ICO
embedded resources
#11375
base: main
Are you sure you want to change the base?
Conversation
@MichalStrehovsky here is a PR with all the ico files converted. It resulted in errors in Winforms team might be able to help there. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #11375 +/- ##
===================================================
+ Coverage 74.26256% 74.36247% +0.09991%
===================================================
Files 3025 3028 +3
Lines 626861 627537 +676
Branches 46742 46765 +23
===================================================
+ Hits 465523 466652 +1129
+ Misses 157993 157531 -462
- Partials 3345 3354 +9
Flags with carried forward coverage won't be shown. Click here to find out more. |
@merriemcgaw Are embedded resources considered a part of the public API? Would a change like this be a breaking change? |
src/System.Drawing.Common/src/System/Drawing/ToolboxBitmapAttribute.cs
Outdated
Show resolved
Hide resolved
No, it shouldn't be considered a breaking change. |
What were the errors? It feels like the code this is touching should already handle PNG. Icon.ToBitmap used to throw but support for this was added long time ago (there was even a breaking change notice because the compat bar on .NET Framework is so high that even "something throwing a non-sensical exception" to "something working" is a breaking change, but fortunately we don't have to worry about that here). |
@@ -275,6 +288,11 @@ public override bool Equals([NotNullWhen(true)] object? value) | |||
img = GetBitmapFromResource(t, rawbmpname, large, scaled); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prior to this change, we would hit an ArgumentException
here bubbled up from trying to create a bitmap from the png icon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ultimately we should probably check the type via the stream header and then pass it to the correct function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have the stack around you could paste here?
I tried this in a small scratch project with one of the icons in this PR and it doesn't throw so I wonder how this could be caused by changing the icon format.
using System.Diagnostics.CodeAnalysis;
using System;
using System.Drawing;
Icon icon = new Icon(@"C:\Users\michals\Downloads\ScrollButtonUp.ico");
Icon sized = new Icon(icon, new Size(32, 32));
var b = sized.ToBitmap();
b = DpiHelper.ScaleBitmapToSize(b, new Size(64, 64));
b.Save(@"C:\Users\michals\Downloads\ScrollButtonUp.bmp");
internal static class DpiHelper
{
public static Bitmap ScaleBitmapToSize(Bitmap logicalImage, Size deviceImageSize)
{
Bitmap deviceImage = new(deviceImageSize.Width, deviceImageSize.Height, logicalImage.PixelFormat);
using var graphics = Graphics.FromImage(deviceImage);
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
RectangleF sourceRect = new(0, 0, logicalImage.Size.Width, logicalImage.Size.Height);
RectangleF destRect = new(0, 0, deviceImageSize.Width, deviceImageSize.Height);
// Specify a source rectangle shifted by half of pixel to account for GDI+ considering the source origin the center of top-left pixel
// Failing to do so will result in the right and bottom of the bitmap lines being interpolated with the graphics' background color,
// and will appear black even if we cleared the background with transparent color.
// The apparition of these artifacts depends on the interpolation mode, on the dpi scaling factor, etc.
// E.g. at 150% DPI, Bicubic produces them and NearestNeighbor is fine, but at 200% DPI NearestNeighbor also shows them.
sourceRect.Offset(-0.5f, -0.5f);
graphics.DrawImage(logicalImage, destRect, sourceRect, GraphicsUnit.Pixel);
return deviceImage;
}
/// <summary>
/// Create and return a new bitmap scaled to the specified size.
/// Note: this method should be called only inside an if (DpiHelper.IsScalingRequired) clause
/// </summary>
/// <param name="logicalImage">The image to scale from logical units to device units</param>
/// <param name="targetImageSize">The size to scale image to</param>
[return: NotNullIfNotNull(nameof(logicalImage))]
public static Bitmap? CreateResizedBitmap(Bitmap? logicalImage, Size targetImageSize)
{
if (logicalImage is null)
{
return null;
}
return ScaleBitmapToSize(logicalImage, targetImageSize);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exception was originally thrown at this call stack:
System.Private.Windows.Core.dll!Windows.Win32.Graphics.GdiPlus.StatusExtensions.ThrowIfFailed(Windows.Win32.Graphics.GdiPlus.Status status) Line 15 C#
System.Drawing.Common.dll!System.Drawing.Bitmap.Bitmap(System.IO.Stream stream, bool useIcm) Line 63 C#
System.Drawing.Common.dll!System.Drawing.Bitmap.Bitmap(System.IO.Stream stream) Line 50 C#
System.Drawing.Common.dll!System.Drawing.ToolboxBitmapAttribute.GetBitmapFromResource(System.Type t, string bitmapname, bool large, bool scaled) Line 191 C#
System.Drawing.Common.dll!System.Drawing.ToolboxBitmapAttribute.GetImageFromResource(System.Type t, string imageName, bool large, bool scaled) Line 275 C#
System.Drawing.Common.dll!System.Drawing.ToolboxBitmapAttribute.GetImageFromResource(System.Type t, string imageName, bool large) Line 222 C#
System.Drawing.Common.dll!System.Drawing.ToolboxBitmapAttribute.GetImage(System.Type type, string imgName, bool large) Line 79 C#
System.Drawing.Common.dll!System.Drawing.ToolboxBitmapAttribute.GetImage(System.Type type, bool large) Line 72 C#
System.Drawing.Common.dll!System.Drawing.ToolboxBitmapAttribute.GetImage(object component, bool large) Line 64 C#
System.Windows.Forms.Design.dll!System.Windows.Forms.Design.ComponentTray.TrayControl.UpdateIconInfo() Line 2410 C#
System.Windows.Forms.Design.dll!System.Windows.Forms.Design.ComponentTray.TrayControl.TrayControl(System.Windows.Forms.Design.ComponentTray tray, System.ComponentModel.IComponent component) Line 1902 C#
System.Windows.Forms.Design.dll!System.Windows.Forms.Design.ComponentTray.AddComponent(System.ComponentModel.IComponent component) Line 699 C#
System.Windows.Forms.Design.dll!System.Windows.Forms.Design.DocumentDesigner.OnComponentAdded(object source, System.ComponentModel.Design.ComponentEventArgs ce) Line 941 C#
System.Windows.Forms.Design.dll!System.ComponentModel.Design.DesignerHost.AddToContainerPostProcess(System.ComponentModel.IComponent component, System.ComponentModel.IContainer containerToAddTo) Line 296 C#
System.Windows.Forms.Design.dll!System.ComponentModel.Design.DesignerHost.PerformAdd(System.ComponentModel.IComponent component, string name) Line 156 C#
System.Windows.Forms.Design.dll!System.ComponentModel.Design.DesignerHost.System.ComponentModel.Design.IDesignerHost.CreateComponent(System.Type componentType, string name) Line 971 C#
System.Windows.Forms.Design.dll!System.ComponentModel.Design.DesignerHost.System.ComponentModel.Design.IDesignerHost.CreateComponent(System.Type componentType) Line 924 C#
DesignSurfaceExt.dll!DesignSurfaceExt.DesignSurfaceExt.CreateComponent<System.Windows.Forms.ContextMenuStrip>(out System.ComponentModel.Design.IDesignerHost host) Line 208 C#
DesignSurfaceExt.dll!DesignSurfaceExt.DesignSurfaceExt.CreateComponent<System.Windows.Forms.ContextMenuStrip>() Line 197 C#
DesignSurface.dll!TestConsole.MainForm.CreateDesignSurface(int n) Line 154 C#
DesignSurface.dll!TestConsole.MainForm.InitFormDesigner() Line 23 C#
DesignSurface.dll!TestConsole.MainForm.MainForm_Load(object sender, System.EventArgs e) Line 363 C#
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They use different PInvokes, so I guess that is why.
winforms/src/System.Drawing.Common/src/System/Drawing/Bitmap.cs
Lines 54 to 68 in 74caf32
public Bitmap(Stream stream, bool useIcm) | |
{ | |
ArgumentNullException.ThrowIfNull(stream); | |
using var iStream = stream.ToIStream(makeSeekable: true); | |
GpBitmap* bitmap = null; | |
Status status = useIcm | |
? PInvoke.GdipCreateBitmapFromStreamICM(iStream, &bitmap) | |
: PInvoke.GdipCreateBitmapFromStream(iStream, &bitmap); | |
status.ThrowIfFailed(); | |
ValidateImage((GpImage*)bitmap); | |
SetNativeImage((GpImage*)bitmap); | |
GetAnimatedGifRawData(this, filename: null, stream); | |
} |
winforms/src/System.Drawing.Common/src/System/Drawing/Icon.cs
Lines 117 to 124 in 74caf32
public Icon(Stream stream, int width, int height) : this() | |
{ | |
ArgumentNullException.ThrowIfNull(stream); | |
_iconData = new byte[(int)stream.Length]; | |
stream.ReadExactly(_iconData); | |
Initialize(width, height); | |
} |
winforms/src/System.Drawing.Common/src/System/Drawing/Icon.cs
Lines 545 to 562 in 74caf32
if ((_bestImageOffset % sizeof(nint)) != 0) | |
{ | |
// Beginning of icon's content is misaligned. | |
using BufferScope<byte> alignedBuffer = new((int)_bestBytesInRes); | |
bestImage.CopyTo(alignedBuffer.AsSpan()); | |
fixed (byte* b = alignedBuffer) | |
{ | |
_handle = PInvoke.CreateIconFromResourceEx(b, (uint)bestImage.Length, fIcon: true, 0x00030000, 0, 0, 0); | |
} | |
} | |
else | |
{ | |
fixed (byte* b = bestImage) | |
{ | |
_handle = PInvoke.CreateIconFromResourceEx(b, (uint)bestImage.Length, fIcon: true, 0x00030000, 0, 0, 0); | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, so because these embedded resources don't end in .ico
, we take the codepath that tries to create a Bitmap for this, but one cannot create a Bitmap for a PNG icon for whatever reason.
But are both of these try/catches needed? The one I was most puzzled about was the GetIconFromStream (that's the one I extracted into the standalone app and didn't see a problem).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was being overly safe there I think.
@LeafShi1 / @Olina-Zhang can your team please test this? The main test would be to open the new icon and compare it to the old icon. Also if you can trigger the CI to run the tests again that would be great. |
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
Thanks for taking this on @elachlan! I did a look through and compared the icons before/after change and AFAICT visually the icons look the same. I did, however, spot some differences when comparing sizes and perhaps some of them are expected:
|
@lonitra thank you so much for reviewing these. How are you checking the bit depth and sizing? |
I had opened the icons in Visual Studio and there was a bar on the side that showed the sizing and bits |
I've raised an ImageMagick issue at: ImageMagick/ImageMagick#7341 |
|
Hi @elachlan, we tested this PR by checking all ico files in your repo: Icon-Resize branch and Winforms repo: Main branch to compare them, found following result:
|
@Olina-Zhang I think the designer issue will need to be looked at by the team. I don't have access to the source code for the designer. ImageMagick are fixing one of the issues which should resolve some of the sizing corruption. |
@merriemcgaw changing the underlying BMP format to PNG in the ICO files is a breaking change if a user is creating a bitmap directly instead of first creating an Icon. Are we sure that changing resources is not a breaking change? |
If we didn't replace the binaries built from your PR, that issue doesn't repro. |
…m so they handles failures, as they are already nullable
ImageMagick fixed the sizing issue. So now the icons have appropriate sizes. The bit depth is changed on purpose by ImageMagick to save space when there are limited colors in an image. The artifacts we are seeing look like they are caused by |
Looks like ImageMagick might not support PNG in ICO files because of specification reasons: @MichalStrehovsky could you live with bitmap based ICO files? |
In a blank WinForms app, we currently have 48 icons that take over 51 kB each (plus some smaller ones I'm not counting). That's 2.4 MB. A blank WinForms app with AOT and a couple size savings options is 9.2 MB. So these icons are more than 25% of the blank app. If we make them 15% of the blank app, it's certainly an improvement. Still feels a bit excessive. In other places (ASP.NET, Blazor, etc.) we jump on and try to fix anything above 1%. First thing I'm getting from this is that the VS icon designer cannot be trusted (it shows various artifacts in both before and after versions). I'd ignore the VS icon designer completely and focus on differences that matter - e.g. load the icons as a .NET If the above fails or we don't want to go in that direction - can we instead get the icons out of the app? Then it doesn't matter how big they are. E.g. are these Designer icons? Could they be moved to the Designer assembly? Or could we instruct trimming to drop them based on some condition? What are they used for? 15% leaves a lot on the table. Last week I converted one of my WinForms apps to Native AOT. It's a fully functional and useful app. These icons would still constitute 10% of the app even if we cut their size in half because the whole app size was a little over 10 MB. |
Thinking about it more, even in the best case 4+% of the executable size would be icons. It might be the best to look for ways to trim these out of the app. A 4% size saving is still pretty interesting. Assuming these are actually not normally needed - understanding what they are used for would be the best. |
@Olina-Zhang could your team please report the VS artifact issue? I'll whip up a quick program to do the icon to bitmap conversion and see what before/after renders out to. With regards to trimming out resources, I am unsure how the trimmer works or how the compiler handles resources. Someone much more skilled them myself would need to investigate that. |
You mean this problem I listed: An exception pops up when adding ToolStrip/MenuStrip/StatusStrip/ContextMenuStrip control from toolBox to form designer? I'm not quite sure what you mean. Do you request a details about how to repro it or others? |
Sorry about that. Can you please report the issue when viewing the existing icons in visual studio? Visual Studio should display the icons properly. |
You mean open a Winforms designer bug in Winforms designer repo? That issue just reproduces when replaced these three binaries: System.Drawing.Common.dll, System.Windows.Forms.Design.dll and System.Windows.Forms.dll built from this PR, I am not sure if our testing is a valid validation. |
Haha, I see the problem you're referring to. I will open an issue for Visual Studio. |
Opened an internal VS artifact issue: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2067266. |
A couple options, not sure which one is applicable.
|
I am concerned, to be honest. |
@KlausLoeffelmann I think we can adjust Bitmap to check the streams leading bytes to establish what file type it is then create the bitmap accordingly. |
ICO
resource files and improve error tolerance in ToolboxBitmapAttribute
ToolboxBitmapAttribute
to improve error tolerance and better support PNG
based ICO
embedded resources
ToolboxBitmapAttribute
to improve error tolerance and better support PNG
based ICO
embedded resourcesToolboxBitmapAttribute
to improve error tolerance and better support PNG
based ICO
embedded resources
src/System.Drawing.Common/src/System/Drawing/ToolboxBitmapAttribute.cs
Outdated
Show resolved
Hide resolved
src/System.Drawing.Common/src/System/Drawing/ToolboxBitmapAttribute.cs
Outdated
Show resolved
Hide resolved
@@ -275,6 +288,11 @@ public override bool Equals([NotNullWhen(true)] object? value) | |||
img = GetBitmapFromResource(t, rawbmpname, large, scaled); | |||
} | |||
|
|||
if (img is null && rawbmpname is not null) | |||
{ | |||
img = GetIconFromResource(t, rawbmpname, large, scaled); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering:
What's the deal in general renaming variable name in something not abbreviated, which get not flagged by the spell checker? Are we changing this in the runtime in the context of touching the code? As an extra PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just included it here as a separate commit. As long as its not squashed it should be okay.
Changes to
ToolboxBitmapAttribute
were to avoid errors when a bitmap was expected but the icon file was a png based icon. The try catch method was used elsewhere in the class to avoid similar errors.Used the method outlined in the linked issue and wrote the following python script to automate:
Microsoft Reviewers: Open in CodeFlow