Files
AX-Copilot-Codex/tools/IconGenerator/Program.cs
lacvet d6a8ab0ddb AX Copilot 아이콘 점유율과 트레이 DPI 프레임을 키운다
작업 표시줄과 트레이에서 AX Copilot 아이콘이 다른 앱보다 작게 보이던 원인은 icon.ico 내부 여백이 커서 실제 도형 점유율이 낮았기 때문이다. 현재 4다이아몬드 계열 형태는 유지한 채 내부 여백을 줄이고 캔버스를 더 넓게 쓰는 새 멀티사이즈 아이콘으로 자산을 재생성했다.

아이콘 생성 경로도 함께 정리했다. tools/IconGenerator는 현재 AX 아이콘 스타일을 기본으로 생성하고 16 20 24 32 40 48 64 128 256 프레임을 포함하도록 바꿨다. src/AxCopilot/Assets/diamond_pixel.svg도 같은 비율로 맞춰 소스 SVG와 실제 ico 자산이 덜 어긋나게 정리했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_icon_size\ -p:IntermediateOutputPath=obj\verify_icon_size\ / 경고 0 오류 0
검증: System.Drawing.Icon 확인 결과 16 20 24 32 프레임이 요청 크기 그대로 로드됨
2026-04-15 21:20:55 +09:00

329 lines
11 KiB
C#

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
var options = IconGeneratorOptions.Parse(args);
var pngFrames = new List<byte[]>();
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<byte[]> pngs, IReadOnlyList<int> 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,
};
}
}