Files
SbLauncher/tools/MakeIcon.cs
T

288 lines
12 KiB
C#
Raw Permalink Normal View History

2026-09-26 14:41:04 +03:00
// MakeIcon — из исходного PNG с логотипом делает app.ico и logo.png.
//
// MakeIcon.exe <input.png> [app.ico] [logo.png]
//
// Что делает:
// 1. оценивает цвет фона по рамке изображения;
// 2. считает альфу по расстоянию до фона и «размачивает» (un-matte) цвет,
// чтобы по краям не осталось белой каймы;
// 3. обрезает по границам непрозрачного, добавляет поля, доводит до квадрата;
// 4. пишет .ico: размеры <=48 как BMP-записи, >=64 как PNG-записи
// (так делают современные иконки, Windows 10/11 понимает и то и другое).
//
// Компилятор — тот же встроенный csc.exe (C# 5).
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
public static class MakeIcon
{
[STAThread]
public static int Main(string[] a)
{
if (a.Length < 1) { Console.WriteLine("usage: MakeIcon <input.png> [app.ico] [logo.png]"); return 2; }
string inPath = a[0];
string icoPath = a.Length > 1 ? a[1] : "app.ico";
string pngPath = a.Length > 2 ? a[2] : "logo.png";
if (!File.Exists(inPath)) { Console.WriteLine("not found: " + inPath); return 1; }
try
{
BitmapSource src = Load(inPath);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture,
"source ........ {0}x{1}", src.PixelWidth, src.PixelHeight));
int w = src.PixelWidth, h = src.PixelHeight;
byte[] px = new byte[w * h * 4];
src.CopyPixels(px, w * 4, 0);
byte[] bg = EstimateBackground(px, w, h);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture,
"background .... #{0:X2}{1:X2}{2:X2}", bg[2], bg[1], bg[0]));
Unmatte(px, w, h, bg);
int x0, y0, x1, y1;
if (!Bbox(px, w, h, out x0, out y0, out x1, out y1))
{ Console.WriteLine("nothing but background found"); return 1; }
Console.WriteLine(string.Format(CultureInfo.InvariantCulture,
"logo bbox ..... {0},{1} .. {2},{3} ({4}x{5})", x0, y0, x1, y1, x1 - x0 + 1, y1 - y0 + 1));
BitmapSource cleaned = BitmapSource.Create(w, h, 96, 96, PixelFormats.Bgra32, null, px, w * 4);
BitmapSource squared = CropSquare(cleaned, x0, y0, x1, y1, 0.06);
Console.WriteLine(string.Format(CultureInfo.InvariantCulture,
"squared ....... {0}x{1}", squared.PixelWidth, squared.PixelHeight));
SavePng(Resize(squared, 512), pngPath);
Console.WriteLine("wrote ......... " + pngPath);
int[] sizes = new int[] { 16, 20, 24, 32, 40, 48, 64, 128, 256 };
WriteIco(squared, sizes, icoPath);
Console.WriteLine("wrote ......... " + icoPath + " (" + new FileInfo(icoPath).Length + " bytes, "
+ sizes.Length + " sizes)");
return 0;
}
catch (Exception ex)
{
Console.WriteLine("FAIL: " + ex.GetType().Name + ": " + ex.Message);
return 1;
}
}
static BitmapSource Load(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
BitmapDecoder d = BitmapDecoder.Create(fs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
FormatConvertedBitmap c = new FormatConvertedBitmap(d.Frames[0], PixelFormats.Bgra32, null, 0);
c.Freeze();
return c;
}
}
// Медиана по рамке шириной 8px — устойчива к виньетке и водяному знаку.
static byte[] EstimateBackground(byte[] px, int w, int h, int band = 8)
{
List<byte> b = new List<byte>(), g = new List<byte>(), r = new List<byte>();
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
if (x >= band && x < w - band && y >= band && y < h - band) continue;
int i = (y * w + x) * 4;
b.Add(px[i]); g.Add(px[i + 1]); r.Add(px[i + 2]);
}
b.Sort(); g.Sort(); r.Sort();
return new byte[] { b[b.Count / 2], g[g.Count / 2], r[r.Count / 2], 255 };
}
const double T0 = 14.0; // ниже — считаем чистым фоном
const double T1 = 46.0; // выше — считаем чистым логотипом
static void Unmatte(byte[] px, int w, int h, byte[] bg)
{
for (int i = 0; i < px.Length; i += 4)
{
double db = Math.Abs(px[i] - bg[0]);
double dg = Math.Abs(px[i + 1] - bg[1]);
double dr = Math.Abs(px[i + 2] - bg[2]);
double d = Math.Max(db, Math.Max(dg, dr));
double alpha = (d - T0) / (T1 - T0);
if (alpha <= 0) { px[i] = px[i + 1] = px[i + 2] = px[i + 3] = 0; continue; }
if (alpha > 1) alpha = 1;
// O = A*F + (1-A)*B -> F = (O - (1-A)*B) / A
for (int c = 0; c < 3; c++)
{
double o = px[i + c];
double f = (o - (1 - alpha) * bg[c]) / alpha;
px[i + c] = (byte)(f < 0 ? 0 : f > 255 ? 255 : f);
}
px[i + 3] = (byte)Math.Round(alpha * 255);
}
}
// Глобальная рамка не годится: в неё попадает водяной знак в углу.
// Берём КРУПНЕЙШУЮ связную область, всё остальное затираем.
static bool Bbox(byte[] px, int w, int h, out int x0, out int y0, out int x1, out int y1)
{
x0 = 0; y0 = 0; x1 = -1; y1 = -1;
int[] label = new int[w * h];
int best = 0, bestSize = 0, bx0 = 0, by0 = 0, bx1 = 0, by1 = 0, next = 0;
int[] stack = new int[w * h];
for (int start = 0; start < w * h; start++)
{
if (label[start] != 0 || px[start * 4 + 3] <= 12) continue;
next++;
int sp = 0, size = 0;
int cx0 = w, cy0 = h, cx1 = -1, cy1 = -1;
stack[sp++] = start;
label[start] = next;
while (sp > 0)
{
int p = stack[--sp];
int py = p / w, pxx = p % w;
size++;
if (pxx < cx0) cx0 = pxx;
if (pxx > cx1) cx1 = pxx;
if (py < cy0) cy0 = py;
if (py > cy1) cy1 = py;
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
{
int nx = pxx + dx, ny = py + dy;
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
int q = ny * w + nx;
if (label[q] != 0 || px[q * 4 + 3] <= 12) continue;
label[q] = next;
stack[sp++] = q;
}
}
if (size > bestSize) { bestSize = size; best = next; bx0 = cx0; by0 = cy0; bx1 = cx1; by1 = cy1; }
}
if (bestSize == 0) return false;
Console.WriteLine(string.Format(CultureInfo.InvariantCulture,
"components .... {0}, largest {1} px", next, bestSize));
for (int i = 0; i < w * h; i++) // всё, что не логотип — в ноль
if (label[i] != best) { px[i * 4] = px[i * 4 + 1] = px[i * 4 + 2] = px[i * 4 + 3] = 0; }
x0 = bx0; y0 = by0; x1 = bx1; y1 = by1;
return true;
}
static BitmapSource CropSquare(BitmapSource src, int x0, int y0, int x1, int y1, double padFrac)
{
int bw = x1 - x0 + 1, bh = y1 - y0 + 1;
int side = (int)Math.Round(Math.Max(bw, bh) * (1 + 2 * padFrac));
int cx = (x0 + x1) / 2, cy = (y0 + y1) / 2;
// рисуем на прозрачный квадрат — так не упрёмся в границы исходника
DrawingVisual dv = new DrawingVisual();
using (DrawingContext dc = dv.RenderOpen())
{
RenderOptions.SetBitmapScalingMode(dv, BitmapScalingMode.HighQuality);
dc.DrawImage(src, new Rect(-(cx - side / 2.0), -(cy - side / 2.0), src.PixelWidth, src.PixelHeight));
}
RenderTargetBitmap rtb = new RenderTargetBitmap(side, side, 96, 96, PixelFormats.Pbgra32);
rtb.Render(dv);
rtb.Freeze();
return rtb;
}
static BitmapSource Resize(BitmapSource src, int size)
{
DrawingVisual dv = new DrawingVisual();
using (DrawingContext dc = dv.RenderOpen())
{
RenderOptions.SetBitmapScalingMode(dv, BitmapScalingMode.HighQuality);
dc.DrawImage(src, new Rect(0, 0, size, size));
}
RenderTargetBitmap rtb = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
rtb.Render(dv);
rtb.Freeze();
return rtb;
}
static void SavePng(BitmapSource bmp, string path)
{
PngBitmapEncoder e = new PngBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(bmp));
using (FileStream fs = new FileStream(path, FileMode.Create)) e.Save(fs);
}
static byte[] PngBytes(BitmapSource bmp)
{
PngBitmapEncoder e = new PngBitmapEncoder();
e.Frames.Add(BitmapFrame.Create(bmp));
using (MemoryStream ms = new MemoryStream()) { e.Save(ms); return ms.ToArray(); }
}
// 32-битная BMP-запись внутри ICO: BITMAPINFOHEADER + XOR(BGRA, снизу вверх) + AND-маска
static byte[] BmpBytes(BitmapSource bmp)
{
int w = bmp.PixelWidth, h = bmp.PixelHeight;
FormatConvertedBitmap c = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0);
byte[] px = new byte[w * h * 4];
c.CopyPixels(px, w * 4, 0);
int maskStride = ((w + 31) / 32) * 4;
using (MemoryStream ms = new MemoryStream())
using (BinaryWriter bw = new BinaryWriter(ms))
{
bw.Write(40); bw.Write(w); bw.Write(h * 2);
bw.Write((short)1); bw.Write((short)32);
bw.Write(0); bw.Write(w * h * 4 + maskStride * h);
bw.Write(0); bw.Write(0); bw.Write(0); bw.Write(0);
for (int y = h - 1; y >= 0; y--)
bw.Write(px, y * w * 4, w * 4);
byte[] mask = new byte[maskStride * h]; // все нули = пиксель виден
bw.Write(mask);
bw.Flush();
return ms.ToArray();
}
}
static void WriteIco(BitmapSource square, int[] sizes, string path)
{
// Все записи — BMP, включая 256. PNG внутри ICO формально разрешён
// с Vista, но System.Drawing.Icon такие кадры не выбирает (проверено:
// запрос 256 возвращал 128). Файл выходит ~350 КБ вместо ~110 — для
// иконки приложения это несущественно, зато работает везде.
List<byte[]> blobs = new List<byte[]>();
foreach (int s in sizes)
blobs.Add(BmpBytes(Resize(square, s)));
using (FileStream fs = new FileStream(path, FileMode.Create))
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write((short)0); bw.Write((short)1); bw.Write((short)sizes.Length);
int offset = 6 + 16 * sizes.Length;
for (int i = 0; i < sizes.Length; i++)
{
int s = sizes[i];
bw.Write((byte)(s >= 256 ? 0 : s));
bw.Write((byte)(s >= 256 ? 0 : s));
bw.Write((byte)0); bw.Write((byte)0);
bw.Write((short)1); bw.Write((short)32);
bw.Write(blobs[i].Length);
bw.Write(offset);
offset += blobs[i].Length;
}
foreach (byte[] b in blobs) bw.Write(b);
}
}
}