Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`LogsDirectory`. The log rotates at 5 MB.

### Fixed
- Tray icon initialisation crash, take two
(`ArgumentException: Argument 'picture' must be a picture that can be
used as a Icon.`): once H.NotifyIcon could read the file (PR #15) it
passed the bytes to `new System.Drawing.Icon(stream, size)`, which
only accepts ICO-format input. The tray service now wraps the
identicon PNG in a minimal ICO container (modern Windows accepts
PNG-in-ICO) and writes `%LOCALAPPDATA%\Snipdeck\tray-icon.ico`.
- Tray icon initialisation crash (`NullReferenceException` inside
`H.NotifyIcon.ImageExtensions.ToStream(Uri)`): H.NotifyIcon resolves
`TaskbarIcon.IconSource` by reading `BitmapImage.UriSource`, but the
tray service was loading the identicon via `BitmapImage.SetSourceAsync`
from an in-memory stream — pixels populated, URI null. The service now
persists the identicon to `%LOCALAPPDATA%\Snipdeck\tray-icon.png` and
persists the identicon to `%LOCALAPPDATA%\Snipdeck\tray-icon.ico` and
loads the `BitmapImage` from that URI, which is the shape H.NotifyIcon
expects.
- First-run crash on startup (`RPC_E_WRONG_THREAD` / `0x8001010E`):
Expand Down
47 changes: 40 additions & 7 deletions src/Snipdeck.App/Services/HNotifyIconTrayService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Buffers.Binary;

using H.NotifyIcon;

using Microsoft.UI.Xaml.Controls;
Expand All @@ -12,7 +14,8 @@ internal sealed partial class HNotifyIconTrayService : ITrayService
{
// Stable seed so the tray icon never changes between runs of Snipdeck.
private static readonly Guid _iconSeed = Guid.Parse("5147DEC0-0000-0000-0000-000000000001");
private const string _trayIconFileName = "tray-icon.png";
private const string _trayIconFileName = "tray-icon.ico";
private const int _trayIconPixels = 32;

private readonly IPathProvider _paths;
private TaskbarIcon? _icon;
Expand All @@ -37,10 +40,10 @@ public async Task InitialiseAsync()
return;
}

// H.NotifyIcon resolves IconSource by reading BitmapImage.UriSource,
// not the pixel data — a stream-loaded BitmapImage NREs deep inside
// the library. Persist the identicon to a stable file path and load
// the BitmapImage from that URI instead.
// H.NotifyIcon resolves IconSource by reading BitmapImage.UriSource
// and then hands the file bytes to System.Drawing.Icon — which only
// accepts ICO format. Wrap the identicon PNG in a minimal ICO
// container before writing it out.
var iconPath = await WriteTrayIconFileAsync();
var image = new BitmapImage(new Uri(iconPath, UriKind.Absolute));

Expand Down Expand Up @@ -102,13 +105,43 @@ private void RaiseShowRequested()

private async Task<string> WriteTrayIconFileAsync()
{
var bytes = IdenticonService.GeneratePng(_iconSeed, size: 32);
var pngBytes = IdenticonService.GeneratePng(_iconSeed, size: _trayIconPixels);
var icoBytes = WrapPngAsIco(pngBytes, _trayIconPixels, _trayIconPixels);
_ = Directory.CreateDirectory(_paths.AppDataDirectory);
var path = Path.Combine(_paths.AppDataDirectory, _trayIconFileName);
await File.WriteAllBytesAsync(path, bytes);
await File.WriteAllBytesAsync(path, icoBytes);
return path;
}

// Modern Windows accepts a PNG embedded inside an ICO container —
// just a 6-byte ICONDIR header plus a 16-byte ICONDIRENTRY pointing
// at the PNG bytes. Format ref: ICONDIR / ICONDIRENTRY on MSDN.
private static byte[] WrapPngAsIco(byte[] png, int width, int height)
{
const int headerSize = 6;
const int entrySize = 16;
const int dataOffset = headerSize + entrySize;

var ico = new byte[dataOffset + png.Length];
var span = ico.AsSpan();

// ICONDIR: reserved(0), type(1 = icon), count(1)
BinaryPrimitives.WriteUInt16LittleEndian(span[2..4], 1);
BinaryPrimitives.WriteUInt16LittleEndian(span[4..6], 1);

// ICONDIRENTRY: width/height bytes are 0 for >=256, otherwise the literal size.
ico[6] = width >= 256 ? (byte)0 : (byte)width;
ico[7] = height >= 256 ? (byte)0 : (byte)height;
// ico[8] colorCount = 0 (>= 8bpp), ico[9] reserved = 0 — already zero.
BinaryPrimitives.WriteUInt16LittleEndian(span[10..12], 1); // planes
BinaryPrimitives.WriteUInt16LittleEndian(span[12..14], 32); // bits per pixel
BinaryPrimitives.WriteUInt32LittleEndian(span[14..18], (uint)png.Length);
BinaryPrimitives.WriteUInt32LittleEndian(span[18..22], dataOffset);

Buffer.BlockCopy(png, 0, ico, dataOffset, png.Length);
return ico;
}

private sealed partial class RelayCommand(Action execute) : System.Windows.Input.ICommand
{
#pragma warning disable CS0067 // 'CanExecuteChanged' is never used — relay never changes.
Expand Down
Loading