Phase 3: app icon (multi-resolution ICO + master PNG)

scripts/generate-icons.ps1 renders the icon programmatically with
System.Drawing - rounded teal square (#0E7C66) with a stylized white
hook glyph - at 16/24/32/48/64/128/256 px and assembles a proper
multi-resolution Microsoft ICO. The PNG and ICO outputs land in
resources/. The script is the source of truth; re-run after editing
the design.

GUI csproj uses ApplicationIcon for the EXE icon and embeds the .ico
+ .png as Resources so MainWindow and AboutDialog can use them via
WPF's resource URI scheme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 09:48:33 -04:00
parent a45d994c18
commit f3bca1e8ff
6 changed files with 148 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+138
View File
@@ -0,0 +1,138 @@
<#
.SYNOPSIS
Generates webhook-server.ico (multi-resolution) and webhook-server.png from
a programmatic design. Re-run after changing Draw-Icon to refresh assets.
.DESCRIPTION
Renders the icon at 16/24/32/48/64/128/256 px using System.Drawing, then
assembles a Microsoft-format ICO file with each size embedded as PNG. No
external tools required.
Design: a rounded-square teal background (#0E7C66) with a stylized white
hook shape (a "J"-like curve with an arrow tip).
#>
[CmdletBinding()]
param(
[string]$OutputDir = (Join-Path $PSScriptRoot '..\resources')
)
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
function New-IconBitmap([int]$size) {
$bmp = New-Object System.Drawing.Bitmap $size, $size, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
$g = [System.Drawing.Graphics]::FromImage($bmp)
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
# Background: rounded square in brand teal.
$bgColor = [System.Drawing.Color]::FromArgb(0xFF, 0x0E, 0x7C, 0x66)
$bgBrush = New-Object System.Drawing.SolidBrush $bgColor
$radius = [int]($size * 0.22)
$rect = New-Object System.Drawing.RectangleF 0, 0, $size, $size
$path = New-Object System.Drawing.Drawing2D.GraphicsPath
$d = $radius * 2
$path.AddArc($rect.X, $rect.Y, $d, $d, 180, 90)
$path.AddArc($rect.Right - $d, $rect.Y, $d, $d, 270, 90)
$path.AddArc($rect.Right - $d, $rect.Bottom - $d, $d, $d, 0, 90)
$path.AddArc($rect.X, $rect.Bottom - $d, $d, $d, 90, 90)
$path.CloseFigure()
$g.FillPath($bgBrush, $path)
# Foreground: white hook shape - a thick curved stroke shaped like a "J"
# tipped with an arrowhead, sized relative to the canvas.
$fgColor = [System.Drawing.Color]::White
$stroke = [Math]::Max(2, [int]($size * 0.12))
$pen = New-Object System.Drawing.Pen $fgColor, $stroke
$pen.StartCap = [System.Drawing.Drawing2D.LineCap]::Round
$pen.EndCap = [System.Drawing.Drawing2D.LineCap]::Round
$pen.LineJoin = [System.Drawing.Drawing2D.LineJoin]::Round
# Hook curve: vertical down-stroke on the right, then a half-circle arc
# curling to the left and ending in a small filled dot for the hook tip.
$cx = [single]($size * 0.62)
$top = [single]($size * 0.22)
$bottom = [single]($size * 0.58)
$arcD = [single]($size * 0.34) # arc diameter
$arcLeft = [single]($cx - $arcD) # left edge of arc circle
# Vertical stroke from (cx, top) to (cx, bottom).
$g.DrawLine($pen, $cx, $top, $cx, $bottom)
# Half-circle arc beneath: starts at (cx, bottom), curls to (cx - arcD, bottom).
$arcRect = New-Object System.Drawing.RectangleF $arcLeft, ([single]($bottom - $arcD / 2)), $arcD, $arcD
$g.DrawArc($pen, $arcRect, 0, 180)
# Filled circle at the tip end of the arc.
$tipR = [single]($stroke * 0.6)
$tipX = $arcLeft
$tipY = [single]($bottom)
$brushFg = New-Object System.Drawing.SolidBrush $fgColor
$g.FillEllipse($brushFg, [single]($tipX - $tipR), [single]($tipY - $tipR), [single]($tipR * 2), [single]($tipR * 2))
$brushFg.Dispose(); $pen.Dispose(); $bgBrush.Dispose(); $path.Dispose()
$g.Dispose()
return $bmp
}
# Generate each size as PNG bytes. Hashtable keys are prefixed with "s" because
# PowerShell hashtable lookups by integer key behave inconsistently with PSObject
# wrapping; string keys round-trip cleanly.
$sizes = @(16, 24, 32, 48, 64, 128, 256)
$pngs = @{}
foreach ($s in $sizes) {
$bmp = New-IconBitmap $s
$ms = New-Object System.IO.MemoryStream
$bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
$pngs["s$s"] = $ms.ToArray()
$ms.Dispose()
$bmp.Dispose()
}
# Save the master 256 PNG separately for places that need a transparent PNG.
$pngPath = Join-Path $OutputDir 'webhook-server.png'
[System.IO.File]::WriteAllBytes($pngPath, [byte[]]$pngs['s256'])
# Assemble multi-resolution ICO.
$icoPath = Join-Path $OutputDir 'webhook-server.ico'
$ms = New-Object System.IO.MemoryStream
$bw = New-Object System.IO.BinaryWriter $ms
try {
$count = $sizes.Count
$bw.Write([UInt16]0) # idReserved
$bw.Write([UInt16]1) # idType: 1 = ICO
$bw.Write([UInt16]$count) # idCount
# Directory entries (16 bytes each).
$offset = 6 + 16 * $count
foreach ($s in $sizes) {
$bytes = $pngs["s$s"]
$w = if ($s -ge 256) { 0 } else { $s }
$h = if ($s -ge 256) { 0 } else { $s }
$bw.Write([byte]$w) # width
$bw.Write([byte]$h) # height
$bw.Write([byte]0) # colorCount
$bw.Write([byte]0) # reserved
$bw.Write([UInt16]1) # planes
$bw.Write([UInt16]32) # bitCount
$bw.Write([UInt32]$bytes.Length)
$bw.Write([UInt32]$offset)
$offset += $bytes.Length
}
# Image data.
foreach ($s in $sizes) { $bw.Write($pngs["s$s"]) }
$bw.Flush()
[System.IO.File]::WriteAllBytes($icoPath, $ms.ToArray())
}
finally {
$bw.Dispose(); $ms.Dispose()
}
Write-Host "Wrote $icoPath ($((Get-Item $icoPath).Length) bytes)"
Write-Host "Wrote $pngPath ($((Get-Item $pngPath).Length) bytes)"
+1
View File
@@ -7,6 +7,7 @@
xmlns:models="clr-namespace:WebhookServer.Core.Models;assembly=WebhookServer.Core" xmlns:models="clr-namespace:WebhookServer.Core.Models;assembly=WebhookServer.Core"
mc:Ignorable="d" mc:Ignorable="d"
Title="Webhook Server" Height="600" Width="1000" Title="Webhook Server" Height="600" Width="1000"
Icon="/webhook-server.ico"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"> d:DataContext="{d:DesignInstance Type=vm:MainViewModel}">
<Window.InputBindings> <Window.InputBindings>
<KeyBinding Key="N" Modifiers="Control" Command="{Binding AddEndpointCommand}"/> <KeyBinding Key="N" Modifiers="Control" Command="{Binding AddEndpointCommand}"/>
+2 -1
View File
@@ -2,9 +2,10 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About Webhook Server" Title="About Webhook Server"
Height="320" Width="420" Height="360" Width="440"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
Icon="/webhook-server.ico"
ShowInTaskbar="False"> ShowInTaskbar="False">
<Grid Margin="20"> <Grid Margin="20">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -14,6 +14,13 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<ApplicationIcon>..\..\resources\webhook-server.ico</ApplicationIcon>
<AssemblyTitle>Webhook Server</AssemblyTitle>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Resource Include="..\..\resources\webhook-server.ico" Link="webhook-server.ico" />
<Resource Include="..\..\resources\webhook-server.png" Link="webhook-server.png" />
</ItemGroup>
</Project> </Project>