Browse Source

Merge branch 'master' into color-picker-improvement

# Conflicts:
#	src/PixiEditor/Data/Localization/Languages/en.json
CPKreuz 2 years ago
parent
commit
9e2f8c1227
30 changed files with 1009 additions and 615 deletions
  1. 1 2
      README.md
  2. 1 0
      src/ChunkyImageLib/ChunkyImageLib.csproj
  3. 1 0
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  4. 9 0
      src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs
  5. 1 0
      src/PixiEditor.DrawingApi.Core/PixiEditor.DrawingApi.Core.csproj
  6. 1 0
      src/PixiEditor.DrawingApi.Skia/PixiEditor.DrawingApi.Skia.csproj
  7. 1 1
      src/PixiEditor.MSIX/Package.appxmanifest
  8. 1 1
      src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj
  9. 2 0
      src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj
  10. 1 0
      src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj
  11. 503 584
      src/PixiEditor/Data/Localization/Languages/en.json
  12. 7 2
      src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  13. 1 1
      src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs
  14. 1 0
      src/PixiEditor/Models/IO/PaletteFileData.cs
  15. 14 2
      src/PixiEditor/Models/IO/PaletteFileParser.cs
  16. 2 2
      src/PixiEditor/Models/IO/PaletteParsers/ClsFileParser.cs
  17. 91 0
      src/PixiEditor/Models/IO/PaletteParsers/GimpGplParser.cs
  18. 66 0
      src/PixiEditor/Models/IO/PaletteParsers/HexPaletteParser.cs
  19. 1 1
      src/PixiEditor/Models/IO/PaletteParsers/JascPalFile/JascFileException.cs
  20. 4 7
      src/PixiEditor/Models/IO/PaletteParsers/JascPalFile/JascFileParser.cs
  21. 76 0
      src/PixiEditor/Models/IO/PaletteParsers/PaintNetTxtParser.cs
  22. 39 0
      src/PixiEditor/Models/IO/PaletteParsers/PixiPaletteParser.cs
  23. 123 0
      src/PixiEditor/Models/IO/PaletteParsers/PngPaletteParser.cs
  24. 8 0
      src/PixiEditor/Models/IO/PaletteParsers/SavingNotSupportedException.cs
  25. 6 6
      src/PixiEditor/PixiEditor.csproj
  26. 8 1
      src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs
  27. 6 0
      src/PixiEditor/Views/MainWindow.xaml
  28. 11 3
      src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml
  29. 22 2
      src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml.cs
  30. 1 0
      src/PixiEditorGen/PixiEditorGen.csproj

+ 1 - 2
README.md

@@ -22,8 +22,7 @@ Want to create beautiful pixel art for your games? PixiEditor can help you! Our
 
 Have you ever used Photoshop or Gimp? Reinventing the wheel is unnecessary, we wanted users to get familiar with the tool quickly and with ease. 
 
-![](https://user-images.githubusercontent.com/45312141/146670495-ae521a18-a89e-4e94-9317-6838b51407fa.png)
-
+![](https://user-images.githubusercontent.com/45312141/235351211-e00bcaea-9c63-4ecd-a2ee-e4fb2b2c9651.png)
 
 ### Fast
 

+ 1 - 0
src/ChunkyImageLib/ChunkyImageLib.csproj

@@ -8,6 +8,7 @@
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 1 - 0
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -8,6 +8,7 @@
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 9 - 0
src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs

@@ -208,5 +208,14 @@ namespace PixiEditor.DrawingApi.Core.ColorsImpl
           return false;
       }
     }
+
+    /// <summary>
+    ///     Returns hex string representation of the color.
+    /// </summary>
+    /// <returns>Color string in format: AARRGGBB</returns>
+    public string? ToHex()
+    {
+        return this == Empty ? null : $"{this._colorValue:X8}";
+    }
   }
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/PixiEditor.DrawingApi.Core.csproj

@@ -7,6 +7,7 @@
         <LangVersion>10</LangVersion>
         <Configurations>Debug;Release;Steam;DevRelease</Configurations>
         <Platforms>AnyCPU;x64;x86</Platforms>
+      <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
     </PropertyGroup>
 
     <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 1 - 0
src/PixiEditor.DrawingApi.Skia/PixiEditor.DrawingApi.Skia.csproj

