From ea686aa66485196cfee45c87bb209b7f59fd18db Mon Sep 17 00:00:00 2001 From: Alexey 'Cluster' Avdyukhin Date: Sun, 30 Oct 2022 12:54:24 +0400 Subject: Pipeline --- .github/workflows/release.yml | 6 +- NesTiler/CmdArgs.cs | 486 ++++++++++++++++++------------------ NesTiler/ColorFinder.cs | 388 ++++++++++++++--------------- NesTiler/Config.cs | 554 +++++++++++++++++++++--------------------- NesTiler/NesTiler.csproj | 3 + 5 files changed, 720 insertions(+), 717 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 578c205..01259db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: - name: Build env: SC_OPS: ${{ matrix.sc == 'self-contained' && '--self-contained true -p:PublishTrimmed=False' || '--no-self-contained' }} - run: dotnet publish ${{ env.PROJECT_PATH }} -c ${{ env.CONFIGURATION }} -r ${{ matrix.os }}-${{ matrix.arch }} -p:PublishSingleFile=true ${{ env.SC_OPS }} -o ${{ env.OUTPUT_DIR }}/${{ env.OUTPUT_SUBDIR }}/${{ env.APP_NAME }} -p:IncludeAllContentForSelfExtract=true + run: dotnet publish ${{ env.PROJECT_PATH }} -c ${{ env.CONFIGURATION }} -r ${{ matrix.os }}-${{ matrix.arch }} ${{ env.SC_OPS }} -o ${{ env.OUTPUT_DIR }}/${{ env.OUTPUT_SUBDIR }}/${{ env.APP_NAME }} - name: Archive working-directory: ${{ env.OUTPUT_DIR }}/${{ env.OUTPUT_SUBDIR }} env: @@ -76,9 +76,9 @@ jobs: TAG_REF_NAME: ${{ github.ref }} REPOSITORY_NAME: ${{ github.repository }} run: | - echo ::set-output name=file_name::${REPOSITORY_NAME##*/}-${TAG_REF_NAME##*/v} + echo file_name=${REPOSITORY_NAME##*/}-${TAG_REF_NAME##*/v} >> $GITHUB_OUTPUT value=`cat release_url/release_url.txt` - echo ::set-output name=upload_url::$value + echo name=upload_url=$value >> $GITHUB_OUTPUT - name: Upload uses: actions/upload-release-asset@v1 env: diff --git a/NesTiler/CmdArgs.cs b/NesTiler/CmdArgs.cs index fc1d398..98411f9 100644 --- a/NesTiler/CmdArgs.cs +++ b/NesTiler/CmdArgs.cs @@ -1,243 +1,243 @@ -namespace com.clusterrr.Famicom.NesTiler -{ - interface IArg - { - public string Short { get; } - public string Long { get; } - public string? Params { get; } - public string Description { get; } - public bool HasIndex { get; } - - public static IArg[] Args = new IArg[] - { - new ArgIn(), - new ArgColors(), - new ArgMode(), - new ArgBgColor(), - new ArgEnablePalettes(), - new ArgPalette(), - new ArgPatternOffset(), - new ArgAttributeTableYOffset(), - new ArgSharePatternTable(), - new ArgLossy(), - new ArgOutPreview(), - new ArgOutPalette(), - new ArgOutPatternTable(), - new ArgOutNameTable(), - new ArgOutAttributeTable(), - new ArgOutTilesCsv(), - new ArgOutPalettesCsv(), - new ArgOutColorsTable(), - new ArgQuiet() - }; - } - - class ArgIn : IArg - { - public const string S = "i"; - public const string L = "in"; - public string? Params { get; } = "[:offset[:height]]"; - public string Description { get; } = "input filename number #, optionally cropped vertically"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgColors : IArg - { - public const string S = "c"; - public const string L = "colors"; - public string? Params { get; } = ""; - public string Description { get; } = $"JSON or PAL file with the list of available colors\n(default - {Config.DEFAULT_COLORS_FILE})"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgMode : IArg - { - public const string S = "m"; - public const string L = "mode"; - public string? Params { get; } = "bg|sprites8x8|sprites8x16"; - public string Description { get; } = "mode: backgrounds, 8x8 sprites or 8x16 sprites (default - bg)"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgBgColor : IArg - { - public const string S = "b"; - public const string L = "bg-color"; - public string? Params { get; } = ""; - public string Description { get; } = "background color in HTML color format (default - auto)"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgEnablePalettes : IArg - { - public const string S = "e"; - public const string L = "enable-palettes"; - public string? Params { get; } = ""; - public string Description { get; } = "zero-based comma separated list of palette numbers to use\n(default - 0,1,2,3)"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgPalette : IArg - { - public const string S = "p"; - public const string L = "palette"; - public string? Params { get; } = ""; - public string Description { get; } = "comma separated list of colors to use in palette number #\n(default - auto)"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgPatternOffset : IArg - { - public const string S = "o"; - public const string L = "pattern-offset"; - public string? Params { get; } = ""; - public string Description { get; } = "first tile index for pattern table for file number # (default - 0)"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgAttributeTableYOffset : IArg - { - public const string S = "y"; - public const string L = "attribute-table-y-offset"; - public string? Params { get; } = ""; - public string Description { get; } = "vertical offset for attribute table in pixels (default - 0)"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgSharePatternTable : IArg - { - public const string S = "s"; - public const string L = "share-pattern-table"; - public string? Params { get; } = null; - public string Description { get; } = "vertical offset for attribute table in pixels (default - 0)"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgLossy : IArg - { - public const string S = "l"; - public const string L = "lossy"; - public string? Params { get; } = ""; - public string Description { get; } = "lossy level: 0-3, defines how many color distortion is allowed\nwithout throwing an error (default - 2)"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutPreview : IArg - { - public const string S = "v"; - public const string L = "out-preview"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for preview of image number #"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutPalette : IArg - { - public const string S = "t"; - public const string L = "out-palette"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for palette number #"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutPatternTable : IArg - { - public const string S = "n"; - public const string L = "out-pattern-table"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for pattern table of image number #"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutNameTable : IArg - { - public const string S = "a"; - public const string L = "out-name-table"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for nametable of image number #"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutAttributeTable : IArg - { - public const string S = "u"; - public const string L = "out-attribute-table"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for attribute table of image number #"; - public bool HasIndex { get; } = true; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutTilesCsv : IArg - { - public const string S = "z"; - public const string L = "out-tiles-csv"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for tiles info in CSV format"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutPalettesCsv : IArg - { - public const string S = "x"; - public const string L = "out-palettes-csv"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for palettes info in CSV format"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgOutColorsTable : IArg - { - public const string S = "g"; - public const string L = "out-colors-table"; - public string? Params { get; } = ""; - public string Description { get; } = "output filename for graphical table of available colors\n(from \"--colors\" option)"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } - - class ArgQuiet : IArg - { - public const string S = "q"; - public const string L = "quiet"; - public string? Params { get; } = null; - public string Description { get; } = "suppress console output"; - public bool HasIndex { get; } = false; - public string Short { get; } = S; - public string Long { get; } = L; - } -} +namespace com.clusterrr.Famicom.NesTiler +{ + interface IArg + { + public string Short { get; } + public string Long { get; } + public string? Params { get; } + public string Description { get; } + public bool HasIndex { get; } + + public static IArg[] Args = new IArg[] + { + new ArgIn(), + new ArgColors(), + new ArgMode(), + new ArgBgColor(), + new ArgEnablePalettes(), + new ArgPalette(), + new ArgPatternOffset(), + new ArgAttributeTableYOffset(), + new ArgSharePatternTable(), + new ArgLossy(), + new ArgOutPreview(), + new ArgOutPalette(), + new ArgOutPatternTable(), + new ArgOutNameTable(), + new ArgOutAttributeTable(), + new ArgOutTilesCsv(), + new ArgOutPalettesCsv(), + new ArgOutColorsTable(), + new ArgQuiet() + }; + } + + class ArgIn : IArg + { + public const string S = "i"; + public const string L = "in"; + public string? Params { get; } = "[:offset[:height]]"; + public string Description { get; } = "input filename number #, optionally cropped vertically"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgColors : IArg + { + public const string S = "c"; + public const string L = "colors"; + public string? Params { get; } = ""; + public string Description { get; } = $"JSON or PAL file with the list of available colors\n(default - {Config.DEFAULT_COLORS_FILE})"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgMode : IArg + { + public const string S = "m"; + public const string L = "mode"; + public string? Params { get; } = "bg|sprites8x8|sprites8x16"; + public string Description { get; } = "mode: backgrounds, 8x8 sprites or 8x16 sprites (default - bg)"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgBgColor : IArg + { + public const string S = "b"; + public const string L = "bg-color"; + public string? Params { get; } = ""; + public string Description { get; } = "background color in HTML color format (default - auto)"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgEnablePalettes : IArg + { + public const string S = "e"; + public const string L = "enable-palettes"; + public string? Params { get; } = ""; + public string Description { get; } = "zero-based comma separated list of palette numbers to use\n(default - 0,1,2,3)"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgPalette : IArg + { + public const string S = "p"; + public const string L = "palette"; + public string? Params { get; } = ""; + public string Description { get; } = "comma separated list of colors to use in palette number #\n(default - auto)"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgPatternOffset : IArg + { + public const string S = "o"; + public const string L = "pattern-offset"; + public string? Params { get; } = ""; + public string Description { get; } = "first tile index for pattern table for file number # (default - 0)"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgAttributeTableYOffset : IArg + { + public const string S = "y"; + public const string L = "attribute-table-y-offset"; + public string? Params { get; } = ""; + public string Description { get; } = "vertical offset for attribute table in pixels (default - 0)"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgSharePatternTable : IArg + { + public const string S = "s"; + public const string L = "share-pattern-table"; + public string? Params { get; } = null; + public string Description { get; } = "vertical offset for attribute table in pixels (default - 0)"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgLossy : IArg + { + public const string S = "l"; + public const string L = "lossy"; + public string? Params { get; } = ""; + public string Description { get; } = "lossy level: 0-3, defines how many color distortion is allowed\nwithout throwing an error (default - 2)"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutPreview : IArg + { + public const string S = "v"; + public const string L = "out-preview"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for preview of image number #"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutPalette : IArg + { + public const string S = "t"; + public const string L = "out-palette"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for palette number #"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutPatternTable : IArg + { + public const string S = "n"; + public const string L = "out-pattern-table"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for pattern table of image number #"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutNameTable : IArg + { + public const string S = "a"; + public const string L = "out-name-table"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for nametable of image number #"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutAttributeTable : IArg + { + public const string S = "u"; + public const string L = "out-attribute-table"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for attribute table of image number #"; + public bool HasIndex { get; } = true; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutTilesCsv : IArg + { + public const string S = "z"; + public const string L = "out-tiles-csv"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for tiles info in CSV format"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutPalettesCsv : IArg + { + public const string S = "x"; + public const string L = "out-palettes-csv"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for palettes info in CSV format"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgOutColorsTable : IArg + { + public const string S = "g"; + public const string L = "out-colors-table"; + public string? Params { get; } = ""; + public string Description { get; } = "output filename for graphical table of available colors\n(from \"--colors\" option)"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } + + class ArgQuiet : IArg + { + public const string S = "q"; + public const string L = "quiet"; + public string? Params { get; } = null; + public string Description { get; } = "suppress console output"; + public bool HasIndex { get; } = false; + public string Short { get; } = S; + public string Long { get; } = L; + } +} diff --git a/NesTiler/ColorFinder.cs b/NesTiler/ColorFinder.cs index 257e9b1..0a615cb 100644 --- a/NesTiler/ColorFinder.cs +++ b/NesTiler/ColorFinder.cs @@ -1,194 +1,194 @@ -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Text.Json; - -namespace com.clusterrr.Famicom.NesTiler -{ - class ColorFinder - { - static byte[] FORBIDDEN_COLORS = { 0x0D, 0x0E, 0x0F, 0x1E, 0x1F, 0x2E, 0x2F, 0x3E, 0x3F }; - - public readonly Dictionary Colors; - private readonly Dictionary cache = new(); - - public ColorFinder(string filename) - { - this.Colors = LoadColors(filename); - } - - private static Dictionary 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 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(); - for (byte c = 0; c < 64; c++) - { - var color = new SKColor(data[c * 3], data[(c * 3) + 1], data[(c * 3) + 2]); - nesColors[c] = color; - } - } - else - { - var paletteJson = File.ReadAllText(filename); - var nesColorsStr = JsonSerializer.Deserialize>(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 > 0x3F) 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 - { - var color = ColorTranslator.FromHtml(kv.Value); ; - return new SKColor(color.R, color.G, color.B); - } - 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; - } - - /// - /// Find index of most similar color from NES colors - /// - /// Input color - /// Output color index - public byte FindSimilarColorIndex(SKColor color) - { - if (cache.ContainsKey(color)) - return cache[color]; - byte result = byte.MaxValue; - double minDelta = double.MaxValue; - SKColor c = SKColors.Transparent; - foreach (var index in Colors.Keys) - { - var delta = color.GetDelta(Colors[index]); - if (delta < minDelta) - { - minDelta = delta; - result = index; - c = Colors[index]; - } - } - if (result == byte.MaxValue) - throw new KeyNotFoundException($"Invalid color: {color}."); - if (cache != null) - cache[color] = result; - return result; - } - - /// - /// Find most similar color from list of colors - /// - /// Haystack - /// Niddle - /// Output color - public SKColor FindSimilarColor(IEnumerable colors, SKColor color) - { - SKColor result = SKColors.Black; - double minDelta = double.MaxValue; - foreach (var c in colors) - { - var delta = color.GetDelta(c); - if (delta < minDelta) - { - minDelta = delta; - result = c; - } - } - return result; - } - - /// - /// Find most similar color from NES colors - /// - /// Input colo - /// Output color - public SKColor FindSimilarColor(SKColor 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++) - { - SKColor color; - SKPaint paint; - if (Colors.TryGetValue((byte)((y * colorColumns) + x), out color)) - { - paint = new SKPaint() { Color = color }; - canvas.DrawRegion(new SKRegion(new SKRectI(x * colorSize, y * colorSize, (x + 1) * colorSize, (y + 1) * colorSize)), paint); - - color = new SKColor((byte)(0xFF - color.Red), (byte)(0xFF - color.Green), (byte)(0xFF - color.Blue)); // invert color - paint = new SKPaint() - { - Color = color, - 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()); - } - } -} +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace com.clusterrr.Famicom.NesTiler +{ + class ColorFinder + { + static byte[] FORBIDDEN_COLORS = { 0x0D, 0x0E, 0x0F, 0x1E, 0x1F, 0x2E, 0x2F, 0x3E, 0x3F }; + + public readonly Dictionary Colors; + private readonly Dictionary cache = new(); + + public ColorFinder(string filename) + { + this.Colors = LoadColors(filename); + } + + private static Dictionary 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 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(); + for (byte c = 0; c < 64; c++) + { + var color = new SKColor(data[c * 3], data[(c * 3) + 1], data[(c * 3) + 2]); + nesColors[c] = color; + } + } + else + { + var paletteJson = File.ReadAllText(filename); + var nesColorsStr = JsonSerializer.Deserialize>(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 > 0x3F) 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 + { + var color = ColorTranslator.FromHtml(kv.Value); ; + return new SKColor(color.R, color.G, color.B); + } + 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; + } + + /// + /// Find index of most similar color from NES colors + /// + /// Input color + /// Output color index + public byte FindSimilarColorIndex(SKColor color) + { + if (cache.ContainsKey(color)) + return cache[color]; + byte result = byte.MaxValue; + double minDelta = double.MaxValue; + SKColor c = SKColors.Transparent; + foreach (var index in Colors.Keys) + { + var delta = color.GetDelta(Colors[index]); + if (delta < minDelta) + { + minDelta = delta; + result = index; + c = Colors[index]; + } + } + if (result == byte.MaxValue) + throw new KeyNotFoundException($"Invalid color: {color}."); + if (cache != null) + cache[color] = result; + return result; + } + + /// + /// Find most similar color from list of colors + /// + /// Haystack + /// Niddle + /// Output color + public SKColor FindSimilarColor(IEnumerable colors, SKColor color) + { + SKColor result = SKColors.Black; + double minDelta = double.MaxValue; + foreach (var c in colors) + { + var delta = color.GetDelta(c); + if (delta < minDelta) + { + minDelta = delta; + result = c; + } + } + return result; + } + + /// + /// Find most similar color from NES colors + /// + /// Input colo + /// Output color + public SKColor FindSimilarColor(SKColor 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++) + { + SKColor color; + SKPaint paint; + if (Colors.TryGetValue((byte)((y * colorColumns) + x), out color)) + { + paint = new SKPaint() { Color = color }; + canvas.DrawRegion(new SKRegion(new SKRectI(x * colorSize, y * colorSize, (x + 1) * colorSize, (y + 1) * colorSize)), paint); + + color = new SKColor((byte)(0xFF - color.Red), (byte)(0xFF - color.Green), (byte)(0xFF - color.Blue)); // invert color + paint = new SKPaint() + { + Color = color, + 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/Config.cs b/NesTiler/Config.cs index 927bf76..9c1c8f4 100644 --- a/NesTiler/Config.cs +++ b/NesTiler/Config.cs @@ -1,277 +1,277 @@ -using SkiaSharp; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; - -namespace com.clusterrr.Famicom.NesTiler -{ - class Config - { - public const string DEFAULT_COLORS_FILE = @"nestiler-colors.json"; - - public enum TilesMode - { - Backgrounds, - Sprites8x8, - Sprites8x16 - } - - public string ColorsFile { get; private set; } - public Dictionary ImageFiles { get; private set; } = new Dictionary(); - public SKColor? BgColor { get; private set; } = null; - public bool[] PaletteEnabled { get; private set; } = new bool[4] { true, true, true, true }; - public Palette?[] FixedPalettes { get; private set; } = new Palette?[4] { null, null, null, null }; - public TilesMode Mode { get; private set; } = TilesMode.Backgrounds; - public int TileWidth { get; private set; } = 8; - public int TileHeight { get; private set; } = 8; - public int TilePalWidth { get; private set; } = 16; - public int TilePalHeight { get; private set; } = 16; - public bool SharePatternTable { get; private set; } = false; - public int LossyLevel { get; private set; } = 2; - public int PatternTableStartOffsetShared { get; private set; } = 0; - public Dictionary PatternTableStartOffsets { get; private set; } = new Dictionary(); - public Dictionary PattributeTableYOffsets { get; private set; } = new Dictionary(); - public bool Quiet { get; private set; } = false; - - // Filenames - public Dictionary OutPreview { get; private set; } = new Dictionary(); - public Dictionary OutPalette { get; private set; } = new Dictionary(); - public Dictionary OutPatternTable { get; private set; } = new Dictionary(); - public Dictionary OutNameTable { get; private set; } = new Dictionary(); - public Dictionary OutAttributeTable { get; private set; } = new Dictionary(); - public string? OutPatternTableShared { get; private set; } = null; - public string? OutTilesCsv { get; private set; } = null; - public string? OutPalettesCsv { get; private set; } = null; - public string? OutColorsTable { get; private set; } = null; - - private Config() - { - ColorsFile = Path.Combine(AppContext.BaseDirectory, DEFAULT_COLORS_FILE); - if (!File.Exists(ColorsFile)) - ColorsFile = Path.Combine(Directory.GetCurrentDirectory(), DEFAULT_COLORS_FILE); - if (!File.Exists(ColorsFile) && !OperatingSystem.IsWindows()) - ColorsFile = Path.Combine("/etc", DEFAULT_COLORS_FILE); - } - - public static Config Parse(string[] args) - { - Config config = new Config(); - var paramRegex = new Regex(@"^--?(?[a-zA-Z-]+?)-?(?[0-9]*)$"); - for (int i = 0; i < args.Length; i++) - { - var match = paramRegex.Match(args[i]); - if (!match.Success) - throw new ArgumentException($"Invalid argument.", args[i]); - string param = match.Groups["param"].Value; - string indexStr = match.Groups["index"].Value; - int indexNum = 0; - if (!string.IsNullOrEmpty(indexStr)) - indexNum = int.Parse(indexStr); - string value = i < args.Length - 1 ? args[i + 1] : ""; - int valueInt; - switch (param) - { - case ArgIn.S: - case ArgIn.L: - config.ImageFiles[indexNum] = value; - i++; - break; - case ArgColors.S: - case ArgColors.L: - config.ColorsFile = value; - i++; - break; - case ArgMode.S: - case ArgMode.L: - switch (value.ToLower()) - { - case "sprite": - case "sprites": - case "sprites8x8": - config.Mode = TilesMode.Sprites8x8; - config.TileWidth = 8; - config.TileHeight = 8; - config.TilePalWidth = 8; - config.TilePalHeight = 8; - break; - case "sprite8x16": - case "sprites8x16": - config.Mode = TilesMode.Sprites8x16; - config.TileWidth = 8; - config.TileHeight = 16; - config.TilePalWidth = 8; - config.TilePalHeight = 16; - break; - case "bg": - case "background": - case "backgrounds": - config.Mode = TilesMode.Backgrounds; - config.TileWidth = 8; - config.TileHeight = 8; - config.TilePalWidth = 16; - config.TilePalHeight = 16; - break; - default: - throw new ArgumentException($"{value} - invalid mode.", param); - } - i++; - break; - case ArgBgColor.S: - case ArgBgColor.L: - if (value != "auto") - { - try - { - config.BgColor = ColorTranslator.FromHtml(value).ToSKColor(); - } - catch (FormatException) - { - throw new ArgumentException($"{value} - invalid color.", param); - } - } - i++; - break; - case ArgEnablePalettes.S: - case ArgEnablePalettes.L: - { - var paletteNumbersStr = value.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); - for (int pal = 0; pal < config.PaletteEnabled.Length; pal++) - config.PaletteEnabled[pal] = false; // disable all palettes - foreach (var palNumStr in paletteNumbersStr) - { - if (!int.TryParse(palNumStr, out valueInt)) - throw new ArgumentException($"\"{palNumStr}\" is not valid integer value.", param); - if (valueInt < 0 || valueInt > 3) - throw new ArgumentException($"Palette index must be between 0 and 3.", param); - config.PaletteEnabled[valueInt] = true; - } - if (!config.PaletteEnabled.Where(p => p).Any()) // will never be executed? - throw new ArgumentException($"You need to enable at least one palette.", param); - } - i++; - break; - case ArgPalette.S: - case ArgPalette.L: - { - var colors = value.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(c => - { - try - { - return ColorTranslator.FromHtml(c).ToSKColor(); - } - catch (FormatException) - { - throw new ArgumentException($"{c} - invalid color.", param); - } - }); - config.FixedPalettes[indexNum] = new Palette(colors); - } - i++; - break; - case ArgPatternOffset.S: - case ArgPatternOffset.L: - if (!int.TryParse(value, out valueInt)) - throw new ArgumentException($"\"{value}\" is not valid integer value.", param); - if (valueInt < 0 || valueInt >= 256) - throw new ArgumentException($"Value ({valueInt}) must be between 0 and 255.", param); - config.PatternTableStartOffsets[indexNum] = valueInt; - config.PatternTableStartOffsetShared = config.PatternTableStartOffsets[indexNum]; - i++; - break; - case ArgAttributeTableYOffset.S: - case ArgAttributeTableYOffset.L: - if (!int.TryParse(value, out valueInt)) - throw new ArgumentException($"\"{value}\" is not valid integer value.", param); - if (valueInt % 8 != 0) - throw new ArgumentException($"Value ({valueInt}) must be divisible by 8.", param); - if (valueInt < 0 || valueInt >= 256) - throw new ArgumentException($"Value ({valueInt}) must be between 0 and 255.", param); - config.PattributeTableYOffsets[indexNum] = valueInt; - i++; - break; - case ArgSharePatternTable.S: - case ArgSharePatternTable.L: - config.SharePatternTable = true; - break; - case ArgLossy.S: - case ArgLossy.L: - if (!int.TryParse(value, out valueInt)) - throw new ArgumentException($"\"{value}\" is not valid integer value.", param); - if (valueInt < 0 || valueInt > 3) - throw new ArgumentException($"Value ({valueInt}) must be between 0 and 3.", param); - config.LossyLevel = valueInt; - i++; - break; - case ArgOutPreview.S: - case ArgOutPreview.L: - config.OutPreview[indexNum] = value; - i++; - break; - case ArgOutPalette.S: - case ArgOutPalette.L: - if (indexNum < 0 || indexNum > 3) - throw new ArgumentException($"Palette index must be between 0 and 3.", param); - config.OutPalette[indexNum] = value; - i++; - break; - case ArgOutPatternTable.S: - case ArgOutPatternTable.L: - config.OutPatternTable[indexNum] = value; - config.OutPatternTableShared = value; - i++; - break; - case ArgOutNameTable.S: - case ArgOutNameTable.L: - config.OutNameTable[indexNum] = value; - i++; - break; - case ArgOutAttributeTable.S: - case ArgOutAttributeTable.L: - config.OutAttributeTable[indexNum] = value; - i++; - break; - case ArgOutTilesCsv.S: - case ArgOutTilesCsv.L: - config.OutTilesCsv = value; - i++; - break; - case ArgOutPalettesCsv.S: - case ArgOutPalettesCsv.L: - config.OutPalettesCsv = value; - i++; - break; - case ArgOutColorsTable.S: - case ArgOutColorsTable.L: - config.OutColorsTable = value; - i++; - break; - case ArgQuiet.S: - case ArgQuiet.L: - config.Quiet = true; - break; - default: - throw new ArgumentException($"Unknown argument.", args[i]); - } - } - - // Some input data checks - switch (config.Mode) - { - case TilesMode.Sprites8x8: - case TilesMode.Sprites8x16: - if (!config.BgColor.HasValue) throw new InvalidDataException("You must specify background color for sprites mode."); - break; - } - // Check output files - foreach (var c in new Dictionary[] { config.OutPreview, config.OutPatternTable, config.OutNameTable, config.OutAttributeTable }) - foreach (var f in c) - if (!config.ImageFiles.ContainsKey(f.Key)) - throw new ArgumentException($"Can't write {f.Value} - there is no input image with index {f.Key}."); - - return config; - } - } -} +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace com.clusterrr.Famicom.NesTiler +{ + class Config + { + public const string DEFAULT_COLORS_FILE = @"nestiler-colors.json"; + + public enum TilesMode + { + Backgrounds, + Sprites8x8, + Sprites8x16 + } + + public string ColorsFile { get; private set; } + public Dictionary ImageFiles { get; private set; } = new Dictionary(); + public SKColor? BgColor { get; private set; } = null; + public bool[] PaletteEnabled { get; private set; } = new bool[4] { true, true, true, true }; + public Palette?[] FixedPalettes { get; private set; } = new Palette?[4] { null, null, null, null }; + public TilesMode Mode { get; private set; } = TilesMode.Backgrounds; + public int TileWidth { get; private set; } = 8; + public int TileHeight { get; private set; } = 8; + public int TilePalWidth { get; private set; } = 16; + public int TilePalHeight { get; private set; } = 16; + public bool SharePatternTable { get; private set; } = false; + public int LossyLevel { get; private set; } = 2; + public int PatternTableStartOffsetShared { get; private set; } = 0; + public Dictionary PatternTableStartOffsets { get; private set; } = new Dictionary(); + public Dictionary PattributeTableYOffsets { get; private set; } = new Dictionary(); + public bool Quiet { get; private set; } = false; + + // Filenames + public Dictionary OutPreview { get; private set; } = new Dictionary(); + public Dictionary OutPalette { get; private set; } = new Dictionary(); + public Dictionary OutPatternTable { get; private set; } = new Dictionary(); + public Dictionary OutNameTable { get; private set; } = new Dictionary(); + public Dictionary OutAttributeTable { get; private set; } = new Dictionary(); + public string? OutPatternTableShared { get; private set; } = null; + public string? OutTilesCsv { get; private set; } = null; + public string? OutPalettesCsv { get; private set; } = null; + public string? OutColorsTable { get; private set; } = null; + + private Config() + { + ColorsFile = Path.Combine(AppContext.BaseDirectory, DEFAULT_COLORS_FILE); + if (!File.Exists(ColorsFile)) + ColorsFile = Path.Combine(Directory.GetCurrentDirectory(), DEFAULT_COLORS_FILE); + if (!File.Exists(ColorsFile) && !OperatingSystem.IsWindows()) + ColorsFile = Path.Combine("/etc", DEFAULT_COLORS_FILE); + } + + public static Config Parse(string[] args) + { + Config config = new Config(); + var paramRegex = new Regex(@"^--?(?[a-zA-Z-]+?)-?(?[0-9]*)$"); + for (int i = 0; i < args.Length; i++) + { + var match = paramRegex.Match(args[i]); + if (!match.Success) + throw new ArgumentException($"Invalid argument.", args[i]); + string param = match.Groups["param"].Value; + string indexStr = match.Groups["index"].Value; + int indexNum = 0; + if (!string.IsNullOrEmpty(indexStr)) + indexNum = int.Parse(indexStr); + string value = i < args.Length - 1 ? args[i + 1] : ""; + int valueInt; + switch (param) + { + case ArgIn.S: + case ArgIn.L: + config.ImageFiles[indexNum] = value; + i++; + break; + case ArgColors.S: + case ArgColors.L: + config.ColorsFile = value; + i++; + break; + case ArgMode.S: + case ArgMode.L: + switch (value.ToLower()) + { + case "sprite": + case "sprites": + case "sprites8x8": + config.Mode = TilesMode.Sprites8x8; + config.TileWidth = 8; + config.TileHeight = 8; + config.TilePalWidth = 8; + config.TilePalHeight = 8; + break; + case "sprite8x16": + case "sprites8x16": + config.Mode = TilesMode.Sprites8x16; + config.TileWidth = 8; + config.TileHeight = 16; + config.TilePalWidth = 8; + config.TilePalHeight = 16; + break; + case "bg": + case "background": + case "backgrounds": + config.Mode = TilesMode.Backgrounds; + config.TileWidth = 8; + config.TileHeight = 8; + config.TilePalWidth = 16; + config.TilePalHeight = 16; + break; + default: + throw new ArgumentException($"{value} - invalid mode.", param); + } + i++; + break; + case ArgBgColor.S: + case ArgBgColor.L: + if (value != "auto") + { + try + { + config.BgColor = ColorTranslator.FromHtml(value).ToSKColor(); + } + catch (FormatException) + { + throw new ArgumentException($"{value} - invalid color.", param); + } + } + i++; + break; + case ArgEnablePalettes.S: + case ArgEnablePalettes.L: + { + var paletteNumbersStr = value.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + for (int pal = 0; pal < config.PaletteEnabled.Length; pal++) + config.PaletteEnabled[pal] = false; // disable all palettes + foreach (var palNumStr in paletteNumbersStr) + { + if (!int.TryParse(palNumStr, out valueInt)) + throw new ArgumentException($"\"{palNumStr}\" is not valid integer value.", param); + if (valueInt < 0 || valueInt > 3) + throw new ArgumentException($"Palette index must be between 0 and 3.", param); + config.PaletteEnabled[valueInt] = true; + } + if (!config.PaletteEnabled.Where(p => p).Any()) // will never be executed? + throw new ArgumentException($"You need to enable at least one palette.", param); + } + i++; + break; + case ArgPalette.S: + case ArgPalette.L: + { + var colors = value.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries).Select(c => + { + try + { + return ColorTranslator.FromHtml(c).ToSKColor(); + } + catch (FormatException) + { + throw new ArgumentException($"{c} - invalid color.", param); + } + }); + config.FixedPalettes[indexNum] = new Palette(colors); + } + i++; + break; + case ArgPatternOffset.S: + case ArgPatternOffset.L: + if (!int.TryParse(value, out valueInt)) + throw new ArgumentException($"\"{value}\" is not valid integer value.", param); + if (valueInt < 0 || valueInt >= 256) + throw new ArgumentException($"Value ({valueInt}) must be between 0 and 255.", param); + config.PatternTableStartOffsets[indexNum] = valueInt; + config.PatternTableStartOffsetShared = config.PatternTableStartOffsets[indexNum]; + i++; + break; + case ArgAttributeTableYOffset.S: + case ArgAttributeTableYOffset.L: + if (!int.TryParse(value, out valueInt)) + throw new ArgumentException($"\"{value}\" is not valid integer value.", param); + if (valueInt % 8 != 0) + throw new ArgumentException($"Value ({valueInt}) must be divisible by 8.", param); + if (valueInt < 0 || valueInt >= 256) + throw new ArgumentException($"Value ({valueInt}) must be between 0 and 255.", param); + config.PattributeTableYOffsets[indexNum] = valueInt; + i++; + break; + case ArgSharePatternTable.S: + case ArgSharePatternTable.L: + config.SharePatternTable = true; + break; + case ArgLossy.S: + case ArgLossy.L: + if (!int.TryParse(value, out valueInt)) + throw new ArgumentException($"\"{value}\" is not valid integer value.", param); + if (valueInt < 0 || valueInt > 3) + throw new ArgumentException($"Value ({valueInt}) must be between 0 and 3.", param); + config.LossyLevel = valueInt; + i++; + break; + case ArgOutPreview.S: + case ArgOutPreview.L: + config.OutPreview[indexNum] = value; + i++; + break; + case ArgOutPalette.S: + case ArgOutPalette.L: + if (indexNum < 0 || indexNum > 3) + throw new ArgumentException($"Palette index must be between 0 and 3.", param); + config.OutPalette[indexNum] = value; + i++; + break; + case ArgOutPatternTable.S: + case ArgOutPatternTable.L: + config.OutPatternTable[indexNum] = value; + config.OutPatternTableShared = value; + i++; + break; + case ArgOutNameTable.S: + case ArgOutNameTable.L: + config.OutNameTable[indexNum] = value; + i++; + break; + case ArgOutAttributeTable.S: + case ArgOutAttributeTable.L: + config.OutAttributeTable[indexNum] = value; + i++; + break; + case ArgOutTilesCsv.S: + case ArgOutTilesCsv.L: + config.OutTilesCsv = value; + i++; + break; + case ArgOutPalettesCsv.S: + case ArgOutPalettesCsv.L: + config.OutPalettesCsv = value; + i++; + break; + case ArgOutColorsTable.S: + case ArgOutColorsTable.L: + config.OutColorsTable = value; + i++; + break; + case ArgQuiet.S: + case ArgQuiet.L: + config.Quiet = true; + break; + default: + throw new ArgumentException($"Unknown argument.", args[i]); + } + } + + // Some input data checks + switch (config.Mode) + { + case TilesMode.Sprites8x8: + case TilesMode.Sprites8x16: + if (!config.BgColor.HasValue) throw new InvalidDataException("You must specify background color for sprites mode."); + break; + } + // Check output files + foreach (var c in new Dictionary[] { config.OutPreview, config.OutPatternTable, config.OutNameTable, config.OutAttributeTable }) + foreach (var f in c) + if (!config.ImageFiles.ContainsKey(f.Key)) + throw new ArgumentException($"Can't write {f.Value} - there is no input image with index {f.Key}."); + + return config; + } + } +} diff --git a/NesTiler/NesTiler.csproj b/NesTiler/NesTiler.csproj index 4f12212..40a55ba 100644 --- a/NesTiler/NesTiler.csproj +++ b/NesTiler/NesTiler.csproj @@ -4,6 +4,9 @@ net6.0 com.clusterrr.Famicom.NesTiler nestiler + true + true + true False -- cgit v1.2.3