Files
SbLauncher/tools/MakeIcon.cs
T

288 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
}
}