@@ -6,6 +6,7 @@
         <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
         <Configurations>Debug;Release;Steam;DevRelease</Configurations>
         <Platforms>AnyCPU;x64;x86</Platforms>
+      <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
     </PropertyGroup>
 
     <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 1 - 1
src/PixiEditor.MSIX/Package.appxmanifest

@@ -9,7 +9,7 @@
   <Identity
     Name="56069PixiEditorOrganizati.PixiEditor"
     Publisher="CN=0AFA75AD-56A3-481D-B5E4-D3C6274DD38A"
-    Version="1.0.0.0" />
+    Version="1.0.3.0" />
 
   <Properties>
     <DisplayName>PixiEditor</DisplayName>

+ 1 - 1
src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj

@@ -59,7 +59,7 @@
     <PackageCertificateKeyFile>PixiEditor.MSIX_TemporaryKey.pfx</PackageCertificateKeyFile>
     <GenerateAppInstallerFile>False</GenerateAppInstallerFile>
     <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
-    <AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
+    <AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
     <GenerateTestArtifacts>True</GenerateTestArtifacts>
     <AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
     <GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>

+ 2 - 0
src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj

@@ -4,6 +4,8 @@
     <TargetFramework>net7.0</TargetFramework>
     <Platforms>AnyCPU;x64;x86</Platforms>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 1 - 0
src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj

@@ -7,6 +7,7 @@
     <WarningsAsErrors>Nullable</WarningsAsErrors>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

File diff suppressed because it is too large
+ 503 - 584
src/PixiEditor/Data/Localization/Languages/en.json


+ 7 - 2
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -4,8 +4,8 @@ using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataProviders;
 using PixiEditor.Models.IO;
-using PixiEditor.Models.IO.ClsFile;
-using PixiEditor.Models.IO.JascPalFile;
+using PixiEditor.Models.IO.PaletteParsers;
+using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -64,6 +64,11 @@ internal static class ServiceCollectionHelpers
         // Palette Parsers
         .AddSingleton<PaletteFileParser, JascFileParser>()
         .AddSingleton<PaletteFileParser, ClsFileParser>()
+        .AddSingleton<PaletteFileParser, PngPaletteParser>()
+        .AddSingleton<PaletteFileParser, PaintNetTxtParser>()
+        .AddSingleton<PaletteFileParser, HexPaletteParser>()
+        .AddSingleton<PaletteFileParser, GimpGplParser>()
+        .AddSingleton<PaletteFileParser, PixiPaletteParser>()
         // Palette data sources
         .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>();
 }

+ 1 - 1
src/PixiEditor/Models/DataProviders/LocalPalettesFetcher.cs

@@ -3,7 +3,7 @@ using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders.Palettes;
 using PixiEditor.Models.IO;
-using PixiEditor.Models.IO.JascPalFile;
+using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.UserPreferences;
 
 namespace PixiEditor.Models.DataProviders;

+ 1 - 0
src/PixiEditor/Models/IO/PaletteFileData.cs

@@ -7,6 +7,7 @@ internal class PaletteFileData
     public string Title { get; set; }
     public Color[] Colors { get; set; }
     public bool IsCorrupted { get; set; } = false;
