commit 5b8bc0c5214332deb7e6c289abe17432994fb54a Author: cosmonaut Date: Wed Mar 15 11:11:34 2023 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3962eca --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +output/ + +.idea diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..462a13a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/StbImageSharp"] + path = lib/StbImageSharp + url = https://github.com/StbSharp/StbImageSharp.git +[submodule "lib/StbImageWriteSharp"] + path = lib/StbImageWriteSharp + url = https://github.com/StbSharp/StbImageWriteSharp.git diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..95d7eff --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net5.0/Palettizer.dll", + "args": ["project", "../SamuraiGunn2_GMS2/GMS2/SamuraiGunn2/SamuraiGunn2.yyp", "TP_LevelPalette", "pal_Forest", "pal_Cemetery", "pal_Mountain"], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..dbcc2a5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Palettizer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Palettizer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "${workspaceFolder}/Palettizer.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Palettizer2.csproj b/Palettizer2.csproj new file mode 100644 index 0000000..5f8b0ce --- /dev/null +++ b/Palettizer2.csproj @@ -0,0 +1,28 @@ + + + + Exe + net5.0 + 8.0 + false + false + Palettizer + + + + $(DefaultItemExcludes);lib\**\* + + + + + + + + + + + + + + + diff --git a/Palettizer2.sln b/Palettizer2.sln new file mode 100644 index 0000000..8c37fda --- /dev/null +++ b/Palettizer2.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Palettizer2", "Palettizer2.csproj", "{CC59DC79-B418-411E-BA42-017422F30821}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StbImageSharp", "lib\StbImageSharp\src\StbImageSharp.csproj", "{6C5AEE4F-1549-4E86-A1C0-6115781A86F5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StbImageWriteSharp", "lib\StbImageWriteSharp\src\StbImageWriteSharp.csproj", "{7551F435-4D46-4291-821D-A8102667C90A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CC59DC79-B418-411E-BA42-017422F30821}.Debug|x64.ActiveCfg = Debug|x64 + {CC59DC79-B418-411E-BA42-017422F30821}.Debug|x64.Build.0 = Debug|x64` + {CC59DC79-B418-411E-BA42-017422F30821}.Release|x64.ActiveCfg = Release|x86 + {CC59DC79-B418-411E-BA42-017422F30821}.Release|x64.Build.0 = Release|x86 + {6C5AEE4F-1549-4E86-A1C0-6115781A86F5}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C5AEE4F-1549-4E86-A1C0-6115781A86F5}.Debug|x64.Build.0 = Debug|Any CPU + {6C5AEE4F-1549-4E86-A1C0-6115781A86F5}.Release|x64.ActiveCfg = Release|Any CPU + {6C5AEE4F-1549-4E86-A1C0-6115781A86F5}.Release|x64.Build.0 = Release|Any CPU + {7551F435-4D46-4291-821D-A8102667C90A}.Debug|x64.ActiveCfg = Debug|Any CPU + {7551F435-4D46-4291-821D-A8102667C90A}.Debug|x64.Build.0 = Debug|Any CPU + {7551F435-4D46-4291-821D-A8102667C90A}.Release|x64.ActiveCfg = Release|Any CPU + {7551F435-4D46-4291-821D-A8102667C90A}.Release|x64.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/lib/StbImageSharp b/lib/StbImageSharp new file mode 160000 index 0000000..9feb076 --- /dev/null +++ b/lib/StbImageSharp @@ -0,0 +1 @@ +Subproject commit 9feb07693cde152ebd1cceff7e995db4d7fe6d8d diff --git a/lib/StbImageWriteSharp b/lib/StbImageWriteSharp new file mode 160000 index 0000000..6c99960 --- /dev/null +++ b/lib/StbImageWriteSharp @@ -0,0 +1 @@ +Subproject commit 6c999609700e5a3450c54b78d099c56918a2d1c6 diff --git a/src/Color.cs b/src/Color.cs new file mode 100644 index 0000000..885199c --- /dev/null +++ b/src/Color.cs @@ -0,0 +1,15 @@ +namespace Palettizer2 +{ + public struct Color + { + public byte R { get; set; } + public byte G { get; set; } + public byte B { get; set; } + public byte A { get; set; } + + public override string ToString() + { + return $"{{R: {R.ToString()}, G: {G.ToString()}, B: {B.ToString()}, A: {A.ToString()}}}"; + } + } +} diff --git a/src/Palette.cs b/src/Palette.cs new file mode 100644 index 0000000..1b8f654 --- /dev/null +++ b/src/Palette.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; + +namespace Palettizer2 +{ + public class Palette + { + public Dictionary GrayscaleToColorMap { get; } + public Dictionary ColorToGrayscaleMap { get; } + public int GrayscaleCount { get; set; } + public List AlternateColorRows { get; } + + public int Width { get => 256; } + public int Height { get => AlternateColorRows.Count + 1; } + + public Palette() + { + GrayscaleToColorMap = new Dictionary(); + ColorToGrayscaleMap = new Dictionary(); + GrayscaleCount = 0; + AlternateColorRows = new List(); + } + + public void AddColor(Color color) + { + var average = (color.R + color.G + color.B) / 3; + if (GrayscaleToColorMap.ContainsKey((byte)average)) + { + var increment = 1; + for (var i = 0; i < 256; i += 1) + { + average = (average + increment) % 256; + + if (!GrayscaleToColorMap.ContainsKey((byte)average)) + { + break; + } + + increment = ((increment > 0) ? (increment + 1) : (increment - 1)) * -1; // up by 1, down by 2, up by 3, etc + } + } + + GrayscaleToColorMap.Add((byte)average, color); + ColorToGrayscaleMap.Add(color, (byte)average); + GrayscaleCount += 1; + } + + public void AddAlternateColorRow(Color[] colors) + { + var byteArray = new byte[256 * 4]; + + for (var i = 0; i < 256; i += 1) + { + byteArray[i * 4] = colors[i].R; + byteArray[i * 4 + 1] = colors[i].G; + byteArray[i * 4 + 2] = colors[i].B; + byteArray[i * 4 + 3] = colors[i].A; + } + + AlternateColorRows.Add(byteArray); + } + + public void ClearAlternateColorRows() + { + AlternateColorRows.Clear(); + } + + public byte[] CreateIndexedPaletteBitmap() + { + var paletteBitmap = new byte[256 * (AlternateColorRows.Count + 1) * 4]; + + for (var i = 0; i < 256; i += 1) + { + var color = new Color(); + color.A = 255; + + if (GrayscaleToColorMap.ContainsKey((byte)i)) + { + color = GrayscaleToColorMap[(byte)i]; + } + + paletteBitmap[i * 4] = color.R; + paletteBitmap[i * 4 + 1] = color.G; + paletteBitmap[i * 4 + 2] = color.B; + paletteBitmap[i * 4 + 3] = color.A; + } + + for (var i = 0; i < 256; i += 1) + { + for (var j = 0; j < AlternateColorRows.Count; j += 1) + { + var alpha = AlternateColorRows[j][i * 4 + 3]; + if (alpha == 0) + { + // grab from top row + paletteBitmap[(i + (256 * (j + 1))) * 4] = paletteBitmap[i * 4]; + paletteBitmap[(i + (256 * (j + 1))) * 4 + 1] = paletteBitmap[i * 4 + 1]; + paletteBitmap[(i + (256 * (j + 1))) * 4 + 2] = paletteBitmap[i * 4 + 2]; + paletteBitmap[(i + (256 * (j + 1))) * 4 + 3] = paletteBitmap[i * 4 + 3]; + } + else + { + paletteBitmap[(i + (256 * (j + 1))) * 4] = AlternateColorRows[j][i * 4]; + paletteBitmap[(i + (256 * (j + 1))) * 4 + 1] = AlternateColorRows[j][i * 4 + 1]; + paletteBitmap[(i + (256 * (j + 1))) * 4 + 2] = AlternateColorRows[j][i * 4 + 2]; + paletteBitmap[(i + (256 * (j + 1))) * 4 + 3] = alpha; + } + } + } + + return paletteBitmap; + } + } +} diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..548109c --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,136 @@ +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using StbImageSharp; + +namespace Palettizer2 +{ + class Program + { + static int Main(string[] args) + { + var palettize = new Command("palettize") + { + new Argument( + "directory", + "Path to a directory containing PNG files." + ), + new Argument( + "paletteName", + "The name of the palette sprite to output." + ) + }; + + var root = new RootCommand{ + palettize + }; + + palettize.Handler = CommandHandler.Create(HandlePalettize); + return root.Invoke(args); + } + + static void HandlePalettize(DirectoryInfo directory, string paletteName) + { + var outputDir = Directory.CreateDirectory("output"); + var palette = new Palette(); + var writer = new StbImageWriteSharp.ImageWriter(); + + // add all sprite colors to palette + foreach (var imageFile in directory.EnumerateFiles("*.png")) + { + System.Console.WriteLine(imageFile.FullName); + + var spriteDir = Path.GetDirectoryName(imageFile.FullName); + + ImageResult image; + byte[] grayscaleImageBytes; + using (var stream = File.OpenRead(imageFile.FullName)) + { + image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + grayscaleImageBytes = AddSpriteToPalette(palette, image); + } + + var spriteWriteDir = Path.Combine( + outputDir.FullName, + Path.GetRelativePath(directory.FullName, spriteDir)); + + Directory.CreateDirectory(spriteWriteDir); + var spriteWritePath = Path.Combine(spriteWriteDir, imageFile.Name); + + using (var stream = File.OpenWrite(spriteWritePath)) + { + writer.WritePng( + grayscaleImageBytes, + image.Width, + image.Height, + StbImageWriteSharp.ColorComponents.RedGreenBlueAlpha, + stream); + } + } + + // write palette sprite to PNG + var paletteOutputPath = Path.Combine(outputDir.FullName, paletteName + ".png"); + using (var stream = File.OpenWrite(paletteOutputPath)) + { + writer.WritePng( + palette.CreateIndexedPaletteBitmap(), + palette.Width, + palette.Height, + StbImageWriteSharp.ColorComponents.RedGreenBlueAlpha, + stream); + } + } + + static byte[] AddSpriteToPalette(Palette palette, ImageResult image) + { + var grayscaleImage = new byte[image.Width * image.Height * 4]; + + for (var i = 0; i < image.Width * image.Height * 4; i += 4) + { + var color = new Color + { + R = image.Data[i], + G = image.Data[i + 1], + B = image.Data[i + 2], + A = image.Data[i + 3] + }; + + if (color.A == 0) + { + continue; + } + + if (palette.ColorToGrayscaleMap.ContainsKey(color)) + { + var grayscaleColor = palette.ColorToGrayscaleMap[color]; + + grayscaleImage[i] = grayscaleColor; + grayscaleImage[i + 1] = grayscaleColor; + grayscaleImage[i + 2] = grayscaleColor; + grayscaleImage[i + 3] = color.A; + } + else + { + if (palette.GrayscaleCount == 256) + { + System.Console.WriteLine("Too many colors! Bailing!"); + return grayscaleImage; + } + else + { + palette.AddColor(color); + + var grayscaleColor = palette.ColorToGrayscaleMap[color]; + + grayscaleImage[i] = grayscaleColor; + grayscaleImage[i + 1] = grayscaleColor; + grayscaleImage[i + 2] = grayscaleColor; + grayscaleImage[i + 3] = color.A; + } + } + } + + return grayscaleImage; + } + } +}