/*
* Copyright (c) 2026 James Maes (KofTwentyTwo)
* SPDX-License-Identifier: MIT
*/
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using KofTwentyTwo.AppKit;
namespace BrandTool;
///
/// brandtool --out <dir> [--start #RRGGBB] [--end #RRGGBB] [--glyph "<svg path data>"] [--stroke 56]
/// Writes app-icon.svg and app.ico (16..256 px PNG frames) into <dir>.
///
internal static class Program
{
private const double Box = 512;
private const double TileInset = 48;
private const double TileRadius = 104;
private static readonly int[] s_iconSizes = [16, 20, 24, 32, 40, 48, 64, 128, 256];
/// Entry point: reads the options, then writes app-icon.svg and a multi-size app.ico to the output folder.
[STAThread]
private static int Main(string[] args)
{
Dictionary options = Parse(args);
if(!options.TryGetValue("out", out string? outDir))
{
Console.Error.WriteLine("usage: brandtool --out [--start #RRGGBB] [--end #RRGGBB] [--glyph \"\"] [--stroke ]");
return 2;
}
var spec = new MarkSpec(
options.GetValueOrDefault("start", "#3B4CCA"),
options.GetValueOrDefault("end", "#2A2F8F"),
options.GetValueOrDefault("glyph", "M 200 152 L 312 256 L 200 360"),
double.Parse(options.GetValueOrDefault("stroke", "56"), CultureInfo.InvariantCulture));
Directory.CreateDirectory(outDir);
File.WriteAllText(Path.Combine(outDir, "app-icon.svg"), Svg(spec));
WriteIco(Path.Combine(outDir, "app.ico"), s_iconSizes.Select(size => Png(spec, size)).ToList(), s_iconSizes);
Console.WriteLine($"Wrote app-icon.svg and app.ico to {Path.GetFullPath(outDir)}");
return 0;
}
/// Parses --name value pairs into a case-insensitive dictionary.
private static Dictionary Parse(string[] args)
{
var options = new Dictionary(StringComparer.OrdinalIgnoreCase);
for(int i = 0; i + 1 < args.Length; i += 2)
{
options[args[i].TrimStart('-')] = args[i + 1];
}
return options;
}
/// The mark as SVG text: the rounded gradient tile and the white stroked glyph.
private static string Svg(MarkSpec spec)
=> $"""
""";
/// Renders the mark with WPF as a PNG of the given pixel size.
private static byte[] Png(MarkSpec spec, int size)
{
var visual = new DrawingVisual();
using(DrawingContext context = visual.RenderOpen())
{
double scale = size / Box;
context.PushTransform(new ScaleTransform(scale, scale));
var gradient = new LinearGradientBrush(ToColor(spec.Start), ToColor(spec.End), new Point(0, 0), new Point(1, 1));
context.DrawRoundedRectangle(gradient, null, new Rect(TileInset, TileInset, Box - 2 * TileInset, Box - 2 * TileInset), TileRadius, TileRadius);
var pen = new Pen(Brushes.White, spec.Stroke) { StartLineCap = PenLineCap.Round, EndLineCap = PenLineCap.Round, LineJoin = PenLineJoin.Round };
context.DrawGeometry(null, pen, Geometry.Parse(spec.Glyph));
}
var bitmap = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(visual);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmap));
using var stream = new MemoryStream();
encoder.Save(stream);
return stream.ToArray();
}
/// Writes an .ico whose frames are embedded PNGs (supported since Windows Vista).
private static void WriteIco(string path, List pngs, int[] sizes)
{
using var writer = new BinaryWriter(File.Create(path));
writer.Write((ushort)0); // reserved
writer.Write((ushort)1); // type: icon
writer.Write((ushort)pngs.Count);
int offset = 6 + 16 * pngs.Count;
for(int i = 0; i < pngs.Count; i++)
{
writer.Write((byte)(sizes[i] >= 256 ? 0 : sizes[i])); // 0 means 256
writer.Write((byte)(sizes[i] >= 256 ? 0 : sizes[i]));
writer.Write((byte)0); // palette
writer.Write((byte)0); // reserved
writer.Write((ushort)1); // planes
writer.Write((ushort)32); // bits per pixel
writer.Write(pngs[i].Length);
writer.Write(offset);
offset += pngs[i].Length;
}
foreach(byte[] png in pngs)
{
writer.Write(png);
}
}
/// Parses a #RRGGBB or #AARRGGBB string into a WPF color.
private static Color ToColor(string hex)
{
var color = ArgbColor.Parse(hex);
return Color.FromArgb(color.A, color.R, color.G, color.B);
}
/// The mark definition: gradient colors, the glyph's SVG path data, and its stroke width.
private sealed record MarkSpec(string Start, string End, string Glyph, double Stroke);
}