C#で作るアプリケーションアイコンを生成するコンソールアプリ

コンピュータ

文字列の先頭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;
        }
    }
}

コメント