using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; var options = IconGeneratorOptions.Parse(args); var pngFrames = new List(); foreach (var size in options.Sizes) { using var bitmap = options.Style switch { IconStyle.LegacyGem => DrawLegacyGemDiamond(size), _ => DrawAxDiamondPixel(size), }; using var stream = new MemoryStream(); bitmap.Save(stream, ImageFormat.Png); pngFrames.Add(stream.ToArray()); Console.WriteLine($" {size}x{size} OK"); if (options.PreviewSize == size && !string.IsNullOrWhiteSpace(options.PreviewPath)) bitmap.Save(options.PreviewPath!, ImageFormat.Png); } CreateIco(pngFrames, options.Sizes, options.OutputPath); Console.WriteLine($"Icon saved: {options.OutputPath}"); static Bitmap DrawAxDiamondPixel(int size) { var bitmap = new Bitmap(size, size, PixelFormat.Format32bppArgb); using var g = Graphics.FromImage(bitmap); g.Clear(Color.Transparent); g.SmoothingMode = SmoothingMode.AntiAlias; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.CompositingQuality = CompositingQuality.HighQuality; var canvas = size; var groupSpan = canvas * 0.55f; var tileSize = canvas * 0.245f; var gap = Math.Max(1f, groupSpan - tileSize * 2f); var left = (canvas - groupSpan) / 2f; var top = left; var radius = Math.Max(2f, tileSize * 0.16f); var center = new PointF(canvas / 2f, canvas / 2f); var tiles = new[] { new IconTile( new RectangleF(left, top, tileSize, tileSize), Color.FromArgb(0x74, 0x8E, 0xFF), Color.FromArgb(0x5B, 0x70, 0xF7)), new IconTile( new RectangleF(left + tileSize + gap, top, tileSize, tileSize), Color.FromArgb(0x7C, 0xF0, 0x88), Color.FromArgb(0x42, 0xD6, 0x63)), new IconTile( new RectangleF(left, top + tileSize + gap, tileSize, tileSize), Color.FromArgb(0x64, 0xDB, 0x8B), Color.FromArgb(0x36, 0xC6, 0x78)), new IconTile( new RectangleF(left + tileSize + gap, top + tileSize + gap, tileSize, tileSize), Color.FromArgb(0xFF, 0x6C, 0x84), Color.FromArgb(0xF2, 0x47, 0x69)), }; var shadowOffset = Math.Max(0.45f, canvas * 0.02f); using var shadowBrush = new SolidBrush(Color.FromArgb((int)(255 * 0.16f), 0x2A, 0x34, 0x56)); foreach (var tile in tiles) { using var shadowPath = CreateRoundedRectangle(new RectangleF( tile.Bounds.X + shadowOffset, tile.Bounds.Y + shadowOffset, tile.Bounds.Width, tile.Bounds.Height), radius); using var matrix = new Matrix(); matrix.RotateAt(45f, center); shadowPath.Transform(matrix); g.FillPath(shadowBrush, shadowPath); } foreach (var tile in tiles) { using var path = CreateRoundedRectangle(tile.Bounds, radius); using var matrix = new Matrix(); matrix.RotateAt(45f, center); path.Transform(matrix); using var brush = new LinearGradientBrush(tile.Bounds, tile.Highlight, tile.Base, 45f, true); g.FillPath(brush, path); using var outline = new Pen(Color.FromArgb((int)(255 * 0.26f), 255, 255, 255), Math.Max(0.7f, canvas / 60f)) { LineJoin = LineJoin.Round }; g.DrawPath(outline, path); } using var shineBrush = new SolidBrush(Color.FromArgb((int)(255 * 0.18f), 255, 255, 255)); var shineSize = Math.Max(1.2f, canvas * 0.07f); foreach (var point in new[] { new PointF(canvas * 0.30f, canvas * 0.28f), new PointF(canvas * 0.67f, canvas * 0.30f), new PointF(canvas * 0.34f, canvas * 0.66f), }) { g.FillEllipse(shineBrush, point.X, point.Y, shineSize, shineSize); } return bitmap; } static Bitmap DrawLegacyGemDiamond(int size) { var bmp = new Bitmap(size, size, PixelFormat.Format32bppArgb); using var g = Graphics.FromImage(bmp); g.SmoothingMode = SmoothingMode.HighQuality; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.Clear(Color.Transparent); var s = size; var cx = s / 2f; var tableL = s * 0.22f; var tableR = s * 0.78f; var tableY = s * 0.18f; var girdleL = s * 0.06f; var girdleR = s * 0.94f; var girdleY = s * 0.40f; var culetX = cx; var culetY = s * 0.92f; PointF tl = new(tableL, tableY); PointF tr = new(tableR, tableY); PointF gl = new(girdleL, girdleY); PointF gr = new(girdleR, girdleY); PointF bt = new(culetX, culetY); PointF cm = new(cx, girdleY); PointF ct = new(cx, tableY); var blue = Color.FromArgb(50, 110, 230); var blueBright = Color.FromArgb(80, 150, 255); var green = Color.FromArgb(60, 200, 80); var greenBright = Color.FromArgb(100, 235, 110); var red = Color.FromArgb(230, 50, 65); var redBright = Color.FromArgb(255, 90, 100); var greenDk = Color.FromArgb(45, 170, 75); var greenDkBr = Color.FromArgb(80, 210, 100); FillPolygon(g, new[] { tl, gl, cm, ct }, blueBright, blue); FillPolygon(g, new[] { tr, ct, cm, gr }, greenBright, green); FillPolygon(g, new[] { gl, cm, bt }, redBright, red); FillPolygon(g, new[] { cm, gr, bt }, greenDkBr, greenDk); using var crownShadow = new SolidBrush(Color.FromArgb(25, 0, 0, 0)); g.FillPolygon(crownShadow, new[] { tl, gl, new PointF(cx * 0.7f, girdleY * 0.85f) }); using var pavHighlight = new SolidBrush(Color.FromArgb(20, 255, 255, 255)); g.FillPolygon(pavHighlight, new[] { cm, bt, new PointF(cx - s * 0.08f, s * 0.62f) }); using var facetPen = new Pen(Color.FromArgb(180, 255, 255, 255), Math.Max(0.8f, s / 140f)) { LineJoin = LineJoin.Round, StartCap = LineCap.Round, EndCap = LineCap.Round }; g.DrawLine(facetPen, tl, tr); g.DrawLine(facetPen, tl, gl); g.DrawLine(facetPen, tr, gr); g.DrawLine(facetPen, ct, cm); g.DrawLine(facetPen, tl, cm); g.DrawLine(facetPen, tr, cm); g.DrawLine(facetPen, tl, new PointF(girdleL + (cx - girdleL) * 0.5f, girdleY)); g.DrawLine(facetPen, tr, new PointF(cx + (girdleR - cx) * 0.5f, girdleY)); g.DrawLine(facetPen, gl, gr); g.DrawLine(facetPen, gl, bt); g.DrawLine(facetPen, gr, bt); g.DrawLine(facetPen, cm, bt); var crossY = girdleY + (culetY - girdleY) * 0.45f; PointF crossL = new(girdleL + (culetX - girdleL) * 0.45f, crossY); PointF crossR = new(girdleR - (girdleR - culetX) * 0.45f, crossY); g.DrawLine(facetPen, gl, crossR); g.DrawLine(facetPen, gr, crossL); g.DrawLine(facetPen, new PointF(girdleL + (cx - girdleL) * 0.5f, girdleY), bt); g.DrawLine(facetPen, new PointF(cx + (girdleR - cx) * 0.5f, girdleY), bt); using var outlinePen = new Pen(Color.FromArgb(220, 255, 255, 255), Math.Max(1.2f, s / 100f)) { LineJoin = LineJoin.Round }; g.DrawPolygon(outlinePen, new[] { tl, tr, gr, bt, gl }); using var tableHighlight = new LinearGradientBrush( tl, new PointF(cx, girdleY), Color.FromArgb(45, 255, 255, 255), Color.Transparent); g.FillPolygon(tableHighlight, new[] { tl, tr, ct }); return bmp; } static GraphicsPath CreateRoundedRectangle(RectangleF bounds, float radius) { var path = new GraphicsPath(); var diameter = radius * 2f; if (radius <= 0.01f) { path.AddRectangle(bounds); return path; } path.AddArc(bounds.Left, bounds.Top, diameter, diameter, 180, 90); path.AddArc(bounds.Right - diameter, bounds.Top, diameter, diameter, 270, 90); path.AddArc(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter, 0, 90); path.AddArc(bounds.Left, bounds.Bottom - diameter, diameter, diameter, 90, 90); path.CloseFigure(); return path; } static void FillPolygon(Graphics g, PointF[] points, Color topColor, Color bottomColor) { var minX = points.Min(p => p.X); var maxX = points.Max(p => p.X); var minY = points.Min(p => p.Y); var maxY = points.Max(p => p.Y); var rect = new RectangleF(minX, minY, Math.Max(1, maxX - minX), Math.Max(1, maxY - minY)); using var brush = new LinearGradientBrush(rect, topColor, bottomColor, LinearGradientMode.Vertical); g.FillPolygon(brush, points); } static void CreateIco(IReadOnlyList pngs, IReadOnlyList sizes, string outputPath) { using var memoryStream = new MemoryStream(); using var writer = new BinaryWriter(memoryStream); writer.Write((short)0); writer.Write((short)1); writer.Write((short)pngs.Count); var offset = 6 + 16 * pngs.Count; for (var i = 0; i < pngs.Count; i++) { var dimension = (byte)(sizes[i] >= 256 ? 0 : sizes[i]); writer.Write(dimension); writer.Write(dimension); writer.Write((byte)0); writer.Write((byte)0); writer.Write((short)1); writer.Write((short)32); writer.Write(pngs[i].Length); writer.Write(offset); offset += pngs[i].Length; } foreach (var png in pngs) writer.Write(png); File.WriteAllBytes(outputPath, memoryStream.ToArray()); } file sealed record IconTile(RectangleF Bounds, Color Highlight, Color Base); file enum IconStyle { AxDiamond, LegacyGem, } file sealed class IconGeneratorOptions { public string OutputPath { get; init; } = ""; public string? PreviewPath { get; init; } public int PreviewSize { get; init; } = 256; public int[] Sizes { get; init; } = [16, 20, 24, 32, 40, 48, 64, 128, 256]; public IconStyle Style { get; init; } = IconStyle.AxDiamond; public static IconGeneratorOptions Parse(string[] args) { string? outputPath = null; string? previewPath = null; var previewSize = 256; var style = IconStyle.AxDiamond; foreach (var arg in args) { if (arg.StartsWith("--preview=", StringComparison.OrdinalIgnoreCase)) { previewPath = arg["--preview=".Length..].Trim('"'); continue; } if (arg.StartsWith("--preview-size=", StringComparison.OrdinalIgnoreCase) && int.TryParse(arg["--preview-size=".Length..], out var parsedPreviewSize)) { previewSize = parsedPreviewSize; continue; } if (arg.StartsWith("--style=", StringComparison.OrdinalIgnoreCase)) { var styleValue = arg["--style=".Length..].Trim().ToLowerInvariant(); style = styleValue switch { "legacy-gem" or "gem" => IconStyle.LegacyGem, _ => IconStyle.AxDiamond, }; continue; } if (!arg.StartsWith("--", StringComparison.Ordinal)) outputPath ??= arg.Trim('"'); } outputPath ??= Path.Combine(AppContext.BaseDirectory, "icon.ico"); return new IconGeneratorOptions { OutputPath = outputPath, PreviewPath = previewPath, PreviewSize = previewSize, Style = style, }; } }