文字列の先頭3文字からアイコンを生成します。
生成されたアイコンは以下の様になりました。
アプリケーションごとにアイコンを用意するのが面倒なので、アプリケーション名でアイコンを生成することが目的です。
ファイル名:IconGenerator01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms> <!-- System.Drawing on Windows -->
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
</ItemGroup>
</Project>
ファイル名:Program.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
// ビルド
// dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true /p:SelfContained=true --output ./output
// 使用例
// output\IconGenerator01.exe -o App04.ico -name "IconGenerator01" -sizes 16,32,48,256
class Program
{
// 例:
// RingIconGen.exe -o MyApp.ico -name "My Application"
// 省略時: name=出力名(拡張子無し)。中央3文字=name先頭3文字(大文字)。
// 複数サイズは 16,24,32,48,64,128,256 を内包。
static int Main(string[] args)
{
try
{
var opt = Options.Parse(args) ?? throw new ArgumentException("Usage: -o <output.ico> [-name \"App Name\"]");
var appName = string.IsNullOrWhiteSpace(opt.AppName)
? Path.GetFileNameWithoutExtension(opt.OutputPath)
: opt.AppName;
var mid3 = new string(appName.Trim().Replace("_","").Replace("-","").Replace(" ","").Take(3).ToArray())
.ToUpperInvariant();
if (mid3.Length == 0) mid3 = "APP";
// パレット(決定的)
var (c0, c1) = MakePalette(appName);
var ringColor = Darken(c0, 0.25f);
var fgCenter = AutoContrastBlend(c0, c1) > 0.5 ? Color.Black : Color.White;
var images = new List<(int size, byte[] png)>();
foreach (var s in opt.Sizes.Distinct().OrderBy(x => x))
{
using var bmp = Render(s, mid3, c0, c1, ringColor, fgCenter, opt.FontFamily);
images.Add((s, ToPng(bmp)));
}
using var fs = File.Create(opt.OutputPath);
WriteIcoFromPngs(images, fs);
Console.WriteLine($"OK: {opt.OutputPath}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine("ERROR: " + ex.Message);
return 1;
}
}
static Bitmap Render(int size, string mid3, Color grad0, Color grad1, Color ring, Color fgCenter, string fontFamily)
{
var bmp = new Bitmap(size, size, PixelFormat.Format32bppPArgb);
using var g = Graphics.FromImage(bmp);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
// 背景(円+グラデ)
using (var bg = new GraphicsPath())
{
bg.AddEllipse(0, 0, size - 1, size - 1);
using var lg = new LinearGradientBrush(new Rectangle(0, 0, size, size), grad0, grad1, 135f);
g.FillPath(lg, bg);
}
// 太リング
float ringThickness = Math.Max(2f, size * 0.12f);
float inset = ringThickness / 2f + size * 0.02f;
using (var pen = new Pen(ring, ringThickness))
{
g.DrawEllipse(pen, inset, inset, size - 1 - inset * 2, size - 1 - inset * 2);
}
// 中央3文字
float fontPx = size * 0.5f;
using var f = new Font(fontFamily, fontPx, FontStyle.Bold, GraphicsUnit.Pixel);
var sz = g.MeasureString(mid3, f);
float scale = Math.Min((size * 0.78f) / sz.Width, (size * 0.62f) / sz.Height);
using var f2 = scale < 1f ? new Font(fontFamily, fontPx * scale, FontStyle.Bold, GraphicsUnit.Pixel) : (Font)f.Clone();
var sz2 = g.MeasureString(mid3, f2);
float tx = (size - sz2.Width) / 2f;
float ty = (size - sz2.Height) / 2f;
using (var sh = new SolidBrush(Color.FromArgb(80, 0, 0, 0)))
g.DrawString(mid3, f2, sh, tx + size * 0.02f, ty + size * 0.02f);
using (var fb = new SolidBrush(fgCenter))
g.DrawString(mid3, f2, fb, tx, ty);
return bmp;
}
// --- ICO (PNG) ---
static byte[] ToPng(Bitmap bmp){ using var ms = new MemoryStream(); bmp.Save(ms, ImageFormat.Png); return ms.ToArray(); }
static void WriteIcoFromPngs(List<(int size, byte[] png)> images, Stream output)
{
void U16(ushort v){ output.WriteByte((byte)(v & 0xFF)); output.WriteByte((byte)(v >> 8)); }
void U32(uint v){ output.WriteByte((byte)(v & 0xFF)); output.WriteByte((byte)((v>>8)&0xFF)); output.WriteByte((byte)((v>>16)&0xFF)); output.WriteByte((byte)((v>>24)&0xFF)); }
var ordered = images.OrderBy(x => x.size).ToList();
U16(0); U16(1); U16((ushort)ordered.Count);
int offset = 6 + 16 * ordered.Count;
foreach (var (s, png) in ordered)
{
output.WriteByte((byte)(s >= 256 ? 0 : s)); // width
output.WriteByte((byte)(s >= 256 ? 0 : s)); // height
output.WriteByte(0); output.WriteByte(0); // colors/reserved
U16(1); U16(32); // planes, bpp
U32((uint)png.Length);
U32((uint)offset);
offset += png.Length;
}
foreach (var (_, png) in ordered) output.Write(png, 0, png.Length);
}
// --- 色ユーティリティ ---
static (Color, Color) MakePalette(string key)
{
unchecked
{
int h = key.Aggregate(0, (a, ch) => a * 31 + ch);
double baseHue = (h & 0xFF) / 255.0 * 360.0;
double hue2 = (baseHue + 35) % 360;
var c0 = Hsl(baseHue, 0.58, 0.56);
var c1 = Hsl(hue2, 0.62, 0.48);
return (c0, c1);
}
}
static Color Hsl(double h, double s, double l)
{
h /= 360.0;
double r, g, b;
if (s == 0){ r = g = b = l; }
else
{
double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
double p = 2 * l - q;
r = Hue(p, q, h + 1.0/3); g = Hue(p, q, h); b = Hue(p, q, h - 1.0/3);
}
return Color.FromArgb(255, (int)(r * 255), (int)(g * 255), (int)(b * 255));
}
static double Hue(double p, double q, double t)
{
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1.0 / 6) return p + (q - p) * 6 * t;
if (t < 1.0 / 2) return q;
if (t < 2.0 / 3) return p + (q - p) * (2.0 / 3 - t) * 6;
return p;
}
static Color Darken(Color c, float f) { int D(int v)=>Math.Max(0,(int)(v*(1f-f))); return Color.FromArgb(255, D(c.R), D(c.G), D(c.B)); }
static double AutoContrastBlend(Color a, Color b)
{
double Lin(byte c){ double v=c/255.0; return v<=0.03928? v/12.92: Math.Pow((v+0.055)/1.055,2.4); }
byte R = (byte)((a.R + b.R)/2), G = (byte)((a.G + b.G)/2), B = (byte)((a.B + b.B)/2);
return 0.2126*Lin(R)+0.7152*Lin(G)+0.0722*Lin(B);
}
// --- オプション ---
sealed class Options
{
public string OutputPath = "";
public string AppName = "";
public string FontFamily = "Segoe UI Semibold";
public int[] Sizes = new[] {16,24,32,48,64,128,256};
public static Options? Parse(string[] a)
{
if (a.Length == 0) return null;
var o = new Options();
for (int i = 0; i < a.Length; i++)
{
switch (a[i])
{
case "-o": case "--out": o.OutputPath = a[++i]; break;
case "-name": o.AppName = a[++i]; break;
case "-font": o.FontFamily = a[++i]; break;
case "-sizes":
o.Sizes = a[++i].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(int.Parse).ToArray();
break;
case "-h":
case "--help": return null;
default:
if (string.IsNullOrEmpty(o.OutputPath) && !a[i].StartsWith("-")) { o.OutputPath = a[i]; break; }
throw new ArgumentException($"Unknown arg: {a[i]}");
}
}
if (string.IsNullOrWhiteSpace(o.OutputPath)) return null;
if (string.IsNullOrWhiteSpace(o.AppName))
o.AppName = Path.GetFileNameWithoutExtension(o.OutputPath);
return o;
}
}
}
コメント