288 lines
12 KiB
C#
288 lines
12 KiB
C#
// 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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|