From be65cbaa935899195b6c5de439ee77767cb1969b Mon Sep 17 00:00:00 2001 From: Ryan Newington Date: Fri, 21 Aug 2026 09:32:53 +1000 Subject: [PATCH] Add transparent bitmap support for credential tiles --- .../BitmapControlTests.cs | 97 +++++++++++++++++++ ...et.CredentialProvider.UnitTests.x86.csproj | 1 + .../Controls/BitmapControl.cs | 56 ++++++++--- .../Controls/CredentialProviderLogoControl.cs | 5 +- .../Controls/UserTileControl.cs | 6 +- ...ialTile3.ICredentialProviderCredential3.cs | 4 +- .../CredentialTile3.cs | 2 +- 7 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs diff --git a/src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs b/src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs new file mode 100644 index 0000000..d543897 --- /dev/null +++ b/src/Lithnet.CredentialProvider.UnitTests.x64/BitmapControlTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using NUnit.Framework; + +namespace Lithnet.CredentialProvider.UnitTests +{ + public class BitmapControlTests + { + [Test] + public void TransparentBufferPreservesAlphaChannel() + { + using (Bitmap source = new Bitmap(2, 1, PixelFormat.Format32bppArgb)) + { + source.SetPixel(0, 0, Color.FromArgb(0, 10, 20, 30)); + source.SetPixel(1, 0, Color.FromArgb(128, 40, 50, 60)); + + var control = new UserTileControl("image", "Image", source); + + byte[] bytes = GetBitmapBuffer(control); + + Assert.That(bytes, Has.Length.GreaterThan(8)); + Assert.That(bytes[0], Is.EqualTo(0x89)); + Assert.That(bytes[1], Is.EqualTo(0x50)); + Assert.That(bytes[2], Is.EqualTo(0x4e)); + Assert.That(bytes[3], Is.EqualTo(0x47)); + + using (MemoryStream stream = new MemoryStream(bytes)) + using (Bitmap decoded = new Bitmap(stream)) + { + Assert.That(decoded.GetPixel(0, 0).A, Is.EqualTo(0)); + Assert.That(decoded.GetPixel(1, 0).A, Is.EqualTo(128)); + Assert.That(decoded.GetPixel(1, 0).R, Is.EqualTo(40)); + Assert.That(decoded.GetPixel(1, 0).G, Is.EqualTo(50)); + Assert.That(decoded.GetPixel(1, 0).B, Is.EqualTo(60)); + } + } + } + + [Test] + public void BitmapBufferDoesNotApplyConfiguredBackgroundColor() + { + using (Bitmap source = new Bitmap(1, 1, PixelFormat.Format32bppArgb)) + { + source.SetPixel(0, 0, Color.Transparent); + + var control = new UserTileControl("image", "Image", source) + { + BackgroundColor = Color.FromArgb(12, 34, 56) + }; + + byte[] bytes = GetBitmapBuffer(control); + + using (MemoryStream stream = new MemoryStream(bytes)) + using (Bitmap decoded = new Bitmap(stream)) + { + Assert.That(decoded.GetPixel(0, 0).A, Is.EqualTo(0)); + } + } + } + + [Test] + public void CloneCopiesBitmapAndBackgroundColor() + { + using (Bitmap source = new Bitmap(1, 1)) + { + var control = new UserTileControl("image", "Image", source) + { + BackgroundColor = Color.CornflowerBlue + }; + + var clone = (UserTileControl)control.Clone(); + + Assert.That(clone.Bitmap, Is.SameAs(source)); + Assert.That(clone.BackgroundColor, Is.EqualTo(Color.CornflowerBlue)); + } + } + + private static byte[] GetBitmapBuffer(BitmapControl control) + { + IntPtr buffer = control.GetBitmapBuffer(out uint size); + + try + { + byte[] bytes = new byte[size]; + Marshal.Copy(buffer, bytes, 0, checked((int)size)); + return bytes; + } + finally + { + Marshal.FreeCoTaskMem(buffer); + } + } + } +} diff --git a/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj b/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj index 31f57a7..70b55c6 100644 --- a/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj +++ b/src/Lithnet.CredentialProvider.UnitTests.x86/Lithnet.CredentialProvider.UnitTests.x86.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs b/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs index 15fa7ef..35c71b5 100644 --- a/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/BitmapControl.cs @@ -22,11 +22,16 @@ namespace Lithnet.CredentialProvider this.backgroundColor = Color.FromArgb(70, 70, 70); } - protected BitmapControl(BitmapControl source) : base(source) { } + protected BitmapControl(BitmapControl source) : base(source) + { + this.bitmap = source.bitmap; + this.backgroundColor = source.backgroundColor; + } /// - /// Specifies the background color that should replace any transparent elements of the image. This defaults to #707070 + /// Specifies the background color used to replace transparent pixels for and . This defaults to #707070. /// + /// This property does not apply to . public Color BackgroundColor { get { return this.backgroundColor; } @@ -53,10 +58,7 @@ namespace Lithnet.CredentialProvider { this.bitmap = value; - if (this.Events is ICredentialProviderCredentialEvents2 e) - { - e.SetFieldBitmap(this.Credential, this.Id, this.GetHBitmap()); - } + this.UpdateBitmap(); this.RaisePropertyChanged(); } @@ -76,26 +78,48 @@ namespace Lithnet.CredentialProvider internal IntPtr GetBitmapBuffer(out uint size) { size = 0; - var hbitmap = this.GetHBitmap(); - if (hbitmap == IntPtr.Zero) + if (this.bitmap == null) { return IntPtr.Zero; } - var image = Bitmap.FromHbitmap(hbitmap); - - IntPtr buffer = IntPtr.Zero; using (MemoryStream ms = new MemoryStream()) { - image.Save(ms, ImageFormat.Bmp); + this.bitmap.Save(ms, ImageFormat.Png); var bitmapBytes = ms.ToArray(); - size = (uint)bitmapBytes.Length; - buffer = Marshal.AllocCoTaskMem(bitmapBytes.Length); + size = checked((uint)bitmapBytes.Length); + IntPtr buffer = Marshal.AllocCoTaskMem(bitmapBytes.Length); Marshal.Copy(bitmapBytes, 0, buffer, bitmapBytes.Length); + return buffer; + } + } + + private void UpdateBitmap() + { + if (this.Credential is ICredentialProviderCredential3 && this.Events is ICredentialProviderCredentialEvents3 events3) + { + IntPtr buffer = this.GetBitmapBuffer(out uint size); + + try + { + events3.SetFieldBitmapBuffer(this.Credential, this.Id, size, buffer); + } + finally + { + if (buffer != IntPtr.Zero) + { + Marshal.FreeCoTaskMem(buffer); + } + } + + return; } - return buffer; + if (this.Events is ICredentialProviderCredentialEvents2 events2) + { + events2.SetFieldBitmap(this.Credential, this.Id, this.GetHBitmap()); + } } } -} \ No newline at end of file +} diff --git a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs index 0f93a27..f8b11b2 100644 --- a/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/CredentialProviderLogoControl.cs @@ -35,10 +35,7 @@ namespace Lithnet.CredentialProvider internal override ControlBase Clone() { - var clone = new CredentialProviderLogoControl(this); - clone.Bitmap = this.Bitmap; - clone.BackgroundColor = this.BackgroundColor; - return clone; + return new CredentialProviderLogoControl(this); } } } \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs b/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs index 92eb0cf..f37deb3 100644 --- a/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs +++ b/src/Lithnet.CredentialProvider/Controls/UserTileControl.cs @@ -32,11 +32,7 @@ namespace Lithnet.CredentialProvider internal override ControlBase Clone() { - var clone = new UserTileControl(this); - clone.Bitmap = this.Bitmap; - clone.BackgroundColor = this.BackgroundColor; - - return clone; + return new UserTileControl(this); } } } \ No newline at end of file diff --git a/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs b/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs index 088b9e5..892d37f 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile3.ICredentialProviderCredential3.cs @@ -3,7 +3,7 @@ using Lithnet.CredentialProvider.Interop; namespace Lithnet.CredentialProvider { - internal abstract partial class CredentialTile3 : ICredentialProviderCredential3 + public abstract partial class CredentialTile3 : ICredentialProviderCredential3 { int ICredentialProviderCredential3.GetBitmapBufferValue(uint dwFieldID, out uint pImageBufferSize, out IntPtr ppImageBuffer) { @@ -18,7 +18,7 @@ namespace Lithnet.CredentialProvider if (this.Controls.TryGetControl(dwFieldID, FieldType.TileImage, out var instance)) { - var hbitmap = instance.GetBitmapBuffer(out pImageBufferSize); + ppImageBuffer = instance.GetBitmapBuffer(out pImageBufferSize); return HRESULT.S_OK; } diff --git a/src/Lithnet.CredentialProvider/CredentialTile3.cs b/src/Lithnet.CredentialProvider/CredentialTile3.cs index 20207b1..fa0f365 100644 --- a/src/Lithnet.CredentialProvider/CredentialTile3.cs +++ b/src/Lithnet.CredentialProvider/CredentialTile3.cs @@ -7,7 +7,7 @@ namespace Lithnet.CredentialProvider /// Represents a user credential tile that implements the functionality of and , but includes support for dynamically updating bitmap images. /// /// This interface is public, but undocumented by Microsoft. It is recommended to use tiles unless this specific functionality is needed - internal abstract partial class CredentialTile3 : CredentialTile2 + public abstract partial class CredentialTile3 : CredentialTile2 { protected CredentialTile3(CredentialProviderBase credentialProvider) : this(credentialProvider, null) { }