Add transparent bitmap support for credential tiles

This commit is contained in:
Ryan Newington
2026-08-21 09:32:53 +10:00
parent 459470a5ce
commit be65cbaa93
7 changed files with 143 additions and 28 deletions
@@ -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);
}
}
}
}
@@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\Lithnet.CredentialProvider\Lithnet.CredentialProvider.csproj" />
<Compile Include="..\Lithnet.CredentialProvider.UnitTests.x64\BitmapControlTests.cs" Link="BitmapControlTests.cs" />
</ItemGroup>
<ItemGroup>
@@ -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;
}
/// <summary>
/// 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 <see cref="CredentialTile"/> and <see cref="CredentialTile2"/>. This defaults to #707070.
/// </summary>
/// <remarks>This property does not apply to <see cref="CredentialTile3"/>.</remarks>
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());
}
}
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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<BitmapControl>(dwFieldID, FieldType.TileImage, out var instance))
{
var hbitmap = instance.GetBitmapBuffer(out pImageBufferSize);
ppImageBuffer = instance.GetBitmapBuffer(out pImageBufferSize);
return HRESULT.S_OK;
}
@@ -7,7 +7,7 @@ namespace Lithnet.CredentialProvider
/// Represents a user credential tile that implements the functionality of <see cref="CredentialTile"/> and <see cref="CredentialTile2"/>, but includes support for dynamically updating bitmap images.
/// </summary>
/// <remarks>This interface is public, but undocumented by Microsoft. It is recommended to use <see cref="CredentialTile2"/> tiles unless this specific functionality is needed</remarks>
internal abstract partial class CredentialTile3 : CredentialTile2
public abstract partial class CredentialTile3 : CredentialTile2
{
protected CredentialTile3(CredentialProviderBase credentialProvider) : this(credentialProvider, null) { }