+    public static PaletteFileData Corrupted => new ("Corrupted", Array.Empty<Color>()) { IsCorrupted = true };
 
     public PaletteFileData(Color[] colors)
     {

+ 14 - 2
src/PixiEditor/Models/IO/PaletteFileParser.cs

@@ -1,9 +1,21 @@
-namespace PixiEditor.Models.IO;
+using System.IO;
+
+namespace PixiEditor.Models.IO;
 
 internal abstract class PaletteFileParser
 {
     public abstract Task<PaletteFileData> Parse(string path);
-    public abstract Task Save(string path, PaletteFileData data);
+    public abstract Task<bool> Save(string path, PaletteFileData data);
     public abstract string FileName { get; }
     public abstract string[] SupportedFileExtensions { get; }
+
+    public virtual bool CanSave => true;
+
+    protected static async Task<string[]> ReadTextLines(string path)
+    {
+        using var stream = File.OpenText(path);
+        string fileContent = await stream.ReadToEndAsync();
+        string[] lines = fileContent.Split('\n');
+        return lines;
+    }
 }

+ 2 - 2
src/PixiEditor/Models/IO/ClsFile/ClsFileParser.cs → src/PixiEditor/Models/IO/PaletteParsers/ClsFileParser.cs

@@ -2,7 +2,7 @@
 using CLSEncoderDecoder;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 
-namespace PixiEditor.Models.IO.ClsFile;
+namespace PixiEditor.Models.IO.PaletteParsers;
 
 internal class ClsFileParser : PaletteFileParser
 {
@@ -21,7 +21,7 @@ internal class ClsFileParser : PaletteFileParser
             }
             catch
             {
-                return new PaletteFileData("Corrupted", Array.Empty<Color>()) { IsCorrupted = true };
+                return PaletteFileData.Corrupted;
             }
 
             PaletteFileData data = new(

+ 91 - 0
src/PixiEditor/Models/IO/PaletteParsers/GimpGplParser.cs

@@ -0,0 +1,91 @@
+using System.IO;
+using System.Text;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class GimpGplParser : PaletteFileParser
+{
+    public override string FileName { get; } = "GIMP Palette";
+    public override string[] SupportedFileExtensions { get; } = { ".gpl" };
+
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        var lines = await ReadTextLines(path);
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        lines = lines.Where(x => !x.StartsWith("#") && !String.Equals(x.Trim(), "GIMP Palette", StringComparison.CurrentCultureIgnoreCase)).ToArray();
+
+        if(lines.Length == 0) return PaletteFileData.Corrupted;
+
+        List<Color> colors = new();
+        char[] separators = new[] { '\t', ' ' };
+        foreach (var colorLine in lines)
+        {
+            var colorParts = colorLine.Split(separators, StringSplitOptions.RemoveEmptyEntries);
+
+            if (colorParts.Length < 3)
+            {
+                continue;
+            }
+
+            if(colorParts.Length < 3) continue;
+
+            bool parsed = false;
+
+            parsed = byte.TryParse(colorParts[0], out byte r);
+            if(!parsed) continue;
+
+            parsed = byte.TryParse(colorParts[1], out byte g);
+            if(!parsed) continue;
+
+            parsed = byte.TryParse(colorParts[2], out byte b);
+            if(!parsed) continue;
+
+            var color = new Color(r, g, b, 255); // alpha is ignored in PixiEditor
+            if (colors.Contains(color)) continue;
+
+            colors.Add(color);
+        }
+
+        return new PaletteFileData(name, colors.ToArray());
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        StringBuilder sb = new();
+        string name = string.IsNullOrEmpty(data.Title) ? Path.GetFileNameWithoutExtension(path) : data.Title;
+        sb.AppendLine("GIMP Palette");
+        sb.AppendLine($"#Name: {name}");
+        sb.AppendLine($"#Colors {data.Colors.Length}");
+        sb.AppendLine("#Made with PixiEditor");
+        sb.AppendLine("#");
+        foreach (var color in data.Colors)
+        {
+            string hex = $"{color.R:X}{color.G:X}{color.B:X}";
+            sb.AppendLine($"{color.R}\t{color.G}\t{color.B}\t{hex}");
+        }
+
+        try
+        {
+            await File.WriteAllTextAsync(path, sb.ToString());
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+}

+ 66 - 0
src/PixiEditor/Models/IO/PaletteParsers/HexPaletteParser.cs

@@ -0,0 +1,66 @@
+using System.Globalization;
+using System.IO;
+using System.Text;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class HexPaletteParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Hex Palette";
+    public override string[] SupportedFileExtensions { get; } = { ".hex" };
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        var lines = await ReadTextLines(path);
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        List<Color> colors = new();
+        foreach (var colorLine in lines)
+        {
+            if (colorLine.Length < 6)
+                continue;
+
+            byte r = byte.Parse(colorLine.Substring(0, 2), NumberStyles.HexNumber);
+            byte g = byte.Parse(colorLine.Substring(2, 2), NumberStyles.HexNumber);
+            byte b = byte.Parse(colorLine.Substring(4, 2), NumberStyles.HexNumber);
+            var color = new Color(r, g, b, 255); // alpha is ignored in PixiEditor
+            if (colors.Contains(color)) continue;
+
+            colors.Add(color);
+        }
+
+        return new PaletteFileData(name, colors.ToArray());
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        StringBuilder sb = new();
+        foreach (var color in data.Colors)
+        {
+            string hex = $"{color.R:X}{color.G:X}{color.B:X}";
+            sb.AppendLine(hex);
+        }
+
+        try
+        {
+            await File.WriteAllTextAsync(path, sb.ToString());
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+}

+ 1 - 1
src/PixiEditor/Models/IO/JascPalFile/JascFileException.cs → src/PixiEditor/Models/IO/PaletteParsers/JascPalFile/JascFileException.cs

@@ -1,4 +1,4 @@
-namespace PixiEditor.Models.IO.JascPalFile;
+namespace PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 
 internal class JascFileException : Exception
 {

+ 4 - 7
src/PixiEditor/Models/IO/JascPalFile/JascFileParser.cs → src/PixiEditor/Models/IO/PaletteParsers/JascPalFile/JascFileParser.cs

@@ -1,7 +1,7 @@
 using System.IO;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 
-namespace PixiEditor.Models.IO.JascPalFile;
+namespace PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 
 /// <summary>
 ///     This class is responsible for parsing JASC-PAL files. Which holds the color palette data.
@@ -14,10 +14,7 @@ internal class JascFileParser : PaletteFileParser
 
     private static async Task<PaletteFileData> ParseFile(string path)
     {
-        using var stream = File.OpenText(path);
-
-        string fileContent = await stream.ReadToEndAsync();
-        string[] lines = fileContent.Split('\n');
+        string[] lines = await ReadTextLines(path);
         string name = Path.GetFileNameWithoutExtension(path);
         string fileType = lines[0];
         string magicBytes = lines[1];
@@ -60,11 +57,11 @@ internal class JascFileParser : PaletteFileParser
         }
         catch
         {
-            return new PaletteFileData("Corrupted", Array.Empty<Color>()) { IsCorrupted = true };
+            return PaletteFileData.Corrupted;
         }
     }
 
-    public override async Task Save(string path, PaletteFileData data) => await SaveFile(path, data);
+    public override async Task<bool> Save(string path, PaletteFileData data) => await SaveFile(path, data);
 
     private static bool ValidateFile(string fileType, string magicBytes)
     {

+ 76 - 0
src/PixiEditor/Models/IO/PaletteParsers/PaintNetTxtParser.cs

@@ -0,0 +1,76 @@
+using System.Globalization;
+using System.IO;
+using System.Text;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Helpers;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+// https://www.getpaint.net/doc/latest/WorkingWithPalettes.html
+
+internal class PaintNetTxtParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Paint.NET Palette";
+    public override string[] SupportedFileExtensions { get; } = new string[] { ".txt" };
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private static async Task<PaletteFileData> ParseFile(string path)
+    {
+        var lines = await ReadTextLines(path);
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        lines = lines.Where(x => !x.StartsWith(";")).ToArray();
+
+        List<Color> colors = new();
+        for (int i = 0; i < lines.Length; i++)
+        {
+            // Color format aarrggbb
+            string colorLine = lines[i];
+            if(colorLine.Length < 8)
+                continue;
+
+            byte a = byte.Parse(colorLine.Substring(0, 2), NumberStyles.HexNumber);
+            byte r = byte.Parse(colorLine.Substring(2, 2), NumberStyles.HexNumber);
+            byte g = byte.Parse(colorLine.Substring(4, 2), NumberStyles.HexNumber);
+            byte b = byte.Parse(colorLine.Substring(6, 2), NumberStyles.HexNumber);
+            var color = new Color(r, g, b, 255); // alpha is ignored in PixiEditor
+            if(colors.Contains(color)) continue;
+
+            colors.Add(color);
+        }
+
+        return new PaletteFileData(name, colors.ToArray());
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.AppendLine("; Paint.NET Palette File");
+        sb.AppendLine($"; Made using PixiEditor {VersionHelpers.GetCurrentAssemblyVersion().ToString()}");
+        sb.AppendLine($"; {data.Colors.Length} colors");
+        foreach (Color color in data.Colors)
+        {
+            sb.AppendLine(color.ToHex());
+        }
+
+        try
+        {
+            await File.WriteAllTextAsync(path, sb.ToString());
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+}

+ 39 - 0
src/PixiEditor/Models/IO/PaletteParsers/PixiPaletteParser.cs

@@ -0,0 +1,39 @@
+using System.IO;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
+using PixiEditor.Parser;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class PixiPaletteParser : PaletteFileParser
+{
+    public override string FileName { get; } = "Palette from PixiEditor .pixi";
+    public override string[] SupportedFileExtensions { get; } = { ".pixi" };
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+        try
+        {
+            return await ParseFile(path);
+        }
+        catch
+        {
+            return PaletteFileData.Corrupted;
+        }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        var file = await PixiParser.DeserializeAsync(path);
+        if(file.Palette == null) return PaletteFileData.Corrupted;
+
+        string name = Path.GetFileNameWithoutExtension(path);
+
+        return new PaletteFileData(name, file.Palette.Select(x => new Color(x.R, x.G, x.B, x.A)).ToArray());
+    }
+
+    public override bool CanSave => false;
+
+    public override Task<bool> Save(string path, PaletteFileData data)
+    {
+        throw new SavingNotSupportedException("Saving palette as .pixi directly is not supported.");
+    }
+}

+ 123 - 0
src/PixiEditor/Models/IO/PaletteParsers/PngPaletteParser.cs

@@ -0,0 +1,123 @@
+using System.IO;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
+
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+internal class PngPaletteParser : PaletteFileParser
+{
+    public override string FileName { get; } = "PNG Palette";
+    public override string[] SupportedFileExtensions { get; } = { ".png" };
+
+    public override async Task<PaletteFileData> Parse(string path)
+    {
+           try
+           {
+               return await ParseFile(path);
+           }
+           catch
+           {
+               return PaletteFileData.Corrupted;
+           }
+    }
+
+    private async Task<PaletteFileData> ParseFile(string path)
+    {
+        return await Task.Run(() =>
+        {
+            PngBitmapDecoder decoder = new PngBitmapDecoder(new Uri(path), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default);
+
+            BitmapFrame frame = decoder.Frames[0];
+
+            Color[] colors = ExtractFromBitmap(frame);
+
+            PaletteFileData data = new(
+                Path.GetFileNameWithoutExtension(path), colors);
+
+            return data;
+        });
+    }
+
+    private Color[] ExtractFromBitmap(BitmapFrame frame)
+    {
+        if (frame.Palette is not null && frame.Palette.Colors.Count > 0)
+        {
+            return ExtractFromBitmapPalette(frame.Palette);
+        }
+
+        return ExtractFromBitmapSource(frame);
+    }
+
+    private Color[] ExtractFromBitmapSource(BitmapFrame frame)
+    {
+        if (frame.PixelWidth == 0 || frame.PixelHeight == 0)
+        {
+            return Array.Empty<Color>();
+        }
+
+        List<Color> colors = new();
+
+        byte[] pixels = new byte[frame.PixelWidth * frame.PixelHeight * 4];
+        frame.CopyPixels(pixels, frame.PixelWidth * 4, 0);
+        int pixelCount = pixels.Length / 4;
+        for (int i = 0; i < pixelCount; i++)
+        {
+            var color = GetColorFromBytes(pixels, i);
+            if (!colors.Contains(color))
+            {
+                colors.Add(color);
+            }
+        }
+
+        return colors.ToArray();
+    }
+
+    private Color GetColorFromBytes(byte[] pixels, int i)
+    {
+        return new Color(pixels[i * 4 + 2], pixels[i * 4 + 1], pixels[i * 4]);
+    }
+
+    private Color[] ExtractFromBitmapPalette(BitmapPalette palette)
+    {
+        if (palette.Colors == null || palette.Colors.Count == 0)
+        {
+            return Array.Empty<Color>();
+        }
+
+        return palette.Colors.Select(color => color.ToColor()).ToArray();
+    }
+
+    public override async Task<bool> Save(string path, PaletteFileData data)
+    {
+        try
+        {
+            await SaveFile(path, data);
+            return true;
+        }
+        catch
+        {
+            return false;
+        }
+    }
+
+    private async Task SaveFile(string path, PaletteFileData data)
+    {
+        await Task.Run(() =>
+        {
+            WriteableBitmap bitmap = new(data.Colors.Length, 1, 96, 96, PixelFormats.Bgra32, null);
+            bitmap.Lock();
+            for (int i = 0; i < data.Colors.Length; i++)
+            {
+                Color color = data.Colors[i];
+                bitmap.SetPixel(i, 0, color.ToOpaqueMediaColor());
+            }
+
+            bitmap.Unlock();
+            PngBitmapEncoder encoder = new();
+            encoder.Frames.Add(BitmapFrame.Create(bitmap));
+            using FileStream stream = new(path, FileMode.Create);
+            encoder.Save(stream);
+        });
+    }
+}

+ 8 - 0
src/PixiEditor/Models/IO/PaletteParsers/SavingNotSupportedException.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Models.IO.PaletteParsers;
+
+public class SavingNotSupportedException : Exception
+{
+    public SavingNotSupportedException(string message) : base(message)
+    {
+    }
+}

+ 6 - 6
src/PixiEditor/PixiEditor.csproj

@@ -227,14 +227,14 @@
 	</ItemGroup>
 	<ItemGroup>
 		<PackageReference Include="CLSEncoderDecoder" Version="1.0.0" />
-		<PackageReference Include="Dirkster.AvalonDock" Version="4.70.3" />
+		<PackageReference Include="Dirkster.AvalonDock" Version="4.72.0" />
 		<PackageReference Include="ByteSize" Version="2.1.1" />
-		<PackageReference Include="DiscordRichPresence" Version="1.1.1.14" />
-		<PackageReference Include="Hardware.Info" Version="10.1.0" />
-		<PackageReference Include="MessagePack" Version="2.4.35" />
+		<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
+		<PackageReference Include="Hardware.Info" Version="11.0.0" />
+		<PackageReference Include="MessagePack" Version="2.5.108" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
-		<PackageReference Include="Newtonsoft.Json" Version="13.0.2-beta2" />
-		<PackageReference Include="OneOf" Version="3.0.223" />
+		<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+		<PackageReference Include="OneOf" Version="3.0.243" />
 		<PackageReference Include="PixiEditor.ColorPicker" Version="3.3.1" />
 		<PackageReference Include="PixiEditor.Parser" Version="3.3.0" />
 		<PackageReference Include="PixiEditor.Parser.Skia" Version="3.0.0" />

+ 8 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/ColorsViewModel.cs

@@ -4,7 +4,7 @@ using System.Windows.Media;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers;
 using PixiEditor.Localization;
-using PixiEditor.Models.Commands.Attributes.Commands;
+using PixiEditor.Models.Commands.XAML;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders.Palettes;
@@ -16,6 +16,7 @@ using PixiEditor.Models.IO;
 using PixiEditor.Views.Dialogs;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;
+using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;
 
@@ -319,6 +320,12 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
         PrimaryColor = color;
     }
 
+    [Command.Internal("PixiEditor.CloseContextMenu")]
+    public void CloseContextMenu(System.Windows.Controls.ContextMenu menu)
+    {
+        menu.IsOpen = false;
+    }
+
     public void SetupPaletteParsers(IServiceProvider services)
     {
         PaletteParsers = new WpfObservableRangeCollection<PaletteFileParser>(services.GetServices<PaletteFileParser>());

+ 6 - 0
src/PixiEditor/Views/MainWindow.xaml

@@ -665,6 +665,12 @@
                                                                                                     Command="{cmds:Command PixiEditor.Colors.SelectColor, UseProvided=True}"
                                                                                                     CommandParameter="{Binding}" />
                                                                                             </b:EventTrigger>
+                                                                                            <b:EventTrigger EventName="MouseLeftButtonUp">
+                                                                                                <b:InvokeCommandAction
+                                                                                                    Command="{cmds:Command PixiEditor.CloseContextMenu, UseProvided=True}"
+                                                                                                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
+                                                                                                     AncestorType={x:Type ContextMenu}}}" />
+                                                                                            </b:EventTrigger>
                                                                                         </b:Interaction.Triggers>
                                                                                     </palettes:PaletteColor>
                                                                                 </DataTemplate>

+ 11 - 3
src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml

@@ -28,7 +28,7 @@
                                             Swatches="{Binding ElementName=paletteControl, Path=Swatches}"
                                             HintColor="{Binding ElementName=paletteControl, Path=HintColor}"
                                             Colors="{Binding ElementName=paletteControl, Path=Colors}"/>
-                    <StackPanel Margin="0, 0, 5, 0" HorizontalAlignment="Right" Width="85" VerticalAlignment="Center" Orientation="Horizontal">
+                    <StackPanel Margin="0, 0, 5, 0" HorizontalAlignment="Right" Width="110" VerticalAlignment="Center" Orientation="Horizontal">
                         <Button Margin="0, 0, 5, 0" Style="{StaticResource ToolButtonStyle}" Click="BrowsePalettes_Click"
                 Cursor="Hand" Height="24" Width="24" views:Translator.TooltipKey="BROWSE_PALETTES">
                             <Button.Background>
@@ -41,12 +41,20 @@
                                 <ImageBrush ImageSource="/Images/Folder.png"/>
                             </Button.Background>
                         </Button>
-                        <Button Height="24" Width="24" Margin="0" Style="{StaticResource ToolButtonStyle}" 
-                Cursor="Hand" views:Translator.TooltipKey="SAVE_PALETTE" Click="SavePalette_OnClick">
+                        <Button Height="24" Width="24" Margin="0, 0, 2.5, 0" Style="{StaticResource ToolButtonStyle}"
+                                IsEnabled="{Binding ElementName=paletteControl, Path=Colors.Count}"
+                                Cursor="Hand" views:Translator.TooltipKey="SAVE_PALETTE" Click="SavePalette_OnClick">
                             <Button.Background>
                                 <ImageBrush ImageSource="/Images/Save.png"/>
                             </Button.Background>
                         </Button>
+                        <Button Height="24" Width="24" Margin="0, 0, 5, 0" Style="{StaticResource ToolButtonStyle}"
+                                IsEnabled="{Binding ElementName=paletteControl, Path=Colors.Count}"
+                                Cursor="Hand" views:Translator.TooltipKey="DISCARD_PALETTE" Click="DiscardPalette_OnClick">
+                            <Button.Background>
+                                <ImageBrush ImageSource="/Images/Trash.png"/>
+                            </Button.Background>
+                        </Button>
                     </StackPanel>
                 </DockPanel>
             </StackPanel>

+ 22 - 2
src/PixiEditor/Views/UserControls/Palettes/PaletteViewer.xaml.cs

@@ -7,6 +7,8 @@ using Microsoft.Win32;
 using PixiEditor.Helpers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataProviders;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.IO;
 using PixiEditor.Views.Dialogs;
 using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
@@ -131,14 +133,23 @@ internal partial class PaletteViewer : UserControl
     {
         SaveFileDialog saveFileDialog = new SaveFileDialog
         {
-            Filter = PaletteHelpers.GetFilter(FileParsers, false)
+            Filter = PaletteHelpers.GetFilter(FileParsers.Where(x => x.CanSave).ToList(), false)
         };
 
         if (saveFileDialog.ShowDialog() == true)
         {
             string fileName = saveFileDialog.FileName;
             var foundParser = FileParsers.First(x => x.SupportedFileExtensions.Contains(Path.GetExtension(fileName)));
-            await foundParser.Save(fileName, new PaletteFileData(Colors.ToArray()));
+            if (Colors == null || Colors.Count == 0)
+            {
+                NoticeDialog.Show("NO_COLORS_TO_SAVE", "ERROR");
+                return;
+            }
+            bool saved = await foundParser.Save(fileName, new PaletteFileData(Colors.ToArray()));
+            if (!saved)
+            {
+                NoticeDialog.Show("COULD_NOT_SAVE_PALETTE", "ERROR");
+            }
         }
     }
 
@@ -180,6 +191,7 @@ internal partial class PaletteViewer : UserControl
             return;
         }
 
+        e.Handled = true;
         await ImportPalette(filePath);
         dragDropGrid.Visibility = Visibility.Hidden;
     }
@@ -244,4 +256,12 @@ internal partial class PaletteViewer : UserControl
             SelectColorCommand.Execute(origin.CommandParameter);
         }
     }
+
+    private void DiscardPalette_OnClick(object sender, RoutedEventArgs e)
+    {
+        if(ConfirmationDialog.Show("DISCARD_PALETTE_CONFIRMATION", "DISCARD_PALETTE") == ConfirmationType.Yes)
+        {
+            Colors.Clear();
+        }
+    }
 }

+ 1 - 0
src/PixiEditorGen/PixiEditorGen.csproj

@@ -8,6 +8,7 @@
     <LangVersion>latest</LangVersion>
     <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

Some files were not shown because too many files changed in this diff