// MakeIcon — из исходного PNG с логотипом делает app.ico и logo.png. // // MakeIcon.exe [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 [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 b = new List(), g = new List(), r = new List(); 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 blobs = new List(); 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); } } }