/* * 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); }