diff options
author | Alexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com> | 2022-10-27 10:04:03 +0300 |
---|---|---|
committer | Alexey 'Cluster' Avdyukhin <clusterrr@clusterrr.com> | 2022-10-27 10:04:03 +0300 |
commit | dcc1340b4154579155d660943f2652a623a29778 (patch) | |
tree | b7891948a95c622ec04cfbb31f7c41d8f2c7fbeb | |
parent | 7393388358b1b2ce6b614f99e146569e94853c76 (diff) |
Moved color methods to separate class.
-rw-r--r-- | NesTiler/ColorsFinder.cs | 153 | ||||
-rw-r--r-- | NesTiler/Program.cs | 131 |
2 files changed, 153 insertions, 131 deletions
diff --git a/NesTiler/ColorsFinder.cs b/NesTiler/ColorsFinder.cs index 2772f69..77ba1f0 100644 --- a/NesTiler/ColorsFinder.cs +++ b/NesTiler/ColorsFinder.cs @@ -1,37 +1,104 @@ -using System; +using SkiaSharp; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; +using System.IO; using System.Linq; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace com.clusterrr.Famicom.NesTiler { class ColorsFinder { - private readonly Dictionary<byte, Color> colors; + static byte[] FORBIDDEN_COLORS = new byte[] { 0x0D, 0x0E, 0x0F, 0x1E, 0x1F, 0x2E, 0x2F, 0x3E, 0x3F }; + + public readonly Dictionary<byte, Color> Colors; private readonly Dictionary<Color, byte> cache = new(); - public ColorsFinder(Dictionary<byte, Color> colors) + public ColorsFinder(string filename) { - this.colors = colors; + this.Colors = LoadColors(filename); + } + + private static Dictionary<byte, Color> LoadColors(string filename) + { + Trace.WriteLine($"Loading colors from {filename}..."); + if (!File.Exists(filename)) throw new FileNotFoundException($"Could not find file '{filename}'.", filename); + var data = File.ReadAllBytes(filename); + Dictionary<byte, Color> nesColors; + // Detect file type + if ((Path.GetExtension(filename) == ".pal") || ((data.Length == 192 || data.Length == 1536) && data.Where(b => b >= 128).Any())) + { + // Binary file + nesColors = new Dictionary<byte, Color>(); + for (byte c = 0; c < 64; c++) + { + var color = Color.FromArgb(data[c * 3], data[(c * 3) + 1], data[(c * 3) + 2]); + nesColors[c] = color; + } + } + else + { + var paletteJson = File.ReadAllText(filename); + var nesColorsStr = JsonSerializer.Deserialize<Dictionary<string, string>>(paletteJson); + if (nesColorsStr == null) throw new InvalidDataException($"Can't parse {filename}"); + nesColors = nesColorsStr.ToDictionary( + kv => + { + try + { + var index = kv.Key.ToLower().StartsWith("0x") ? Convert.ToByte(kv.Key.Substring(2), 16) : byte.Parse(kv.Key); + if (FORBIDDEN_COLORS.Contains(index)) + Trace.WriteLine($"WARNING! color #{kv.Key} is forbidden color, it will be ignored."); + if (index > 0x3D) throw new ArgumentException($"{kv.Key} - invalid color index.", filename); + return index; + } + catch (Exception ex) when (ex is FormatException || ex is OverflowException) + { + throw new ArgumentException($"{kv.Key} - invalid color index.", filename); + } + }, + kv => + { + try + { + return ColorTranslator.FromHtml(kv.Value); + } + catch (FormatException) + { + throw new ArgumentException($"{kv.Value} - invalid color.", filename); + } + } + ); + } + // filter out invalid colors; + nesColors = nesColors.Where(kv => !FORBIDDEN_COLORS.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value); + return nesColors; } - public byte FindSimilarColor(Color color) + /// <summary> + /// Find index of most similar color from NES colors + /// </summary> + /// <param name="color">Input color</param> + /// <returns>Output color index</returns> + public byte FindSimilarColorIndex(Color color) { if (cache.ContainsKey(color)) return cache[color]; byte result = byte.MaxValue; double minDelta = double.MaxValue; Color c = Color.Transparent; - foreach (var index in colors.Keys) + foreach (var index in Colors.Keys) { - var delta = color.GetDelta(colors[index]); + var delta = color.GetDelta(Colors[index]); if (delta < minDelta) { minDelta = delta; result = index; - c = colors[index]; + c = Colors[index]; } } if (result == byte.MaxValue) @@ -41,6 +108,12 @@ namespace com.clusterrr.Famicom.NesTiler return result; } + /// <summary> + /// Find most similar color from list of colors + /// </summary> + /// <param name="colors">Haystack</param> + /// <param name="color">Niddle</param> + /// <returns>Output color</returns> public Color FindSimilarColor(IEnumerable<Color> colors, Color color) { Color result = Color.Black; @@ -56,5 +129,69 @@ namespace com.clusterrr.Famicom.NesTiler } return result; } + + /// <summary> + /// Find most similar color from NES colors + /// </summary> + /// <param name="color">Input colo</param> + /// <returns>Output color</returns> + public Color FindSimilarColor(Color color) => Colors[FindSimilarColorIndex(color)]; + + public void WriteColorsTable(string filename) + { + // Export colors to nice table image + const int colorSize = 64; + const int colorColumns = 16; + const int colorRows = 4; + const int strokeWidth = 5; + float textSize = 20; + float textYOffset = 39; + using var image = new SKBitmap(colorSize * colorColumns, colorSize * colorRows); + using var canvas = new SKCanvas(image); + for (int y = 0; y < colorRows; y++) + { + for (int x = 0; x < colorColumns; x++) + { + Color color; + SKColor skColor; + SKPaint paint; + if (Colors.TryGetValue((byte)((y * colorColumns) + x), out color)) + { + skColor = new SKColor(color.R, color.G, color.B); + paint = new SKPaint() { Color = skColor }; + canvas.DrawRegion(new SKRegion(new SKRectI(x * colorSize, y * colorSize, (x + 1) * colorSize, (y + 1) * colorSize)), paint); + + skColor = new SKColor((byte)(0xFF - color.R), (byte)(0xFF - color.G), (byte)(0xFF - color.B)); // invert color + paint = new SKPaint() + { + Color = skColor, + TextAlign = SKTextAlign.Center, + TextSize = textSize, + FilterQuality = SKFilterQuality.High, + IsAntialias = true + }; + canvas.DrawText($"{(y * colorColumns) + x:X02}", (x * colorSize) + (colorSize / 2), (y * colorSize) + textYOffset, paint); + } + else + { + paint = new SKPaint() { Color = SKColors.Black }; + SKPath path = new SKPath(); + canvas.DrawRegion(new SKRegion(new SKRectI(x * colorSize, y * colorSize, (x + 1) * colorSize, (y + 1) * colorSize)), paint); + paint = new SKPaint() + { + Color = SKColors.Red, + Style = SKPaintStyle.Stroke, + StrokeCap = SKStrokeCap.Round, + StrokeWidth = strokeWidth, + FilterQuality = SKFilterQuality.High, + IsAntialias = true + }; + canvas.DrawLine((x * colorSize) + strokeWidth, (y * colorSize) + strokeWidth, ((x + 1) * colorSize) - strokeWidth, ((y + 1) * colorSize) - strokeWidth, paint); + canvas.DrawLine(((x + 1) * colorSize) - strokeWidth, (y * colorSize) + strokeWidth, (x * colorSize) + strokeWidth, ((y + 1) * colorSize) - strokeWidth, paint); + } + }; + } + File.WriteAllBytes(filename, image.Encode(SKEncodedImageFormat.Png, 0).ToArray()); + } } } diff --git a/NesTiler/Program.cs b/NesTiler/Program.cs index c0cd90d..f607adf 100644 --- a/NesTiler/Program.cs +++ b/NesTiler/Program.cs @@ -17,7 +17,6 @@ namespace com.clusterrr.Famicom.NesTiler public const string REPO_PATH = "https://github.com/ClusterM/nestiler";
public static DateTime BUILD_TIME = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(long.Parse(Properties.Resources.buildtime.Trim()));
public const int MAX_BG_COLOR_AUTODETECT_ITERATIONS = 5;
- static byte[] FORBIDDEN_COLORS = new byte[] { 0x0D, 0x0E, 0x0F, 0x1E, 0x1F, 0x2E, 0x2F, 0x3E, 0x3F };
static void PrintAppInfo()
{
@@ -78,8 +77,7 @@ namespace com.clusterrr.Famicom.NesTiler int tileID = 0;
// Loading and parsing palette JSON
- var nesColors = LoadColors(c.ColorsFile);
- var colorsFinder = new ColorsFinder(nesColors);
+ var nesColors = new ColorsFinder(c.ColorsFile);
// CSV output
var outTilesCsvLines = !string.IsNullOrEmpty(c.OutTilesCsv) ? new List<string>() : null;
@@ -88,7 +86,7 @@ namespace com.clusterrr.Famicom.NesTiler if (c.OutColorsTable != null)
{
Trace.WriteLine($"Writing color tables to {c.OutColorsTable}...");
- WriteColorsTable(nesColors, c.OutColorsTable);
+ nesColors.WriteColorsTable(c.OutColorsTable);
}
// Stop if there are no images
@@ -100,7 +98,7 @@ namespace com.clusterrr.Famicom.NesTiler if (c.FixedPalettes[i] == null) continue;
var colorsInPalette = c.FixedPalettes[i]!.ToArray();
for (int j = 0; j < colorsInPalette.Length; j++)
- colorsInPalette[j] = nesColors[colorsFinder.FindSimilarColor(colorsInPalette[j])];
+ colorsInPalette[j] = nesColors.FindSimilarColor(colorsInPalette[j]);
c.FixedPalettes[i] = new Palette(colorsInPalette);
}
@@ -143,7 +141,7 @@ namespace com.clusterrr.Famicom.NesTiler var color = image.GetPixelColor(x, y);
if (color.A >= 0x80 || c.Mode == Config.TilesMode.Backgrounds)
{
- var similarColor = nesColors[colorsFinder.FindSimilarColor(color)];
+ var similarColor = nesColors.FindSimilarColor(color);
image.SetPixelColor(x, y, similarColor);
}
else
@@ -163,7 +161,7 @@ namespace com.clusterrr.Famicom.NesTiler if (c.BgColor.HasValue)
{
// Manually
- bgColor = nesColors[colorsFinder.FindSimilarColor(c.BgColor.Value)];
+ bgColor = nesColors.FindSimilarColor(c.BgColor.Value);
calculatedPalettes = CalculatePalettes(images,
c.PaletteEnabled,
c.FixedPalettes,
@@ -264,7 +262,7 @@ namespace com.clusterrr.Famicom.NesTiler }
// Calculate palette as color indices and save them to files
- var bgColorIndex = colorsFinder.FindSimilarColor(bgColor);
+ var bgColorIndex = nesColors.FindSimilarColorIndex(bgColor);
for (int p = 0; p < palettes.Length; p++)
{
if (c.PaletteEnabled[p] && c.OutPalette.ContainsKey(p))
@@ -276,7 +274,7 @@ namespace com.clusterrr.Famicom.NesTiler if (palettes[p] == null)
paletteRaw[colorIndex] = 0;
else if (palettes[p]![colorIndex].HasValue)
- paletteRaw[colorIndex] = colorsFinder.FindSimilarColor(palettes[p]![colorIndex]!.Value);
+ paletteRaw[colorIndex] = nesColors.FindSimilarColorIndex(palettes[p]![colorIndex]!.Value);
}
File.WriteAllBytes(c.OutPalette[p], paletteRaw);
Trace.WriteLine($"Palette #{p} saved to {c.OutPalette[p]}");
@@ -324,7 +322,7 @@ namespace com.clusterrr.Famicom.NesTiler var cy = (tilePalY * c.TilePalHeight) + y - attributeTableOffset;
if (cy < 0) continue;
var color = image.GetPixelColor((tilePalX * c.TilePalWidth) + x, cy);
- var similarColor = colorsFinder.FindSimilarColor(Enumerable.Concat(
+ var similarColor = nesColors.FindSimilarColor(Enumerable.Concat(
bestPalette,
new Color[] { bgColor }
), color);
@@ -541,119 +539,6 @@ namespace com.clusterrr.Famicom.NesTiler }
}
- private static Dictionary<byte, Color> LoadColors(string filename)
- {
- Trace.WriteLine($"Loading colors from {filename}...");
- if (!File.Exists(filename)) throw new FileNotFoundException($"Could not find file '{filename}'.", filename);
- var data = File.ReadAllBytes(filename);
- Dictionary<byte, Color> nesColors;
- // Detect file type
- if ((Path.GetExtension(filename) == ".pal") || ((data.Length == 192 || data.Length == 1536) && data.Where(b => b >= 128).Any()))
- {
- // Binary file
- nesColors = new Dictionary<byte, Color>();
- for (byte c = 0; c < 64; c++)
- {
- var color = Color.FromArgb(data[c * 3], data[(c * 3) + 1], data[(c * 3) + 2]);
- nesColors[c] = color;
- }
- }
- else
- {
- var paletteJson = File.ReadAllText(filename);
- var nesColorsStr = JsonSerializer.Deserialize<Dictionary<string, string>>(paletteJson);
- if (nesColorsStr == null) throw new InvalidDataException($"Can't parse {filename}");
- nesColors = nesColorsStr.ToDictionary(
- kv =>
- {
- try
- {
- var index = kv.Key.ToLower().StartsWith("0x") ? Convert.ToByte(kv.Key.Substring(2), 16) : byte.Parse(kv.Key);
- if (FORBIDDEN_COLORS.Contains(index))
- Trace.WriteLine($"WARNING! color #{kv.Key} is forbidden color, it will be ignored.");
- if (index > 0x3D) throw new ArgumentException($"{kv.Key} - invalid color index.", filename);
- return index;
- }
- catch (Exception ex) when (ex is FormatException || ex is OverflowException)
- {
- throw new ArgumentException($"{kv.Key} - invalid color index.", filename);
- }
- },
- kv =>
- {
- try
- {
- return ColorTranslator.FromHtml(kv.Value);
- }
- catch (FormatException)
- {
- throw new ArgumentException($"{kv.Value} - invalid color.", filename);
- }
- }
- );
- }
- // filter out invalid colors;
- nesColors = nesColors.Where(kv => !FORBIDDEN_COLORS.Contains(kv.Key)).ToDictionary(kv => kv.Key, kv => kv.Value);
- return nesColors;
- }
-
- static void WriteColorsTable(Dictionary<byte, Color> nesColors, string filename)
- {
- // Export colors to nice table image
- const int colorSize = 64;
- const int colorColumns = 16;
- const int colorRows = 4;
- const int strokeWidth = 5;
- float textSize = 20;
- float textYOffset = 39;
- using var image = new SKBitmap(colorSize * colorColumns, colorSize * colorRows);
- using var canvas = new SKCanvas(image);
- for (int y = 0; y < colorRows; y++)
- {
- for (int x = 0; x < colorColumns; x++)
- {
- Color color;
- SKColor skColor;
- SKPaint paint;
- if (nesColors.TryGetValue((byte)((y * colorColumns) + x), out color))
- {
- skColor = new SKColor(color.R, color.G, color.B);
- paint = new SKPaint() { Color = skColor };
- canvas.DrawRegion(new SKRegion(new SKRectI(x * colorSize, y * colorSize, (x + 1) * colorSize, (y + 1) * colorSize)), paint);
-
- skColor = new SKColor((byte)(0xFF - color.R), (byte)(0xFF - color.G), (byte)(0xFF - color.B)); // invert color
- paint = new SKPaint()
- {
- Color = skColor,
- TextAlign = SKTextAlign.Center,
- TextSize = textSize,
- FilterQuality = SKFilterQuality.High,
- IsAntialias = true
- };
- canvas.DrawText($"{(y * colorColumns) + x:X02}", (x * colorSize) + (colorSize / 2), (y * colorSize) + textYOffset, paint);
- }
- else
- {
- paint = new SKPaint() { Color = SKColors.Black };
- SKPath path = new SKPath();
- canvas.DrawRegion(new SKRegion(new SKRectI(x * colorSize, y * colorSize, (x + 1) * colorSize, (y + 1) * colorSize)), paint);
- paint = new SKPaint()
- {
- Color = SKColors.Red,
- Style = SKPaintStyle.Stroke,
- StrokeCap = SKStrokeCap.Round,
- StrokeWidth = strokeWidth,
- FilterQuality = SKFilterQuality.High,
- IsAntialias = true
- };
- canvas.DrawLine((x * colorSize) + strokeWidth, (y * colorSize) + strokeWidth, ((x + 1) * colorSize) - strokeWidth, ((y + 1) * colorSize) - strokeWidth, paint);
- canvas.DrawLine(((x + 1) * colorSize) - strokeWidth, (y * colorSize) + strokeWidth, (x * colorSize) + strokeWidth, ((y + 1) * colorSize) - strokeWidth, paint);
- }
- };
- }
- File.WriteAllBytes(filename, image.Encode(SKEncodedImageFormat.Png, 0).ToArray());
- }
-
static Palette[] CalculatePalettes(Dictionary<int, FastBitmap> images, bool[] paletteEnabled, Palette?[] fixedPalettes, Dictionary<int, int> attributeTableOffsets, int tilePalWidth, int tilePalHeight, Color bgColor)
{
var required = Enumerable.Range(0, 4).Select(i => paletteEnabled[i] && fixedPalettes[i] == null);
|