diff --git a/ImageMagitek.Benchmarks/Snes3bppDecodeToImage.cs b/ImageMagitek.Benchmarks/Snes3bppDecodeToImage.cs index 5e5deb8c..ff441f7b 100644 --- a/ImageMagitek.Benchmarks/Snes3bppDecodeToImage.cs +++ b/ImageMagitek.Benchmarks/Snes3bppDecodeToImage.cs @@ -105,7 +105,7 @@ public void DecodeNative() var outputFileName = Path.Combine(outputDirectory, $"Native.{i}.bmp"); var image = new IndexedImage(arranger); - image.ExportImage(outputFileName, new ImageFileAdapter()); + image.ExportImage(outputFileName, new ImageSharpFileAdapter()); } } @@ -117,7 +117,7 @@ public void DecodeGeneric() var outputFileName = Path.Combine(outputDirectory, $"Generic.{i}.bmp"); var image = new IndexedImage(arranger); - image.ExportImage(outputFileName, new ImageFileAdapter()); + image.ExportImage(outputFileName, new ImageSharpFileAdapter()); } } } diff --git a/ImageMagitek.PluginSamples/ImageMagitek.PluginSamples.csproj b/ImageMagitek.PluginSamples/ImageMagitek.PluginSamples.csproj new file mode 100644 index 00000000..f6110fd3 --- /dev/null +++ b/ImageMagitek.PluginSamples/ImageMagitek.PluginSamples.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/ImageMagitek.PluginSamples/Snes4bppCodec.cs b/ImageMagitek.PluginSamples/Snes4bppCodec.cs new file mode 100644 index 00000000..554901eb --- /dev/null +++ b/ImageMagitek.PluginSamples/Snes4bppCodec.cs @@ -0,0 +1,135 @@ +using System; +using ImageMagitek.Codec; + +namespace ImageMagitek.PluginSample +{ + public class Snes4bppCodec : IndexedCodec + { + public override string Name => "SNES 4bpp Plugin"; + public override int Width { get; } + public override int Height { get; } + public override int StorageSize => 4 * Width * Height; + public override ImageLayout Layout => ImageLayout.Tiled; + public override int ColorDepth => 4; + + public override int DefaultWidth => 8; + public override int DefaultHeight => 8; + public override int WidthResizeIncrement => 1; + public override int HeightResizeIncrement => 1; + public override bool CanResize => true; + + private BitStream _bitStream; + + public Snes4bppCodec() + { + Width = DefaultWidth; + Height = DefaultHeight; + Initialize(); + } + + public Snes4bppCodec(int width, int height) + { + Width = width; + Height = height; + Initialize(); + } + + private void Initialize() + { + _foreignBuffer = new byte[(StorageSize + 7) / 8]; + _nativeBuffer = new byte[Width, Height]; + _bitStream = BitStream.OpenRead(_foreignBuffer, StorageSize); + } + + public override byte[,] DecodeElement(in ArrangerElement el, ReadOnlySpan encodedBuffer) + { + if (encodedBuffer.Length * 8 < StorageSize) // Decoding would require data past the end of the buffer + throw new ArgumentException(nameof(encodedBuffer)); + + encodedBuffer.Slice(0, _foreignBuffer.Length).CopyTo(_foreignBuffer); + + _bitStream = BitStream.OpenRead(_foreignBuffer, StorageSize); + + var offsetPlane1 = 0; + var offsetPlane2 = Width; + var offsetPlane3 = Width * Height * 2; + var offsetPlane4 = Width * Height * 2 + Width; + + for (int y = 0; y < Height; y++) + { + for (int x = 0; x < Width; x++) + { + _bitStream.SeekAbsolute(offsetPlane1); + var bp1 = _bitStream.ReadBit(); + _bitStream.SeekAbsolute(offsetPlane2); + var bp2 = _bitStream.ReadBit(); + _bitStream.SeekAbsolute(offsetPlane3); + var bp3 = _bitStream.ReadBit(); + _bitStream.SeekAbsolute(offsetPlane4); + var bp4 = _bitStream.ReadBit(); + + var palIndex = (bp1 << 0) | (bp2 << 1) | (bp3 << 2) | (bp4 << 3); + _nativeBuffer[x, y] = (byte)palIndex; + + offsetPlane1++; + offsetPlane2++; + offsetPlane3++; + offsetPlane4++; + } + + offsetPlane1 += Width; + offsetPlane2 += Width; + offsetPlane3 += Width; + offsetPlane4 += Width; + } + + return _nativeBuffer; + } + + public override ReadOnlySpan EncodeElement(in ArrangerElement el, byte[,] imageBuffer) + { + if (imageBuffer.GetLength(0) != Width || imageBuffer.GetLength(1) != Height) + throw new ArgumentException(nameof(imageBuffer)); + + var bs = BitStream.OpenWrite(StorageSize, 8); + + var offsetPlane1 = 0; + var offsetPlane2 = Width; + var offsetPlane3 = Width * Height * 2; + var offsetPlane4 = Width * Height * 2 + Width; + + for (int y = 0; y < Height; y++) + { + for (int x = 0; x < Width; x++) + { + var index = imageBuffer[x, y]; + + byte bp1 = (byte)(index & 1); + byte bp2 = (byte)((index >> 1) & 1); + byte bp3 = (byte)((index >> 2) & 1); + byte bp4 = (byte)((index >> 3) & 1); + + bs.SeekAbsolute(offsetPlane1); + bs.WriteBit(bp1); + bs.SeekAbsolute(offsetPlane2); + bs.WriteBit(bp2); + bs.SeekAbsolute(offsetPlane3); + bs.WriteBit(bp3); + bs.SeekAbsolute(offsetPlane4); + bs.WriteBit(bp4); + + offsetPlane1++; + offsetPlane2++; + offsetPlane3++; + offsetPlane3++; + } + offsetPlane1 += Width; + offsetPlane2 += Width; + offsetPlane3 += Width; + offsetPlane4 += Width; + } + + return bs.Data; + } + } +} diff --git a/ImageMagitek.Services/CodecService.cs b/ImageMagitek.Services/CodecService.cs index 1496ea20..a69d256b 100644 --- a/ImageMagitek.Services/CodecService.cs +++ b/ImageMagitek.Services/CodecService.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using ImageMagitek.Codec; using ImageMagitek.Colors; +using McMaster.NETCore.Plugins; namespace ImageMagitek.Services { @@ -12,6 +14,7 @@ public interface ICodecService IEnumerable GetSupportedCodecNames(); MagitekResults LoadXmlCodecs(string codecsPath); + void AddOrUpdateCodec(Type codecType); } public class CodecService : ICodecService @@ -19,17 +22,15 @@ public class CodecService : ICodecService public ICodecFactory CodecFactory { get; private set; } private readonly string _schemaFileName; - private readonly Palette _defaultPalette; - public CodecService(string schemaFileName, Palette defaultPalette) + public CodecService(string schemaFileName) { _schemaFileName = schemaFileName; - _defaultPalette = defaultPalette; } public MagitekResults LoadXmlCodecs(string codecsPath) { - var formats = new Dictionary(); + var formats = new Dictionary(); var serializer = new XmlGraphicsFormatReader(_schemaFileName); var errors = new List(); @@ -43,11 +44,12 @@ public MagitekResults LoadXmlCodecs(string codecsPath) }, fail => { + errors.Add($"Failed to load XML codec '{formatFileName}'"); errors.AddRange(fail.Reasons); }); } - CodecFactory = new CodecFactory(formats, _defaultPalette); + CodecFactory = new CodecFactory(formats); if (errors.Any()) return new MagitekResults.Failed(errors); @@ -55,6 +57,9 @@ public MagitekResults LoadXmlCodecs(string codecsPath) return MagitekResults.SuccessResults; } + public void AddOrUpdateCodec(Type codecType) => + CodecFactory.AddOrUpdateCodec(codecType); + public IEnumerable GetSupportedCodecNames() => CodecFactory?.GetSupportedCodecNames(); } } diff --git a/ImageMagitek.Services/ImageMagitek.Services.csproj b/ImageMagitek.Services/ImageMagitek.Services.csproj index 66992322..a57cadd8 100644 --- a/ImageMagitek.Services/ImageMagitek.Services.csproj +++ b/ImageMagitek.Services/ImageMagitek.Services.csproj @@ -1,9 +1,13 @@ - netstandard2.1 + netcoreapp3.1 + + + + diff --git a/ImageMagitek.Services/PaletteService.cs b/ImageMagitek.Services/PaletteService.cs index 73fd1caa..338ea91e 100644 --- a/ImageMagitek.Services/PaletteService.cs +++ b/ImageMagitek.Services/PaletteService.cs @@ -1,60 +1,46 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using ImageMagitek.Colors; namespace ImageMagitek.Services { public interface IPaletteService { + IColorFactory ColorFactory { get; } + Palette DefaultPalette { get; } List GlobalPalettes { get; } Palette NesPalette { get; } - void LoadGlobalPalette(string paletteFileName); + Palette ReadJsonPalette(string paletteFileName); void SetDefaultPalette(Palette pal); - void LoadNesPalette(string nesPaletteFileName); } public class PaletteService : IPaletteService { + public IColorFactory ColorFactory { get; private set; } + public Palette DefaultPalette { get; private set; } public List GlobalPalettes { get; } = new List(); public Palette NesPalette { get; private set; } - /// - /// Loads a palette from a JSON file and adds it to GlobalPalettes - /// - /// - public void LoadGlobalPalette(string paletteFileName) + public PaletteService(IColorFactory colorFactory) { - if (!File.Exists(paletteFileName)) - throw new FileNotFoundException($"{nameof(LoadGlobalPalette)}: Could not locate file {paletteFileName}"); - - string json = File.ReadAllText(paletteFileName); - var pal = PaletteJsonSerializer.ReadPalette(json); - GlobalPalettes.Add(pal); - - if (GlobalPalettes.Count == 1) - DefaultPalette = GlobalPalettes.First(); + ColorFactory = colorFactory; } /// - /// Loads a palette from a JSON file and sets it as the NesPalette + /// Read a palette from a JSON file /// - /// - public void LoadNesPalette(string nesPaletteFileName) + /// Path to the JSON palette file + public Palette ReadJsonPalette(string paletteFileName) { - if (!File.Exists(nesPaletteFileName)) - throw new FileNotFoundException($"{nameof(LoadNesPalette)}: Could not locate file {nesPaletteFileName}"); - - NesPalette = LoadJsonPalette(nesPaletteFileName); - } + if (!File.Exists(paletteFileName)) + throw new FileNotFoundException($"{nameof(ReadJsonPalette)}: Could not locate file {paletteFileName}"); - private Palette LoadJsonPalette(string paletteFileName) - { string json = File.ReadAllText(paletteFileName); - return PaletteJsonSerializer.ReadPalette(json); + var pal = PaletteJsonSerializer.ReadPalette(json, ColorFactory); + return pal; } /// @@ -68,5 +54,14 @@ public void SetDefaultPalette(Palette pal) DefaultPalette = pal; } + + /// + /// Sets the NES Palette + /// + /// + public void SetNesPalette(Palette nesPalette) + { + NesPalette = nesPalette; + } } } diff --git a/ImageMagitek.Services/PluginService.cs b/ImageMagitek.Services/PluginService.cs new file mode 100644 index 00000000..c88a497b --- /dev/null +++ b/ImageMagitek.Services/PluginService.cs @@ -0,0 +1,50 @@ +using ImageMagitek.Codec; +using McMaster.NETCore.Plugins; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace ImageMagitek.Services +{ + public interface IPluginService + { + public IDictionary CodecPlugins { get; } + + void LoadCodecPlugins(string pluginsPath); + } + + public class PluginService : IPluginService + { + public IDictionary CodecPlugins { get; } = new Dictionary(); + + public void LoadCodecPlugins(string pluginsPath) + { + var loaders = new List(); + + foreach (var dir in Directory.GetDirectories(pluginsPath)) + { + { + var dirName = Path.GetFileName(dir); + var pluginDll = Path.Combine(dir, dirName + ".dll"); + if (File.Exists(pluginDll)) + { + var codecLoader = PluginLoader.CreateFromAssemblyFile( + pluginDll, + sharedTypes: new[] { typeof(IGraphicsCodec) }); + + var pluginTypes = codecLoader.LoadDefaultAssembly() + .GetTypes() + .Where(t => typeof(IGraphicsCodec).IsAssignableFrom(t) && !t.IsAbstract); + + foreach (var codecType in pluginTypes) + { + var codec = (IGraphicsCodec)Activator.CreateInstance(codecType); + CodecPlugins.Add(codec.Name, codecType); + } + } + } + } + } + } +} diff --git a/ImageMagitek.Services/ProjectService.cs b/ImageMagitek.Services/ProjectService.cs index 669003a9..4aae5603 100644 --- a/ImageMagitek.Services/ProjectService.cs +++ b/ImageMagitek.Services/ProjectService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Xml; using System.Xml.Schema; +using ImageMagitek.Colors; using ImageMagitek.Project; using ImageMagitek.Project.Serialization; using Monaco.PathTree; @@ -41,15 +42,18 @@ public class ProjectService : IProjectService private XmlSchemaSet _schemas = new XmlSchemaSet(); private readonly ICodecService _codecService; + private readonly IColorFactory _colorFactory; - public ProjectService(ICodecService codecService) + public ProjectService(ICodecService codecService, IColorFactory colorFactory) { _codecService = codecService; + _colorFactory = colorFactory; } - public ProjectService(ICodecService codecService, IEnumerable globalResources) + public ProjectService(ICodecService codecService, IColorFactory colorFactory, IEnumerable globalResources) { _codecService = codecService; + _colorFactory = colorFactory; GlobalResources = globalResources.ToHashSet(); } @@ -101,7 +105,7 @@ public MagitekResults OpenProjectFile(string projectFileName) try { - var deserializer = new XmlGameDescriptorReader(_schemas, _codecService.CodecFactory, GlobalResources); + var deserializer = new XmlGameDescriptorReader(_schemas, _codecService.CodecFactory, _colorFactory, GlobalResources); var result = deserializer.ReadProject(projectFileName); return result.Match( diff --git a/ImageMagitek.UnitTests/BroadcastListTests.cs b/ImageMagitek.UnitTests/BroadcastListTests.cs deleted file mode 100644 index f3fc4895..00000000 --- a/ImageMagitek.UnitTests/BroadcastListTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ImageMagitek.Codec; -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Text; - -namespace ImageMagitek.UnitTests -{ - [TestFixture] - public class BroadcastListTests - { - [TestCase(new int[] { 5 }, new int[] { 5 })] - [TestCase(new int[] { 1, 2, 4, 5 }, new int[] { 1, 2, 4, 5 })] - public void Add_AsExpected(int[] items, int[] expected) - { - var list = new BroadcastList(); - - foreach (var item in items) - list.Add(item); - - CollectionAssert.AreEqual(expected, list); - } - - [TestCase(new int[] { 5 }, 0, 5)] - [TestCase(new int[] { 5 }, 3, 5)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 0, 1)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 2, 3)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 4, 5)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 5, 1)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 7, 3)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 9, 5)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, 102, 3)] - public void Index_PositiveIndex_ReturnsExpected(int [] items, int index, int expected) - { - var list = new BroadcastList(items); - var actual = list[index]; - - Assert.AreEqual(expected, actual); - } - - [TestCase(new int[] { 5 }, -1, 5)] - [TestCase(new int[] { 5 }, -3, 5)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, -1, 5)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, -5, 1)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, -8, 3)] - [TestCase(new int[] { 1, 2, 3, 4, 5 }, -14, 2)] - public void Index_NegativeIndex_ReturnsExpected(int[] items, int index, int expected) - { - var list = new BroadcastList(items); - var actual = list[index]; - - Assert.AreEqual(expected, actual); - } - } -} diff --git a/ImageMagitek.UnitTests/ForeignColorTests.cs b/ImageMagitek.UnitTests/ForeignColorTests.cs index 13c11ff7..5b0e6484 100644 --- a/ImageMagitek.UnitTests/ForeignColorTests.cs +++ b/ImageMagitek.UnitTests/ForeignColorTests.cs @@ -8,7 +8,8 @@ public class ForeignColorTests [TestCaseSource(typeof(ForeignColorTestCases), "ToNativeTestCases")] public void ToNative_AsExpected(IColor32 fc, ColorRgba32 expected) { - var actual = ColorConverter.ToNative(fc); + var colorFactory = new ColorFactory(); + var actual = colorFactory.ToNative(fc); Assert.Multiple(() => { diff --git a/ImageMagitek.UnitTests/ImageMagitek.UnitTests.csproj b/ImageMagitek.UnitTests/ImageMagitek.UnitTests.csproj index 08c5534d..c951bf28 100644 --- a/ImageMagitek.UnitTests/ImageMagitek.UnitTests.csproj +++ b/ImageMagitek.UnitTests/ImageMagitek.UnitTests.csproj @@ -6,6 +6,7 @@ + all diff --git a/ImageMagitek.UnitTests/NativeColorTestCases.cs b/ImageMagitek.UnitTests/NativeColorTestCases.cs index 122bc57c..6fc877e7 100644 --- a/ImageMagitek.UnitTests/NativeColorTestCases.cs +++ b/ImageMagitek.UnitTests/NativeColorTestCases.cs @@ -11,11 +11,11 @@ public static IEnumerable ToForeignTestCases get { // Native -> BGR15 - yield return new TestCaseData(new ColorRgba32(0, 0, 0, 0), new ColorBgr15(0, 0, 0), ColorModel.BGR15); - yield return new TestCaseData(new ColorRgba32(200, 128, 39, 0), new ColorBgr15(25, 16, 4), ColorModel.BGR15); - yield return new TestCaseData(new ColorRgba32(0, 255, 0, 50), new ColorBgr15(0, 31, 0), ColorModel.BGR15); - yield return new TestCaseData(new ColorRgba32(48, 248, 248, 0), new ColorBgr15(6, 31, 31), ColorModel.BGR15); - yield return new TestCaseData(new ColorRgba32(55, 255, 255, 0), new ColorBgr15(6, 31, 31), ColorModel.BGR15); + yield return new TestCaseData(new ColorRgba32(0, 0, 0, 0), new ColorBgr15(0, 0, 0), ColorModel.Bgr15); + yield return new TestCaseData(new ColorRgba32(200, 128, 39, 0), new ColorBgr15(25, 16, 4), ColorModel.Bgr15); + yield return new TestCaseData(new ColorRgba32(0, 255, 0, 50), new ColorBgr15(0, 31, 0), ColorModel.Bgr15); + yield return new TestCaseData(new ColorRgba32(48, 248, 248, 0), new ColorBgr15(6, 31, 31), ColorModel.Bgr15); + yield return new TestCaseData(new ColorRgba32(55, 255, 255, 0), new ColorBgr15(6, 31, 31), ColorModel.Bgr15); } } } diff --git a/ImageMagitek.UnitTests/NativeColorTests.cs b/ImageMagitek.UnitTests/NativeColorTests.cs index d8dd305d..ab86b8e4 100644 --- a/ImageMagitek.UnitTests/NativeColorTests.cs +++ b/ImageMagitek.UnitTests/NativeColorTests.cs @@ -9,16 +9,20 @@ public class NativeColorTests [TestCaseSource(typeof(NativeColorTestCases), "ToForeignTestCases")] public void ToForeignColor_Converts_Correctly(ColorRgba32 nc, ColorBgr15 expected, ColorModel colorModel) { - var actual = ColorConverter.ToForeign(nc, colorModel); + var colorFactory = new ColorFactory(); + var actual = colorFactory.ToForeign(nc, colorModel); - Assert.Multiple(() => + if (actual is IColor32 actual32) { - Assert.AreEqual(expected.Color, actual.Color, ".Color components not equal"); - Assert.AreEqual(expected.R, actual.R, "Red components not equal"); - Assert.AreEqual(expected.G, actual.G, "Green components not equal"); - Assert.AreEqual(expected.B, actual.B, "Blue components not equal"); - Assert.AreEqual(expected.A, actual.A, "Alpha components not equal"); - }); + Assert.Multiple(() => + { + Assert.AreEqual(expected.Color, actual.Color, ".Color components not equal"); + Assert.AreEqual(expected.R, actual32.R, "Red components not equal"); + Assert.AreEqual(expected.G, actual32.G, "Green components not equal"); + Assert.AreEqual(expected.B, actual32.B, "Blue components not equal"); + Assert.AreEqual(expected.A, actual32.A, "Alpha components not equal"); + }); + } } } } diff --git a/ImageMagitek.UnitTests/PatternListTestCases.cs b/ImageMagitek.UnitTests/PatternListTestCases.cs new file mode 100644 index 00000000..74993067 --- /dev/null +++ b/ImageMagitek.UnitTests/PatternListTestCases.cs @@ -0,0 +1,201 @@ +using System.Collections.Generic; +using System.Linq; +using MoreLinq; +using NUnit.Framework; +using PC = ImageMagitek.Codec.PlaneCoordinate; + +namespace ImageMagitek.Codec.UnitTests +{ + public class PatternListTestCases + { + public static IEnumerable TryCreateRemapPlanarPatternTestCases + { + get + { + yield return new TestCaseData + ( + new[] { "AAAAAAAACCCCCCCCBBBBBBBB" }, 24, 4, 1, 24, + new[] + { + new PC(0, 0, 0), new PC(1, 0, 0), new PC(2, 0, 0), new PC(3, 0, 0), new PC(4, 0, 0), new PC(5, 0, 0), new PC(6, 0, 0), new PC(7, 0, 0), new PC(16, 0, 0), new PC(17, 0, 0), new PC(18, 0, 0), new PC(19, 0, 0), new PC(20, 0, 0), new PC(21, 0, 0), new PC(22, 0, 0), new PC(23, 0, 0), new PC(8, 0, 0), new PC(9, 0, 0), new PC(10, 0, 0), new PC(11, 0, 0), new PC(12, 0, 0), new PC(13, 0, 0), new PC(14, 0, 0), new PC(15, 0, 0), + new PC(0, 1, 0), new PC(1, 1, 0), new PC(2, 1, 0), new PC(3, 1, 0), new PC(4, 1, 0), new PC(5, 1, 0), new PC(6, 1, 0), new PC(7, 1, 0), new PC(16, 1, 0), new PC(17, 1, 0), new PC(18, 1, 0), new PC(19, 1, 0), new PC(20, 1, 0), new PC(21, 1, 0), new PC(22, 1, 0), new PC(23, 1, 0), new PC(8, 1, 0), new PC(9, 1, 0), new PC(10, 1, 0), new PC(11, 1, 0), new PC(12, 1, 0), new PC(13, 1, 0), new PC(14, 1, 0), new PC(15, 1, 0), + new PC(0, 2, 0), new PC(1, 2, 0), new PC(2, 2, 0), new PC(3, 2, 0), new PC(4, 2, 0), new PC(5, 2, 0), new PC(6, 2, 0), new PC(7, 2, 0), new PC(16, 2, 0), new PC(17, 2, 0), new PC(18, 2, 0), new PC(19, 2, 0), new PC(20, 2, 0), new PC(21, 2, 0), new PC(22, 2, 0), new PC(23, 2, 0), new PC(8, 2, 0), new PC(9, 2, 0), new PC(10, 2, 0), new PC(11, 2, 0), new PC(12, 2, 0), new PC(13, 2, 0), new PC(14, 2, 0), new PC(15, 2, 0), + new PC(0, 3, 0), new PC(1, 3, 0), new PC(2, 3, 0), new PC(3, 3, 0), new PC(4, 3, 0), new PC(5, 3, 0), new PC(6, 3, 0), new PC(7, 3, 0), new PC(16, 3, 0), new PC(17, 3, 0), new PC(18, 3, 0), new PC(19, 3, 0), new PC(20, 3, 0), new PC(21, 3, 0), new PC(22, 3, 0), new PC(23, 3, 0), new PC(8, 3, 0), new PC(9, 3, 0), new PC(10, 3, 0), new PC(11, 3, 0), new PC(12, 3, 0), new PC(13, 3, 0), new PC(14, 3, 0), new PC(15, 3, 0), + }, + new[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23, 8, 9, 10, 11, 12, 13, 14, 15, + 24, 25, 26, 27, 28, 29, 30, 31, 40, 41, 42, 43, 44, 45, 46, 47, 32, 33, 34, 35, 36, 37, 38, 39, + 48, 49, 50, 51, 52, 53, 54, 55, 64, 65, 66, 67, 68, 69, 70, 71, 56, 57, 58, 59, 60, 61, 62, 63, + 72, 73, 74, 75, 76, 77, 78, 79, 88, 89, 90, 91, 92, 93, 94, 95, 80, 81, 82, 83, 84, 85, 86, 87 + } + ); + + yield return new TestCaseData + ( + new[] + { + "AAAABBBBDDDDCCCC", + "CCCCDDDDAAAABBBB" + }, 16, 4, 2, 32, + new[] + { + // Map from bit index 0...127 + new PC(0, 0, 0), new PC(1, 0, 0), new PC(2, 0, 0), new PC(3, 0, 0), new PC(8, 0, 1), new PC(9, 0, 1), new PC(10, 0, 1), new PC(11, 0, 1), new PC(4, 0, 0), new PC(5, 0, 0), new PC(6, 0, 0), new PC(7, 0, 0), new PC(12, 0, 1), new PC(13, 0, 1), new PC(14, 0, 1), new PC(15, 0, 1), + new PC(12, 0, 0), new PC(13, 0, 0), new PC(14, 0, 0), new PC(15, 0, 0), new PC(0, 0, 1), new PC(1, 0, 1), new PC(2, 0, 1), new PC(3, 0, 1), new PC(8, 0, 0), new PC(9, 0, 0), new PC(10, 0, 0), new PC(11, 0, 0), new PC(4, 0, 1), new PC(5, 0, 1), new PC(6, 0, 1), new PC(7, 0, 1), + + new PC(0, 1, 0), new PC(1, 1, 0), new PC(2, 1, 0), new PC(3, 1, 0), new PC(8, 1, 1), new PC(9, 1, 1), new PC(10, 1, 1), new PC(11, 1, 1), new PC(4, 1, 0), new PC(5, 1, 0), new PC(6, 1, 0), new PC(7, 1, 0), new PC(12, 1, 1), new PC(13, 1, 1), new PC(14, 1, 1), new PC(15, 1, 1), + new PC(12, 1, 0), new PC(13, 1, 0), new PC(14, 1, 0), new PC(15, 1, 0), new PC(0, 1, 1), new PC(1, 1, 1), new PC(2, 1, 1), new PC(3, 1, 1), new PC(8, 1, 0), new PC(9, 1, 0), new PC(10, 1, 0), new PC(11, 1, 0), new PC(4, 1, 1), new PC(5, 1, 1), new PC(6, 1, 1), new PC(7, 1, 1), + + new PC(0, 2, 0), new PC(1, 2, 0), new PC(2, 2, 0), new PC(3, 2, 0), new PC(8, 2, 1), new PC(9, 2, 1), new PC(10, 2, 1), new PC(11, 2, 1), new PC(4, 2, 0), new PC(5, 2, 0), new PC(6, 2, 0), new PC(7, 2, 0), new PC(12, 2, 1), new PC(13, 2, 1), new PC(14, 2, 1), new PC(15, 2, 1), + new PC(12, 2, 0), new PC(13, 2, 0), new PC(14, 2, 0), new PC(15, 2, 0), new PC(0, 2, 1), new PC(1, 2, 1), new PC(2, 2, 1), new PC(3, 2, 1), new PC(8, 2, 0), new PC(9, 2, 0), new PC(10, 2, 0), new PC(11, 2, 0), new PC(4, 2, 1), new PC(5, 2, 1), new PC(6, 2, 1), new PC(7, 2, 1), + + new PC(0, 3, 0), new PC(1, 3, 0), new PC(2, 3, 0), new PC(3, 3, 0), new PC(8, 3, 1), new PC(9, 3, 1), new PC(10, 3, 1), new PC(11, 3, 1), new PC(4, 3, 0), new PC(5, 3, 0), new PC(6, 3, 0), new PC(7, 3, 0), new PC(12, 3, 1), new PC(13, 3, 1), new PC(14, 3, 1), new PC(15, 3, 1), + new PC(12, 3, 0), new PC(13, 3, 0), new PC(14, 3, 0), new PC(15, 3, 0), new PC(0, 3, 1), new PC(1, 3, 1), new PC(2, 3, 1), new PC(3, 3, 1), new PC(8, 3, 0), new PC(9, 3, 0), new PC(10, 3, 0), new PC(11, 3, 0), new PC(4, 3, 1), new PC(5, 3, 1), new PC(6, 3, 1), new PC(7, 3, 1), + }, + new[] + { + // Map from PlaneCoordinate(0, 0, 0) to PlaneCoordinate(15, 3, 1) in x, then y, then plane incrementing order + // Plane 0 + 0, 1, 2, 3, 8, 9, 10, 11, 24, 25, 26, 27, 16, 17, 18, 19, + 32, 33, 34, 35, 40, 41, 42, 43, 56, 57, 58, 59, 48, 49, 50, 51, + 64, 65, 66, 67, 72, 73, 74, 75, 88, 89, 90, 91, 80, 81, 82, 83, + 96, 97, 98, 99, 104, 105, 106, 107, 120, 121, 122, 123, 112, 113, 114, 115, + + // Plane 1 + 20, 21, 22, 23, 28, 29, 30, 31, 4, 5, 6, 7, 12, 13, 14, 15, + 52, 53, 54, 55, 60, 61, 62, 63, 36, 37, 38, 39, 44, 45, 46, 47, + 84, 85, 86, 87, 92, 93, 94, 95, 68, 69, 70, 71, 76, 77, 78, 79, + 116, 117, 118, 119, 124, 125, 126, 127, 100, 101, 102, 103, 108, 109, 110, 111 + } + ); + + yield return new TestCaseData + ( + new[] + { + "AABBCCDD", + "AABBCCDD", + "AABBCCDD", + "AABBCCDD" + }, 8, 2, 4, 32, + new[] + { + new PC(0, 0, 0), new PC(1, 0, 0), new PC(0, 0, 1), new PC(1, 0, 1), new PC(0, 0, 2), new PC(1, 0, 2), new PC(0, 0, 3), new PC(1, 0, 3), new PC(2, 0, 0), new PC(3, 0, 0), new PC(2, 0, 1), new PC(3, 0, 1), new PC(2, 0, 2), new PC(3, 0, 2), new PC(2, 0, 3), new PC(3, 0, 3), + new PC(4, 0, 0), new PC(5, 0, 0), new PC(4, 0, 1), new PC(5, 0, 1), new PC(4, 0, 2), new PC(5, 0, 2), new PC(4, 0, 3), new PC(5, 0, 3), new PC(6, 0, 0), new PC(7, 0, 0), new PC(6, 0, 1), new PC(7, 0, 1), new PC(6, 0, 2), new PC(7, 0, 2), new PC(6, 0, 3), new PC(7, 0, 3), + + new PC(0, 1, 0), new PC(1, 1, 0), new PC(0, 1, 1), new PC(1, 1, 1), new PC(0, 1, 2), new PC(1, 1, 2), new PC(0, 1, 3), new PC(1, 1, 3), new PC(2, 1, 0), new PC(3, 1, 0), new PC(2, 1, 1), new PC(3, 1, 1), new PC(2, 1, 2), new PC(3, 1, 2), new PC(2, 1, 3), new PC(3, 1, 3), + new PC(4, 1, 0), new PC(5, 1, 0), new PC(4, 1, 1), new PC(5, 1, 1), new PC(4, 1, 2), new PC(5, 1, 2), new PC(4, 1, 3), new PC(5, 1, 3), new PC(6, 1, 0), new PC(7, 1, 0), new PC(6, 1, 1), new PC(7, 1, 1), new PC(6, 1, 2), new PC(7, 1, 2), new PC(6, 1, 3), new PC(7, 1, 3) + }, + new[] + { + // Map from PlaneCoordinate(0, 0, 0) to PlaneCoordinate(7, 1, 3) in x, then y, then plane incrementing order + // Plane 0 + 0, 1, 8, 9, 16, 17, 24, 25, + 32, 33, 40, 41, 48, 49, 56, 57, + + // Plane 1 + 2, 3, 10, 11, 18, 19, 26, 27, + 34, 35, 42, 43, 50, 51, 58, 59, + + // Plane 2 + 4, 5, 12, 13, 20, 21, 28, 29, + 36, 37, 44, 45, 52, 53, 60, 61, + + // Plane 3 + 6, 7, 14, 15, 22, 23, 30, 31, + 38, 39, 46, 47, 54, 55, 62, 63 + } + ); + } + } + + public static IEnumerable TryCreateRemapChunkyPatternTestCases + { + get + { + yield return new TestCaseData + ( + new[] + { + "AABBCCDD", + }, 8, 2, 4, 32, + new[] + { + new PC(0, 0, 0), new PC(0, 0, 1), new PC(0, 0, 2), new PC(0, 0, 3), new PC(1, 0, 0), new PC(1, 0, 1), new PC(1, 0, 2), new PC(1, 0, 3), new PC(2, 0, 0), new PC(2, 0, 1), new PC(2, 0, 2), new PC(2, 0, 3), new PC(3, 0, 0), new PC(3, 0, 1), new PC(3, 0, 2), new PC(3, 0, 3), new PC(4, 0, 0), new PC(4, 0, 1), new PC(4, 0, 2), new PC(4, 0, 3), new PC(5, 0, 0), new PC(5, 0, 1), new PC(5, 0, 2), new PC(5, 0, 3), new PC(6, 0, 0), new PC(6, 0, 1), new PC(6, 0, 2), new PC(6, 0, 3), new PC(7, 0, 0), new PC(7, 0, 1), new PC(7, 0, 2), new PC(7, 0, 3), + new PC(0, 1, 0), new PC(0, 1, 1), new PC(0, 1, 2), new PC(0, 1, 3), new PC(1, 1, 0), new PC(1, 1, 1), new PC(1, 1, 2), new PC(1, 1, 3), new PC(2, 1, 0), new PC(2, 1, 1), new PC(2, 1, 2), new PC(2, 1, 3), new PC(3, 1, 0), new PC(3, 1, 1), new PC(3, 1, 2), new PC(3, 1, 3), new PC(4, 1, 0), new PC(4, 1, 1), new PC(4, 1, 2), new PC(4, 1, 3), new PC(5, 1, 0), new PC(5, 1, 1), new PC(5, 1, 2), new PC(5, 1, 3), new PC(6, 1, 0), new PC(6, 1, 1), new PC(6, 1, 2), new PC(6, 1, 3), new PC(7, 1, 0), new PC(7, 1, 1), new PC(7, 1, 2), new PC(7, 1, 3), + + }, + new[] + { + // Map from PlaneCoordinate(0, 0, 0) to PlaneCoordinate(7, 1, 3) in plane, then x, then y incrementing order + // Row 1 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + //Row 2 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63 + } + ); + } + } + + public static IEnumerable TryCreateRemapPatternTooManyLettersTestCases + { + get + { + yield return new TestCaseData + ( + new[] { "AAAAAAAACCCCCCCCCBBBBBBB" }, 24, 8, 1, 24 + ); + } + } + + public static IEnumerable TryCreateRemapPatternOutOfRangeTestCases + { + get + { + yield return new TestCaseData + ( + new[] { "AAAAAAAACCCCCCCCCBBBBBBBB" }, 24, 8, 1, 23 + ); + + yield return new TestCaseData + ( + new[] { "AAAAAAAACCCCCCCCCBBBBBBBBD" }, 24, 8, 1, 23 + ); + } + } + + public static IEnumerable TryCreateRemapPatternInvalidSizeTestCases + { + get + { + yield return new TestCaseData + ( + new[] { "" }, 8, 8, 1, 0 + ); + + yield return new TestCaseData + ( + new[] { "AAAAAAAA" }, 8, 8, 1, -1 + ); + } + } + + public static IEnumerable TryCreateRemapPatternInvalidCharacterTestCases + { + get + { + yield return new TestCaseData + ( + new[] { "AAAAAAAACCCCCCCC00000000" }, 24, 8, 1, 24 + ); + + yield return new TestCaseData + ( + new[] { "11111111CCCCCCCC,,,,,,,," }, 24, 8, 1, 24 + ); + } + } + } +} diff --git a/ImageMagitek.UnitTests/PatternListTests.cs b/ImageMagitek.UnitTests/PatternListTests.cs new file mode 100644 index 00000000..0b33c553 --- /dev/null +++ b/ImageMagitek.UnitTests/PatternListTests.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using System.Linq; +using MoreLinq; +using NUnit.Framework; + +namespace ImageMagitek.Codec.UnitTests +{ + [TestFixture] + public class PatternListTests + { + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapPlanarPatternTestCases")] + public void TryCreateRemapPattern_DecodePlanar_AsExpected(IList patterns, int width, int height, + int planes, int size, IList expectedDecoded, IList expectedEncoded) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Planar, width, height, planes, size); + + result.Switch( + success => + { + var actual = Enumerable.Range(0, width * height * planes) + .Select(x => success.Result.GetDecodeIndex(x)) + .ToArray(); + + CollectionAssert.AreEqual(expectedDecoded, actual); + }, + failed => Assert.Fail(failed.Reason)); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapPlanarPatternTestCases")] + public void TryCreateRemapPattern_EncodePlanar_AsExpected(IList patterns, int width, int height, + int planes, int size, IList expectedDecoded, IList expectedEncoded) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Planar, width, height, planes, size); + + result.Switch( + success => + { + var actual = Enumerable.Range(0, planes) + .Cartesian( + Enumerable.Range(0, height), + Enumerable.Range(0, width), + (p, y, x) => new PlaneCoordinate((short)x, (short)y, (short)p)) + .Select(x => success.Result.GetEncodeIndex(x)) + .ToArray(); + + CollectionAssert.AreEqual(expectedEncoded, actual); + }, + failed => Assert.Fail(failed.Reason)); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapChunkyPatternTestCases")] + public void TryCreateRemapPattern_DecodeChunky_AsExpected(IList patterns, int width, int height, + int planes, int size, IList expectedDecoded, IList expectedEncoded) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Chunky, width, height, planes, size); + + result.Switch( + success => + { + var actual = Enumerable.Range(0, width * height * planes) + .Select(x => success.Result.GetDecodeIndex(x)) + .ToArray(); + + CollectionAssert.AreEqual(expectedDecoded, actual); + }, + failed => Assert.Fail(failed.Reason)); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapChunkyPatternTestCases")] + public void TryCreateRemapPattern_EncodeChunky_AsExpected(IList patterns, int width, int height, + int planes, int size, IList expectedDecoded, IList expectedEncoded) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Chunky, width, height, planes, size); + + result.Switch( + success => + { + var actual = Enumerable.Range(0, height) + .Cartesian( + Enumerable.Range(0, width), + Enumerable.Range(0, planes), + (y, x, p) => new PlaneCoordinate((short)x, (short)y, (short)p)) + .Select(x => success.Result.GetEncodeIndex(x)) + .ToArray(); + + CollectionAssert.AreEqual(expectedEncoded, actual); + }, + failed => Assert.Fail(failed.Reason)); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapPatternTooManyLettersTestCases")] + public void TryCreateRemapPattern_TooManyLetters_Fails(IList patterns, int width, int height, int planes, int size) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Planar, width, height, planes, size); + + result.Switch( + success => Assert.Fail("TryCreateRemapPattern should have failed, but succeeded"), + failed => { }); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapPatternOutOfRangeTestCases")] + public void TryCreateRemapPattern_OutOfRange_Fails(IList patterns, int width, int height, int planes, int size) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Planar, width, height, planes, size); + + result.Switch( + success => Assert.Fail("TryCreateRemapPattern should have failed, but succeeded"), + failed => { }); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapPatternInvalidSizeTestCases")] + public void TryCreateRemapPattern_InvalidSize_Fails(IList patterns, int width, int height, int planes, int size) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Planar, width, height, planes, size); + + result.Switch( + success => Assert.Fail("TryCreateRemapPattern should have failed, but succeeded"), + failed => { }); + } + + [TestCaseSource(typeof(PatternListTestCases), "TryCreateRemapPatternInvalidCharacterTestCases")] + public void TryCreateRemapPattern_InvalidCharacter_Fails(IList patterns, int width, int height, int planes, int size) + { + var result = PatternList.TryCreatePatternList(patterns, PixelPacking.Planar, width, height, planes, size); + + result.Switch( + success => Assert.Fail("TryCreateRemapPattern should have failed, but succeeded"), + failed => { }); + } + } +} diff --git a/ImageMagitek.sln b/ImageMagitek.sln index a706ef46..51516e23 100644 --- a/ImageMagitek.sln +++ b/ImageMagitek.sln @@ -15,7 +15,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TileShop.WPF", "TileShop.WP EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TileShop.Shared", "TileShop.Shared\TileShop.Shared.csproj", "{6B68BC85-0708-46DD-9197-2CA5273B306F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageMagitek.Services", "ImageMagitek.Services\ImageMagitek.Services.csproj", "{8932A93B-0074-4508-BFCB-39E9ED2E887C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageMagitek.Services", "ImageMagitek.Services\ImageMagitek.Services.csproj", "{8932A93B-0074-4508-BFCB-39E9ED2E887C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageMagitek.PluginSamples", "ImageMagitek.PluginSamples\ImageMagitek.PluginSamples.csproj", "{7872050E-73EF-4757-8918-B7971FE8E2F8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -47,6 +49,10 @@ Global {8932A93B-0074-4508-BFCB-39E9ED2E887C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8932A93B-0074-4508-BFCB-39E9ED2E887C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8932A93B-0074-4508-BFCB-39E9ED2E887C}.Release|Any CPU.Build.0 = Release|Any CPU + {7872050E-73EF-4757-8918-B7971FE8E2F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7872050E-73EF-4757-8918-B7971FE8E2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7872050E-73EF-4757-8918-B7971FE8E2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7872050E-73EF-4757-8918-B7971FE8E2F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ImageMagitek/Arranger/Arranger.cs b/ImageMagitek/Arranger/Arranger.cs index dbfbbc95..14202d62 100644 --- a/ImageMagitek/Arranger/Arranger.cs +++ b/ImageMagitek/Arranger/Arranger.cs @@ -14,7 +14,7 @@ namespace ImageMagitek /// ScatteredArrangers are capable of accessing many files, file offsets, palettes, and codecs in a single arranger /// MemoryArrangers are used as a scratchpad (currently unimplemented) /// - public enum ArrangerMode { Sequential = 0, Scattered, Memory }; + public enum ArrangerMode { Sequential = 1, Scattered, Memory }; /// /// Layout of graphics for the arranger @@ -22,19 +22,19 @@ public enum ArrangerMode { Sequential = 0, Scattered, Memory }; /// Tiled will snap selection rectangles to tile boundaries /// Single will snap selection rectangles to pixel boundaries /// - public enum ArrangerLayout { Tiled = 0, Single }; + public enum ArrangerLayout { Tiled = 1, Single }; /// /// Specifies how the pixels' colors are determined for the graphic /// Indexed graphics have their full color determined by a palette /// Direct graphics have their full color determined by the pixel image data alone /// - public enum PixelColorType { Indexed = 0, Direct } + public enum PixelColorType { Indexed = 1, Direct } /// /// Move operations for sequential arrangers /// - public enum ArrangerMoveType { ByteDown = 0, ByteUp, RowDown, RowUp, ColRight, ColLeft, PageDown, PageUp, Home, End, Absolute }; + public enum ArrangerMoveType { ByteDown = 1, ByteUp, RowDown, RowUp, ColRight, ColLeft, PageDown, PageUp, Home, End, Absolute }; /// /// Arranger base class for graphical screen elements diff --git a/ImageMagitek/Arranger/ArrangerExtensions.cs b/ImageMagitek/Arranger/ArrangerExtensions.cs index d8f5948a..446d1920 100644 --- a/ImageMagitek/Arranger/ArrangerExtensions.cs +++ b/ImageMagitek/Arranger/ArrangerExtensions.cs @@ -149,14 +149,14 @@ public static FileBitAddress Move(this SequentialArranger arranger, ArrangerMove if (arranger.Layout == ArrangerLayout.Tiled) delta = arranger.ActiveCodec.StorageSize; else if (arranger.Layout == ArrangerLayout.Single) - delta = 16 * arranger.ActiveCodec.StorageSize / ((arranger.ActiveCodec.RowStride + arranger.ActiveCodec.Width) * arranger.ActiveCodec.Height); + delta = 16 * arranger.ActiveCodec.StorageSize * arranger.ActiveCodec.Height / arranger.ActiveCodec.Width; newAddress += delta; break; case ArrangerMoveType.ColLeft: if (arranger.Layout == ArrangerLayout.Tiled) delta = arranger.ActiveCodec.StorageSize; else if (arranger.Layout == ArrangerLayout.Single) - delta = 16 * arranger.ActiveCodec.StorageSize / ((arranger.ActiveCodec.RowStride + arranger.ActiveCodec.Width) * arranger.ActiveCodec.Height); + delta = 16 * arranger.ActiveCodec.StorageSize * arranger.ActiveCodec.Height / arranger.ActiveCodec.Width; newAddress -= delta; break; case ArrangerMoveType.PageDown: diff --git a/ImageMagitek/Arranger/SequentialArranger.cs b/ImageMagitek/Arranger/SequentialArranger.cs index 31d47f45..6cdb671b 100644 --- a/ImageMagitek/Arranger/SequentialArranger.cs +++ b/ImageMagitek/Arranger/SequentialArranger.cs @@ -61,9 +61,16 @@ public SequentialArranger(int arrangerWidth, int arrangerHeight, DataFile dataFi ActivePalette = palette; _codecs = codecFactory; - ActiveCodec = _codecs.GetCodec(codecName); + ActiveCodec = _codecs.GetCodec(codecName, default); ColorType = ActiveCodec.ColorType; + Layout = ActiveCodec.Layout switch + { + ImageLayout.Tiled => ArrangerLayout.Tiled, + ImageLayout.Single => ArrangerLayout.Single, + _ => throw new InvalidOperationException($"{nameof(SequentialArranger)}.ctor was called with an invalid {nameof(ImageLayout)}") + }; + ElementPixelSize = new Size(ActiveCodec.Width, ActiveCodec.Height); Resize(arrangerWidth, arrangerHeight); @@ -155,7 +162,7 @@ public override void Resize(int arrangerWidth, int arrangerHeight) if (el.Codec.Layout == ImageLayout.Tiled) address += ActiveCodec.StorageSize; else if (el.Codec.Layout == ImageLayout.Single) - address += (ElementPixelSize.Width + ActiveCodec.RowStride) * ActiveCodec.ColorDepth / 4; // TODO: Fix sequential arranger offsets to be bit-wise + address += ElementPixelSize.Width * ActiveCodec.ColorDepth / 4; // TODO: Fix sequential arranger offsets to be bit-wise else throw new NotSupportedException(); @@ -237,7 +244,7 @@ public void ChangeCodec(IGraphicsCodec codec, int arrangerWidth, int arrangerHei if (ActiveCodec.Layout == ImageLayout.Tiled) address += ActiveCodec.StorageSize; else if (ActiveCodec.Layout == ImageLayout.Single) - address += (ElementPixelSize.Width + ActiveCodec.RowStride) * ActiveCodec.ColorDepth / 4; // TODO: Fix sequential arranger offsets to be bit-wise + address += ElementPixelSize.Width * ActiveCodec.ColorDepth / 4; // TODO: Fix sequential arranger offsets to be bit-wise else throw new NotSupportedException(); } diff --git a/ImageMagitek/BitStream.cs b/ImageMagitek/BitStream.cs index 72095a65..6ead0e0c 100644 --- a/ImageMagitek/BitStream.cs +++ b/ImageMagitek/BitStream.cs @@ -13,28 +13,28 @@ private enum BitStreamAccess { Read, Write, ReadWrite }; /// /// Current working bit index /// - private int BitIndex; + private int _bitIndex; /// /// Current working index into data array where bits are read/written to /// - private int Index; + private int _index; /// /// Bits remaining in the stream /// - private int BitsRemaining; + private int _bitsRemaining; /// /// Type of access to the stream /// - private BitStreamAccess Access; + private BitStreamAccess _access; - private int StreamStartOffset; + private int _streamStartOffset; - private int StreamEndOffset; + private int _streamEndOffset; - private int StreamSize { get => StreamEndOffset - StreamStartOffset; } + private int StreamSize { get => _streamEndOffset - _streamStartOffset; } public byte[] Data { get; private set; } @@ -51,12 +51,12 @@ public static BitStream OpenRead(byte[] ReadData, int DataBits) BitStream bs = new BitStream(); bs.Data = ReadData; - bs.BitsRemaining = DataBits; - bs.BitIndex = 8; - bs.Index = 0; - bs.Access = BitStreamAccess.Read; - bs.StreamStartOffset = 0; - bs.StreamEndOffset = DataBits; + bs._bitsRemaining = DataBits; + bs._bitIndex = 8; + bs._index = 0; + bs._access = BitStreamAccess.Read; + bs._streamStartOffset = 0; + bs._streamEndOffset = DataBits; return bs; } @@ -89,12 +89,12 @@ public static BitStream OpenRead(Stream stream, int DataBits, int FirstByteBits) byte mask = (byte)((1 << FirstByteBits) - 1); bs.Data[0] = (byte)(bs.Data[0] & mask); - bs.BitIndex = FirstByteBits; - bs.BitsRemaining = DataBits; - bs.Index = 0; - bs.Access = BitStreamAccess.Read; - bs.StreamStartOffset = 8 - FirstByteBits; - bs.StreamEndOffset = DataBits - bs.StreamStartOffset; + bs._bitIndex = FirstByteBits; + bs._bitsRemaining = DataBits; + bs._index = 0; + bs._access = BitStreamAccess.Read; + bs._streamStartOffset = 8 - FirstByteBits; + bs._streamEndOffset = DataBits - bs._streamStartOffset; return bs; } @@ -119,12 +119,12 @@ public static BitStream OpenWrite(byte[] Buffer, int DataBits, int FirstByteBits bs.Data = Buffer; - bs.BitIndex = FirstByteBits; - bs.BitsRemaining = DataBits; - bs.Index = 0; - bs.Access = BitStreamAccess.Write; - bs.StreamStartOffset = 8 - FirstByteBits; - bs.StreamEndOffset = DataBits - bs.StreamStartOffset; + bs._bitIndex = FirstByteBits; + bs._bitsRemaining = DataBits; + bs._index = 0; + bs._access = BitStreamAccess.Write; + bs._streamStartOffset = 8 - FirstByteBits; + bs._streamEndOffset = DataBits - bs._streamStartOffset; return bs; } @@ -134,21 +134,21 @@ public void SeekAbsolute(int seekBits) if (seekBits < 0 || seekBits >= StreamSize) throw new ArgumentOutOfRangeException($"{nameof(SeekAbsolute)} parameter '{nameof(seekBits)} is out of range ({seekBits})'"); - Index = (StreamStartOffset + seekBits) / 8; - BitIndex = 8 - (StreamStartOffset + seekBits) % 8; - BitsRemaining = StreamEndOffset - (StreamStartOffset + seekBits); + _index = (_streamStartOffset + seekBits) / 8; + _bitIndex = 8 - (_streamStartOffset + seekBits) % 8; + _bitsRemaining = _streamEndOffset - (_streamStartOffset + seekBits); } public void SeekRelative(int seekBits) { - int seekOffset = Index * 8 + (8 - BitIndex) + seekBits; + int seekOffset = _index * 8 + (8 - _bitIndex) + seekBits; if (seekOffset < 0 || seekOffset >= StreamSize) throw new ArgumentOutOfRangeException($"{nameof(SeekRelative)} parameter '{nameof(seekOffset)} is out of range ({seekOffset})'"); - Index = (StreamStartOffset + seekOffset) / 8; - BitIndex = 8 - (StreamStartOffset + seekOffset) % 8; - BitsRemaining = StreamEndOffset - (StreamStartOffset + seekOffset); + _index = (_streamStartOffset + seekOffset) / 8; + _bitIndex = 8 - (_streamStartOffset + seekOffset) % 8; + _bitsRemaining = _streamEndOffset - (_streamStartOffset + seekOffset); } /// @@ -157,23 +157,23 @@ public void SeekRelative(int seekBits) /// public int ReadBit() { - if (Access != BitStreamAccess.Read && Access != BitStreamAccess.ReadWrite) + if (_access != BitStreamAccess.Read && _access != BitStreamAccess.ReadWrite) throw new InvalidOperationException($"{nameof(ReadBit)} does not have read access"); - if (BitsRemaining == 0) + if (_bitsRemaining == 0) throw new EndOfStreamException($"{nameof(ReadBit)} read past end of stream"); - if (BitIndex == 0) + if (_bitIndex == 0) { - Index++; - if (Index == Data.Length) + _index++; + if (_index == Data.Length) throw new EndOfStreamException($"{nameof(ReadBit)} read past end of stream"); - BitIndex = 8; + _bitIndex = 8; } - int bit = (Data[Index] >> (BitIndex - 1)) & 1; - BitsRemaining--; - BitIndex--; + int bit = (Data[_index] >> (_bitIndex - 1)) & 1; + _bitsRemaining--; + _bitIndex--; return bit; } @@ -184,19 +184,19 @@ public int ReadBit() /// public byte ReadByte() { - if (BitsRemaining < 8) + if (_bitsRemaining < 8) throw new EndOfStreamException($"{nameof(ReadByte)} read past end of stream"); byte result = 0; - if (BitIndex == 8) + if (_bitIndex == 8) { - result = Data[Index]; + result = Data[_index]; Advance(8); } else { - int readSize = BitIndex; + int readSize = _bitIndex; result = (byte)(PartialRead(readSize) << (8 - readSize)); readSize = 8 - readSize; result = (byte)(result | PartialRead(readSize)); @@ -215,16 +215,16 @@ public int ReadBits(int numBits) if (numBits > 32 || numBits < 1) throw new ArgumentOutOfRangeException($"{nameof(ReadBits)} parameter {nameof(numBits)} ({numBits}) is out of range"); - if (numBits > BitsRemaining) + if (numBits > _bitsRemaining) throw new EndOfStreamException($"{nameof(ReadBits)} read past end of stream"); var readRemaining = numBits; // Number of bits remaining to be read int result = 0; // Unaligned, partial read - if (BitIndex != 8) + if (_bitIndex != 8) { - int readLength = Math.Min(BitIndex, readRemaining); + int readLength = Math.Min(_bitIndex, readRemaining); result = PartialRead(readLength); readRemaining -= readLength; } @@ -232,7 +232,7 @@ public int ReadBits(int numBits) // Multiple aligned byte reads while(readRemaining >= 8) { - result = (result << 8) | Data[Index]; + result = (result << 8) | Data[_index]; Advance(8); readRemaining -= 8; } @@ -252,9 +252,9 @@ public int ReadBits(int numBits) private int PartialRead(int bitReadLength) { int mask = ((1 << bitReadLength) - 1); // Make mask for the bits to be read - mask <<= (BitIndex - bitReadLength); // Shift mask to the bit index + mask <<= (_bitIndex - bitReadLength); // Shift mask to the bit index - int result = (Data[Index] & mask) >> (BitIndex - bitReadLength); + int result = (Data[_index] & mask) >> (_bitIndex - bitReadLength); Advance(bitReadLength); @@ -267,55 +267,55 @@ private int PartialRead(int bitReadLength) /// Number of bits to advance private void Advance(int advanceLength) { - int offset = (8 - BitIndex) + advanceLength; - Index += offset / 8; - BitIndex = 8 - (offset % 8); - BitsRemaining -= advanceLength; + int offset = (8 - _bitIndex) + advanceLength; + _index += offset / 8; + _bitIndex = 8 - (offset % 8); + _bitsRemaining -= advanceLength; } public void WriteBit(int bit) { if (bit > 1) throw new ArgumentOutOfRangeException(); - if (Access != BitStreamAccess.Write && Access != BitStreamAccess.ReadWrite) + if (_access != BitStreamAccess.Write && _access != BitStreamAccess.ReadWrite) throw new InvalidOperationException($"{nameof(WriteBit)} does not have write access"); - if (BitsRemaining == 0) + if (_bitsRemaining == 0) throw new EndOfStreamException($"{nameof(WriteBit)} attempted to write past end of stream"); - if(BitIndex == 0) + if(_bitIndex == 0) { - if (Index == Data.Length) + if (_index == Data.Length) throw new EndOfStreamException($"{nameof(WriteBit)} attempted to write past end of stream"); - Index++; - BitIndex = 8; + _index++; + _bitIndex = 8; } if (bit == 0) { - byte mask = (byte) ~(1 << (BitIndex - 1)); - Data[Index] &= mask; + byte mask = (byte) ~(1 << (_bitIndex - 1)); + Data[_index] &= mask; } else - Data[Index] |= (byte)(1 << (BitIndex - 1)); + Data[_index] |= (byte)(1 << (_bitIndex - 1)); - BitsRemaining--; - BitIndex--; + _bitsRemaining--; + _bitIndex--; } public void WriteByte(byte val) { - if (BitsRemaining < 8) + if (_bitsRemaining < 8) throw new EndOfStreamException($"{nameof(WriteByte)} read past end of stream"); - if (BitIndex == 8) + if (_bitIndex == 8) { - Data[Index] = val; + Data[_index] = val; Advance(8); } else { - int writeSize = BitIndex; + int writeSize = _bitIndex; int writeValue = val >> (8 - writeSize); PartialWrite(writeValue, writeSize); writeSize = 8 - writeSize; @@ -329,15 +329,15 @@ public void WriteBits(int val, int numBits) if (numBits > 32 || numBits < 1) throw new ArgumentOutOfRangeException($"{nameof(WriteBits)} parameter {nameof(numBits)} ({numBits}) is out of range"); - if (numBits > BitsRemaining) + if (numBits > _bitsRemaining) throw new EndOfStreamException($"{nameof(WriteBits)} read past end of stream"); var writeRemaining = numBits; // Unaligned, partial write - if (BitIndex != 8) + if (_bitIndex != 8) { - int writeLength = Math.Min(BitIndex, writeRemaining); + int writeLength = Math.Min(_bitIndex, writeRemaining); int writeValue = val >> (writeRemaining - writeLength); PartialWrite(writeValue, writeLength); writeRemaining -= writeLength; @@ -348,7 +348,7 @@ public void WriteBits(int val, int numBits) { int writeValue = val >> (writeRemaining - 8); writeValue &= (1 << 8) - 1; - Data[Index] = (byte) writeValue; + Data[_index] = (byte) writeValue; Advance(8); writeRemaining -= 8; } @@ -364,10 +364,10 @@ public void WriteBits(int val, int numBits) private void PartialWrite(int val, int bitWriteLength) { int mask = ((1 << bitWriteLength) - 1); // Make mask for the bits to be read - mask <<= (BitIndex - bitWriteLength); // Shift mask to the bit index + mask <<= (_bitIndex - bitWriteLength); // Shift mask to the bit index - Data[Index] &= (byte) ~mask; // Clear bits - Data[Index] |= (byte) (val << (BitIndex - bitWriteLength)); + Data[_index] &= (byte) ~mask; // Clear bits + Data[_index] |= (byte) (val << (_bitIndex - bitWriteLength)); Advance(bitWriteLength); } diff --git a/ImageMagitek/Codec/DirectCodec.cs b/ImageMagitek/Codec/Base/DirectCodec.cs similarity index 100% rename from ImageMagitek/Codec/DirectCodec.cs rename to ImageMagitek/Codec/Base/DirectCodec.cs diff --git a/ImageMagitek/Codec/IGraphicsCodec.cs b/ImageMagitek/Codec/Base/IGraphicsCodec.cs similarity index 87% rename from ImageMagitek/Codec/IGraphicsCodec.cs rename to ImageMagitek/Codec/Base/IGraphicsCodec.cs index 192abcb0..5c58bb92 100644 --- a/ImageMagitek/Codec/IGraphicsCodec.cs +++ b/ImageMagitek/Codec/Base/IGraphicsCodec.cs @@ -5,7 +5,7 @@ /// Tiled is capable of rendering a grid of multiple images /// Single will render a single image /// - public enum ImageLayout { Tiled = 0, Single } + public enum ImageLayout { Tiled = 1, Single } public interface IGraphicsCodec { @@ -16,8 +16,6 @@ public interface IGraphicsCodec PixelColorType ColorType { get; } int ColorDepth { get; } int StorageSize { get; } - int RowStride { get; } - int ElementStride { get; } int DefaultWidth { get; } int DefaultHeight { get; } diff --git a/ImageMagitek/Codec/IGraphicsCodec{T}.cs b/ImageMagitek/Codec/Base/IGraphicsCodec{T}.cs similarity index 100% rename from ImageMagitek/Codec/IGraphicsCodec{T}.cs rename to ImageMagitek/Codec/Base/IGraphicsCodec{T}.cs diff --git a/ImageMagitek/Codec/IndexedCodec.cs b/ImageMagitek/Codec/Base/IndexedCodec.cs similarity index 96% rename from ImageMagitek/Codec/IndexedCodec.cs rename to ImageMagitek/Codec/Base/IndexedCodec.cs index 5b7edf2d..b8ccf275 100644 --- a/ImageMagitek/Codec/IndexedCodec.cs +++ b/ImageMagitek/Codec/Base/IndexedCodec.cs @@ -15,8 +15,6 @@ public abstract class IndexedCodec : IIndexedCodec public PixelColorType ColorType => PixelColorType.Indexed; public abstract int ColorDepth { get; } public abstract int StorageSize { get; } - public abstract int RowStride { get; } - public abstract int ElementStride { get; } public abstract int DefaultWidth { get; } public abstract int DefaultHeight { get; } diff --git a/ImageMagitek/Codec/BroadcastList.cs b/ImageMagitek/Codec/BroadcastList.cs deleted file mode 100644 index 6b840c85..00000000 --- a/ImageMagitek/Codec/BroadcastList.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections; -using System.Collections.Generic; - -namespace ImageMagitek.Codec -{ - /// - /// A list that implements index-wrapping - /// - /// - public class BroadcastList : IEnumerable - { - private List _list; - - public int Capacity => _list.Capacity; - public int Count => _list.Count; - - public BroadcastList() : this(4) { } - public BroadcastList(int capacity) - { - _list = new List(capacity); - } - public BroadcastList(IEnumerable items) - { - _list = new List(items); - } - - public void Add(T item) => _list.Add(item); - public IEnumerator GetEnumerator() => _list.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); - - public T this[int index] - { - get - { - if (index < 0) - return _list[((index % Count) + Count) % Count]; - - return _list[index % Count]; - } - - set - { - if (index < 0) - _list[((index % Count) + Count) % Count] = value; - else - _list[index % Count] = value; - } - } - } -} diff --git a/ImageMagitek/Codec/CodecFactory.cs b/ImageMagitek/Codec/CodecFactory.cs index c66f677e..a11f0306 100644 --- a/ImageMagitek/Codec/CodecFactory.cs +++ b/ImageMagitek/Codec/CodecFactory.cs @@ -1,109 +1,88 @@ using System; using System.Collections.Generic; -using ImageMagitek.Colors; +using System.Drawing; +using System.Linq; namespace ImageMagitek.Codec { public class CodecFactory : ICodecFactory { - private readonly Dictionary _formats; + private readonly Dictionary _formats; + private readonly Dictionary _codecs; - public Palette DefaultPalette { get; set; } - - public CodecFactory(Dictionary formats, Palette defaultPalette) + public CodecFactory(Dictionary formats) { - _formats = formats ?? new Dictionary(); - DefaultPalette = defaultPalette; + _formats = formats ?? new Dictionary(); + _codecs = new Dictionary + { + { "SNES 3bpp", typeof(Snes3bppCodec) }, { "PSX 4bpp", typeof(Psx4bppCodec) }, { "PSX 8bpp", typeof(Psx8bppCodec) } + }; } - public IGraphicsCodec GetCodec(string codecName) + public void AddOrUpdateCodec(Type codecType) { - switch (codecName) + if (typeof(IGraphicsCodec).IsAssignableFrom(codecType) && !codecType.IsAbstract) { - //case "NES 1bpp": - // return new Nes1bppCodec(_defaultWidth, _defaultHeight); - case "SNES 3bpp": - return new Snes3bppCodec(); - case "PSX 4bpp": - return new Psx4bppCodec(); - case "PSX 8bpp": - return new Psx8bppCodec(); - //case "PSX 16bpp": - // return new Psx16bppCodec(width, height); - //case "PSX 24bpp": - // return new Psx24bppCodec(width, height); - default: - if (_formats.ContainsKey(codecName)) - { - var format = _formats[codecName].Clone(); - format.Name = codecName; - format.Width = format.DefaultWidth; - format.Height = format.DefaultHeight; - - if (format.ColorType == PixelColorType.Indexed) - return new IndexedGraphicsCodec(format, DefaultPalette); - else if (format.ColorType == PixelColorType.Direct) - throw new NotSupportedException(); - - throw new NotSupportedException(); - } - else - throw new KeyNotFoundException($"{nameof(GetCodec)} could not locate a codec for '{nameof(codecName)}'"); + var codec = (IGraphicsCodec)Activator.CreateInstance(codecType); + _codecs[codec.Name] = codecType; } + else + throw new ArgumentException($"{nameof(AddOrUpdateCodec)} parameter '{nameof(codecType)}' is not of type {typeof(IGraphicsCodec)} or is not instantiable"); } - public IGraphicsCodec GetCodec(string codecName, int width, int height, int rowStride = 0) + public IGraphicsCodec GetCodec(string codecName, Size? elementSize) { - switch (codecName) + if (_codecs.ContainsKey(codecName)) + { + var codecType = _codecs[codecName]; + if (elementSize.HasValue) + return (IGraphicsCodec) Activator.CreateInstance(codecType, elementSize.Value.Width, elementSize.Value.Height); + else + return (IGraphicsCodec) Activator.CreateInstance(codecType); + } + else if (_formats.ContainsKey(codecName)) { - //case "NES 1bpp": - // return new Nes1bppCodec(width, height); - case "SNES 3bpp": - return new Snes3bppCodec(width, height); - case "PSX 4bpp": - return new Psx4bppCodec(width, height); - case "PSX 8bpp": - return new Psx8bppCodec(width, height); - //case "PSX 16bpp": - // return new Psx16bppCodec(width, height); - //case "PSX 24bpp": - // return new Psx24bppCodec(width, height); - default: - if (_formats.ContainsKey(codecName)) + var format = _formats[codecName].Clone(); + + if (format is FlowGraphicsFormat flowFormat) + { + if (elementSize.HasValue) { - var format = _formats[codecName].Clone(); - format.Name = codecName; - format.Width = width; - format.Height = height; + flowFormat.Width = elementSize.Value.Width; + flowFormat.Height = elementSize.Value.Height; + } - if (format.ColorType == PixelColorType.Indexed) - return new IndexedGraphicsCodec(format, DefaultPalette); - else if (format.ColorType == PixelColorType.Direct) - throw new NotSupportedException(); + if (format.ColorType == PixelColorType.Indexed) + return new IndexedGraphicsCodec(flowFormat); + else if (format.ColorType == PixelColorType.Direct) + throw new NotImplementedException(); + } + else if (format is PatternGraphicsFormat patternFormat) + { + if (format.ColorType == PixelColorType.Indexed) + return new IndexedPatternGraphicsCodec(patternFormat); + else if (format.ColorType == PixelColorType.Direct) + throw new NotImplementedException(); + } - throw new NotSupportedException(); - } - else - throw new KeyNotFoundException($"{nameof(GetCodec)} could not locate a codec for '{nameof(codecName)}'"); + throw new NotSupportedException($"Graphics format of type '{format}' is not supported"); + } + else + { + throw new KeyNotFoundException($"{nameof(GetCodec)} could not locate a codec for '{codecName}'"); } } public IGraphicsCodec CloneCodec(IGraphicsCodec codec) { - return GetCodec(codec.Name, codec.Width, codec.Height, codec.RowStride); + return GetCodec(codec.Name, new Size(codec.Width, codec.Height)); } public IEnumerable GetSupportedCodecNames() { - //yield return "NES 1bpp"; - yield return "SNES 3bpp"; - yield return "PSX 4bpp"; - yield return "PSX 8bpp"; - //yield return "PSX 16bpp"; - //yield return "PSX 24bpp"; - - foreach (var format in _formats.Values) - yield return format.Name; + return _formats.Keys + .Concat(_codecs.Keys) + .OrderBy(x => x); } } } diff --git a/ImageMagitek/Codec/Generalized/GraphicsFormat.cs b/ImageMagitek/Codec/Generalized/FlowGraphicsFormat.cs similarity index 51% rename from ImageMagitek/Codec/Generalized/GraphicsFormat.cs rename to ImageMagitek/Codec/Generalized/FlowGraphicsFormat.cs index 5f4d17fb..07eba846 100644 --- a/ImageMagitek/Codec/Generalized/GraphicsFormat.cs +++ b/ImageMagitek/Codec/Generalized/FlowGraphicsFormat.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.Linq; namespace ImageMagitek.Codec { /// - /// GraphicsFormat describes properties relating to decoding/encoding a general graphics format + /// FlowGraphicsFormat describes properties relating to decoding/encoding a + /// general graphics format that is resizable /// - public class GraphicsFormat + public class FlowGraphicsFormat : IGraphicsFormat { /// /// The name of the codec @@ -24,7 +26,7 @@ public class GraphicsFormat public ImageLayout Layout { get; set; } /// - /// The color depth of the format in bits per pixel + /// The color depth of each pixel in bits per pixel /// public int ColorDepth { get; set; } @@ -41,51 +43,46 @@ public class GraphicsFormat public int[] MergePlanePriority { get; set; } /// - /// Current width of the elements to encode/decode - /// - public int Width { get; set; } - - /// - /// Current height of elements to encode/decode - /// - public int Height { get; set; } - - /// - /// Default width of an element as specified by the XML file + /// Default width of an element /// - public int DefaultWidth { get; set; } + public int DefaultWidth { get; } /// - /// Default height of an element as specified by the XML file + /// Default height of an element /// - public int DefaultHeight { get; set; } + public int DefaultHeight { get; } /// - /// Number of bits to skip after each row + /// Current width of the elements to encode/decode /// - public int RowStride { get; set; } + public int Width { get; set; } /// - /// Number of bits to skip after each element + /// Current height of elements to encode/decode /// - public int ElementStride { get; set; } + public int Height { get; set; } /// /// Storage size of an element in bits /// /// - public int StorageSize => (Width + RowStride) * Height * ColorDepth + ElementStride; + public int StorageSize => Width * Height * ColorDepth; public IList ImageProperties { get; set; } = new List(); - // Processing Operations - public bool HFlip { get; set; } - public bool VFlip { get; set; } - public bool Remap { get; set; } - - // Pixel remap operations (TODO) - - public GraphicsFormat() { } + public FlowGraphicsFormat(string name, PixelColorType colorType, int colorDepth, + ImageLayout layout, int defaultHeight, int defaultWidth) + { + Name = name; + ColorType = colorType; + ColorDepth = colorDepth; + Layout = layout; + DefaultHeight = defaultHeight; + DefaultWidth = defaultWidth; + + Height = defaultHeight; + Width = defaultWidth; + } public void Resize(int width, int height) { @@ -93,33 +90,18 @@ public void Resize(int width, int height) Height = height; } - public GraphicsFormat Clone() + public IGraphicsFormat Clone() { - var clone = new GraphicsFormat(); - clone.Name = Name; - clone.FixedSize = FixedSize; - clone.Layout = Layout; - clone.ColorDepth = ColorDepth; - clone.ColorType = ColorType; - clone.Width = Width; - clone.Height = Height; - clone.DefaultWidth = DefaultWidth; - clone.DefaultHeight = DefaultHeight; - clone.RowStride = RowStride; - clone.ElementStride = ElementStride; - clone.HFlip = HFlip; - clone.VFlip = VFlip; - clone.Remap = Remap; - - clone.MergePlanePriority = new int[MergePlanePriority.Length]; - Array.Copy(MergePlanePriority, clone.MergePlanePriority, MergePlanePriority.Length); - - clone.ImageProperties = new List(); - foreach(var prop in ImageProperties) + var clone = new FlowGraphicsFormat(Name, ColorType, ColorDepth, Layout, DefaultWidth, DefaultHeight) { - var pattern = new BroadcastList(prop.RowPixelPattern); + FixedSize = FixedSize, + MergePlanePriority = MergePlanePriority.ToArray(), + ImageProperties = new List() + }; - var propclone = new ImageProperty(prop.ColorDepth, prop.RowInterlace, pattern); + foreach (var prop in ImageProperties) + { + var propclone = new ImageProperty(prop.ColorDepth, prop.RowInterlace, prop.RowPixelPattern); clone.ImageProperties.Add(propclone); } diff --git a/ImageMagitek/Codec/Generalized/IGraphicsFormat.cs b/ImageMagitek/Codec/Generalized/IGraphicsFormat.cs new file mode 100644 index 00000000..b61c4d25 --- /dev/null +++ b/ImageMagitek/Codec/Generalized/IGraphicsFormat.cs @@ -0,0 +1,23 @@ +namespace ImageMagitek.Codec +{ + public interface IGraphicsFormat + { + string Name { get; } + + PixelColorType ColorType { get; } + int ColorDepth { get; } + + ImageLayout Layout { get; } + + int DefaultHeight { get; } + int DefaultWidth { get; } + + int Height { get; } + int Width { get; } + + bool FixedSize { get; } + int StorageSize { get; } + + IGraphicsFormat Clone(); + } +} \ No newline at end of file diff --git a/ImageMagitek/Codec/Generalized/IndexedGraphicsCodec.cs b/ImageMagitek/Codec/Generalized/IndexedGraphicsCodec.cs index aae37a0b..d5877c36 100644 --- a/ImageMagitek/Codec/Generalized/IndexedGraphicsCodec.cs +++ b/ImageMagitek/Codec/Generalized/IndexedGraphicsCodec.cs @@ -1,29 +1,25 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using ImageMagitek.Colors; using ImageMagitek.ExtensionMethods; namespace ImageMagitek.Codec { - public class IndexedGraphicsCodec : IIndexedCodec + class IndexedGraphicsCodec : IIndexedCodec { public string Name { get; set; } - public GraphicsFormat Format { get; private set; } + public FlowGraphicsFormat Format { get; private set; } public int StorageSize => Format.StorageSize; public ImageLayout Layout => Format.Layout; public PixelColorType ColorType => Format.ColorType; public int ColorDepth => Format.ColorDepth; public int Width => Format.Width; public int Height => Format.Height; - public int RowStride => Format.RowStride; - public int ElementStride => Format.ElementStride; - public Palette DefaultPalette { get; set; } public virtual ReadOnlySpan ForeignBuffer => _foreignBuffer; protected byte[] _foreignBuffer; + protected byte[,] _nativeBuffer; public virtual byte[,] NativeBuffer => _nativeBuffer; public int DefaultWidth => Format.DefaultWidth; @@ -32,25 +28,22 @@ public class IndexedGraphicsCodec : IIndexedCodec public int WidthResizeIncrement { get; } public int HeightResizeIncrement => 1; - protected byte[,] _nativeBuffer; - /// /// Preallocated buffer that separates and stores pixel color data /// - private List ElementData; + private List _elementData; /// /// Preallocated buffer that stores merged pixel color data /// - private byte[] MergedData; + private byte[] _mergedData; private BitStream _bitStream; - public IndexedGraphicsCodec(GraphicsFormat format, Palette defaultPalette) + public IndexedGraphicsCodec(FlowGraphicsFormat format) { Format = format; Name = format.Name; - DefaultPalette = defaultPalette; AllocateBuffers(); // Consider implementing resize increment with more accurate LCM approach @@ -60,14 +53,14 @@ public IndexedGraphicsCodec(GraphicsFormat format, Palette defaultPalette) private void AllocateBuffers() { - ElementData = new List(); + _elementData = new List(); for (int i = 0; i < Format.ColorDepth; i++) { byte[] data = new byte[Format.Width * Format.Height]; - ElementData.Add(data); + _elementData.Add(data); } - MergedData = new byte[Format.Width * Format.Height]; + _mergedData = new byte[Format.Width * Format.Height]; _foreignBuffer = new byte[(StorageSize + 7) / 8]; _nativeBuffer = new byte[Width, Height]; @@ -100,7 +93,7 @@ private void AllocateBuffers() { var mergePlane = Format.MergePlanePriority[curPlane]; var pixelPosition = scanlinePosition + ip.RowPixelPattern[x]; - ElementData[mergePlane][pixelPosition] = (byte)_bitStream.ReadBit(); + _elementData[mergePlane][pixelPosition] = (byte)_bitStream.ReadBit(); } } } @@ -116,7 +109,7 @@ private void AllocateBuffers() { var mergePlane = Format.MergePlanePriority[curPlane]; int pixelPosition = scanlinePosition + ip.RowPixelPattern[x]; - ElementData[mergePlane][pixelPosition] = (byte)_bitStream.ReadBit(); + _elementData[mergePlane][pixelPosition] = (byte)_bitStream.ReadBit(); } } } @@ -128,18 +121,18 @@ private void AllocateBuffers() // Merge into foreign pixel data byte foreignPixelData; - for (scanlinePosition = 0; scanlinePosition < MergedData.Length; scanlinePosition++) + for (scanlinePosition = 0; scanlinePosition < _mergedData.Length; scanlinePosition++) { foreignPixelData = 0; for (int i = 0; i < Format.ColorDepth; i++) - foreignPixelData |= (byte)(ElementData[i][scanlinePosition] << i); // Works for SNES image data and palettes, may need customization later - MergedData[scanlinePosition] = foreignPixelData; + foreignPixelData |= (byte)(_elementData[i][scanlinePosition] << i); // Works for SNES image data and palettes, may need customization later + _mergedData[scanlinePosition] = foreignPixelData; } scanlinePosition = 0; for (int y = 0; y < Height; y++) for (int x = 0; x < Width; x++, scanlinePosition++) - _nativeBuffer[x, y] = MergedData[scanlinePosition]; + _nativeBuffer[x, y] = _mergedData[scanlinePosition]; return NativeBuffer; } @@ -152,13 +145,13 @@ public ReadOnlySpan EncodeElement(in ArrangerElement el, byte[,] imageBuff int pos = 0; for (int y = 0; y < Height; y++) for (int x = 0; x < Width; x++, pos++) - MergedData[pos] = imageBuffer[x, y]; + _mergedData[pos] = imageBuffer[x, y]; // Loop over MergedData to split foreign colors into bit planes in ElementData - for (pos = 0; pos < MergedData.Length; pos++) + for (pos = 0; pos < _mergedData.Length; pos++) { for (int i = 0; i < Format.ColorDepth; i++) - ElementData[i][pos] = (byte)((MergedData[pos] >> i) & 0x1); + _elementData[i][pos] = (byte)((_mergedData[pos] >> i) & 0x1); } // Loop over planes and write bits to data buffer with proper interlacing @@ -179,7 +172,8 @@ public ReadOnlySpan EncodeElement(in ArrangerElement el, byte[,] imageBuff for (int x = 0; x < Format.Width; x++) { int priorityPos = pos + ip.RowPixelPattern[x]; - bs.WriteBit(ElementData[curPlane][priorityPos]); + int mergedPlane = Format.MergePlanePriority[curPlane]; + bs.WriteBit(_elementData[mergedPlane][priorityPos]); } } } @@ -189,10 +183,14 @@ public ReadOnlySpan EncodeElement(in ArrangerElement el, byte[,] imageBuff for (int y = 0; y < Format.Height; y++, pos += Format.Width) { for (int x = 0; x < Format.Width; x++) + { for (int curPlane = plane; curPlane < plane + ip.ColorDepth; curPlane++) { - bs.WriteBit(ElementData[curPlane][pos + ip.RowPixelPattern[x]]); + int priorityPos = pos + ip.RowPixelPattern[x]; + int mergedPlane = Format.MergePlanePriority[curPlane]; + bs.WriteBit(_elementData[mergedPlane][priorityPos]); } + } } } diff --git a/ImageMagitek/Codec/Generalized/IndexedPatternGraphicsCodec.cs b/ImageMagitek/Codec/Generalized/IndexedPatternGraphicsCodec.cs new file mode 100644 index 00000000..6cca8903 --- /dev/null +++ b/ImageMagitek/Codec/Generalized/IndexedPatternGraphicsCodec.cs @@ -0,0 +1,134 @@ +using ImageMagitek.ExtensionMethods; +using System; +using System.Collections.Generic; + +namespace ImageMagitek.Codec +{ + public class IndexedPatternGraphicsCodec : IIndexedCodec + { + public string Name { get; set; } + public PatternGraphicsFormat Format { get; } + public int StorageSize => Format.StorageSize; + public ImageLayout Layout => Format.Layout; + public PixelColorType ColorType => Format.ColorType; + public int ColorDepth => Format.ColorDepth; + public int Width => Format.Width; + public int Height => Format.Height; + + private byte[] _foreignBuffer; + public ReadOnlySpan ForeignBuffer => _foreignBuffer; + + private byte[,] _nativeBuffer; + public byte[,] NativeBuffer => _nativeBuffer; + + private BitStream _bitStream; + private List _planeImages; + + public int DefaultWidth => Format.DefaultWidth; + public int DefaultHeight => Format.DefaultHeight; + public bool CanResize => !Format.FixedSize; + public int WidthResizeIncrement { get; } + public int HeightResizeIncrement => 1; + + public IndexedPatternGraphicsCodec(PatternGraphicsFormat format) + { + Format = format; + Name = format.Name; + AllocateBuffers(); + } + + public byte[,] DecodeElement(in ArrangerElement el, ReadOnlySpan encodedBuffer) + { + if (encodedBuffer.Length * 8 < StorageSize) // Decoding would require data past the end of the buffer + throw new ArgumentException(nameof(encodedBuffer)); + + encodedBuffer.Slice(0, _foreignBuffer.Length).CopyTo(_foreignBuffer); + _bitStream.SeekAbsolute(0); + + for (int i = 0; i < StorageSize; i++) + { + var bit = _bitStream.ReadBit(); + var coordinate = Format.Pattern.GetDecodeIndex(i); + _planeImages[coordinate.P][coordinate.X, coordinate.Y] = bit; + } + + for (int y = 0; y < Height; y++) + { + for (int x = 0; x < Width; x++) + { + byte color = 0; + int xpos = Format.RowPixelPattern[x]; + + for (int i = 0; i < Format.ColorDepth; i++) + { + color |= (byte)(_planeImages[i][x, y] << Format.MergePlanePriority[i]); + } + NativeBuffer[xpos, y] = color; + } + } + + return NativeBuffer; + } + + public ReadOnlySpan EncodeElement(in ArrangerElement el, byte[,] imageBuffer) + { + if (imageBuffer.GetLength(0) != Width || imageBuffer.GetLength(1) != Height) + throw new ArgumentException(nameof(imageBuffer)); + + var bs = BitStream.OpenWrite(StorageSize, 8); + + for (short y = 0; y < Height; y++) + { + for (short x = 0; x < Width; x++) + { + int color = imageBuffer[x, y]; + for (short i = 0; i < Format.ColorDepth; i++) + { + var bit = (color >> i) & 1; + short xpos = (short)Format.RowPixelPattern[x]; + short plane = (short)Format.MergePlanePriority[i]; + var index = Format.Pattern.GetEncodeIndex(new PlaneCoordinate(xpos, y, plane)); + bs.SeekAbsolute(index); + bs.WriteBit(bit); + } + } + } + + return bs.Data; + } + + private void AllocateBuffers() + { + _foreignBuffer = new byte[(StorageSize + 7) / 8]; + _nativeBuffer = new byte[Width, Height]; + + _bitStream = BitStream.OpenRead(_foreignBuffer, StorageSize); + + _planeImages = new List(); + for (int i = 0; i < Format.ColorDepth; i++) + _planeImages.Add(new int[Width, Height]); + } + + public int GetPreferredWidth(int width) => DefaultWidth; + public int GetPreferredHeight(int height) => DefaultHeight; + + public ReadOnlySpan ReadElement(in ArrangerElement el) + { + var buffer = new byte[(StorageSize + 7) / 8]; + var fs = el.DataFile.Stream; + + if (el.FileAddress + StorageSize > fs.Length * 8) + return null; + + fs.ReadShifted(el.FileAddress, StorageSize, buffer); + + return buffer; + } + + public void WriteElement(in ArrangerElement el, ReadOnlySpan encodedBuffer) + { + var fs = el.DataFile.Stream; + fs.WriteShifted(el.FileAddress, StorageSize, encodedBuffer); + } + } +} diff --git a/ImageMagitek/Codec/Generalized/PatternGraphicsFormat.cs b/ImageMagitek/Codec/Generalized/PatternGraphicsFormat.cs new file mode 100644 index 00000000..538155cb --- /dev/null +++ b/ImageMagitek/Codec/Generalized/PatternGraphicsFormat.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ImageMagitek.Codec +{ + public enum PixelPacking { Planar, Chunky } + + public class PatternGraphicsFormat : IGraphicsFormat + { + public PatternList Pattern { get; protected set; } + + /// + /// The name of the codec + /// + public string Name { get; } + + public int ColorDepth { get; } + public PixelColorType ColorType { get; } + public ImageLayout Layout { get; } + public PixelPacking Packing { get; } + + /// + /// Specifies how individual bits of each color are merged according to priority + /// Ex: [3, 2, 0, 1] implies the first bit read will merge into plane 3, + /// second bit read into plane 2, third bit read into plane 0, fourth bit read into plane 1 + /// + public int[] MergePlanePriority { get; set; } + + public RepeatList RowPixelPattern { get; set; } + + public int DefaultWidth { get; } + public int DefaultHeight { get; } + public int Width => DefaultWidth; + public int Height => DefaultHeight; + + public bool FixedSize => true; + public int StorageSize => Width * Height * ColorDepth; + + public PatternGraphicsFormat(string name, PixelColorType colorType, int colorDepth, + ImageLayout layout, PixelPacking packing, int defaultWidth, int defaultHeight) + { + Name = name; + ColorType = colorType; + ColorDepth = colorDepth; + Layout = layout; + Packing = packing; + DefaultWidth = defaultWidth; + DefaultHeight = defaultHeight; + } + + public IGraphicsFormat Clone() + { + var format = new PatternGraphicsFormat(Name, ColorType, ColorDepth, Layout, Packing, DefaultWidth, DefaultHeight); + format.SetPattern(Pattern); + format.MergePlanePriority = MergePlanePriority.ToArray(); + format.RowPixelPattern = new RepeatList(RowPixelPattern); + + return format; + } + + public void SetPattern(PatternList pattern) + { + Pattern = pattern; + } + } +} diff --git a/ImageMagitek/Codec/Generalized/PatternList.cs b/ImageMagitek/Codec/Generalized/PatternList.cs new file mode 100644 index 00000000..9c0d21c3 --- /dev/null +++ b/ImageMagitek/Codec/Generalized/PatternList.cs @@ -0,0 +1,279 @@ +using MoreLinq; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ImageMagitek.Codec +{ + /// + /// Provides index remapping with Feidian-style patterns + /// + /// + /// The pattern precedence is [A-Z] [a-z] [2-9] [!?@*] for a total of 64 characters + /// + public class PatternList + { + public static int MaxPatternSize { get; } = 64 * 8; + + private const string _letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "23456789" + "!?@*"; + private readonly static Dictionary _letterMapper = new Dictionary + ( + _letters.Select((x, i) => new KeyValuePair(x, i)) + ); + + public int PatternSize { get; } + public int ImageSize { get; set; } + public int Width { get; } + public int Height { get; } + public int Planes { get; } + + private readonly PlaneCoordinate[] _decodePattern; + public Dictionary _encodePattern; + + private PatternList(PlaneCoordinate[] decodePattern, Dictionary encodePattern, int width, int height, int planes) + { + Width = width; + Height = height; + Planes = planes; + ImageSize = Width * Height * Planes; + + _decodePattern = decodePattern; + _encodePattern = encodePattern; + } + + public PlaneCoordinate GetDecodeIndex(int bitIndex) + { + if (bitIndex >= 0 && bitIndex < ImageSize) + return _decodePattern[bitIndex]; // % PatternSize]; + else + throw new ArgumentOutOfRangeException($"{nameof(GetDecodeIndex)} argument {nameof(bitIndex)} is out of range"); + } + + public int GetEncodeIndex(PlaneCoordinate coordinate) + { + if (_encodePattern.TryGetValue(coordinate, out var bitIndex)) + return bitIndex; + throw new KeyNotFoundException($"{nameof(GetEncodeIndex)} argument {nameof(coordinate)} ({coordinate.X}, {coordinate.Y}, {coordinate.P}) was found in the encoder remapping"); + } + + /// + /// Tries to create a PatternList + /// + /// List of pattern strings containing only valid pattern characters + /// Width of image in pixels + /// Height of image in pixels + /// Number of planes in the image + /// Combined size of all patterns in number of characters (bits) + /// + public static MagitekResult TryCreatePatternList(IList patterns, PixelPacking packing, int width, int height, int planes, int patternSize) + { + if (patternSize <= 0) + return new MagitekResult.Failed($"Pattern size ({patternSize}) must be greater than 0"); + + if (patterns?.Any() is false) + throw new ArgumentException($"{nameof(TryCreatePatternList)} parameter '{nameof(patterns)}' must contain items"); + + if (patterns.Any(x => string.IsNullOrWhiteSpace(x))) + throw new ArgumentException($"{nameof(TryCreatePatternList)} parameter '{nameof(patterns)}' contains items that are null or empty"); + + int patternsLengthSum = default; + if (packing == PixelPacking.Planar) + patternsLengthSum = patterns.Sum(x => x.Length); + else + patternsLengthSum = patterns.First().Length * planes; + + if (patternSize != patternsLengthSum) + return new MagitekResult.Failed($"The specified pattern size ({patternSize}) did not match the size of the pattern sequences ({patternsLengthSum})"); + + if (packing == PixelPacking.Planar && patterns.Count != planes) + throw new ArgumentException($"{nameof(PixelPacking)}.{PixelPacking.Planar} must contain the same number of patterns as the color depth"); + + if (packing == PixelPacking.Chunky && patterns.Count != 1) + return new MagitekResult.Failed($"{nameof(PixelPacking)}.{PixelPacking.Chunky} must contain only one pattern"); + + var imageSize = width * height * planes; + if (imageSize % patternSize != 0) + return new MagitekResult.Failed($"The image size ({imageSize}) is not an even multiple of the pattern size ({patternSize})"); + + if (patterns.Any(x => x.Length != patterns[0].Length)) + return new MagitekResult.Failed($"The pattern length of all patterns must be equal"); + + if (packing == PixelPacking.Planar) + return TryCreatePlanarPatternList(patterns, width, height, planes, patternSize); + else if (packing == PixelPacking.Chunky) + return TryCreateChunkyPatternList(patterns, width, height, planes, patternSize); + else + throw new NotSupportedException($"{nameof(TryCreatePatternList)} does not support {nameof(PixelPacking)} of value {packing}"); + } + + private static MagitekResult TryCreatePlanarPatternList(IList patterns, + int width, int height, int planes, int patternSize) + { + var freqMap = new Dictionary(_letterMapper.Keys.Select(x => new KeyValuePair(x, 0))); + int planeSize = height * width; + var imageSize = width * height * planes; + int planePatternSize = patternSize / planes; + + var decodePattern = Enumerable.Repeat(new PlaneCoordinate(0, 0, 0), imageSize).ToArray(); + short plane = 0; + + foreach (var pattern in patterns) + { + var planeDecodePattern = new int[pattern.Length]; + + var pixel = 0; + + // Map the pattern-defined portion of the plane + foreach (char letter in pattern) + { + if (!_letterMapper.TryGetValue(letter, out var baseIndex)) + return new MagitekResult.Failed($"Letter '{letter}' is not a valid pattern character"); + + var letterCount = freqMap[letter]; + + if (letterCount > 7) + return new MagitekResult.Failed($"Letter '{letter}' occurs more than 8 times across all patterns"); + + var mapIndex = freqMap[letter] + baseIndex * 8; + freqMap[letter]++; + + if (mapIndex > patternSize) + return new MagitekResult.Failed($"Letter '{letter}' cannot be mapped to index '{mapIndex}' because the max index is '{patternSize - 1}'"); + + planeDecodePattern[pixel] = mapIndex; + pixel++; + } + + // Extend pattern to fill entire plane + var extendedPattern = ExtendPattern(planeDecodePattern, width, height, plane, planePatternSize, patternSize); + foreach (var item in extendedPattern) + decodePattern[item.MapIndex] = item.Coordinate; + + plane++; + } + + var encodePattern = decodePattern.Select((x, i) => new { Coordinate = x, Index = i }) + .ToDictionary(x => x.Coordinate, x => x.Index); + + var patternList = new PatternList(decodePattern, encodePattern, width, height, planes); + return new MagitekResult.Success(patternList); + + static IEnumerable<(PlaneCoordinate Coordinate, int MapIndex)> ExtendPattern + (IList decodePattern, int width, int height, short plane, int planePatternSize, int patternSize) + { + int imageSize = width * height; + int pixelIndex = 0; + int repeat = 0; + + while (pixelIndex < width * height) + { + int extendSize = Math.Min(planePatternSize, imageSize); + for (int i = 0; i < extendSize; i++) + { + var bitIndex = decodePattern[pixelIndex % planePatternSize]; + short x = (short)(pixelIndex % width); + short y = (short)(pixelIndex / width); + var coord = new PlaneCoordinate(x, y, plane); + var index = bitIndex + repeat * patternSize; + + yield return (coord, index); + pixelIndex++; + } + + repeat++; + } + } + } + + private static MagitekResult TryCreateChunkyPatternList(IList patterns, + int width, int height, int planes, int patternSize) + { + var freqMap = new Dictionary(_letterMapper.Keys.Select(x => new KeyValuePair(x, 0))); + int planeSize = height * width; + var imageSize = width * height * planes; + int planePatternSize = patternSize / planes; + + var imageDecodePattern = Enumerable.Repeat(new PlaneCoordinate(0, 0, 0), imageSize).ToArray(); + + var maxInstancesPerCharacter = planes switch + { + 1 => 8, + 2 => 4, + 3 => 2, + 4 => 2, + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + _ => throw new ArgumentOutOfRangeException($"{nameof(TryCreateChunkyPatternList)} parameter {nameof(planes)} ({planes}) is out of range") + }; + + var pattern = patterns.First(); + var decodePattern = new int[pattern.Length * planes]; + + var pixel = 0; + + // Map the pattern-defined portion of the plane + foreach (char letter in pattern) + { + if (!_letterMapper.TryGetValue(letter, out var baseIndex)) + return new MagitekResult.Failed($"Letter '{letter}' is not a valid pattern character"); + + var letterCount = freqMap[letter]; + + if (letterCount > maxInstancesPerCharacter) + return new MagitekResult.Failed($"Letter '{letter}' occurs more than {maxInstancesPerCharacter} times across the pattern"); + + for (int i = 0; i < planes; i++) + { + var mapIndex = freqMap[letter] * planes + i + baseIndex * maxInstancesPerCharacter * planes; + + if (mapIndex > patternSize) + return new MagitekResult.Failed($"Letter '{letter}' cannot be mapped to index '{mapIndex}' because the max index is '{patternSize - 1}'"); + + decodePattern[pixel] = mapIndex; + pixel++; + } + freqMap[letter]++; + } + + // Extend pattern to fill entire image + var extendedPattern = ExtendChunkyPattern(decodePattern, width, height, planes, patternSize); + foreach (var item in extendedPattern) + imageDecodePattern[item.MapIndex] = item.Coordinate; + + var encodePattern = imageDecodePattern.Select((x, i) => new { Coordinate = x, Index = i }) + .ToDictionary(x => x.Coordinate, x => x.Index); + + var patternList = new PatternList(imageDecodePattern, encodePattern, width, height, planes); + return new MagitekResult.Success(patternList); + + static IEnumerable<(PlaneCoordinate Coordinate, int MapIndex)> ExtendChunkyPattern + (IList decodePattern, int width, int height, int bitsPerPixel, int patternSize) + { + int imageSize = width * height * bitsPerPixel; + int pixelIndex = 0; + int repeat = 0; + + while (pixelIndex < width * height * bitsPerPixel) + { + int extendSize = Math.Min(patternSize, imageSize); + for (int i = 0; i < extendSize; i++) + { + var bitIndex = decodePattern[pixelIndex % patternSize]; + short x = (short)((pixelIndex / bitsPerPixel) % width); + short y = (short)((pixelIndex / bitsPerPixel) / width); + short p = (short)(pixelIndex % bitsPerPixel); + var coord = new PlaneCoordinate(x, y, p); + var index = bitIndex + repeat * patternSize; + + yield return (coord, index); + pixelIndex++; + } + + repeat++; + } + } + } + } +} diff --git a/ImageMagitek/Codec/Generalized/PlaneCoordinate.cs b/ImageMagitek/Codec/Generalized/PlaneCoordinate.cs new file mode 100644 index 00000000..58f53c43 --- /dev/null +++ b/ImageMagitek/Codec/Generalized/PlaneCoordinate.cs @@ -0,0 +1,27 @@ +namespace ImageMagitek.Codec +{ + public readonly struct PlaneCoordinate + { + /// + /// X-coordinate + /// + public short X { get; } + + /// + /// Y-coordinate + /// + public short Y { get; } + + /// + /// Plane-coordinate + /// + public short P { get; } + + public PlaneCoordinate(short x, short y, short plane) + { + X = x; + Y = y; + P = plane; + } + } +} diff --git a/ImageMagitek/Codec/RepeatList.cs b/ImageMagitek/Codec/Generalized/RepeatList.cs similarity index 100% rename from ImageMagitek/Codec/RepeatList.cs rename to ImageMagitek/Codec/Generalized/RepeatList.cs diff --git a/ImageMagitek/Codec/ICodecFactory.cs b/ImageMagitek/Codec/ICodecFactory.cs index e8d70935..e73bb002 100644 --- a/ImageMagitek/Codec/ICodecFactory.cs +++ b/ImageMagitek/Codec/ICodecFactory.cs @@ -1,11 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Drawing; namespace ImageMagitek.Codec { public interface ICodecFactory { - IGraphicsCodec GetCodec(string codecName); - IGraphicsCodec GetCodec(string codecName, int width, int height, int rowStride = 0); + void AddOrUpdateCodec(Type codecType); + IGraphicsCodec GetCodec(string codecName, Size? elementSize); IGraphicsCodec CloneCodec(IGraphicsCodec codec); IEnumerable GetSupportedCodecNames(); } diff --git a/ImageMagitek/Codec/Serialization/IGraphicsFormatReader.cs b/ImageMagitek/Codec/Serialization/IGraphicsFormatReader.cs index 5c8f1b79..ded5b845 100644 --- a/ImageMagitek/Codec/Serialization/IGraphicsFormatReader.cs +++ b/ImageMagitek/Codec/Serialization/IGraphicsFormatReader.cs @@ -2,6 +2,6 @@ { public interface IGraphicsFormatReader { - MagitekResults LoadFromFile(string fileName); + MagitekResults LoadFromFile(string fileName); } } diff --git a/ImageMagitek/Codec/Serialization/XmlGraphicsFormatReader.cs b/ImageMagitek/Codec/Serialization/XmlGraphicsFormatReader.cs index 5be4d98e..5c2b1812 100644 --- a/ImageMagitek/Codec/Serialization/XmlGraphicsFormatReader.cs +++ b/ImageMagitek/Codec/Serialization/XmlGraphicsFormatReader.cs @@ -5,6 +5,7 @@ using System.IO; using System.Xml.Schema; using System.Collections.Generic; +using ImageMagitek.ExtensionMethods; namespace ImageMagitek.Codec { @@ -16,14 +17,24 @@ public XmlGraphicsFormatReader(string schemaFileName) { if (!string.IsNullOrWhiteSpace(schemaFileName)) { - using var schemaStream = File.OpenRead(schemaFileName); - _schemas.Add("", XmlReader.Create(schemaStream)); + if (!File.Exists(schemaFileName)) + { + throw new ArgumentException($"{nameof(schemaFileName)} cannot be found"); + } + else + { + using var schemaStream = File.OpenRead(schemaFileName); + _schemas.Add("", XmlReader.Create(schemaStream)); + } } + else + throw new ArgumentException($"{nameof(schemaFileName)} was null or empty"); } - public MagitekResults LoadFromFile(string fileName) + public MagitekResults LoadFromFile(string fileName) { - var format = new GraphicsFormat(); + if (!File.Exists(fileName)) + return new MagitekResults.Failed($"Codec file {fileName} does not exist"); using var stream = File.OpenRead(fileName); var doc = XDocument.Load(stream, LoadOptions.SetLineInfo); @@ -38,96 +49,287 @@ public MagitekResults LoadFromFile(string fileName) if (validationErrors.Any()) { validationErrors.Insert(0, $"Codec '{fileName}' failed to be validated"); - return new MagitekResults.Failed(validationErrors); + return new MagitekResults.Failed(validationErrors); } + return doc.Root?.Name.LocalName switch + { + "flowcodec" => ReadFlowCodec(doc.Root), + "patterncodec" => ReadPatternCodec(doc.Root), + _ => new MagitekResults.Failed($"Unrecognized codec root element '{doc.Root.Name}'") + }; + } - XElement formatNode = doc.Element("format"); + private MagitekResults ReadFlowCodec(XElement flowElementRoot) + { + var errors = new List(); - format.Name = formatNode.Attribute("name").Value; + var name = flowElementRoot.Attribute("name").Value; - var codecs = formatNode.Descendants("codec") - .Select(e => new - { - colortype = e.Descendants("colortype").First().Value, - colordepth = e.Descendants("colordepth").First().Value, - layout = e.Descendants("layout").First().Value, - height = e.Descendants("defaultheight").First().Value, - width = e.Descendants("defaultwidth").First().Value, - fixedsize = e.Descendants("fixedsize").First().Value, - mergepriority = e.Descendants("mergepriority").First().Value - }).First(); - - if (codecs.colortype == "indexed") - format.ColorType = PixelColorType.Indexed; - else if (codecs.colortype == "direct") - format.ColorType = PixelColorType.Direct; - else - throw new XmlException($"Unsupported colortype '{codecs.colortype}' while parsing codec '{fileName}'"); + var codec = new + { + ColorType = flowElementRoot.Element("colortype"), + ColorDepth = flowElementRoot.Element("colordepth"), + Layout = flowElementRoot.Element("layout"), + DefaultWidth = flowElementRoot.Element("defaultwidth"), + DefaultHeight = flowElementRoot.Element("defaultheight"), + FixedSize = flowElementRoot.Element("fixedsize"), + MergePriority = flowElementRoot.Element("mergepriority"), + Images = flowElementRoot.Element("images") + }; + + if (!int.TryParse(codec.DefaultWidth?.Value, out var defaultWidth)) + errors.Add($"Element width could not be parsed on {codec.DefaultWidth.LineNumber()}"); + + if (defaultWidth <= 1) + errors.Add($"Specified default width is too small on line {codec.DefaultWidth.LineNumber()}"); - format.ColorDepth = int.Parse(codecs.colordepth); + if (!int.TryParse(codec.DefaultHeight?.Value, out var defaultHeight)) + errors.Add($"Element height could not be parsed on {codec.DefaultHeight.LineNumber()}"); - if (codecs.layout == "tiled") - format.Layout = ImageLayout.Tiled; - else if (codecs.layout == "linear") - format.Layout = ImageLayout.Single; + if (defaultHeight <= 1) + errors.Add($"Specified default height is too small on line {codec.DefaultHeight.LineNumber()}"); + + PixelColorType colorType = default; + if (codec.ColorType?.Value == "indexed") + colorType = PixelColorType.Indexed; + else if (codec.ColorType?.Value == "direct") + colorType = PixelColorType.Direct; else - throw new XmlException($"Unsupported layout '{codecs.layout}' while parsing codec '{fileName}'"); + errors.Add($"Unrecognized colortype '{codec.ColorType}' on line {codec.ColorType.LineNumber()}"); - if (codecs.fixedsize == "true") - format.FixedSize = true; + if (!int.TryParse(codec.ColorDepth?.Value, out var colorDepth)) + errors.Add($"colordepth could not be parsed on {codec.ColorDepth.LineNumber()}"); - format.DefaultWidth = int.Parse(codecs.width); - format.DefaultHeight = int.Parse(codecs.height); - format.Width = format.DefaultWidth; - format.Height = format.DefaultHeight; - format.FixedSize = bool.Parse(codecs.fixedsize); + if (colorDepth < 1 && colorDepth > 32) + errors.Add($"colordepth contains an out of range value '{colorDepth}' on {codec.ColorDepth.LineNumber()}"); - string mergestring = codecs.mergepriority; - mergestring.Replace(" ", ""); - string[] mergeInts = mergestring.Split(','); + ImageLayout layout = default; + if (codec.Layout?.Value == "tiled") + layout = ImageLayout.Tiled; + else if (codec.Layout?.Value == "single") + layout = ImageLayout.Single; + else + errors.Add($"Unrecognized layout '{codec.Layout?.Value}' on line {codec.Layout.LineNumber()}"); - if (mergeInts.Length != format.ColorDepth) - throw new Exception("The number of entries in mergepriority does not match the colordepth while parsing codec '{fileName}'"); + bool fixedSize = false; + if (codec.FixedSize?.Value == "true") + fixedSize = true; + else if (codec.FixedSize?.Value == "false") + fixedSize = false; + else + errors.Add($"Unrecognized fixedsize value '{codec.FixedSize?.Value}' on line {codec.FixedSize.LineNumber()}"); - format.MergePlanePriority = new int[format.ColorDepth]; + var format = new FlowGraphicsFormat(name, colorType, colorDepth, layout, defaultWidth, defaultHeight); + format.FixedSize = fixedSize; - for (int i = 0; i < mergeInts.Length; i++) - format.MergePlanePriority[i] = int.Parse(mergeInts[i]); + string mergeString = codec.MergePriority?.Value ?? ""; + mergeString = mergeString.Replace(" ", ""); + var mergeItems = mergeString.Split(','); - var images = formatNode.Descendants("image") - .Select(e => new - { - colordepth = e.Descendants("colordepth").First().Value, - rowinterlace = e.Descendants("rowinterlace").First().Value, - rowpixelpattern = e.Descendants("rowpixelpattern") - }); + if (mergeItems.Length == format.ColorDepth) + { + format.MergePlanePriority = new int[format.ColorDepth]; + + for (int i = 0; i < mergeItems.Length; i++) + { + if (int.TryParse(mergeItems[i], out var mergePlane)) + format.MergePlanePriority[i] = mergePlane; + else + errors.Add($"Merge priority '{mergeItems[i]}' could not be parsed on {codec.MergePriority.LineNumber()}"); + } + } + else + errors.Add($"The number of entries in mergepriority does not match the colordepth on line {codec.MergePriority.LineNumber()}"); + + var imageElements = flowElementRoot.Element("images").Elements("image"); + var images = imageElements.Select(x => new + { + ColorDepth = x.Element("colordepth"), + RowInterlace = x.Element("rowinterlace"), + RowPixelPattern = x.Element("rowpixelpattern") + }); foreach (var image in images) { int[] rowPixelPattern; - if (image.rowpixelpattern.Any()) // Parse rowpixelpattern + if (image.RowPixelPattern is object) // Parse rowpixelpattern { - string order = image.rowpixelpattern.First().Value; - order = order.Replace(" ", ""); - string[] orderInts = order.Split(','); + string patternString = image.RowPixelPattern.Value; + patternString = patternString.Replace(" ", ""); + var patternInputs = patternString.Split(','); - rowPixelPattern = new int[orderInts.Length]; + rowPixelPattern = new int[patternInputs.Length]; - for (int i = 0; i < orderInts.Length; i++) - rowPixelPattern[i] = int.Parse(orderInts[i]); + for (int i = 0; i < patternInputs.Length; i++) + { + if (int.TryParse(patternInputs[i], out var patternPriority)) + rowPixelPattern[i] = patternPriority; + else + errors.Add($"rowpixelpattern value '{patternInputs[i]}' could not be parsed on {image.RowPixelPattern.LineNumber()}"); + } } else // Create a default rowpixelpattern { rowPixelPattern = new int[1]; } - ImageProperty ip = new ImageProperty(int.Parse(image.colordepth), bool.Parse(image.rowinterlace), rowPixelPattern); + if (!int.TryParse(image.ColorDepth.Value, out var imageColorDepth)) + errors.Add($"colordepth could not be parsed on {codec.DefaultHeight.LineNumber()}"); + + if (!bool.TryParse(image.RowInterlace.Value, out var imageRowInterlace)) + errors.Add($"rowinterlace could not be parsed on {codec.DefaultHeight.LineNumber()}"); + + ImageProperty ip = new ImageProperty(imageColorDepth, imageRowInterlace, rowPixelPattern); format.ImageProperties.Add(ip); } - return new MagitekResults.Success(format); + var sumColorDepth = format.ImageProperties.Sum(x => x.ColorDepth); + if (format.ColorDepth != sumColorDepth) + { + errors.Add($"Codec's colordepth '{format.ColorDepth}' does not match the sum of all image colordepth elements '{sumColorDepth}'"); + } + + if (errors.Any()) + return new MagitekResults.Failed(errors); + else + return new MagitekResults.Success(format); + } + + private MagitekResults ReadPatternCodec(XElement patternElementRoot) + { + var errors = new List(); + + var name = patternElementRoot.Attribute("name").Value; + + var codec = new + { + ColorType = patternElementRoot.Element("colortype"), + ColorDepth = patternElementRoot.Element("colordepth"), + Layout = patternElementRoot.Element("layout"), + Packing = patternElementRoot.Element("packing"), + MergePriority = patternElementRoot.Element("mergepriority"), + RowPixelPattern = patternElementRoot.Element("rowpixelpattern"), + Width = patternElementRoot.Element("width"), + Height = patternElementRoot.Element("height"), + Patterns = patternElementRoot.Element("patterns") + }; + + if (!int.TryParse(codec.Width?.Value, out var width)) + errors.Add($"Element width could not be parsed on {codec.Width.LineNumber()}"); + + if (width <= 1) + errors.Add($"Specified width is too small on line {codec.Width.LineNumber()}"); + + if (!int.TryParse(codec.Height?.Value, out var height)) + errors.Add($"Element height could not be parsed on {codec.Height.LineNumber()}"); + + if (height <= 1) + errors.Add($"Specified height is too small on line {codec.Height.LineNumber()}"); + + PixelColorType colorType = default; + if (codec.ColorType?.Value == "indexed") + colorType = PixelColorType.Indexed; + else if (codec.ColorType?.Value == "direct") + colorType = PixelColorType.Direct; + else + errors.Add($"Unrecognized colortype '{codec.ColorType}' on line {codec.ColorType.LineNumber()}"); + + if (!int.TryParse(codec.ColorDepth?.Value, out var colorDepth)) + errors.Add($"colordepth could not be parsed on {codec.ColorDepth.LineNumber()}"); + + if (colorDepth < 1 && colorDepth > 32) + errors.Add($"colordepth contains an out of range value '{colorDepth}' on {codec.ColorDepth.LineNumber()}"); + + ImageLayout layout = default; + if (codec.Layout?.Value == "tiled") + layout = ImageLayout.Tiled; + else if (codec.Layout?.Value == "single") + layout = ImageLayout.Single; + else + errors.Add($"Unrecognized layout '{codec.Layout?.Value}' on line {codec.Layout.LineNumber()}"); + + PixelPacking packing = default; + if (codec.Packing?.Value == "planar") + packing = PixelPacking.Planar; + else if (codec.Packing?.Value == "chunky") + packing = PixelPacking.Chunky; + else + errors.Add($"Unrecognized layout '{codec.Packing?.Value}' on line {codec.Packing.LineNumber()}"); + + var format = new PatternGraphicsFormat(name, colorType, colorDepth, layout, packing, width, height); + + int[] rowPixelPattern; + var patternString = codec.RowPixelPattern?.Value; + if (patternString is object) + { + patternString = patternString.Replace(" ", ""); + var patternInputs = patternString.Split(','); + + rowPixelPattern = new int[patternInputs.Length]; + + for (int i = 0; i < patternInputs.Length; i++) + { + if (int.TryParse(patternInputs[i], out var patternPriority)) + rowPixelPattern[i] = patternPriority; + else + errors.Add($"rowpixelpattern value '{patternInputs[i]}' could not be parsed on {codec.RowPixelPattern.LineNumber()}"); + } + } + else // Create a default rowpixelpattern + { + rowPixelPattern = new int[1]; + } + + format.RowPixelPattern = new RepeatList(rowPixelPattern); + + string mergeString = codec.MergePriority?.Value ?? ""; + mergeString = mergeString.Replace(" ", ""); + var mergeItems = mergeString.Split(','); + + if (mergeItems.Length == format.ColorDepth) + { + format.MergePlanePriority = new int[format.ColorDepth]; + + for (int i = 0; i < mergeItems.Length; i++) + { + if (int.TryParse(mergeItems[i], out var mergePlane)) + format.MergePlanePriority[i] = mergePlane; + else + errors.Add($"Merge priority '{mergeItems[i]}' could not be parsed on {codec.MergePriority.LineNumber()}"); + } + } + else + errors.Add($"The number of entries in mergepriority does not match the colordepth on line {codec.MergePriority.LineNumber()}"); + + var sizeElement = patternElementRoot.Element("patterns").Attribute("size"); + + if (!int.TryParse(sizeElement?.Value, out var patternSize)) + errors.Add($"Pattern size could not be parsed on {codec.Width.LineNumber()}"); + + if (patternSize < 1 || patternSize > PatternList.MaxPatternSize) + errors.Add($"Pattern size contains an out of range value '{patternSize}' {codec.Width.LineNumber()}"); + + var patternStrings = patternElementRoot + .Element("patterns") + .Elements("pattern") + .Select(x => string.Join("", x.Value.Where(c => !char.IsWhiteSpace(c)))) + .ToArray(); + + var patternResult = PatternList.TryCreatePatternList(patternStrings, packing, width, height, colorDepth, patternSize); + + patternResult.Switch( + success => + { + format.SetPattern(success.Result); + }, + failed => errors.Add(failed.Reason)); + + if (errors.Any()) + return new MagitekResults.Failed(errors); + else + return new MagitekResults.Success(format); } } } diff --git a/ImageMagitek/Codec/Specialized/Direct/Psx16bppCodec.cs b/ImageMagitek/Codec/Specialized/Direct/Psx16bppCodec.cs index c0df1959..15fd4652 100644 --- a/ImageMagitek/Codec/Specialized/Direct/Psx16bppCodec.cs +++ b/ImageMagitek/Codec/Specialized/Direct/Psx16bppCodec.cs @@ -1,5 +1,6 @@ using System; using ImageMagitek.Colors; +using ImageMagitek.Colors.Converters; namespace ImageMagitek.Codec { @@ -21,6 +22,7 @@ public class Psx16bppCodec : DirectCodec public override int DefaultHeight => 64; private BitStream _bitStream; + private readonly ColorConverterAbgr16 _colorConverter = new ColorConverterAbgr16(); public Psx16bppCodec(int width, int height) { @@ -47,8 +49,8 @@ public Psx16bppCodec(int width, int height) { uint packedColor = _bitStream.ReadByte(); packedColor |= (uint)_bitStream.ReadByte() << 8; - var colorAbgr16 = ColorFactory.CreateColor(ColorModel.ABGR16, packedColor); - _nativeBuffer[x, y] = ColorConverter.ToNative(colorAbgr16); + var abgr16 = new ColorAbgr16(packedColor); + _nativeBuffer[x, y] = _colorConverter.ToNativeColor(abgr16); } } @@ -67,7 +69,7 @@ public override ReadOnlySpan EncodeElement(in ArrangerElement el, ColorRgb for (int x = 0; x < el.Width; x++) { var imageColor = imageBuffer[x, y]; - var fc = ColorConverter.ToForeign(imageColor, ColorModel.ABGR16); + var fc = _colorConverter.ToForeignColor(imageColor); byte high = (byte)(fc.Color & 0xFF00); byte low = (byte)(fc.Color & 0xFF); diff --git a/ImageMagitek/Codec/Specialized/Indexed/Nes1bppCodec.cs b/ImageMagitek/Codec/Specialized/Indexed/Nes1bppCodec.cs index de9d7663..99abbedf 100644 --- a/ImageMagitek/Codec/Specialized/Indexed/Nes1bppCodec.cs +++ b/ImageMagitek/Codec/Specialized/Indexed/Nes1bppCodec.cs @@ -15,8 +15,6 @@ public sealed class Nes1bppCodec : IndexedCodec public override int DefaultWidth => 8; public override int DefaultHeight => 8; - public override int RowStride => 0; - public override int ElementStride => 0; public override int WidthResizeIncrement => 1; public override int HeightResizeIncrement => 1; public override bool CanResize => true; diff --git a/ImageMagitek/Codec/Specialized/Indexed/Psx4bppCodec.cs b/ImageMagitek/Codec/Specialized/Indexed/Psx4bppCodec.cs index 6c46432e..8d1e4f93 100644 --- a/ImageMagitek/Codec/Specialized/Indexed/Psx4bppCodec.cs +++ b/ImageMagitek/Codec/Specialized/Indexed/Psx4bppCodec.cs @@ -13,8 +13,6 @@ public sealed class Psx4bppCodec : IndexedCodec public override int StorageSize => Width * Height * 4; public override int DefaultWidth => 64; public override int DefaultHeight => 64; - public override int RowStride => 0; - public override int ElementStride => 0; public override int WidthResizeIncrement => 2; public override int HeightResizeIncrement => 1; public override bool CanResize => true; diff --git a/ImageMagitek/Codec/Specialized/Indexed/Psx8bppCodec.cs b/ImageMagitek/Codec/Specialized/Indexed/Psx8bppCodec.cs index accb0d17..fafc932e 100644 --- a/ImageMagitek/Codec/Specialized/Indexed/Psx8bppCodec.cs +++ b/ImageMagitek/Codec/Specialized/Indexed/Psx8bppCodec.cs @@ -13,8 +13,6 @@ public sealed class Psx8bppCodec : IndexedCodec public override int DefaultWidth => 64; public override int DefaultHeight => 64; - public override int RowStride => 0; - public override int ElementStride => 0; public override int WidthResizeIncrement => 1; public override int HeightResizeIncrement => 1; public override bool CanResize => true; diff --git a/ImageMagitek/Codec/Specialized/Indexed/Snes3bppCodec.cs b/ImageMagitek/Codec/Specialized/Indexed/Snes3bppCodec.cs index 1d8f3353..e64055d5 100644 --- a/ImageMagitek/Codec/Specialized/Indexed/Snes3bppCodec.cs +++ b/ImageMagitek/Codec/Specialized/Indexed/Snes3bppCodec.cs @@ -13,8 +13,6 @@ public sealed class Snes3bppCodec : IndexedCodec public override int DefaultWidth => 8; public override int DefaultHeight => 8; - public override int RowStride => 0; - public override int ElementStride => 0; public override int WidthResizeIncrement => 1; public override int HeightResizeIncrement => 1; public override bool CanResize => true; diff --git a/ImageMagitek/Colors/ColorConverter.cs b/ImageMagitek/Colors/ColorConverter.cs deleted file mode 100644 index b5e3f7ae..00000000 --- a/ImageMagitek/Colors/ColorConverter.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using ImageMagitek.Colors.Converters; - -namespace ImageMagitek.Colors -{ - public static class ColorConverter - { - public static ColorConverterBgr15 Bgr15 { get; } = new ColorConverterBgr15(); - public static ColorConverterAbgr16 Abgr16 { get; } = new ColorConverterAbgr16(); - - public static ColorRgba32 ToNative(IColor32 color) - { - switch(color) - { - case ColorBgr15 colorBgr15: - return Bgr15.ToNativeColor(colorBgr15); - case ColorAbgr16 colorAbgr16: - return Abgr16.ToNativeColor(colorAbgr16); - case ColorRgba32 _: - return new ColorRgba32(color.Color); - default: - var defaultColor = new ColorRgba32(); - defaultColor.ColorVector = color.ColorVector; - return defaultColor; - } - } - - public static IColor32 ToForeign(ColorRgba32 color, ColorModel colorModel) - { - switch (colorModel) - { - case ColorModel.RGBA32: - return new ColorRgba32(color.Color); - case ColorModel.BGR15: - return Bgr15.ToForeignColor(color); - case ColorModel.ABGR16: - return Abgr16.ToForeignColor(color); - //case ColorModel.RGB24: - // throw new NotImplementedException(); - //case ColorModel.ARGB32: - // throw new NotImplementedException(); - //case ColorModel.RGB15: - // throw new NotImplementedException(); - //case ColorModel.NES: - // throw new NotImplementedException(); - default: - throw new NotImplementedException(); - } - } - } -} diff --git a/ImageMagitek/Colors/ColorFactory.cs b/ImageMagitek/Colors/ColorFactory.cs index 6e6ff877..7d41268b 100644 --- a/ImageMagitek/Colors/ColorFactory.cs +++ b/ImageMagitek/Colors/ColorFactory.cs @@ -1,47 +1,116 @@ using System; +using System.Collections.Generic; +using ImageMagitek.Colors.Converters; namespace ImageMagitek.Colors { - public static class ColorFactory + public interface IColorFactory { - public static IColor32 CreateColor(ColorModel model, uint color = 0) + /// + /// Creates a new color of the specified ColorModel with a default color value + /// + IColor CreateColor(ColorModel model); + + /// + /// Creates a new color of the specified ColorModel with the specified foreign color value + /// + IColor CreateColor(ColorModel model, uint color); + + /// + /// Creates a new color of the specified ColorModel with the specified foreign color value components + /// + IColor CreateColor(ColorModel colorModel, int r, int g, int b, int a); + + /// + /// Deep clones the supplied color + /// + IColor CloneColor(IColor color); + + /// + /// Converts the specified color to the native color type + /// + ColorRgba32 ToNative(IColor color); + + /// + /// Converts the specified native color to a foreign color type + /// + IColor ToForeign(ColorRgba32 color, ColorModel colorModel); + } + + /// + public class ColorFactory : IColorFactory + { + private readonly ColorConverterBgr15 _bgr15Converter = new ColorConverterBgr15(); + private readonly ColorConverterAbgr16 _abgr16Converter = new ColorConverterAbgr16(); + private ColorConverterNes _nesConverter; + + public void SetNesPalette(Palette nesPalette) + { + _nesConverter = new ColorConverterNes(nesPalette); + } + + public IColor CreateColor(ColorModel model) => CreateColor(model, 0); + + public IColor CreateColor(ColorModel colorModel, uint color) + { + return colorModel switch + { + ColorModel.Rgba32 => new ColorRgba32(color), + ColorModel.Bgr15 => new ColorBgr15(color), + ColorModel.Abgr16 => new ColorAbgr16(color), + ColorModel.Nes => new ColorNes(color), + _ => throw new NotSupportedException($"{nameof(ColorModel)} '{colorModel}' is not supported") + }; + } + + public IColor CreateColor(ColorModel colorModel, int r, int g, int b, int a) + { + return colorModel switch + { + ColorModel.Rgba32 => new ColorRgba32((byte)r, (byte)g, (byte)b, (byte)a), + ColorModel.Bgr15 => new ColorBgr15((byte)r, (byte)g, (byte)b), + ColorModel.Abgr16 => new ColorAbgr16((byte)r, (byte)g, (byte)b, (byte)a), + ColorModel.Nes => _nesConverter.ToForeignColor(new ColorRgba32((byte)r, (byte)g, (byte)b, (byte)a)), + _ => throw new NotSupportedException($"{nameof(ColorModel)} '{colorModel}' is not supported") + }; + } + + public IColor CloneColor(IColor color) + { + return color switch + { + ColorRgba32 rgba32 => new ColorRgba32(rgba32.Color), + ColorBgr15 bgr15 => new ColorBgr15(bgr15.R, bgr15.G, bgr15.B), + ColorAbgr16 abgr16 => new ColorAbgr16(abgr16.R, abgr16.G, abgr16.B, abgr16.A), + ColorNes nes => new ColorNes(color.Color), + _ => throw new NotSupportedException($"{nameof(IColor)} '{color}' is not supported") + }; + } + + public ColorRgba32 ToNative(IColor color) { - switch (model) + return color switch { - //case ColorModel.RGB24: - // break; - case ColorModel.RGBA32: - return new ColorRgba32(color); - //case ColorModel.ARGB32: - // break; - case ColorModel.BGR15: - var colorBgr15 = new ColorBgr15(); - colorBgr15.Color = color; - return colorBgr15; - case ColorModel.ABGR16: - var colorAbgr16 = new ColorAbgr16(); - colorAbgr16.Color = color; - return colorAbgr16; - //case ColorModel.RGB15: - // break; - //case ColorModel.NES: - // break; - } - throw new NotImplementedException(); + ColorBgr15 colorBgr15 => _bgr15Converter.ToNativeColor(colorBgr15), + ColorAbgr16 colorAbgr16 => _abgr16Converter.ToNativeColor(colorAbgr16), + ColorRgba32 _ => new ColorRgba32(color.Color), + ColorNes colorNes => _nesConverter?.ToNativeColor(colorNes) ?? + throw new ArgumentException($"{nameof(ToNative)} has no NES color converter defined"), + _ => throw new NotSupportedException($"{nameof(ToNative)} '{color}' is not supported"), + }; } - public static IColor32 CloneColor(IColor32 color) + public IColor ToForeign(ColorRgba32 color, ColorModel colorModel) { - switch(color) + return colorModel switch { - case ColorRgba32 _: - return new ColorRgba32(color.Color); - case ColorBgr15 _: - return new ColorBgr15(color.R, color.G, color.B); - case ColorAbgr16 _: - return new ColorAbgr16(color.R, color.G, color.B, color.A); - } - throw new NotImplementedException(); + ColorModel.Rgba32 => new ColorRgba32(color.Color), + ColorModel.Bgr15 => _bgr15Converter.ToForeignColor(color), + ColorModel.Abgr16 => _abgr16Converter.ToForeignColor(color), + ColorModel.Nes => _nesConverter?.ToForeignColor(color) ?? + throw new ArgumentException($"{nameof(ToForeign)} has no NES color converter defined"), + _ => throw new NotSupportedException($"{nameof(ToForeign)} '{colorModel}' is not supported"), + }; } } } diff --git a/ImageMagitek/Colors/ColorFormats/ColorAbgr16.cs b/ImageMagitek/Colors/ColorFormats/ColorAbgr16.cs index 2faecc15..3bce5b78 100644 --- a/ImageMagitek/Colors/ColorFormats/ColorAbgr16.cs +++ b/ImageMagitek/Colors/ColorFormats/ColorAbgr16.cs @@ -10,6 +10,14 @@ public struct ColorAbgr16 : IColor32 public byte b; public byte a; + public ColorAbgr16(uint foreignColor) + { + r = (byte)(foreignColor & 0x1f); + g = (byte)((foreignColor & 0x3e0) >> 5); + b = (byte)((foreignColor & 0x7c00) >> 10); + a = (byte)((foreignColor & 0x8000) >> 15); + } + public ColorAbgr16(byte red, byte green, byte blue, byte alpha) { r = red; diff --git a/ImageMagitek/Colors/ColorFormats/ColorBgr15.cs b/ImageMagitek/Colors/ColorFormats/ColorBgr15.cs index e47f7c4f..471aa874 100644 --- a/ImageMagitek/Colors/ColorFormats/ColorBgr15.cs +++ b/ImageMagitek/Colors/ColorFormats/ColorBgr15.cs @@ -10,6 +10,14 @@ public struct ColorBgr15 : IColor32 public byte b; public byte a; + public ColorBgr15(uint foreignColor) + { + r = (byte)(foreignColor & 0x1f); + g = (byte)((foreignColor & 0x3e0) >> 5); + b = (byte)((foreignColor & 0x7c00) >> 10); + a = 0; + } + public ColorBgr15(byte red, byte green, byte blue) { r = red; diff --git a/ImageMagitek/Colors/ColorFormats/ColorNes.cs b/ImageMagitek/Colors/ColorFormats/ColorNes.cs new file mode 100644 index 00000000..226ca3cc --- /dev/null +++ b/ImageMagitek/Colors/ColorFormats/ColorNes.cs @@ -0,0 +1,22 @@ +using System; + +namespace ImageMagitek.Colors +{ + public struct ColorNes : ITableColor + { + private uint _color; + public uint Color + { + get => _color; + set => _color = Math.Clamp(value, 0, (uint)ColorMax); + } + + public int Size => 8; + public int ColorMax => 63; + + public ColorNes(uint foreignColor) + { + _color = Math.Clamp(foreignColor, 0, 63); + } + } +} diff --git a/ImageMagitek/Colors/Converters/ColorConverterAbgr16.cs b/ImageMagitek/Colors/Converters/ColorConverterAbgr16.cs index ffcdc8f7..809fd8e0 100644 --- a/ImageMagitek/Colors/Converters/ColorConverterAbgr16.cs +++ b/ImageMagitek/Colors/Converters/ColorConverterAbgr16.cs @@ -1,4 +1,4 @@ -namespace ImageMagitek.Colors +namespace ImageMagitek.Colors.Converters { public enum AlphaBitTransparency { Transparent, BlackTransparent, SemiTransparent, Opaque } public sealed class ColorConverterAbgr16 : IColorConverter diff --git a/ImageMagitek/Colors/Converters/ColorConverterNes.cs b/ImageMagitek/Colors/Converters/ColorConverterNes.cs new file mode 100644 index 00000000..8cc1997c --- /dev/null +++ b/ImageMagitek/Colors/Converters/ColorConverterNes.cs @@ -0,0 +1,28 @@ +using System; + +namespace ImageMagitek.Colors.Converters +{ + public class ColorConverterNes : IColorConverter + { + private readonly Palette _nesPalette; + + public ColorConverterNes(Palette nesPalette) + { + _nesPalette = nesPalette; + } + + public ColorNes ToForeignColor(ColorRgba32 nc) + { + if (_nesPalette.TryGetIndexByNativeColor(nc, ColorMatchStrategy.Nearest, out var index)) + { + return (ColorNes) _nesPalette.GetForeignColor(index); + } + throw new ArgumentException($"{nameof(ToForeignColor)} parameter (R: {nc.R}, G: {nc.G}, B: {nc.B}, A: {nc.A}) could not be matched in palette '{_nesPalette.Name}'"); + } + + public ColorRgba32 ToNativeColor(ColorNes fc) + { + return _nesPalette.GetNativeColor((int) fc.Color); + } + } +} diff --git a/ImageMagitek/Colors/IColorConverter.cs b/ImageMagitek/Colors/Converters/IColorConverter.cs similarity index 78% rename from ImageMagitek/Colors/IColorConverter.cs rename to ImageMagitek/Colors/Converters/IColorConverter.cs index 8e15d190..b5d97017 100644 --- a/ImageMagitek/Colors/IColorConverter.cs +++ b/ImageMagitek/Colors/Converters/IColorConverter.cs @@ -1,6 +1,6 @@ -namespace ImageMagitek.Colors +namespace ImageMagitek.Colors.Converters { - public interface IColorConverter where TColor : IColor32 + public interface IColorConverter where TColor : IColor { TColor ToForeignColor(ColorRgba32 nc); ColorRgba32 ToNativeColor(TColor fc); diff --git a/ImageMagitek/Colors/IColor.cs b/ImageMagitek/Colors/IColor.cs new file mode 100644 index 00000000..96a96313 --- /dev/null +++ b/ImageMagitek/Colors/IColor.cs @@ -0,0 +1,8 @@ +namespace ImageMagitek.Colors +{ + public interface IColor + { + uint Color { get; set; } + int Size { get; } + } +} diff --git a/ImageMagitek/Colors/IColor32.cs b/ImageMagitek/Colors/IColor32.cs index 0dcd4839..350730cc 100644 --- a/ImageMagitek/Colors/IColor32.cs +++ b/ImageMagitek/Colors/IColor32.cs @@ -2,15 +2,14 @@ namespace ImageMagitek.Colors { - public interface IColor32 + public interface IColor32 : IColor { - uint Color { get; set; } Vector4 ColorVector { get; set; } byte R { get; set; } byte G { get; set; } byte B { get; set; } byte A { get; set; } - int Size { get; } + int AlphaMax { get; } int RedMax { get; } int GreenMax { get; } diff --git a/ImageMagitek/Colors/ITableColor.cs b/ImageMagitek/Colors/ITableColor.cs new file mode 100644 index 00000000..4ef6b6e4 --- /dev/null +++ b/ImageMagitek/Colors/ITableColor.cs @@ -0,0 +1,7 @@ +namespace ImageMagitek.Colors +{ + public interface ITableColor : IColor + { + public int ColorMax { get; } + } +} diff --git a/ImageMagitek/Colors/Palette.cs b/ImageMagitek/Colors/Palette.cs index 506f2c84..c2ff8104 100644 --- a/ImageMagitek/Colors/Palette.cs +++ b/ImageMagitek/Colors/Palette.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Drawing; using ColorMine.ColorSpaces.Comparisons; -using ColorMine.ColorSpaces.Conversions; using ImageMagitek.Project; using ImageMagitek.ExtensionMethods; @@ -15,7 +14,7 @@ namespace ImageMagitek.Colors public enum ColorMatchStrategy { Exact, Nearest } //public enum ColorModel { RGBA32 = 0, RGB24, ARGB32, BGR15, ABGR16, RGB15, NES } - public enum ColorModel { RGBA32 = 0, BGR15 = 3, ABGR16 = 4 } + public enum ColorModel { Rgba32 = 0, Bgr15 = 3, Abgr16 = 4, Nes = 6 } /// /// Storage source of the palette @@ -31,6 +30,8 @@ public enum PaletteStorageSource { DataFile = 0, ProjectFile, Json } public class Palette : IProjectResource { private static readonly Cie94Comparison _comparator = new Cie94Comparison(Cie94Comparison.Application.GraphicArts); + private readonly IColorFactory _colorFactory; + private readonly IPaletteBinarySerializer _paletteSerializer; public string Name { get; set; } public bool CanContainChildResources => false; @@ -80,25 +81,30 @@ public class Palette : IProjectResource /// /// Gets the internal palette containing foreign colors /// - IColor32[] ForeignPalette { get => _foreignPalette.Value; } - Lazy _foreignPalette; + IColor[] ForeignPalette { get => _foreignPalette.Value; } + Lazy _foreignPalette; /// /// Constructs a new named Palette object /// /// Name of the palette - public Palette(string PaletteName) + public Palette(string PaletteName, IColorFactory colorFactory) { Name = PaletteName; + _colorFactory = colorFactory; + _paletteSerializer = new PaletteBinarySerializer(_colorFactory); HasAlpha = false; ZeroIndexTransparent = true; } - public Palette(string name, ColorModel colorModel, FileBitAddress fileAddress, + public Palette(string name, IColorFactory colorFactory, ColorModel colorModel, FileBitAddress fileAddress, int entries, bool zeroIndexTransparent, PaletteStorageSource storageSource) { Name = name; + _colorFactory = colorFactory; + _paletteSerializer = new PaletteBinarySerializer(_colorFactory); + ColorModel = colorModel; FileAddress = fileAddress; Entries = entries; @@ -108,12 +114,12 @@ public Palette(string name, ColorModel colorModel, FileBitAddress fileAddress, if(storageSource == PaletteStorageSource.DataFile) { _nativePalette = new Lazy(() => LoadNativePalette()); - _foreignPalette = new Lazy(() => LoadForeignPalette()); + _foreignPalette = new Lazy(() => LoadForeignPalette()); } else if(storageSource == PaletteStorageSource.Json) { _nativePalette = new Lazy(() => new ColorRgba32[Entries]); - _foreignPalette = new Lazy(() => new IColor32[Entries]); + _foreignPalette = new Lazy(() => new IColor[Entries]); } } @@ -148,7 +154,7 @@ public bool LazyLoadPalette(DataFile dataFile, FileBitAddress address, ColorMode StorageSource = PaletteStorageSource.DataFile; _nativePalette = new Lazy(() => LoadNativePalette()); - _foreignPalette = new Lazy(() => LoadForeignPalette()); + _foreignPalette = new Lazy(() => LoadForeignPalette()); return true; } @@ -157,7 +163,7 @@ private ColorRgba32[] LoadNativePalette() { var nativePalette = new ColorRgba32[Entries]; for(int i = 0; i < Entries; i++) - nativePalette[i] = ColorConverter.ToNative(ForeignPalette[i]); // Will load ForeignPalette if not already loaded + nativePalette[i] = _colorFactory.ToNative(ForeignPalette[i]); // Will load ForeignPalette if not already loaded if (ZeroIndexTransparent) nativePalette[0].Color &= 0x00FFFFFF; @@ -173,9 +179,9 @@ private ColorRgba32[] LoadNativePalette() /// or /// Palette formats with entry sizes larger than 4 bytes are not supported /// - private IColor32[] LoadForeignPalette() + private IColor[] LoadForeignPalette() { - return PaletteBinarySerializer.ReadPalette(DataFile, FileAddress, ColorModel, Entries); + return _paletteSerializer.ReadPalette(DataFile, FileAddress, ColorModel, Entries); } /// @@ -209,7 +215,7 @@ public ColorRgba32 GetNativeColor(int index) return NativePalette[index]; } - public IColor32 GetForeignColor(int index) + public IColor GetForeignColor(int index) { if (ForeignPalette is null) throw new ArgumentNullException($"{nameof(GetForeignColor)} property '{nameof(ForeignPalette)}' was null"); @@ -372,7 +378,7 @@ public byte GetIndexByNativeColor(ColorRgba32 color, ColorMatchStrategy matchStr /// /// Zero-based palette index /// Color to assign to the foreign palette - public void SetForeignColor(int index, IColor32 foreignColor) + public void SetForeignColor(int index, IColor foreignColor) { if (ForeignPalette is null) throw new NullReferenceException($"{nameof(SetForeignColor)} property '{nameof(ForeignPalette)}' was null"); @@ -381,7 +387,7 @@ public void SetForeignColor(int index, IColor32 foreignColor) throw new ArgumentOutOfRangeException($"{nameof(GetForeignColor)} parameter '{nameof(index)}' was out of range"); ForeignPalette[index] = foreignColor; - NativePalette[index] = ColorConverter.ToNative(foreignColor); + NativePalette[index] = _colorFactory.ToNative(foreignColor); } /// @@ -391,11 +397,7 @@ public void SetForeignColor(int index, IColor32 foreignColor) /// Zero-based palette index public void SetForeignColor(int index, byte R, byte G, byte B, byte A) { - var fc = ColorFactory.CreateColor(ColorModel); - fc.R = R; - fc.G = G; - fc.B = B; - fc.A = A; + var fc = _colorFactory.CreateColor(ColorModel, R, G, B, A); SetForeignColor(index, fc); } @@ -415,7 +417,7 @@ public void SetNativeColor(int index, ColorRgba32 nativeColor) throw new ArgumentOutOfRangeException($"{nameof(GetNativeColor)} parameter '{nameof(index)}' was out of range"); NativePalette[index] = nativeColor; - ForeignPalette[index] = ColorConverter.ToForeign(nativeColor, ColorModel); + ForeignPalette[index] = _colorFactory.ToForeign(nativeColor, ColorModel); } /// @@ -425,11 +427,7 @@ public void SetNativeColor(int index, ColorRgba32 nativeColor) /// Zero-based palette index public void SetNativeColor(int index, byte R, byte G, byte B, byte A) { - var nc = ColorFactory.CreateColor(ColorModel.RGBA32); - nc.R = R; - nc.G = G; - nc.B = B; - nc.A = A; + var nc = _colorFactory.CreateColor(ColorModel.Rgba32, R, G, B, A); SetForeignColor(index, nc); } @@ -441,7 +439,7 @@ public void SetNativeColor(int index, byte R, byte G, byte B, byte A) public bool SavePalette() { if(StorageSource == PaletteStorageSource.DataFile) - PaletteBinarySerializer.WritePalette(DataFile, FileAddress, ForeignPalette); + _paletteSerializer.WritePalette(DataFile, FileAddress, ForeignPalette); return true; } @@ -455,20 +453,14 @@ public static ColorModel StringToColorModel(string ColorModelName) { switch(ColorModelName) { - //case "RGB24": - // return ColorModel.RGB24; - //case "ARGB32": - // return ColorModel.ARGB32; - case "RGBA32": - return ColorModel.RGBA32; - case "BGR15": - return ColorModel.BGR15; - case "ABGR16": - return ColorModel.ABGR16; - //case "RGB15": - // return ColorModel.RGB15; - //case "NES": - // return ColorModel.NES; + case "Rgba32": + return ColorModel.Rgba32; + case "Bgr15": + return ColorModel.Bgr15; + case "Abgr16": + return ColorModel.Abgr16; + case "Nes": + return ColorModel.Nes; default: throw new ArgumentException($"{nameof(StringToColorModel)} {nameof(ColorModel)} '{ColorModelName}' is not supported"); } @@ -478,20 +470,14 @@ public static string ColorModelToString(ColorModel model) { switch (model) { - //case ColorModel.RGB24: - // return "RGB24"; - //case ColorModel.ARGB32: - // return "ARGB32"; - case ColorModel.BGR15: - return "BGR15"; - case ColorModel.ABGR16: - return "ABGR16"; - case ColorModel.RGBA32: - return "RGBA32"; - //case ColorModel.RGB15: - // return "RGB15"; - //case ColorModel.NES: - // return "NES"; + case ColorModel.Bgr15: + return "Bgr15"; + case ColorModel.Abgr16: + return "Abgr16"; + case ColorModel.Rgba32: + return "Rgba32"; + case ColorModel.Nes: + return "Nes"; default: throw new ArgumentException($"{nameof(ColorModelToString)} {nameof(ColorModel)} '{model}' is not supported"); } diff --git a/ImageMagitek/Colors/PaletteBinarySerializer.cs b/ImageMagitek/Colors/PaletteBinarySerializer.cs index c48c5d9c..dc519adb 100644 --- a/ImageMagitek/Colors/PaletteBinarySerializer.cs +++ b/ImageMagitek/Colors/PaletteBinarySerializer.cs @@ -7,16 +7,29 @@ namespace ImageMagitek.Colors { - public static class PaletteBinarySerializer + public interface IPaletteBinarySerializer { - public static IColor32[] ReadPalette(DataFile df, FileBitAddress address, ColorModel colorModel, int entries) + IColor[] ReadPalette(DataFile df, FileBitAddress address, ColorModel colorModel, int entries); + void WritePalette(DataFile df, FileBitAddress address, IEnumerable colors); + } + + public class PaletteBinarySerializer : IPaletteBinarySerializer + { + private readonly IColorFactory _colorFactory; + + public PaletteBinarySerializer(IColorFactory colorFactory) + { + _colorFactory = colorFactory; + } + + public IColor[] ReadPalette(DataFile df, FileBitAddress address, ColorModel colorModel, int entries) { - var color = ColorFactory.CreateColor(colorModel); + var color = _colorFactory.CreateColor(colorModel); int readSize = (color.Size + 7) / 8; byte[] paletteData = df.Stream.ReadUnshifted(address, readSize * 8 * entries); BitStream bs = BitStream.OpenRead(paletteData, readSize * 8 * entries); - var colors = new IColor32[entries]; + var colors = new IColor[entries]; for (int i = 0; i < entries; i++) { @@ -45,21 +58,21 @@ public static IColor32[] ReadPalette(DataFile df, FileBitAddress address, ColorM else throw new NotSupportedException($"{nameof(ReadPalette)}: Palette formats with entry sizes larger than 4 bytes are not supported"); - color = ColorFactory.CreateColor(colorModel, readColor); + color = _colorFactory.CreateColor(colorModel, readColor); colors[i] = color; } return colors; } - public static void WritePalette(DataFile df, FileBitAddress address, IEnumerable colors) + public void WritePalette(DataFile df, FileBitAddress address, IEnumerable colors) { int writeSize = (colors.First().Size + 7) / 8; df.Stream.Seek(address.FileOffset, SeekOrigin.Begin); using var bw = new BinaryWriter(df.Stream, Encoding.Default, true); - foreach(var color in colors) + foreach (var color in colors) { if (writeSize == 1) bw.Write((byte)color.Color); diff --git a/ImageMagitek/Colors/PaletteJsonSerializer.cs b/ImageMagitek/Colors/PaletteJsonSerializer.cs index d00409d8..9183716e 100644 --- a/ImageMagitek/Colors/PaletteJsonSerializer.cs +++ b/ImageMagitek/Colors/PaletteJsonSerializer.cs @@ -5,12 +5,12 @@ namespace ImageMagitek.Colors { public static class PaletteJsonSerializer { - public static Palette ReadPalette(string json) + public static Palette ReadPalette(string json, IColorFactory colorFactory) { var options = new JsonSerializerOptions(); options.PropertyNameCaseInsensitive = true; var model = JsonSerializer.Deserialize(json, options); - return model.ToPalette(); + return model.ToPalette(colorFactory); } } } diff --git a/ImageMagitek/Colors/SerializationModels/PaletteJsonModel.cs b/ImageMagitek/Colors/SerializationModels/PaletteJsonModel.cs index 82c99834..d5b8a8ec 100644 --- a/ImageMagitek/Colors/SerializationModels/PaletteJsonModel.cs +++ b/ImageMagitek/Colors/SerializationModels/PaletteJsonModel.cs @@ -10,12 +10,12 @@ public class PaletteJsonModel public List Colors { get; set; } public bool ZeroIndexTransparent { get; set; } = true; - public Palette ToPalette() + public Palette ToPalette(IColorFactory colorFactory) { const string colorRegex = "^#([A-Fa-f0-9]){6,8}$"; var regex = new Regex(colorRegex, RegexOptions.Compiled); - var pal = new Palette(Name, ColorModel.RGBA32, 0, Colors.Count, ZeroIndexTransparent, PaletteStorageSource.Json); + var pal = new Palette(Name, colorFactory, ColorModel.Rgba32, 0, Colors.Count, ZeroIndexTransparent, PaletteStorageSource.Json); for (int i = 0; i < Colors.Count; i++) { diff --git a/ImageMagitek/ExtensionMethods/XElementExtensions.cs b/ImageMagitek/ExtensionMethods/XElementExtensions.cs index a68a2f6f..7f39cf42 100644 --- a/ImageMagitek/ExtensionMethods/XElementExtensions.cs +++ b/ImageMagitek/ExtensionMethods/XElementExtensions.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Xml; using System.Xml.Linq; namespace ImageMagitek.ExtensionMethods @@ -34,5 +35,14 @@ public static string NodeKey(this XElement node) string path = node.NodePath(); return Path.Combine(path, node.Attribute("name").Value); } + + public static int? LineNumber(this XElement element) + { + if (element is null) + return default; + + var info = element as IXmlLineInfo; + return info.HasLineInfo() ? info.LineNumber : default; + } } } diff --git a/ImageMagitek/Image/IImageFileAdapter.cs b/ImageMagitek/Image/IImageFileAdapter.cs new file mode 100644 index 00000000..aa7ec8b9 --- /dev/null +++ b/ImageMagitek/Image/IImageFileAdapter.cs @@ -0,0 +1,13 @@ +using ImageMagitek.Colors; + +namespace ImageMagitek +{ + public interface IImageFileAdapter + { + void SaveImage(byte[] image, Arranger arranger, string imagePath); + void SaveImage(ColorRgba32[] image, int width, int height, string imagePath); + byte[] LoadImage(string imagePath, Arranger arranger, ColorMatchStrategy matchStrategy); + MagitekResult TryLoadImage(string imagePath, Arranger arranger, ColorMatchStrategy matchStrategy, out byte[] image); + ColorRgba32[] LoadImage(string imagePath); + } +} diff --git a/ImageMagitek/Image/ImageFileAdapter.cs b/ImageMagitek/Image/ImageSharpFileAdapter.cs similarity index 57% rename from ImageMagitek/Image/ImageFileAdapter.cs rename to ImageMagitek/Image/ImageSharpFileAdapter.cs index 35f87650..2adc36a2 100644 --- a/ImageMagitek/Image/ImageFileAdapter.cs +++ b/ImageMagitek/Image/ImageSharpFileAdapter.cs @@ -5,16 +5,7 @@ namespace ImageMagitek { - public interface IImageFileAdapter - { - void SaveImage(byte[] image, Arranger arranger, string imagePath); - void SaveImage(ColorRgba32[] image, int width, int height, string imagePath); - byte[] LoadImage(string imagePath, Arranger arranger, ColorMatchStrategy matchStrategy); - MagitekResult TryLoadImage(string imagePath, Arranger arranger, ColorMatchStrategy matchStrategy, out byte[] image); - ColorRgba32[] LoadImage(string imagePath); - } - - public class ImageFileAdapter : IImageFileAdapter + public class ImageSharpFileAdapter : IImageFileAdapter { public void SaveImage(byte[] image, Arranger arranger, string imagePath) { @@ -151,96 +142,5 @@ public ColorRgba32[] LoadImage(string imagePath) return outputImage; } - - - //public void SaveImage(DirectImage image, string imagePath) - //{ - // using var outputImage = new Image(image.Width, image.Height); - - // var span = outputImage.GetPixelSpan(); - - // for (int i = 0; i < span.Length; i++) - // { - // var color = image.Image[i].ToRgba32(); - // span[i] = color; - // } - - // using var outputStream = new FileStream(imagePath, FileMode.Create, FileAccess.Write, FileShare.Read); - // outputImage.SaveAsPng(outputStream); - //} - - //public void SaveImage(IndexedImage image, Palette pal, string imagePath) - //{ - // using var outputImage = new Image(image.Width, image.Height); - - // var span = outputImage.GetPixelSpan(); - - // for (int i = 0; i < span.Length; i++) - // { - // var color = pal.GetNativeColor(image.Image[i]); - // span[i] = color.ToRgba32(); - // } - - // using var outputStream = new FileStream(imagePath, FileMode.Create, FileAccess.Write, FileShare.Read); - // outputImage.SaveAsPng(outputStream); - //} - - //public void SaveImage(IndexedImage image, Arranger arranger, string imagePath) - //{ - // using var outputImage = new Image(image.Width, image.Height); - - // for (int y = 0; y < image.Height; y++) - // { - // for (int x = 0; x < image.Width; x++) - // { - // var span = outputImage.GetPixelRowSpan(y); - // var pal = arranger.GetElement(x / image.Width, y / image.Height).Palette; - // var color = pal[image.GetPixel(x, y)]; - // span[x] = color.ToRgba32(); - // } - // } - - // using var outputStream = new FileStream(imagePath, FileMode.Create, FileAccess.Write, FileShare.Read); - // outputImage.SaveAsPng(outputStream); - //} - - //public DirectImage LoadDirectImage(string imagePath) - //{ - // throw new NotImplementedException(); - // //using var image = Image.Load(imagePath); - - // //var direct = new DirectImage(image.Width, image.Height); - - // //var span = image.GetPixelSpan(); - - // //for (int i = 0; i < span.Length; i++) - // //{ - // // var color = new ColorRgba32(span[i].R, span[i].G, span[i].B, span[i].A); - // // direct.Image[i] = color; - // //} - - // //return direct; - //} - - //public IndexedImage LoadIndexedImage(string imagePath, Palette pal) - //{ - // throw new NotImplementedException(); - // //using var image = Image.Load(imagePath); - - // //var indexed = new IndexedImage(image.Width, image.Height); - - // //var span = image.GetPixelSpan(); - - // //for (int i = 0; i < span.Length; i++) - // //{ - // // var nc = new ColorRgba32(span[i].R, span[i].G, span[i].B, span[i].A); - // // if (pal.ContainsNativeColor(nc)) - // // throw new Exception($"{nameof(LoadIndexedImage)}: Image '{imagePath}' contained a color not contained by the palette '{pal.Name}'"); - - // // indexed.Image[i] = pal.GetIndexByNativeColor(nc, true); - // //} - - // //return indexed; - //} } } diff --git a/ImageMagitek/Project/ProjectTree.cs b/ImageMagitek/Project/ProjectTree.cs index 3e1caffd..b46d7c29 100644 --- a/ImageMagitek/Project/ProjectTree.cs +++ b/ImageMagitek/Project/ProjectTree.cs @@ -114,10 +114,13 @@ public MagitekResult AddResource(ResourceNode parentNode, IProject public MagitekResult CanMoveNode(ResourceNode node, ResourceNode parentNode) { if (node is null) - throw new ArgumentNullException($"{nameof(CanMoveNode)} parameter '{node}' was null"); + throw new ArgumentNullException($"{nameof(CanMoveNode)} parameter '{nameof(node)}' was null"); if (parentNode is null) - throw new ArgumentNullException($"{nameof(CanMoveNode)} parameter '{parentNode}' was null"); + throw new ArgumentNullException($"{nameof(CanMoveNode)} parameter '{nameof(parentNode)}' was null"); + + if (node.Parent is null) + return new MagitekResult.Failed($"{node.Name} has no parent"); if (ReferenceEquals(node, parentNode)) return new MagitekResult.Failed($"Cannot move {node.Name} onto itself"); diff --git a/ImageMagitek/Project/Serialization/ProjectTreeBuilder.cs b/ImageMagitek/Project/Serialization/ProjectTreeBuilder.cs index 3e3f020e..50262e1f 100644 --- a/ImageMagitek/Project/Serialization/ProjectTreeBuilder.cs +++ b/ImageMagitek/Project/Serialization/ProjectTreeBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.Linq; using ImageMagitek.Codec; @@ -17,10 +18,12 @@ class ProjectTreeBuilder private readonly List _globalResources; private readonly Palette _globalDefaultPalette; private readonly ICodecFactory _codecFactory; + private readonly IColorFactory _colorFactory; - public ProjectTreeBuilder(ICodecFactory codecFactory, IEnumerable globalResources) + public ProjectTreeBuilder(ICodecFactory codecFactory, IColorFactory colorFactory, IEnumerable globalResources) { _codecFactory = codecFactory; + _colorFactory = colorFactory; _globalResources = globalResources.ToList(); _globalDefaultPalette = _globalResources.OfType().FirstOrDefault(); } @@ -64,7 +67,7 @@ public MagitekResult AddDataFile(DataFileModel dfModel, string parentNodePath) public MagitekResult AddPalette(PaletteModel paletteModel, string parentNodePath) { - var pal = new Palette(paletteModel.Name, paletteModel.ColorModel, paletteModel.FileAddress, paletteModel.Entries, paletteModel.ZeroIndexTransparent, paletteModel.StorageSource); + var pal = new Palette(paletteModel.Name, _colorFactory, paletteModel.ColorModel, paletteModel.FileAddress, paletteModel.Entries, paletteModel.ZeroIndexTransparent, paletteModel.StorageSource); if (!Tree.TryGetValue(paletteModel.DataFileKey, out var df)) return new MagitekResult.Failed($"Palette '{pal.Name}' could not locate DataFile with key '{paletteModel.DataFileKey}'"); @@ -157,7 +160,7 @@ private Palette ResolvePalette(string paletteKey) return new MagitekResult.Failed($"Could not resolve palette '{paletteKey}' referenced by arranger '{arrangerModel.Name}'"); } - codec = _codecFactory.GetCodec(elementModel.CodecName, arrangerModel.ElementPixelSize.Width, arrangerModel.ElementPixelSize.Height); + codec = _codecFactory.GetCodec(elementModel.CodecName, new Size(arrangerModel.ElementPixelSize.Width, arrangerModel.ElementPixelSize.Height)); } else if (arrangerModel.ColorType == PixelColorType.Direct) { @@ -165,7 +168,7 @@ private Palette ResolvePalette(string paletteKey) Tree.TryGetValue(elementModel.DataFileKey, out df); address = elementModel.FileAddress; - codec = _codecFactory.GetCodec(elementModel.CodecName, arrangerModel.ElementPixelSize.Width, arrangerModel.ElementPixelSize.Height); + codec = _codecFactory.GetCodec(elementModel.CodecName, new Size(arrangerModel.ElementPixelSize.Width, arrangerModel.ElementPixelSize.Height)); } else { diff --git a/ImageMagitek/Project/Serialization/XmlGameDescriptorReader.cs b/ImageMagitek/Project/Serialization/XmlGameDescriptorReader.cs index 840ee2d0..f6a0343c 100644 --- a/ImageMagitek/Project/Serialization/XmlGameDescriptorReader.cs +++ b/ImageMagitek/Project/Serialization/XmlGameDescriptorReader.cs @@ -19,24 +19,27 @@ public class XmlGameDescriptorReader : IGameDescriptorReader private readonly XmlSchemaSet _schemaSet; private readonly ICodecFactory _codecFactory; + private readonly IColorFactory _colorFactory; private readonly List _globalResources; private readonly Palette _globalDefaultPalette; private string _baseDirectory; - public XmlGameDescriptorReader(ICodecFactory codecFactory) : - this(new XmlSchemaSet(), codecFactory, Enumerable.Empty()) + public XmlGameDescriptorReader(ICodecFactory codecFactory, IColorFactory colorFactory) : + this(new XmlSchemaSet(), codecFactory, colorFactory, Enumerable.Empty()) { } - public XmlGameDescriptorReader(XmlSchemaSet schemaSet, ICodecFactory codecFactory) : - this(schemaSet, codecFactory, Enumerable.Empty()) + public XmlGameDescriptorReader(XmlSchemaSet schemaSet, ICodecFactory codecFactory, IColorFactory colorFactory) : + this(schemaSet, codecFactory, colorFactory, Enumerable.Empty()) { } - public XmlGameDescriptorReader(XmlSchemaSet schemaSet, ICodecFactory codecFactory, IEnumerable globalResources) + public XmlGameDescriptorReader(XmlSchemaSet schemaSet, ICodecFactory codecFactory, + IColorFactory colorFactory, IEnumerable globalResources) { _schemaSet = schemaSet; _codecFactory = codecFactory; + _colorFactory = colorFactory; _globalResources = globalResources.ToList(); _globalDefaultPalette = _globalResources.OfType().First(); } @@ -68,7 +71,7 @@ public MagitekResults ReadProject(string projectFileName) var projectErrors = new List(); var projectModel = DeserializeImageProject(projectNode); - var builder = new ProjectTreeBuilder(_codecFactory, _globalResources); + var builder = new ProjectTreeBuilder(_codecFactory, _colorFactory, _globalResources); builder.AddProject(projectModel); foreach (var node in projectNode.Descendants("folder")) diff --git a/ImageMagitek/_codecs/CotMFont.xml b/ImageMagitek/_codecs/CotMFont.xml new file mode 100644 index 00000000..13b265fb --- /dev/null +++ b/ImageMagitek/_codecs/CotMFont.xml @@ -0,0 +1,18 @@ + + + indexed + 1 + tiled + 8 + 8 + true + 0 + + + + 1 + 1, 0, 3, 2, 5, 4, 7, 6 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/FF5Font Pattern.xml b/ImageMagitek/_codecs/FF5Font Pattern.xml new file mode 100644 index 00000000..a2ac8940 --- /dev/null +++ b/ImageMagitek/_codecs/FF5Font Pattern.xml @@ -0,0 +1,29 @@ + + + + indexed + 1 + tiled + planar + 12 + 16 + 0 + 0, 1 + + + + AAAAAAAAMMMMMMMM + BBBBBBBBNNNNNNNN + CCCCCCCCOOOOOOOO + DDDDDDDDPPPPPPPP + EEEEEEEEQQQQQQQQ + FFFFFFFFRRRRRRRR + GGGGGGGGSSSSSSSS + HHHHHHHHTTTTTTTT + IIIIIIIIUUUUUUUU + JJJJJJJJVVVVVVVV + KKKKKKKKWWWWWWWW + LLLLLLLLXXXXXXXX + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/FF5Font.xml b/ImageMagitek/_codecs/FF5Font.xml index d625f5e4..69e0504f 100644 --- a/ImageMagitek/_codecs/FF5Font.xml +++ b/ImageMagitek/_codecs/FF5Font.xml @@ -1,15 +1,18 @@ - - - indexed - 1 - tiled - 12 - 8 - true - 0 - - - 1 - false - - \ No newline at end of file + + + + indexed + 1 + tiled + 12 + 8 + true + 0 + + + + 1 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/GBA4bpp Pattern.xml b/ImageMagitek/_codecs/GBA4bpp Pattern.xml new file mode 100644 index 00000000..032b83ff --- /dev/null +++ b/ImageMagitek/_codecs/GBA4bpp Pattern.xml @@ -0,0 +1,17 @@ + + + indexed + 4 + tiled + chunky + 8 + 8 + 3, 2, 1, 0 + 1, 0 + + + + AABBCCDD + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/GBA4bpp.xml b/ImageMagitek/_codecs/GBA4bpp.xml index 349ccf83..dd6092cf 100644 --- a/ImageMagitek/_codecs/GBA4bpp.xml +++ b/ImageMagitek/_codecs/GBA4bpp.xml @@ -1,16 +1,18 @@ - - - indexed - 4 - tiled - 8 - 8 - false - 3, 2, 1, 0 - - - 4 - false - 1, 0 - - \ No newline at end of file + + + indexed + 4 + tiled + 8 + 8 + false + 3, 2, 1, 0 + + + + 4 + false + 1, 0 + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/GG4bpp.xml b/ImageMagitek/_codecs/GG4bpp.xml index 1fbd3564..db7f2e73 100644 --- a/ImageMagitek/_codecs/GG4bpp.xml +++ b/ImageMagitek/_codecs/GG4bpp.xml @@ -1,15 +1,17 @@ - - - indexed - 4 - tiled - 8 - 8 - false - 0, 1, 2, 3 - - - 4 - true - - \ No newline at end of file + + + indexed + 4 + tiled + 8 + 8 + false + 0, 1, 2, 3 + + + + 4 + true + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/Gen4bpp.xml b/ImageMagitek/_codecs/Gen4bpp.xml index d268b6ad..89102a0d 100644 --- a/ImageMagitek/_codecs/Gen4bpp.xml +++ b/ImageMagitek/_codecs/Gen4bpp.xml @@ -1,15 +1,17 @@ - - - indexed - 4 - tiled - 8 - 8 - false - 0, 1, 2, 3 - - - 4 - false - - \ No newline at end of file + + + indexed + 4 + tiled + 8 + 8 + false + 0, 1, 2, 3 + + + + 4 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/NES1bpp.xml b/ImageMagitek/_codecs/NES1bpp.xml index ab7a4703..83cb961a 100644 --- a/ImageMagitek/_codecs/NES1bpp.xml +++ b/ImageMagitek/_codecs/NES1bpp.xml @@ -1,15 +1,17 @@ - - - indexed - 1 - tiled - 8 - 8 - false - 0 - - - 1 - false - - \ No newline at end of file + + + indexed + 1 + tiled + 8 + 8 + false + 0 + + + + 1 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/NES2bpp.xml b/ImageMagitek/_codecs/NES2bpp.xml index 882dc875..825e09e5 100644 --- a/ImageMagitek/_codecs/NES2bpp.xml +++ b/ImageMagitek/_codecs/NES2bpp.xml @@ -1,19 +1,21 @@ - - - indexed - 2 - tiled - 8 - 8 - false - 0, 1 - - - 1 - false - - - 1 - false - - \ No newline at end of file + + + indexed + 2 + tiled + 8 + 8 + false + 0, 1 + + + + 1 + false + + + 1 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/NGPC2bpp.xml b/ImageMagitek/_codecs/NGPC2bpp.xml index 56297c68..0bd7506a 100644 --- a/ImageMagitek/_codecs/NGPC2bpp.xml +++ b/ImageMagitek/_codecs/NGPC2bpp.xml @@ -1,16 +1,18 @@ - - - indexed - 2 - tiled - 8 - 8 - true - 0, 1 - - - 2 - 4, 5, 6, 7, 0, 1, 2, 3 - false - - \ No newline at end of file + + + indexed + 2 + tiled + 8 + 8 + true + 0, 1 + + + + 2 + 4, 5, 6, 7, 0, 1, 2, 3 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/PSX4bpp.xml b/ImageMagitek/_codecs/PSX4bpp.xml index 350f5dc7..d23dcd7c 100644 --- a/ImageMagitek/_codecs/PSX4bpp.xml +++ b/ImageMagitek/_codecs/PSX4bpp.xml @@ -1,16 +1,18 @@ - - - indexed - 4 - linear - 1 - 256 - false - 3, 2, 1, 0 - - - 4 - false - 1, 0 - - \ No newline at end of file + + + indexed + 4 + single + 64 + 64 + false + 3, 2, 1, 0 + + + + 4 + false + 1, 0 + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/PSX8bpp.xml b/ImageMagitek/_codecs/PSX8bpp.xml index 5501eb75..58f55adc 100644 --- a/ImageMagitek/_codecs/PSX8bpp.xml +++ b/ImageMagitek/_codecs/PSX8bpp.xml @@ -1,15 +1,17 @@ - - - indexed - 8 - linear - 1 - 256 - false - 7, 6, 5, 4, 3, 2, 1, 0 - - - 8 - false - - \ No newline at end of file + + + indexed + 8 + single + 64 + 64 + false + 7, 6, 5, 4, 3, 2, 1, 0 + + + + 8 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNES2bpp.xml b/ImageMagitek/_codecs/SNES2bpp.xml index cfe1f38b..efe95963 100644 --- a/ImageMagitek/_codecs/SNES2bpp.xml +++ b/ImageMagitek/_codecs/SNES2bpp.xml @@ -1,15 +1,17 @@ - - - indexed - 2 - tiled - 8 - 8 - false - 0, 1 - - - 2 - true - - \ No newline at end of file + + + indexed + 2 + tiled + 8 + 8 + false + 0, 1 + + + + 2 + true + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNES3bpp Flow.xml b/ImageMagitek/_codecs/SNES3bpp Flow.xml new file mode 100644 index 00000000..b48715e3 --- /dev/null +++ b/ImageMagitek/_codecs/SNES3bpp Flow.xml @@ -0,0 +1,21 @@ + + + indexed + 3 + tiled + 8 + 8 + false + 0, 1, 2 + + + + 2 + true + + + 1 + true + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNES3bpp.xml b/ImageMagitek/_codecs/SNES3bpp.xml deleted file mode 100644 index 44a24216..00000000 --- a/ImageMagitek/_codecs/SNES3bpp.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - indexed - 3 - tiled - 8 - 8 - false - 0, 1, 2 - - - 2 - true - - - 1 - true - - \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNES4bpp Pattern.xml b/ImageMagitek/_codecs/SNES4bpp Pattern.xml new file mode 100644 index 00000000..71c67a43 --- /dev/null +++ b/ImageMagitek/_codecs/SNES4bpp Pattern.xml @@ -0,0 +1,54 @@ + + + indexed + 4 + tiled + 8 + 8 + planar + 0, 1, 2, 3 + 0, 1 + + + + AAAAAAAA + CCCCCCCC + EEEEEEEE + GGGGGGGG + IIIIIIII + KKKKKKKK + MMMMMMMM + OOOOOOOO + + + BBBBBBBB + DDDDDDDD + FFFFFFFF + HHHHHHHH + JJJJJJJJ + LLLLLLLL + NNNNNNNN + PPPPPPPP + + + QQQQQQQQ + SSSSSSSS + UUUUUUUU + WWWWWWWW + YYYYYYYY + aaaaaaaa + cccccccc + eeeeeeee + + + RRRRRRRR + TTTTTTTT + VVVVVVVV + XXXXXXXX + ZZZZZZZZ + bbbbbbbb + dddddddd + ffffffff + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNES4bpp.xml b/ImageMagitek/_codecs/SNES4bpp.xml index 4e6194bc..db9ed059 100644 --- a/ImageMagitek/_codecs/SNES4bpp.xml +++ b/ImageMagitek/_codecs/SNES4bpp.xml @@ -1,19 +1,21 @@ - - - indexed - 4 - tiled - 8 - 8 - false - 0, 1, 2, 3 - - - 2 - true - - - 2 - true - - \ No newline at end of file + + + indexed + 4 + tiled + 8 + 8 + false + 0, 1, 2, 3 + + + + 2 + true + + + 2 + true + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNES8bpp.xml b/ImageMagitek/_codecs/SNES8bpp.xml index 7e6ecc4c..3c2188f8 100644 --- a/ImageMagitek/_codecs/SNES8bpp.xml +++ b/ImageMagitek/_codecs/SNES8bpp.xml @@ -1,27 +1,29 @@ - - - indexed - 8 - tiled - 8 - 8 - false - 0, 1, 2, 3, 4, 5, 6, 7 - - - 2 - true - - - 2 - true - - - 2 - true - - - 2 - true - - \ No newline at end of file + + + indexed + 8 + tiled + 8 + 8 + false + 0, 1, 2, 3, 4, 5, 6, 7 + + + + 2 + true + + + 2 + true + + + 2 + true + + + 2 + true + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/SNESMode7.xml b/ImageMagitek/_codecs/SNESMode7.xml index 1cf8caae..8f53c2c5 100644 --- a/ImageMagitek/_codecs/SNESMode7.xml +++ b/ImageMagitek/_codecs/SNESMode7.xml @@ -1,15 +1,17 @@ - - - indexed - 8 - tiled - 8 - 8 - false - 0, 1, 2, 3, 4, 5, 6, 7 - - - 8 - false - - \ No newline at end of file + + + indexed + 8 + tiled + 8 + 8 + false + 0, 1, 2, 3, 4, 5, 6, 7 + + + + 8 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/Tokimemo1bpp.xml b/ImageMagitek/_codecs/Tokimemo1bpp.xml index 8e1054ad..0bb56df6 100644 --- a/ImageMagitek/_codecs/Tokimemo1bpp.xml +++ b/ImageMagitek/_codecs/Tokimemo1bpp.xml @@ -1,17 +1,19 @@ + - - - indexed - 1 - tiled - 14 - 16 - true - 0 - - - 1 - 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7 - false - - \ No newline at end of file + + indexed + 1 + tiled + 14 + 16 + true + 0 + + + + 1 + 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_codecs/VB2bpp.xml b/ImageMagitek/_codecs/VB2bpp.xml index f6d7933c..a9459105 100644 --- a/ImageMagitek/_codecs/VB2bpp.xml +++ b/ImageMagitek/_codecs/VB2bpp.xml @@ -1,16 +1,18 @@ - - - indexed - 2 - tiled - 8 - 8 - true - 0, 1 - - - 2 - 3, 2, 1, 0, 7, 6, 5, 4 - false - - \ No newline at end of file + + + indexed + 2 + tiled + 8 + 8 + true + 0, 1 + + + + 2 + 3, 2, 1, 0, 7, 6, 5, 4 + false + + + \ No newline at end of file diff --git a/ImageMagitek/_palettes/DefaultNes.json b/ImageMagitek/_palettes/DefaultNes.json index 639352ca..6724cc47 100644 --- a/ImageMagitek/_palettes/DefaultNes.json +++ b/ImageMagitek/_palettes/DefaultNes.json @@ -1,14 +1,14 @@ -{ +{ "name": "DefaultNes", "zeroindextransparent": false, "colors": [ - "#6A6D6AFF", "#801300FF", "#8A001EFF", "#7A0039FF", "#560055FF", "#18005AFF", "#00104FFF", "#001C3DFF", - "#003225FF", "#003D00FF", "#004000FF", "#243900FF", "#552E00FF", "#000000FF", "#000000FF", "#000000FF", - "#B9BCB9FF", "#C75018FF", "#E3304BFF", "#D62273FF", "#A91F95FF", "#5C289DFF", "#003798FF", "#004C7FFF", - "#00645EFF", "#007722FF", "#027E02FF", "#457600FF", "#8A6E00FF", "#000000FF", "#000000FF", "#000000FF", - "#FFFFFFFF", "#FFA668FF", "#FF9C8CFF", "#FF86B5FF", "#FD75D9FF", "#B977E3FF", "#688DE5FF", "#299DD4FF", - "#0CAFB3FF", "#11C27BFF", "#47CA55FF", "#81CB46FF", "#C5C147FF", "#4A4D4AFF", "#000000FF", "#000000FF", - "#FFFFFFFF", "#FFEACCFF", "#FFDEDDFF", "#FFDAECFF", "#FED7F8FF", "#F5D6FCFF", "#CFDBFDFF", "#B5E7F9FF", - "#AAF0F1FF", "#A9FADAFF", "#BCFFC9FF", "#D7FBC3FF", "#F6F6C4FF", "#BEC1BEFF", "#000000FF", "#000000FF" + "#616161FF", "#000088FF", "#1F0D99FF", "#371379FF", "#561260FF", "#5D0010FF", "#520E00FF", "#3A2308FF", + "#21350CFF", "#0D410EFF", "#174417FF", "#003A1FFF", "#002F57FF", "#000000FF", "#000000FF", "#000000FF", + "#AAAAAAFF", "#0D4DC4FF", "#4B24DEFF", "#6912CFFF", "#9014ADFF", "#9D1C48FF", "#923404FF", "#735005FF", + "#5D6913FF", "#167A11FF", "#138008FF", "#127649FF", "#1C6691FF", "#000000FF", "#000000FF", "#000000FF", + "#FCFCFCFF", "#639AFCFF", "#8A7EFCFF", "#B06AFCFF", "#DD6DF2FF", "#E771ABFF", "#E38658FF", "#CC9E22FF", + "#A8B100FF", "#72C100FF", "#5ACD4EFF", "#34C28EFF", "#4FBECEFF", "#424242FF", "#000000FF", "#000000FF", + "#FCFCFCFF", "#BED4FCFF", "#CACAFCFF", "#D9C4FCFF", "#ECC1FCFF", "#FAC3E7FF", "#F7CEC3FF", "#E2CDA7FF", + "#DADB9CFF", "#C8E39EFF", "#BFE5B8FF", "#B2EBC8FF", "#B7E5EBFF", "#ACACACFF", "#000000FF", "#000000FF" ] } \ No newline at end of file diff --git a/ImageMagitek/_schemas/CodecSchema.xsd b/ImageMagitek/_schemas/CodecSchema.xsd index c3eb4079..f56383f8 100644 --- a/ImageMagitek/_schemas/CodecSchema.xsd +++ b/ImageMagitek/_schemas/CodecSchema.xsd @@ -1,65 +1,116 @@  - + - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageMagitek/_schemas/GameDescriptorSchema.xsd b/ImageMagitek/_schemas/GameDescriptorSchema.xsd index 3e756dd0..c0a15bf5 100644 --- a/ImageMagitek/_schemas/GameDescriptorSchema.xsd +++ b/ImageMagitek/_schemas/GameDescriptorSchema.xsd @@ -63,9 +63,10 @@ - - - + + + + diff --git a/ImageMagitek/_xmlprojectsamples/Crystalis.xml b/ImageMagitek/_xmlprojectsamples/Crystalis.xml new file mode 100644 index 00000000..f7a4bb1a --- /dev/null +++ b/ImageMagitek/_xmlprojectsamples/Crystalis.xml @@ -0,0 +1,918 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ImageMagitek/_xmlprojectsamples/ctbossx.xml b/ImageMagitek/_xmlprojectsamples/ctbossx.xml index 0f4da364..ca5d1922 100644 --- a/ImageMagitek/_xmlprojectsamples/ctbossx.xml +++ b/ImageMagitek/_xmlprojectsamples/ctbossx.xml @@ -21,12 +21,12 @@ - - - - - - + + + + + + \ No newline at end of file diff --git a/ImageMagitek/_xmlprojectsamples/ff2extended.xml b/ImageMagitek/_xmlprojectsamples/ff2extended.xml index aea4268c..cf5e9a06 100644 --- a/ImageMagitek/_xmlprojectsamples/ff2extended.xml +++ b/ImageMagitek/_xmlprojectsamples/ff2extended.xml @@ -2022,72 +2022,72 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -3464,30 +3464,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageMagitekConsole/CommandProcessor.cs b/ImageMagitekConsole/CommandProcessor.cs index dc65d5c9..3a4eedf3 100644 --- a/ImageMagitekConsole/CommandProcessor.cs +++ b/ImageMagitekConsole/CommandProcessor.cs @@ -6,24 +6,25 @@ using ImageMagitek.Project; using Monaco.PathTree; using ImageMagitek.Colors; +using ImageMagitek.Project.Serialization; namespace ImageMagitekConsole { public class CommandProcessor { - private readonly IPathTree _resourceTree; + private readonly ProjectTree _projectTree; private readonly Palette _defaultPalette; - public CommandProcessor(IPathTree resourceTree, Palette defaultPalette) + public CommandProcessor(ProjectTree projectTree, Palette defaultPalette) { - _resourceTree = resourceTree; + _projectTree = projectTree; _defaultPalette = defaultPalette; } public bool PrintResources() { - foreach (var res in _resourceTree.EnumerateDepthFirst()) + foreach (var res in _projectTree.Tree.EnumerateDepthFirst()) { string key = res.PathKey; int level = key.Split('\\').Length; @@ -36,7 +37,7 @@ public bool PrintResources() public bool ExportArranger(string arrangerKey, string projectRoot) { - _resourceTree.TryGetNode(arrangerKey, out var node); + _projectTree.Tree.TryGetNode(arrangerKey, out var node); var relativeFile = Path.Combine(node.Paths.ToArray()); var exportFileName = Path.Combine(projectRoot, relativeFile + ".bmp"); @@ -48,13 +49,13 @@ public bool ExportArranger(string arrangerKey, string projectRoot) if (arranger.ColorType == PixelColorType.Indexed) { - var image = new IndexedImage(arranger, _defaultPalette); - image.ExportImage(exportFileName, new ImageFileAdapter()); + var image = new IndexedImage(arranger); + image.ExportImage(exportFileName, new ImageSharpFileAdapter()); } else if (arranger.ColorType == PixelColorType.Direct) { var image = new DirectImage(arranger); - image.ExportImage(exportFileName, new ImageFileAdapter()); + image.ExportImage(exportFileName, new ImageSharpFileAdapter()); } return true; @@ -62,7 +63,7 @@ public bool ExportArranger(string arrangerKey, string projectRoot) public bool ExportAllArrangers(string projectRoot) { - foreach (var node in _resourceTree.EnumerateDepthFirst()) + foreach (var node in _projectTree.Tree.EnumerateDepthFirst()) { if(node.Value is ScatteredArranger) ExportArranger(node.PathKey, projectRoot); @@ -75,18 +76,18 @@ public bool ImportImage(string imageFileName, string arrangerKey) { Console.WriteLine($"Importing {imageFileName} to {arrangerKey}..."); - _resourceTree.TryGetValue(arrangerKey, out ScatteredArranger arranger); + _projectTree.Tree.TryGetValue(arrangerKey, out ScatteredArranger arranger); if (arranger.ColorType == PixelColorType.Indexed) { - var image = new IndexedImage(arranger, _defaultPalette); - image.ImportImage(imageFileName, new ImageFileAdapter(), ColorMatchStrategy.Exact); + var image = new IndexedImage(arranger); + image.ImportImage(imageFileName, new ImageSharpFileAdapter(), ColorMatchStrategy.Exact); image.SaveImage(); } else if (arranger.ColorType == PixelColorType.Direct) { var image = new DirectImage(arranger); - image.ImportImage(imageFileName, new ImageFileAdapter()); + image.ImportImage(imageFileName, new ImageSharpFileAdapter()); image.SaveImage(); } @@ -95,7 +96,7 @@ public bool ImportImage(string imageFileName, string arrangerKey) public bool ImportAllImages(string projectRoot) { - foreach (var node in _resourceTree.EnumerateDepthFirst().Where(x => x.Value is ScatteredArranger)) + foreach (var node in _projectTree.Tree.EnumerateDepthFirst().Where(x => x.Value is ScatteredArranger)) { var arranger = node.Value as ScatteredArranger; var relativeFile = Path.Combine(node.Paths.ToArray()); @@ -109,7 +110,8 @@ public bool ImportAllImages(string projectRoot) public bool ResaveProject(string newProjectFile) { var writer = new XmlGameDescriptorWriter(); - return writer.WriteProject(_resourceTree, newProjectFile); + writer.WriteProject(_projectTree, newProjectFile); + return true; } } } diff --git a/ImageMagitekConsole/Program.cs b/ImageMagitekConsole/Program.cs index 8bb3ac4b..49b083a4 100644 --- a/ImageMagitekConsole/Program.cs +++ b/ImageMagitekConsole/Program.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using ImageMagitek; +using System.Xml; +using System.Xml.Schema; using ImageMagitek.Codec; using ImageMagitek.Colors; using ImageMagitek.Project; +using ImageMagitek.Project.Serialization; using Monaco.PathTree; namespace ImageMagitekConsole @@ -15,8 +17,8 @@ public enum ExitCode { Success = 0, InvalidCommandArguments = -1, ProjectValidat class Program { static readonly HashSet _commands = new HashSet { "export", "exportall", "import", "importall", "print", "resave" }; - static readonly string _projectSchemaFileName = Path.Combine("schema", "GameDescriptorSchema.xsd"); - static readonly string _codecSchemaFileName = Path.Combine("schema", "CodecSchema.xsd"); + static readonly string _projectSchemaFileName = Path.Combine("_schemas", "GameDescriptorSchema.xsd"); + static readonly string _codecSchemaFileName = Path.Combine("_schemas", "CodecSchema.xsd"); static int Main(string[] args) { @@ -46,8 +48,8 @@ static int Main(string[] args) } // Load default graphic formats and palettes - var codecPath = Path.Combine(Directory.GetCurrentDirectory(), "codecs"); - var formats = new Dictionary(); + var codecPath = Path.Combine(Directory.GetCurrentDirectory(), "_codecs"); + var formats = new Dictionary(); var serializer = new XmlGraphicsFormatReader(_codecSchemaFileName); foreach (var formatFileName in Directory.GetFiles(codecPath).Where(x => x.EndsWith(".xml"))) { @@ -63,25 +65,28 @@ static int Main(string[] args) }); } - var palPath = Path.Combine(Directory.GetCurrentDirectory(), "pal"); + var palPath = Path.Combine(Directory.GetCurrentDirectory(), "_palettes"); var palettes = new List(); - foreach (var paletteFileName in Directory.GetFiles(palPath).Where(x => x.EndsWith(".json"))) + var paletteFileNames = Directory.GetFiles(palPath).Where(x => x.EndsWith(".json")); + + foreach (var paletteFileName in paletteFileNames) { string json = File.ReadAllText(paletteFileName); var pal = PaletteJsonSerializer.ReadPalette(json); - palettes.Add(pal); + if (pal.Name == "DefaultRgba32") + palettes.Insert(0, pal); + else + palettes.Add(pal); } - var defaultPalette = palettes.Single(x => x.Name.Contains("DefaultRgba32")); - - var schemaFileName = Path.Combine(Directory.GetCurrentDirectory(), "schema", "GameDescriptorValidator.xsd"); - var deserializer = new XmlGameDescriptorReader(schemaFileName, new CodecFactory(formats, defaultPalette)); + using var schemaStream = File.OpenRead(_projectSchemaFileName); + XmlSchemaSet projectSchema = new XmlSchemaSet(); + projectSchema.Add("", XmlReader.Create(schemaStream)); - //IPathTree tree = default; - var projectResult = deserializer.ReadProject(projectFileName); + var deserializer = new XmlGameDescriptorReader(projectSchema, new CodecFactory(formats), palettes); - IPathTree tree = projectResult.Match( + var tree = deserializer.ReadProject(projectFileName).Match( success => success.Result, fail => { @@ -95,7 +100,7 @@ static int Main(string[] args) if (tree is null) return (int) ExitCode.ProjectValidationError; - var processor = new CommandProcessor(tree, defaultPalette); + var processor = new CommandProcessor(tree, palettes.First()); switch (command) { diff --git a/TileShop.Shared/EventModels/PaletteChangedEvent.cs b/TileShop.Shared/EventModels/PaletteChangedEvent.cs index 7de50b87..91a36026 100644 --- a/TileShop.Shared/EventModels/PaletteChangedEvent.cs +++ b/TileShop.Shared/EventModels/PaletteChangedEvent.cs @@ -1,10 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Text; +using ImageMagitek.Colors; namespace TileShop.Shared.EventModels { - class PaletteChangedEvent + public class PaletteChangedEvent { + public Palette Palette { get; } + + public PaletteChangedEvent(Palette palette) + { + Palette = palette; + } } } diff --git a/TileShop.WPF/Assets/DemoImages/SaveNesPalette.gif b/TileShop.WPF/Assets/DemoImages/SaveNesPalette.gif new file mode 100644 index 00000000..6efa5f46 Binary files /dev/null and b/TileShop.WPF/Assets/DemoImages/SaveNesPalette.gif differ diff --git a/TileShop.WPF/Assets/DemoImages/TileShopLayoutDark10272020.png b/TileShop.WPF/Assets/DemoImages/TileShopLayoutDark10272020.png new file mode 100644 index 00000000..17610628 Binary files /dev/null and b/TileShop.WPF/Assets/DemoImages/TileShopLayoutDark10272020.png differ diff --git a/TileShop.WPF/Features/Arranger Editors/ArrangerEditorViewModel.cs b/TileShop.WPF/Features/Arranger Editors/ArrangerEditorViewModel.cs index 3bd2ab37..aa3d2af5 100644 --- a/TileShop.WPF/Features/Arranger Editors/ArrangerEditorViewModel.cs +++ b/TileShop.WPF/Features/Arranger Editors/ArrangerEditorViewModel.cs @@ -17,7 +17,7 @@ public enum EditMode { ArrangeGraphics, ModifyGraphics } public abstract class ArrangerEditorViewModel : ResourceEditorBaseViewModel, IMouseCaptureProxy, IDropTarget, IDragSource { - protected Arranger _workingArranger; + public Arranger WorkingArranger { get; protected set; } protected IEventAggregator _events; protected IPaletteService _paletteService; @@ -30,8 +30,8 @@ public BitmapAdapter BitmapAdapter set => SetAndNotify(ref _bitmapAdapter, value); } - public bool IsSingleLayout => _workingArranger?.Layout == ArrangerLayout.Single; - public bool IsTiledLayout => _workingArranger?.Layout == ArrangerLayout.Tiled; + public bool IsSingleLayout => WorkingArranger?.Layout == ArrangerLayout.Single; + public bool IsTiledLayout => WorkingArranger?.Layout == ArrangerLayout.Tiled; protected bool _showGridlines = false; public bool ShowGridlines @@ -80,7 +80,7 @@ public virtual bool CanEditSelection if (rect.SnappedWidth == 0 || rect.SnappedHeight == 0) return false; - return !_workingArranger.EnumerateElementsByPixel(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight) + return !WorkingArranger.EnumerateElementsByPixel(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight) .Any(x => x is null || x?.DataFile is null); } @@ -148,9 +148,9 @@ public ArrangerEditorViewModel(IEventAggregator events, IWindowManager windowMan /// protected virtual void CreateGridlines() { - if (_workingArranger is object) - CreateGridlines(0, 0, _workingArranger.ArrangerPixelSize.Width, _workingArranger.ArrangerPixelSize.Height, - _workingArranger.ElementPixelSize.Width, _workingArranger.ElementPixelSize.Height); + if (WorkingArranger is object) + CreateGridlines(0, 0, WorkingArranger.ArrangerPixelSize.Width, WorkingArranger.ArrangerPixelSize.Height, + WorkingArranger.ElementPixelSize.Width, WorkingArranger.ElementPixelSize.Height); } /// @@ -164,7 +164,7 @@ protected virtual void CreateGridlines() /// Spacing between gridlines in pixel coordinates protected void CreateGridlines(int x1, int y1, int x2, int y2, int xSpacing, int ySpacing) { - if (_workingArranger is null) + if (WorkingArranger is null) return; _gridlines = new BindableCollection(); @@ -199,17 +199,17 @@ public virtual void EditSelection() EditArrangerPixelsEvent editEvent; var rect = Selection.SelectionRect; - if (SnapMode == SnapMode.Element && _workingArranger.Layout == ArrangerLayout.Tiled) + if (SnapMode == SnapMode.Element && WorkingArranger.Layout == ArrangerLayout.Tiled) { // Clone a subsection of the arranger and show the full subarranger - _workingArranger.CopyElements(); - var arranger = _workingArranger.CloneArranger(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); + WorkingArranger.CopyElements(); + var arranger = WorkingArranger.CloneArranger(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); editEvent = new EditArrangerPixelsEvent(arranger, Resource as Arranger, 0, 0, rect.SnappedWidth, rect.SnappedHeight); } else { // Clone the entire arranger and show a subsection of the cloned arranger - var arranger = _workingArranger.CloneArranger(); + var arranger = WorkingArranger.CloneArranger(); editEvent = new EditArrangerPixelsEvent(arranger, Resource as Arranger, rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); } @@ -220,14 +220,14 @@ public virtual void EditSelection() public virtual void SelectAll() { CancelOverlay(); - Selection = new ArrangerSelection(_workingArranger, SnapMode); + Selection = new ArrangerSelection(WorkingArranger, SnapMode); Selection.StartSelection(0, 0); - Selection.UpdateSelectionEndpoint(_workingArranger.ArrangerPixelSize.Width, _workingArranger.ArrangerPixelSize.Height); + Selection.UpdateSelectionEndpoint(WorkingArranger.ArrangerPixelSize.Width, WorkingArranger.ArrangerPixelSize.Height); } public virtual void CancelOverlay() { - Selection = new ArrangerSelection(_workingArranger, SnapMode); + Selection = new ArrangerSelection(WorkingArranger, SnapMode); Paste = null; NotifyOfPropertyChange(() => CanEditSelection); @@ -251,7 +251,7 @@ public virtual void CompleteSelection() { if (Selection.SelectionRect.SnappedWidth == 0 || Selection.SelectionRect.SnappedHeight == 0) { - Selection = new ArrangerSelection(_workingArranger, SnapMode); + Selection = new ArrangerSelection(WorkingArranger, SnapMode); } IsSelecting = false; @@ -274,8 +274,8 @@ public virtual void OnMouseMove(object sender, MouseCaptureArgs e) string notifyMessage; var rect = Selection.SelectionRect; if (rect.SnapMode == SnapMode.Element) - notifyMessage = $"Element Selection: {rect.SnappedWidth / _workingArranger.ElementPixelSize.Width} x {rect.SnappedHeight / _workingArranger.ElementPixelSize.Height}" + - $" at ({rect.SnappedLeft / _workingArranger.ElementPixelSize.Width}, {rect.SnappedRight / _workingArranger.ElementPixelSize.Height})"; + notifyMessage = $"Element Selection: {rect.SnappedWidth / WorkingArranger.ElementPixelSize.Width} x {rect.SnappedHeight / WorkingArranger.ElementPixelSize.Height}" + + $" at ({rect.SnappedLeft / WorkingArranger.ElementPixelSize.Width}, {rect.SnappedRight / WorkingArranger.ElementPixelSize.Height})"; else notifyMessage = $"Pixel Selection: {rect.SnappedWidth} x {rect.SnappedHeight}" + $" at ({rect.SnappedLeft}, {rect.SnappedTop})"; @@ -284,7 +284,7 @@ public virtual void OnMouseMove(object sender, MouseCaptureArgs e) } else { - var notifyMessage = $"{_workingArranger.Name}: ({(int)Math.Truncate(e.X / Zoom)}, {(int)Math.Truncate(e.Y / Zoom)})"; + var notifyMessage = $"{WorkingArranger.Name}: ({(int)Math.Truncate(e.X / Zoom)}, {(int)Math.Truncate(e.Y / Zoom)})"; var notifyEvent = new NotifyStatusEvent(notifyMessage, NotifyStatusDuration.Indefinite); _events.PublishOnUIThread(notifyEvent); } @@ -391,20 +391,20 @@ public virtual void StartDrag(IDragInfo dragInfo) ArrangerCopy copy = default; if (SnapMode == SnapMode.Element) { - int x = rect.SnappedLeft / _workingArranger.ElementPixelSize.Width; - int y = rect.SnappedTop / _workingArranger.ElementPixelSize.Height; - int width = rect.SnappedWidth / _workingArranger.ElementPixelSize.Width; - int height = rect.SnappedHeight / _workingArranger.ElementPixelSize.Height; - copy = _workingArranger.CopyElements(x, y, width, height); + int x = rect.SnappedLeft / WorkingArranger.ElementPixelSize.Width; + int y = rect.SnappedTop / WorkingArranger.ElementPixelSize.Height; + int width = rect.SnappedWidth / WorkingArranger.ElementPixelSize.Width; + int height = rect.SnappedHeight / WorkingArranger.ElementPixelSize.Height; + copy = WorkingArranger.CopyElements(x, y, width, height); (copy as ElementCopy).ProjectResource = OriginatingProjectResource; } - else if (SnapMode == SnapMode.Pixel && _workingArranger.ColorType == PixelColorType.Indexed) + else if (SnapMode == SnapMode.Pixel && WorkingArranger.ColorType == PixelColorType.Indexed) { - copy = _workingArranger.CopyPixelsIndexed(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); + copy = WorkingArranger.CopyPixelsIndexed(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); } - else if (SnapMode == SnapMode.Pixel && _workingArranger.ColorType == PixelColorType.Direct) + else if (SnapMode == SnapMode.Pixel && WorkingArranger.ColorType == PixelColorType.Direct) { - copy = _workingArranger.CopyPixelsDirect(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); + copy = WorkingArranger.CopyPixelsDirect(rect.SnappedLeft, rect.SnappedTop, rect.SnappedWidth, rect.SnappedHeight); } var paste = new ArrangerPaste(copy, SnapMode) @@ -415,7 +415,7 @@ public virtual void StartDrag(IDragInfo dragInfo) dragInfo.Data = paste; dragInfo.Effects = DragDropEffects.Copy | DragDropEffects.Move; - Selection = new ArrangerSelection(_workingArranger, SnapMode); + Selection = new ArrangerSelection(WorkingArranger, SnapMode); } else if (Paste is object) { diff --git a/TileShop.WPF/Features/Arranger Editors/ScatteredArrangerEditorViewModel.cs b/TileShop.WPF/Features/Arranger Editors/ScatteredArrangerEditorViewModel.cs index c70a64e5..fc7777e1 100644 --- a/TileShop.WPF/Features/Arranger Editors/ScatteredArrangerEditorViewModel.cs +++ b/TileShop.WPF/Features/Arranger Editors/ScatteredArrangerEditorViewModel.cs @@ -56,7 +56,7 @@ public ScatteredArrangerEditorViewModel(Arranger arranger, IEventAggregator even IPaletteService paletteService, IProjectService projectService) : base(events, windowManager, paletteService) { Resource = arranger; - _workingArranger = arranger.CloneArranger(); + WorkingArranger = arranger.CloneArranger(); DisplayName = Resource?.Name ?? "Unnamed Arranger"; CreateImages(); @@ -72,7 +72,7 @@ public ScatteredArrangerEditorViewModel(Arranger arranger, IEventAggregator even CanAcceptElementPastes = true; } - var palettes = _workingArranger.GetReferencedPalettes(); + var palettes = WorkingArranger.GetReferencedPalettes(); palettes.ExceptWith(_paletteService.GlobalPalettes); var palModels = palettes.OrderBy(x => x.Name) @@ -90,22 +90,22 @@ public ScatteredArrangerEditorViewModel(Arranger arranger, IEventAggregator even public override void SaveChanges() { - if (_workingArranger.Layout == ArrangerLayout.Tiled) + if (WorkingArranger.Layout == ArrangerLayout.Tiled) { var treeArranger = Resource as Arranger; - if (_workingArranger.ArrangerElementSize != treeArranger.ArrangerElementSize) + if (WorkingArranger.ArrangerElementSize != treeArranger.ArrangerElementSize) { if (treeArranger.Layout == ArrangerLayout.Tiled) - treeArranger.Resize(_workingArranger.ArrangerElementSize.Width, _workingArranger.ArrangerElementSize.Height); + treeArranger.Resize(WorkingArranger.ArrangerElementSize.Width, WorkingArranger.ArrangerElementSize.Height); else if (treeArranger.Layout == ArrangerLayout.Single) - treeArranger.Resize(_workingArranger.ArrangerPixelSize.Width, _workingArranger.ArrangerPixelSize.Height); + treeArranger.Resize(WorkingArranger.ArrangerPixelSize.Width, WorkingArranger.ArrangerPixelSize.Height); } - for (int y = 0; y < _workingArranger.ArrangerElementSize.Height; y++) + for (int y = 0; y < WorkingArranger.ArrangerElementSize.Height; y++) { - for (int x = 0; x < _workingArranger.ArrangerElementSize.Width; x++) + for (int x = 0; x < WorkingArranger.ArrangerElementSize.Width; x++) { - var el = _workingArranger.GetElement(x, y); + var el = WorkingArranger.GetElement(x, y); treeArranger.SetElement(el, x, y); } } @@ -129,7 +129,7 @@ public override void SaveChanges() public override void DiscardChanges() { - _workingArranger = (Resource as Arranger).CloneArranger(); + WorkingArranger = (Resource as Arranger).CloneArranger(); CreateImages(); IsModified = false; } @@ -175,8 +175,8 @@ public override void OnMouseLeave(object sender, MouseCaptureArgs e) public override void OnMouseMove(object sender, MouseCaptureArgs e) { - int x = Math.Clamp((int)e.X / Zoom, 0, _workingArranger.ArrangerPixelSize.Width - 1); - int y = Math.Clamp((int)e.Y / Zoom, 0, _workingArranger.ArrangerPixelSize.Height - 1); + int x = Math.Clamp((int)e.X / Zoom, 0, WorkingArranger.ArrangerPixelSize.Width - 1); + int y = Math.Clamp((int)e.Y / Zoom, 0, WorkingArranger.ArrangerPixelSize.Height - 1); if (ActiveTool == ScatteredArrangerTool.ApplyPalette && e.LeftButton) { @@ -184,9 +184,9 @@ public override void OnMouseMove(object sender, MouseCaptureArgs e) } else if (ActiveTool == ScatteredArrangerTool.InspectElement) { - var elX = x / _workingArranger.ElementPixelSize.Width; - var elY = y / _workingArranger.ElementPixelSize.Height; - var el = _workingArranger.GetElement(elX, elY); + var elX = x / WorkingArranger.ElementPixelSize.Width; + var elY = y / WorkingArranger.ElementPixelSize.Height; + var el = WorkingArranger.GetElement(elX, elY); if (el is ArrangerElement element) { @@ -244,14 +244,14 @@ private void CreateImages() { CancelOverlay(); - if (_workingArranger.ColorType == PixelColorType.Indexed) + if (WorkingArranger.ColorType == PixelColorType.Indexed) { - _indexedImage = new IndexedImage(_workingArranger); + _indexedImage = new IndexedImage(WorkingArranger); BitmapAdapter = new IndexedBitmapAdapter(_indexedImage); } - else if (_workingArranger.ColorType == PixelColorType.Direct) + else if (WorkingArranger.ColorType == PixelColorType.Direct) { - _directImage = new DirectImage(_workingArranger); + _directImage = new DirectImage(WorkingArranger); BitmapAdapter = new DirectBitmapAdapter(_directImage); } @@ -260,11 +260,11 @@ private void CreateImages() protected override void CreateGridlines() { - if (_workingArranger.Layout == ArrangerLayout.Single) + if (WorkingArranger.Layout == ArrangerLayout.Single) { - CreateGridlines(0, 0, _workingArranger.ArrangerPixelSize.Width, _workingArranger.ArrangerPixelSize.Height, 8, 8); + CreateGridlines(0, 0, WorkingArranger.ArrangerPixelSize.Width, WorkingArranger.ArrangerPixelSize.Height, 8, 8); } - else if (_workingArranger.Layout == ArrangerLayout.Tiled) + else if (WorkingArranger.Layout == ArrangerLayout.Tiled) { base.CreateGridlines(); } @@ -274,12 +274,12 @@ public override void Render() { CancelOverlay(); - if (_workingArranger.ColorType == PixelColorType.Indexed) + if (WorkingArranger.ColorType == PixelColorType.Indexed) { _indexedImage.Render(); BitmapAdapter.Invalidate(); } - else if (_workingArranger.ColorType == PixelColorType.Direct) + else if (WorkingArranger.ColorType == PixelColorType.Direct) { _directImage.Render(); @@ -311,10 +311,10 @@ private MagitekResult ApplyPasteInternal(ArrangerPaste paste) var destStart = new Point(destX, destY); var sourceStart = new Point(sourceX, sourceY); - int copyWidth = Math.Min(elementCopy.Width - sourceX, _workingArranger.ArrangerElementSize.Width - destX); - int copyHeight = Math.Min(elementCopy.Height - sourceY, _workingArranger.ArrangerElementSize.Height - destY); + int copyWidth = Math.Min(elementCopy.Width - sourceX, WorkingArranger.ArrangerElementSize.Width - destX); + int copyHeight = Math.Min(elementCopy.Height - sourceY, WorkingArranger.ArrangerElementSize.Height - destY); - return ElementCopier.CopyElements(elementCopy, _workingArranger as ScatteredArranger, sourceStart, destStart, copyWidth, copyHeight); + return ElementCopier.CopyElements(elementCopy, WorkingArranger as ScatteredArranger, sourceStart, destStart, copyWidth, copyHeight); } #region Commands @@ -323,17 +323,17 @@ private void TryApplyPalette(int pixelX, int pixelY, Palette palette) bool needsRender = false; if (Selection.HasSelection && Selection.SelectionRect.ContainsPointSnapped(pixelX, pixelY)) { - int top = Selection.SelectionRect.SnappedTop / _workingArranger.ElementPixelSize.Height; - int bottom = Selection.SelectionRect.SnappedBottom / _workingArranger.ElementPixelSize.Height; - int left = Selection.SelectionRect.SnappedLeft / _workingArranger.ElementPixelSize.Width; - int right = Selection.SelectionRect.SnappedRight / _workingArranger.ElementPixelSize.Width; + int top = Selection.SelectionRect.SnappedTop / WorkingArranger.ElementPixelSize.Height; + int bottom = Selection.SelectionRect.SnappedBottom / WorkingArranger.ElementPixelSize.Height; + int left = Selection.SelectionRect.SnappedLeft / WorkingArranger.ElementPixelSize.Width; + int right = Selection.SelectionRect.SnappedRight / WorkingArranger.ElementPixelSize.Width; for (int posY = top; posY < bottom; posY++) { for (int posX = left; posX < right; posX++) { - int applyPixelX = posX * _workingArranger.ElementPixelSize.Width; - int applyPixelY = posY * _workingArranger.ElementPixelSize.Height; + int applyPixelX = posX * WorkingArranger.ElementPixelSize.Width; + int applyPixelY = posY * WorkingArranger.ElementPixelSize.Height; if (TryApplySinglePalette(applyPixelX, applyPixelY, SelectedPalette.Palette, false)) { needsRender = true; @@ -352,10 +352,10 @@ private void TryApplyPalette(int pixelX, int pixelY, Palette palette) bool TryApplySinglePalette(int pixelX, int pixelY, Palette palette, bool notify) { - if (pixelX >= _workingArranger.ArrangerPixelSize.Width || pixelY >= _workingArranger.ArrangerPixelSize.Height) + if (pixelX >= WorkingArranger.ArrangerPixelSize.Width || pixelY >= WorkingArranger.ArrangerPixelSize.Height) return false; - var el = _workingArranger.GetElementAtPixel(pixelX, pixelY); + var el = WorkingArranger.GetElementAtPixel(pixelX, pixelY); if (el is ArrangerElement element) { @@ -386,13 +386,13 @@ bool TryApplySinglePalette(int pixelX, int pixelY, Palette palette, bool notify) private bool TryPickPalette(int pixelX, int pixelY) { - var elX = pixelX / _workingArranger.ElementPixelSize.Width; - var elY = pixelY / _workingArranger.ElementPixelSize.Height; + var elX = pixelX / WorkingArranger.ElementPixelSize.Width; + var elY = pixelY / WorkingArranger.ElementPixelSize.Height; - if (elX >= _workingArranger.ArrangerElementSize.Width || elY >= _workingArranger.ArrangerElementSize.Height) + if (elX >= WorkingArranger.ArrangerElementSize.Width || elY >= WorkingArranger.ArrangerElementSize.Height) return false; - var el = _workingArranger.GetElement(elX, elY); + var el = WorkingArranger.GetElement(elX, elY); if (el is ArrangerElement element) { @@ -405,11 +405,11 @@ private bool TryPickPalette(int pixelX, int pixelY) public void ResizeArranger() { - var model = new ResizeTiledScatteredArrangerViewModel(_windowManager, _workingArranger.ArrangerElementSize.Width, _workingArranger.ArrangerElementSize.Height); + var model = new ResizeTiledScatteredArrangerViewModel(_windowManager, WorkingArranger.ArrangerElementSize.Width, WorkingArranger.ArrangerElementSize.Height); if (_windowManager.ShowDialog(model) is true) { - _workingArranger.Resize(model.Width, model.Height); + WorkingArranger.Resize(model.Width, model.Height); CreateImages(); AddHistoryAction(new ResizeArrangerHistoryAction(model.Width, model.Height)); @@ -422,7 +422,8 @@ public void AssociatePalette() var projectTree = _projectService.GetContainingProject(Resource); var palettes = projectTree.Tree.EnumerateDepthFirst() .Where(x => x.Value is Palette) - .Select(x => new AssociatePaletteModel(x.Value as Palette, x.PathKey)); + .Select(x => new AssociatePaletteModel(x.Value as Palette, x.PathKey)) + .Concat(_projectService.GlobalResources.OfType().Select(x => new AssociatePaletteModel(x, x.Name))); var model = new AssociatePaletteViewModel(palettes); @@ -480,16 +481,16 @@ public void DeleteElementSelection() private void DeleteElementSelection(SnappedRectangle rect) { - int startX = rect.SnappedLeft / _workingArranger.ElementPixelSize.Width; - int startY = rect.SnappedTop / _workingArranger.ElementPixelSize.Height; - int width = rect.SnappedWidth / _workingArranger.ElementPixelSize.Height; - int height = rect.SnappedHeight / _workingArranger.ElementPixelSize.Width; + int startX = rect.SnappedLeft / WorkingArranger.ElementPixelSize.Width; + int startY = rect.SnappedTop / WorkingArranger.ElementPixelSize.Height; + int width = rect.SnappedWidth / WorkingArranger.ElementPixelSize.Height; + int height = rect.SnappedHeight / WorkingArranger.ElementPixelSize.Width; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - _workingArranger.ResetElement(x + startX, y + startY); + WorkingArranger.ResetElement(x + startX, y + startY); } } } @@ -518,7 +519,7 @@ public override void ApplyHistoryAction(HistoryAction action) } else if (action is ResizeArrangerHistoryAction resizeAction) { - _workingArranger.Resize(resizeAction.Width, resizeAction.Height); + WorkingArranger.Resize(resizeAction.Width, resizeAction.Height); CreateImages(); } } @@ -536,7 +537,7 @@ public override void Undo() IsModified = UndoHistory.Count > 0; - _workingArranger = (Resource as Arranger).CloneArranger(); + WorkingArranger = (Resource as Arranger).CloneArranger(); CreateImages(); foreach (var action in UndoHistory) diff --git a/TileShop.WPF/Features/Arranger Editors/SequentialArrangerEditorView.xaml b/TileShop.WPF/Features/Arranger Editors/SequentialArrangerEditorView.xaml index 475b6d05..c29e16fb 100644 --- a/TileShop.WPF/Features/Arranger Editors/SequentialArrangerEditorView.xaml +++ b/TileShop.WPF/Features/Arranger Editors/SequentialArrangerEditorView.xaml @@ -17,7 +17,7 @@ - + @@ -70,10 +70,14 @@ - + @@ -84,12 +88,14 @@ @@ -97,6 +103,7 @@ @@ -132,7 +139,6 @@ @@ -208,6 +214,7 @@ _tiledElementWidth; set { - var preferredWidth = (_workingArranger as SequentialArranger).ActiveCodec.GetPreferredWidth(value); + var preferredWidth = (WorkingArranger as SequentialArranger).ActiveCodec.GetPreferredWidth(value); SetAndNotify(ref _tiledElementWidth, preferredWidth); ChangeCodecDimensions(TiledElementWidth, TiledElementHeight); } @@ -73,7 +74,7 @@ public int TiledElementHeight get => _tiledElementHeight; set { - var preferredHeight = (_workingArranger as SequentialArranger).ActiveCodec.GetPreferredHeight(value); + var preferredHeight = (WorkingArranger as SequentialArranger).ActiveCodec.GetPreferredHeight(value); SetAndNotify(ref _tiledElementHeight, preferredHeight); ChangeCodecDimensions(TiledElementWidth, TiledElementHeight); } @@ -107,7 +108,7 @@ public int LinearArrangerWidth get => _linearArrangerWidth; set { - var preferredWidth = (_workingArranger as SequentialArranger).ActiveCodec.GetPreferredWidth(value); + var preferredWidth = (WorkingArranger as SequentialArranger).ActiveCodec.GetPreferredWidth(value); SetAndNotify(ref _linearArrangerWidth, preferredWidth); ChangeCodecDimensions(LinearArrangerWidth, LinearArrangerHeight); } @@ -119,7 +120,7 @@ public int LinearArrangerHeight get => _linearArrangerHeight; set { - var preferredHeight = (_workingArranger as SequentialArranger).ActiveCodec.GetPreferredHeight(value); + var preferredHeight = (WorkingArranger as SequentialArranger).ActiveCodec.GetPreferredHeight(value); SetAndNotify(ref _linearArrangerHeight, preferredHeight); ChangeCodecDimensions(LinearArrangerWidth, LinearArrangerHeight); } @@ -176,7 +177,7 @@ public SequentialArrangerEditorViewModel(SequentialArranger arranger, IEventAggr base(events, windowManager, paletteService) { Resource = arranger; - _workingArranger = arranger; + WorkingArranger = arranger; _codecService = codecService; _tracker = tracker; DisplayName = Resource?.Name ?? "Unnamed Arranger"; @@ -210,8 +211,8 @@ public SequentialArrangerEditorViewModel(SequentialArranger arranger, IEventAggr Palettes = new BindableCollection(_paletteService.GlobalPalettes.Select(x => new PaletteModel(x))); SelectedPalette = Palettes.First(); - ArrangerPageSize = (int) (_workingArranger as SequentialArranger).ArrangerBitSize / 8; - MaxFileDecodingOffset = (_workingArranger as SequentialArranger).FileSize - ArrangerPageSize; + ArrangerPageSize = (int) (WorkingArranger as SequentialArranger).ArrangerBitSize / 8; + MaxFileDecodingOffset = (WorkingArranger as SequentialArranger).FileSize - ArrangerPageSize; } public override void SaveChanges() @@ -284,12 +285,12 @@ public void NewScatteredArrangerFromSelection() { if (SnapMode == SnapMode.Element) { - int x = Selection.SelectionRect.SnappedLeft / _workingArranger.ElementPixelSize.Width; - int y = Selection.SelectionRect.SnappedTop / _workingArranger.ElementPixelSize.Height; - int width = Selection.SelectionRect.SnappedWidth / _workingArranger.ElementPixelSize.Width; - int height = Selection.SelectionRect.SnappedHeight / _workingArranger.ElementPixelSize.Height; + int x = Selection.SelectionRect.SnappedLeft / WorkingArranger.ElementPixelSize.Width; + int y = Selection.SelectionRect.SnappedTop / WorkingArranger.ElementPixelSize.Height; + int width = Selection.SelectionRect.SnappedWidth / WorkingArranger.ElementPixelSize.Width; + int height = Selection.SelectionRect.SnappedHeight / WorkingArranger.ElementPixelSize.Height; - var copy = new ElementCopy(_workingArranger, x, y, width, height); + var copy = new ElementCopy(WorkingArranger, x, y, width, height); var model = new AddScatteredArrangerFromCopyEvent(copy, OriginatingProjectResource); _events.PublishOnUIThread(model); } @@ -301,8 +302,8 @@ public void NewScatteredArrangerFromSelection() private void Move(ArrangerMoveType moveType) { - var oldAddress = (_workingArranger as SequentialArranger).GetInitialSequentialFileAddress(); - var newAddress = (_workingArranger as SequentialArranger).Move(moveType); + var oldAddress = (WorkingArranger as SequentialArranger).GetInitialSequentialFileAddress(); + var newAddress = (WorkingArranger as SequentialArranger).Move(moveType); if (oldAddress != newAddress) { @@ -314,8 +315,8 @@ private void Move(ArrangerMoveType moveType) private void Move(long offset) { - var oldAddress = (_workingArranger as SequentialArranger).GetInitialSequentialFileAddress(); - var newAddress = (_workingArranger as SequentialArranger).Move(new FileBitAddress(offset, 0)); + var oldAddress = (WorkingArranger as SequentialArranger).GetInitialSequentialFileAddress(); + var newAddress = (WorkingArranger as SequentialArranger).Move(new FileBitAddress(offset, 0)); if (oldAddress != newAddress) { @@ -330,18 +331,18 @@ private void ResizeArranger(int arrangerWidth, int arrangerHeight) if (arrangerWidth <= 0 || arrangerHeight <= 0) return; - if (arrangerWidth == _workingArranger.ArrangerElementSize.Width && - arrangerHeight == _workingArranger.ArrangerElementSize.Height && IsTiledLayout) + if (arrangerWidth == WorkingArranger.ArrangerElementSize.Width && + arrangerHeight == WorkingArranger.ArrangerElementSize.Height && IsTiledLayout) return; - if (arrangerWidth == _workingArranger.ArrangerPixelSize.Width && - arrangerHeight == _workingArranger.ArrangerPixelSize.Height && IsSingleLayout) + if (arrangerWidth == WorkingArranger.ArrangerPixelSize.Width && + arrangerHeight == WorkingArranger.ArrangerPixelSize.Height && IsSingleLayout) return; - (_workingArranger as SequentialArranger).Resize(arrangerWidth, arrangerHeight); + (WorkingArranger as SequentialArranger).Resize(arrangerWidth, arrangerHeight); CreateImages(); - ArrangerPageSize = (int)(_workingArranger as SequentialArranger).ArrangerBitSize / 8; - MaxFileDecodingOffset = (_workingArranger as SequentialArranger).FileSize - ArrangerPageSize; + ArrangerPageSize = (int)(WorkingArranger as SequentialArranger).ArrangerBitSize / 8; + MaxFileDecodingOffset = (WorkingArranger as SequentialArranger).FileSize - ArrangerPageSize; } public void SelectNextCodec() @@ -368,13 +369,13 @@ public void ToggleSnapMode() private void ChangeCodec() { - var codec = _codecService.CodecFactory.GetCodec(SelectedCodecName); + var codec = _codecService.CodecFactory.GetCodec(SelectedCodecName, default); if (codec.Layout == ImageMagitek.Codec.ImageLayout.Tiled) { _tiledElementHeight = codec.Height; _tiledElementWidth = codec.Width; - (_workingArranger as SequentialArranger).ChangeCodec(codec, TiledArrangerWidth, TiledArrangerHeight); + (WorkingArranger as SequentialArranger).ChangeCodec(codec, TiledArrangerWidth, TiledArrangerHeight); SnapMode = SnapMode.Element; NotifyOfPropertyChange(() => TiledElementHeight); @@ -386,16 +387,16 @@ private void ChangeCodec() _linearArrangerHeight = codec.Height; _linearArrangerWidth = codec.Width; - (_workingArranger as SequentialArranger).ChangeCodec(codec, 1, 1); + (WorkingArranger as SequentialArranger).ChangeCodec(codec, 1, 1); SnapMode = SnapMode.Pixel; NotifyOfPropertyChange(() => LinearArrangerHeight); NotifyOfPropertyChange(() => LinearArrangerWidth); } - _fileOffset = (_workingArranger as SequentialArranger).FileAddress; - ArrangerPageSize = (int)(_workingArranger as SequentialArranger).ArrangerBitSize / 8; - MaxFileDecodingOffset = (_workingArranger as SequentialArranger).FileSize - ArrangerPageSize; + _fileOffset = (WorkingArranger as SequentialArranger).FileAddress; + ArrangerPageSize = (int)(WorkingArranger as SequentialArranger).ArrangerBitSize / 8; + MaxFileDecodingOffset = (WorkingArranger as SequentialArranger).FileSize - ArrangerPageSize; CanResize = codec.CanResize; WidthIncrement = codec.WidthResizeIncrement; HeightIncrement = codec.HeightResizeIncrement; @@ -408,14 +409,14 @@ private void ChangeCodec() private void ChangePalette(PaletteModel pal) { - (_workingArranger as SequentialArranger).ChangePalette(pal.Palette); + (WorkingArranger as SequentialArranger).ChangePalette(pal.Palette); Render(); } private void ChangeCodecDimensions(int width, int height) { - var codec = _codecService.CodecFactory.GetCodec(SelectedCodecName, width, height); - (_workingArranger as SequentialArranger).ChangeCodec(codec); + var codec = _codecService.CodecFactory.GetCodec(SelectedCodecName, new Size(width, height)); + (WorkingArranger as SequentialArranger).ChangeCodec(codec); CreateImages(); } @@ -423,14 +424,14 @@ private void CreateImages() { CancelOverlay(); - if (_workingArranger.ColorType == PixelColorType.Indexed) + if (WorkingArranger.ColorType == PixelColorType.Indexed) { - _indexedImage = new IndexedImage(_workingArranger); + _indexedImage = new IndexedImage(WorkingArranger); BitmapAdapter = new IndexedBitmapAdapter(_indexedImage); } - else if (_workingArranger.ColorType == PixelColorType.Direct) + else if (WorkingArranger.ColorType == PixelColorType.Direct) { - _directImage = new DirectImage(_workingArranger); + _directImage = new DirectImage(WorkingArranger); BitmapAdapter = new DirectBitmapAdapter(_directImage); } @@ -439,11 +440,11 @@ private void CreateImages() protected override void CreateGridlines() { - if (_workingArranger.Layout == ArrangerLayout.Single) + if (WorkingArranger.Layout == ArrangerLayout.Single) { - CreateGridlines(0, 0, _workingArranger.ArrangerPixelSize.Width, _workingArranger.ArrangerPixelSize.Height, 8, 8); + CreateGridlines(0, 0, WorkingArranger.ArrangerPixelSize.Width, WorkingArranger.ArrangerPixelSize.Height, 8, 8); } - else if (_workingArranger.Layout == ArrangerLayout.Tiled) + else if (WorkingArranger.Layout == ArrangerLayout.Tiled) { base.CreateGridlines(); } @@ -453,12 +454,12 @@ public override void Render() { CancelOverlay(); - if (_workingArranger.ColorType == PixelColorType.Indexed) + if (WorkingArranger.ColorType == PixelColorType.Indexed) { _indexedImage.Render(); BitmapAdapter.Invalidate(); } - else if (_workingArranger.ColorType == PixelColorType.Direct) + else if (WorkingArranger.ColorType == PixelColorType.Direct) { _directImage.Render(); BitmapAdapter.Invalidate(); diff --git a/TileShop.WPF/Features/Dialogs/AddPaletteView.xaml b/TileShop.WPF/Features/Dialogs/AddPaletteView.xaml index c81a56f8..a5a754b5 100644 --- a/TileShop.WPF/Features/Dialogs/AddPaletteView.xaml +++ b/TileShop.WPF/Features/Dialogs/AddPaletteView.xaml @@ -8,8 +8,8 @@ xmlns:s="https://github.com/canton7/Stylet" xmlns:ui="http://schemas.modernwpf.com/2019" Title="Add a New Palette" - Width="440" - Height="320" + Width="400" + Height="400" MinWidth="220" ui:ThemeManager.IsThemeAware="True" ui:WindowHelper.UseModernWindowStyle="True" @@ -19,75 +19,79 @@ - - - + + - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - + MinWidth="150" + ui:ControlHelper.Header="Color Model" + ItemsSource="{Binding ColorModels}" + SelectedIndex="1" + SelectedItem="{Binding SelectedColorModel}" + ToolTip="Palette Color Model" /> + - + + + + + + + + + + + /// Palette containing the colors - public ColorRemapViewModel(Palette palette) : this(palette, palette.Entries) { } + public ColorRemapViewModel(Palette palette, IColorFactory colorFactory) : this(palette, palette.Entries, colorFactory) { } /// /// ViewModel responsible for remapping Palette colors of an indexed image /// /// Palette containing the colors /// Number of colors to remap starting with the 0-index - public ColorRemapViewModel(Palette palette, int paletteEntries) + /// Factory to create/convert colors + public ColorRemapViewModel(Palette palette, int paletteEntries, IColorFactory colorFactory) { + _colorFactory = colorFactory; + for (int i = 0; i < paletteEntries; i++) { - var nativeColor = ImageMagitek.Colors.ColorConverter.ToNative(palette[i]); + var nativeColor = _colorFactory.ToNative(palette[i]); var color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); InitialColors.Add(new RemappableColorModel(color, i)); FinalColors.Add(new RemappableColorModel(color, i)); diff --git a/TileShop.WPF/Features/Dialogs/ImportImageViewModel.cs b/TileShop.WPF/Features/Dialogs/ImportImageViewModel.cs index 0472c49d..a8a3c567 100644 --- a/TileShop.WPF/Features/Dialogs/ImportImageViewModel.cs +++ b/TileShop.WPF/Features/Dialogs/ImportImageViewModel.cs @@ -12,7 +12,6 @@ namespace TileShop.WPF.ViewModels public class ImportImageViewModel : Screen { private readonly Arranger _arranger; - private readonly IPaletteService _paletteService; private readonly IFileSelectService _fileSelect; private readonly IndexedImage _originalIndexed; @@ -81,10 +80,9 @@ public int Zoom public bool IsTiledArranger => _arranger.Layout == ArrangerLayout.Tiled; public bool IsSingleArranger => _arranger.Layout == ArrangerLayout.Single; - public ImportImageViewModel(Arranger arranger, IPaletteService paletteService, IFileSelectService fileSelect) + public ImportImageViewModel(Arranger arranger, IFileSelectService fileSelect) { _arranger = arranger; - _paletteService = paletteService; _fileSelect = fileSelect; if (_arranger.ColorType == PixelColorType.Indexed) @@ -119,7 +117,7 @@ public void BrowseForImportFile() else if (_arranger.ColorType == PixelColorType.Direct) { _importedDirect = new DirectImage(_arranger); - _importedDirect.ImportImage(ImageFileName, new ImageFileAdapter()); + _importedDirect.ImportImage(ImageFileName, new ImageSharpFileAdapter()); ImportedSource = new DirectBitmapAdapter(_importedDirect); CanImport = true; } @@ -131,7 +129,7 @@ private void ImportIndexed(string fileName) var matchStrategy = UseExactMatching ? ColorMatchStrategy.Exact : ColorMatchStrategy.Nearest; _importedIndexed = new IndexedImage(_arranger); - var result = _importedIndexed.TryImportImage(fileName, new ImageFileAdapter(), matchStrategy); + var result = _importedIndexed.TryImportImage(fileName, new ImageSharpFileAdapter(), matchStrategy); result.Switch( success => diff --git a/TileShop.WPF/Features/Palette Editor/PaletteEditorViewModel.cs b/TileShop.WPF/Features/Palette Editor/PaletteEditorViewModel.cs index 271eb9d6..23b72d96 100644 --- a/TileShop.WPF/Features/Palette Editor/PaletteEditorViewModel.cs +++ b/TileShop.WPF/Features/Palette Editor/PaletteEditorViewModel.cs @@ -4,39 +4,42 @@ using TileShop.WPF.Models; using ImageMagitek.Colors; using System.Linq; +using ImageMagitek.Services; namespace TileShop.WPF.ViewModels { public class PaletteEditorViewModel : ResourceEditorBaseViewModel { - private Palette _palette; - private IEventAggregator _events; + private readonly Palette _palette; + private readonly IPaletteService _paletteService; + private readonly IEventAggregator _events; - private BindableCollection _colors = new BindableCollection(); - public BindableCollection Colors + private BindableCollection _colors = new BindableCollection(); + public BindableCollection Colors { get => _colors; set => SetAndNotify(ref _colors, value); } - private ValidatedColorModel _selectedColor; - public ValidatedColorModel SelectedColor + private ValidatedColor32Model _selectedColor; + public ValidatedColor32Model SelectedColor { get => _selectedColor; set => SetAndNotify(ref _selectedColor, value); } - public PaletteEditorViewModel(Palette palette, IEventAggregator events) + public PaletteEditorViewModel(Palette palette, IPaletteService paletteService, IEventAggregator events) { Resource = palette; _palette = palette; + _paletteService = paletteService; _events = events; events.Subscribe(this); DisplayName = Resource?.Name ?? "Unnamed Palette"; for(int i = 0; i < _palette.Entries; i++) - Colors.Add(new ValidatedColorModel(_palette.GetForeignColor(i), i)); + Colors.Add(new ValidatedColor32Model((IColor32)_palette.GetForeignColor(i), i, _paletteService.ColorFactory)); SelectedColor = Colors.First(); } @@ -52,6 +55,9 @@ public override void SaveChanges() { _palette.SavePalette(); IsModified = false; + + var changeEvent = new PaletteChangedEvent(_palette); + _events.PublishOnUIThread(changeEvent); } public override void DiscardChanges() @@ -60,7 +66,7 @@ public override void DiscardChanges() IsModified = false; } - public void MouseOver(ValidatedColorModel model) + public void MouseOver(ValidatedColor32Model model) { string notifyMessage = $"Palette Index: {model.Index}"; var notifyEvent = new NotifyStatusEvent(notifyMessage, NotifyStatusDuration.Indefinite); diff --git a/TileShop.WPF/Features/Palette Editor/TablePaletteEditorView.xaml b/TileShop.WPF/Features/Palette Editor/TablePaletteEditorView.xaml new file mode 100644 index 00000000..f80530ca --- /dev/null +++ b/TileShop.WPF/Features/Palette Editor/TablePaletteEditorView.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TileShop.WPF/Features/Palette Editor/TablePaletteEditorViewModel.cs b/TileShop.WPF/Features/Palette Editor/TablePaletteEditorViewModel.cs new file mode 100644 index 00000000..6ba803b8 --- /dev/null +++ b/TileShop.WPF/Features/Palette Editor/TablePaletteEditorViewModel.cs @@ -0,0 +1,122 @@ +using System; +using Stylet; +using ImageMagitek.Colors; +using ImageMagitek.Services; +using TileShop.Shared.EventModels; +using TileShop.WPF.Models; +using GongSolutions.Wpf.DragDrop; +using System.Linq; + +namespace TileShop.WPF.ViewModels +{ + class TablePaletteEditorViewModel : ResourceEditorBaseViewModel, IDropTarget + { + private readonly Palette _palette; + private readonly IPaletteService _paletteService; + private readonly IColorFactory _colorFactory; + private readonly IEventAggregator _events; + + private BindableCollection _workingColors = new BindableCollection(); + public BindableCollection WorkingColors + { + get => _workingColors; + set => SetAndNotify(ref _workingColors, value); + } + + private BindableCollection _availableColors = new BindableCollection(); + public BindableCollection AvailableColors + { + get => _availableColors; + set => SetAndNotify(ref _availableColors, value); + } + + public TablePaletteEditorViewModel(Palette palette, IPaletteService paletteService, IEventAggregator events) + { + Resource = palette; + _palette = palette; + _paletteService = paletteService; + _colorFactory = _paletteService.ColorFactory; + _events = events; + events.Subscribe(this); + + DisplayName = Resource?.Name ?? "Unnamed Palette"; + + for (int i = 0; i < _palette.Entries; i++) + { + WorkingColors.Add(new ValidatedTableColorModel((ITableColor)_palette.GetForeignColor(i), i, _paletteService.ColorFactory)); + } + + for (int i = 0; i < 64; i++) + { + var nesColor = new ValidatedTableColorModel(new ColorNes((uint)i), i, _colorFactory); + AvailableColors.Add(nesColor); + } + } + + public override void SaveChanges() + { + for (int i = 0; i < WorkingColors.Count; i++) + { + var color = _colorFactory.CreateColor(_palette.ColorModel, WorkingColors[i].WorkingColor.Color); + _palette.SetForeignColor(i, color); + } + + _palette.SavePalette(); + IsModified = false; + + var changeEvent = new PaletteChangedEvent(_palette); + _events.PublishOnUIThread(changeEvent); + } + + public override void DiscardChanges() + { + _palette.Reload(); + IsModified = false; + } + + public void MouseOver(ValidatedTableColorModel model) + { + string notifyMessage = $"Palette Index: {model.Index}"; + var notifyEvent = new NotifyStatusEvent(notifyMessage, NotifyStatusDuration.Indefinite); + _events.PublishOnUIThread(notifyEvent); + } + + public override void Undo() + { + throw new NotImplementedException(); + } + + public override void Redo() + { + throw new NotImplementedException(); + } + + public override void ApplyHistoryAction(HistoryAction action) + { + throw new NotImplementedException(); + } + + public void DragOver(IDropInfo dropInfo) + { + if (dropInfo.Data is ValidatedTableColorModel model) + { + dropInfo.Effects = System.Windows.DragDropEffects.Copy; + dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight; + } + } + + public void Drop(IDropInfo dropInfo) + { + if (dropInfo.Data is ValidatedTableColorModel dropModel && dropInfo.TargetItem is ValidatedTableColorModel targetModel) + { + var index = targetModel.Index; + var color = new ColorNes(dropModel.WorkingColor.Color); + WorkingColors[index] = new ValidatedTableColorModel(color, index, _colorFactory); + + IsModified = Enumerable + .Range(0, WorkingColors.Count) + .Any(x => WorkingColors[x].WorkingColor.Color != _palette.GetForeignColor(x).Color); + } + } + } +} diff --git a/TileShop.WPF/Features/Pixel Editor/DirectPixelEditorViewModel.cs b/TileShop.WPF/Features/Pixel Editor/DirectPixelEditorViewModel.cs index 6c78af8b..6a0cf116 100644 --- a/TileShop.WPF/Features/Pixel Editor/DirectPixelEditorViewModel.cs +++ b/TileShop.WPF/Features/Pixel Editor/DirectPixelEditorViewModel.cs @@ -32,16 +32,16 @@ public DirectPixelEditorViewModel(Arranger arranger, Arranger projectArranger, i public void Initialize(Arranger arranger, int viewX, int viewY, int viewWidth, int viewHeight) { Resource = arranger; - _workingArranger = arranger.CloneArranger(); + WorkingArranger = arranger.CloneArranger(); _viewX = viewX; _viewY = viewY; _viewWidth = viewWidth; _viewHeight = viewHeight; - _directImage = new DirectImage(_workingArranger, _viewX, _viewY, _viewWidth, _viewHeight); + _directImage = new DirectImage(WorkingArranger, _viewX, _viewY, _viewWidth, _viewHeight); BitmapAdapter = new DirectBitmapAdapter(_directImage); - DisplayName = $"Pixel Editor - {_workingArranger.Name}"; + DisplayName = $"Pixel Editor - {WorkingArranger.Name}"; PrimaryColor = new ColorRgba32(255, 255, 255, 255); SecondaryColor = new ColorRgba32(0, 0, 0, 255); @@ -144,7 +144,7 @@ public MagitekResult ApplyPasteInternal(ArrangerPaste paste) return ImageCopier.CopyPixels(directCopy.Image, _directImage, sourceStart, destStart, copyWidth, copyHeight); } else - throw new InvalidOperationException($"{nameof(ApplyPasteInternal)} attempted to copy from an arranger of type {Paste.Copy.Source.ColorType} to {_workingArranger.ColorType}"); + throw new InvalidOperationException($"{nameof(ApplyPasteInternal)} attempted to copy from an arranger of type {Paste.Copy.Source.ColorType} to {WorkingArranger.ColorType}"); } } } diff --git a/TileShop.WPF/Features/Pixel Editor/IndexedPixelEditorViewModel.cs b/TileShop.WPF/Features/Pixel Editor/IndexedPixelEditorViewModel.cs index d6e8efc0..aadf5990 100644 --- a/TileShop.WPF/Features/Pixel Editor/IndexedPixelEditorViewModel.cs +++ b/TileShop.WPF/Features/Pixel Editor/IndexedPixelEditorViewModel.cs @@ -48,18 +48,18 @@ public IndexedPixelEditorViewModel(Arranger arranger, Arranger projectArranger, private void Initialize(Arranger arranger, int viewX, int viewY, int viewWidth, int viewHeight) { Resource = arranger; - _workingArranger = arranger.CloneArranger(); + WorkingArranger = arranger.CloneArranger(); _viewX = viewX; _viewY = viewY; _viewWidth = viewWidth; _viewHeight = viewHeight; - var maxColors = _workingArranger.EnumerateElementsByPixel(viewX, viewY, viewWidth, viewHeight) + var maxColors = WorkingArranger.EnumerateElementsByPixel(viewX, viewY, viewWidth, viewHeight) .OfType() .Select(x => 1 << x.Codec.ColorDepth) .Max(); - var arrangerPalettes = _workingArranger.EnumerateElementsByPixel(viewX, viewY, viewWidth, viewHeight) + var arrangerPalettes = WorkingArranger.EnumerateElementsByPixel(viewX, viewY, viewWidth, viewHeight) .OfType() .Select(x => x.Palette) .Distinct() @@ -68,10 +68,10 @@ private void Initialize(Arranger arranger, int viewX, int viewY, int viewWidth, Palettes = new BindableCollection(arrangerPalettes); - _indexedImage = new IndexedImage(_workingArranger, _viewX, _viewY, _viewWidth, _viewHeight); + _indexedImage = new IndexedImage(WorkingArranger, _viewX, _viewY, _viewWidth, _viewHeight); BitmapAdapter = new IndexedBitmapAdapter(_indexedImage); - DisplayName = $"Pixel Editor - {_workingArranger.Name}"; + DisplayName = $"Pixel Editor - {WorkingArranger.Name}"; SnapMode = SnapMode.Pixel; Selection = new ArrangerSelection(arranger, SnapMode); @@ -89,22 +89,22 @@ private void Initialize(Arranger arranger, int viewX, int viewY, int viewWidth, protected override void CreateGridlines() { - if (_workingArranger is null) + if (WorkingArranger is null) return; - if (_workingArranger.Layout == ArrangerLayout.Single) + if (WorkingArranger.Layout == ArrangerLayout.Single) { CreateGridlines(0, 0, _viewWidth, _viewHeight, 8, 8); } - else if (_workingArranger.Layout == ArrangerLayout.Tiled) + else if (WorkingArranger.Layout == ArrangerLayout.Tiled) { - var location = _workingArranger.PointToElementLocation(new Point(_viewX, _viewY)); + var location = WorkingArranger.PointToElementLocation(new Point(_viewX, _viewY)); - int x = _workingArranger.ElementPixelSize.Width - (_viewX - location.X * _workingArranger.ElementPixelSize.Width); - int y = _workingArranger.ElementPixelSize.Height - (_viewY - location.Y * _workingArranger.ElementPixelSize.Height); + int x = WorkingArranger.ElementPixelSize.Width - (_viewX - location.X * WorkingArranger.ElementPixelSize.Width); + int y = WorkingArranger.ElementPixelSize.Height - (_viewY - location.Y * WorkingArranger.ElementPixelSize.Height); CreateGridlines(x, y, _viewWidth, _viewHeight, - _workingArranger.ElementPixelSize.Width, _workingArranger.ElementPixelSize.Height); + WorkingArranger.ElementPixelSize.Width, WorkingArranger.ElementPixelSize.Height); } } @@ -138,9 +138,9 @@ public bool CanRemapColors { get { - var palettes = _workingArranger?.GetReferencedPalettes(); + var palettes = WorkingArranger?.GetReferencedPalettes(); if (palettes?.Count <= 1) - return _workingArranger.GetReferencedCodecs().All(x => x.ColorType == PixelColorType.Indexed); + return WorkingArranger.GetReferencedCodecs().All(x => x.ColorType == PixelColorType.Indexed); return false; } @@ -148,14 +148,14 @@ public bool CanRemapColors public void RemapColors() { - var palette = _workingArranger.GetReferencedPalettes().FirstOrDefault(); + var palette = WorkingArranger.GetReferencedPalettes().FirstOrDefault(); if (palette is null) palette = _paletteService.DefaultPalette; - var maxArrangerColors = _workingArranger.EnumerateElements().OfType().Select(x => x.Codec?.ColorDepth ?? 0).Max(); + var maxArrangerColors = WorkingArranger.EnumerateElements().OfType().Select(x => x.Codec?.ColorDepth ?? 0).Max(); var colors = Math.Min(256, 1 << maxArrangerColors); - var remapViewModel = new ColorRemapViewModel(palette, colors); + var remapViewModel = new ColorRemapViewModel(palette, colors, _paletteService.ColorFactory); if (_windowManager.ShowDialog(remapViewModel) is true) { var remap = remapViewModel.FinalColors.Select(x => (byte)x.Index).ToList(); @@ -170,7 +170,7 @@ public void RemapColors() public override void PickColor(int x, int y, ColorPriority priority) { - var el = _workingArranger.GetElementAtPixel(x, y); + var el = WorkingArranger.GetElementAtPixel(x, y); if (el is ArrangerElement element) { @@ -267,7 +267,7 @@ private MagitekResult ApplyPasteInternal(ArrangerPaste paste) // ImageRemapOperation.RemapByExactPaletteColors, ImageRemapOperation.RemapByExactIndex); } else - throw new InvalidOperationException($"{nameof(ApplyPaste)} attempted to copy from an arranger of type {paste.Copy.Source.ColorType} to {_workingArranger.ColorType}"); + throw new InvalidOperationException($"{nameof(ApplyPaste)} attempted to copy from an arranger of type {paste.Copy.Source.ColorType} to {WorkingArranger.ColorType}"); } public override void ApplyHistoryAction(HistoryAction action) diff --git a/TileShop.WPF/Features/Pixel Editor/PixelEditorViewModel.cs b/TileShop.WPF/Features/Pixel Editor/PixelEditorViewModel.cs index d5263a9a..c88d25a0 100644 --- a/TileShop.WPF/Features/Pixel Editor/PixelEditorViewModel.cs +++ b/TileShop.WPF/Features/Pixel Editor/PixelEditorViewModel.cs @@ -187,7 +187,7 @@ public override void OnMouseMove(object sender, MouseCaptureArgs e) int x = (int)e.X / Zoom; int y = (int)e.Y / Zoom; - if (x < 0 || x >= _workingArranger.ArrangerPixelSize.Width || y < 0 || y >= _workingArranger.ArrangerPixelSize.Height) + if (x < 0 || x >= WorkingArranger.ArrangerPixelSize.Width || y < 0 || y >= WorkingArranger.ArrangerPixelSize.Height) return; if (IsDrawing && ActiveTool == PixelTool.Pencil && e.LeftButton) diff --git a/TileShop.WPF/Features/Project/ProjectTreeViewModel.cs b/TileShop.WPF/Features/Project/ProjectTreeViewModel.cs index 44bafd71..b8c27674 100644 --- a/TileShop.WPF/Features/Project/ProjectTreeViewModel.cs +++ b/TileShop.WPF/Features/Project/ProjectTreeViewModel.cs @@ -148,8 +148,10 @@ public void AddNewPalette(ResourceNodeViewModel parentNodeModel) if (_windowManager.ShowDialog(dialogModel) is true) { - var pal = new Palette(dialogModel.PaletteName, Palette.StringToColorModel(dialogModel.SelectedColorModel), new FileBitAddress(dialogModel.FileOffset, 0), + var pal = new Palette(dialogModel.PaletteName, _paletteService.ColorFactory, + Palette.StringToColorModel(dialogModel.SelectedColorModel), new FileBitAddress(dialogModel.FileOffset, 0), dialogModel.Entries, dialogModel.ZeroIndexTransparent, PaletteStorageSource.DataFile); + pal.DataFile = dialogModel.SelectedDataFile; var result = _projectService.AddResource(parentNodeModel.Node, pal, true); @@ -212,12 +214,12 @@ public void ExportArrangerAs(ResourceNodeViewModel nodeModel) if (arranger.ColorType == PixelColorType.Indexed) { var image = new IndexedImage(arranger); - image.ExportImage(exportFileName, new ImageFileAdapter()); + image.ExportImage(exportFileName, new ImageSharpFileAdapter()); } else if (arranger.ColorType == PixelColorType.Direct) { var image = new DirectImage(arranger); - image.ExportImage(exportFileName, new ImageFileAdapter()); + image.ExportImage(exportFileName, new ImageSharpFileAdapter()); } } } @@ -227,8 +229,12 @@ public void ImportImageAs(ResourceNodeViewModel nodeModel) { if (nodeModel is ArrangerNodeViewModel arrNodeModel && arrNodeModel.Node.Value is ScatteredArranger arranger) { - var model = new ImportImageViewModel(arranger, _paletteService, _fileSelect); - _windowManager.ShowDialog(model); + var model = new ImportImageViewModel(arranger, _fileSelect); + if (_windowManager.ShowDialog(model) is true) + { + var changeEvent = new ArrangerChangedEvent(arranger, ArrangerChange.Pixels); + _events.PublishOnUIThread(changeEvent); + } } } diff --git a/TileShop.WPF/Features/Shell/EditorsViewModel.cs b/TileShop.WPF/Features/Shell/EditorsViewModel.cs index 8a0d7bf2..da17096a 100644 --- a/TileShop.WPF/Features/Shell/EditorsViewModel.cs +++ b/TileShop.WPF/Features/Shell/EditorsViewModel.cs @@ -15,7 +15,8 @@ namespace TileShop.WPF.ViewModels { - public class EditorsViewModel : PropertyChangedBase, IHandle, IHandle + public class EditorsViewModel : PropertyChangedBase, IHandle, IHandle, + IHandle { private readonly IWindowManager _windowManager; private readonly Tracker _tracker; @@ -127,8 +128,11 @@ public void ActivateEditor(IProjectResource resource) switch (resource) { - case Palette pal: - newDocument = new PaletteEditorViewModel(pal, _events); + case Palette pal when pal.ColorModel != ColorModel.Nes: + newDocument = new PaletteEditorViewModel(pal, _paletteService, _events); + break; + case Palette pal when pal.ColorModel == ColorModel.Nes: + newDocument = new TablePaletteEditorViewModel(pal, _paletteService, _events); break; case ScatteredArranger scatteredArranger: newDocument = new ScatteredArrangerEditorViewModel(scatteredArranger, _events, _windowManager, _paletteService, _projectService); @@ -312,5 +316,14 @@ public void Handle(ArrangerChangedEvent message) } } } + + public void Handle(PaletteChangedEvent message) + { + var effectedEditors = Editors.OfType() + .Where(x => x.WorkingArranger.GetReferencedPalettes().Contains(message.Palette)); + + foreach (var editor in effectedEditors) + editor.Render(); + } } } diff --git a/TileShop.WPF/Models/ValidatedColorModel.cs b/TileShop.WPF/Models/ValidatedColor32Model.cs similarity index 81% rename from TileShop.WPF/Models/ValidatedColorModel.cs rename to TileShop.WPF/Models/ValidatedColor32Model.cs index bf9747e9..8e83cc63 100644 --- a/TileShop.WPF/Models/ValidatedColorModel.cs +++ b/TileShop.WPF/Models/ValidatedColor32Model.cs @@ -4,9 +4,10 @@ namespace TileShop.WPF.Models { - public class ValidatedColorModel : PropertyChangedBase + public class ValidatedColor32Model : PropertyChangedBase { private IColor32 _foreignColor; + private readonly IColorFactory _colorFactory; public IColor32 WorkingColor { get; set; } @@ -24,7 +25,7 @@ public int Red { WorkingColor.R = (byte) value; OnPropertyChanged(nameof(Red)); - var nativeColor = ImageMagitek.Colors.ColorConverter.ToNative(WorkingColor); + var nativeColor = _colorFactory.ToNative(WorkingColor); Color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); OnPropertyChanged(nameof(CanSaveColor)); } @@ -37,7 +38,7 @@ public int Blue { WorkingColor.B = (byte)value; OnPropertyChanged(nameof(Blue)); - var nativeColor = ImageMagitek.Colors.ColorConverter.ToNative(WorkingColor); + var nativeColor = _colorFactory.ToNative(WorkingColor); Color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); OnPropertyChanged(nameof(CanSaveColor)); } @@ -50,7 +51,7 @@ public int Green { WorkingColor.G = (byte)value; OnPropertyChanged(nameof(Green)); - var nativeColor = ImageMagitek.Colors.ColorConverter.ToNative(WorkingColor); + var nativeColor = _colorFactory.ToNative(WorkingColor); Color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); OnPropertyChanged(nameof(CanSaveColor)); } @@ -63,7 +64,7 @@ public int Alpha { WorkingColor.A = (byte)value; OnPropertyChanged(nameof(Alpha)); - var nativeColor = ImageMagitek.Colors.ColorConverter.ToNative(WorkingColor); + var nativeColor = _colorFactory.ToNative(WorkingColor); Color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); OnPropertyChanged(nameof(CanSaveColor)); } @@ -109,12 +110,14 @@ public int Index set => SetAndNotify(ref _index, value); } - public ValidatedColorModel(IColor32 foreignColor, int index) + public ValidatedColor32Model(IColor32 foreignColor, int index, IColorFactory colorFactory) { _foreignColor = foreignColor; Index = index; - WorkingColor = ColorFactory.CloneColor(foreignColor); - var nativeColor = ImageMagitek.Colors.ColorConverter.ToNative(foreignColor); + _colorFactory = colorFactory; + + WorkingColor = (IColor32)_colorFactory.CloneColor(foreignColor); + var nativeColor = _colorFactory.ToNative(foreignColor); Color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); Red = foreignColor.R; @@ -129,7 +132,7 @@ public ValidatedColorModel(IColor32 foreignColor, int index) public void SaveColor() { - _foreignColor = ColorFactory.CloneColor(WorkingColor); + _foreignColor = (IColor32)_colorFactory.CloneColor(WorkingColor); OnPropertyChanged(nameof(CanSaveColor)); } } diff --git a/TileShop.WPF/Models/ValidatedTableColorModel.cs b/TileShop.WPF/Models/ValidatedTableColorModel.cs new file mode 100644 index 00000000..9c4c55c6 --- /dev/null +++ b/TileShop.WPF/Models/ValidatedTableColorModel.cs @@ -0,0 +1,50 @@ +using System.Windows.Media; +using ImageMagitek.Colors; +using Stylet; + +namespace TileShop.WPF.Models +{ + public class ValidatedTableColorModel : PropertyChangedBase + { + private ITableColor _foreignColor; + private readonly IColorFactory _colorFactory; + + public ITableColor WorkingColor { get; set; } + + private Color _color; + public Color Color + { + get => _color; + set => SetAndNotify(ref _color, value); + } + + public bool CanSaveColor + { + get => WorkingColor.Color != _foreignColor.Color; + } + + private int _index; + public int Index + { + get => _index; + set => SetAndNotify(ref _index, value); + } + + public ValidatedTableColorModel(ITableColor foreignColor, int index, IColorFactory colorFactory) + { + _foreignColor = foreignColor; + Index = index; + _colorFactory = colorFactory; + + WorkingColor = (ITableColor)_colorFactory.CloneColor(foreignColor); + var nativeColor = _colorFactory.ToNative(foreignColor); + Color = Color.FromArgb(nativeColor.A, nativeColor.R, nativeColor.G, nativeColor.B); + } + + public void SaveColor() + { + _foreignColor = (ITableColor)_colorFactory.CloneColor(WorkingColor); + OnPropertyChanged(nameof(CanSaveColor)); + } + } +} diff --git a/TileShop.WPF/TileShop.WPF.csproj b/TileShop.WPF/TileShop.WPF.csproj index 3de9ac39..c1df3ccd 100644 --- a/TileShop.WPF/TileShop.WPF.csproj +++ b/TileShop.WPF/TileShop.WPF.csproj @@ -23,9 +23,9 @@ - - - + + + @@ -36,7 +36,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/TileShop.WPF/TileShopBootstrapper.cs b/TileShop.WPF/TileShopBootstrapper.cs index 3bb1c78a..1306eb7b 100644 --- a/TileShop.WPF/TileShopBootstrapper.cs +++ b/TileShop.WPF/TileShopBootstrapper.cs @@ -16,6 +16,7 @@ using TileShop.WPF.ViewModels; using TileShop.WPF.Views; using System.Windows.Controls; +using ImageMagitek.Colors; namespace TileShop.WPF { @@ -25,11 +26,14 @@ public class TileShopBootstrapper : AutofacBootstrapper private Tracker _tracker = new Tracker(); private IPaletteService _paletteService; private ICodecService _codecService; + private IColorFactory _colorFactory; + private IPluginService _pluginService; private readonly string _logFileName = "errorlog.txt"; private readonly string _configName = "appsettings.json"; private readonly string _palPath = "_palettes"; private readonly string _codecPath = "_codecs"; + private readonly string _pluginPath = "_plugins"; private readonly string _projectSchemaName = Path.Combine("_schemas", "GameDescriptorSchema.xsd"); private readonly string _codecSchemaName = Path.Combine("_schemas", "CodecSchema.xsd"); @@ -39,6 +43,7 @@ protected override void ConfigureIoC(ContainerBuilder builder) ReadConfiguration(_configName, builder); ReadPalettes(_palPath, _settings, builder); ReadCodecs(_codecPath, _codecSchemaName, builder); + LoadPlugins(_pluginPath, _codecService, builder); ConfigureSolutionService(_projectSchemaName, builder); ConfigureServices(builder); ConfigureJotTracker(builder); @@ -46,6 +51,19 @@ protected override void ConfigureIoC(ContainerBuilder builder) ToolTipService.ShowOnDisabledProperty.OverrideMetadata(typeof(Control), new FrameworkPropertyMetadata(true)); } + private void LoadPlugins(string pluginPath, ICodecService codecService, ContainerBuilder builder) + { + _pluginService = new PluginService(); + var fullPath = Path.GetFullPath(pluginPath); + _pluginService.LoadCodecPlugins(fullPath); + foreach (var codecPlugin in _pluginService.CodecPlugins) + { + codecService.AddOrUpdateCodec(codecPlugin.Value); + } + + builder.RegisterInstance(_pluginService); + } + private void ConfigureServices(ContainerBuilder builder) { builder.RegisterType().As(); @@ -79,7 +97,7 @@ protected override void ConfigureViewModels(ContainerBuilder builder) private void ConfigureSolutionService(string schemaFileName, ContainerBuilder builder) { var defaultResources = _paletteService.GlobalPalettes; - var solutionService = new ProjectService(_codecService, defaultResources); + var solutionService = new ProjectService(_codecService, _colorFactory, defaultResources); solutionService.LoadSchemaDefinition(schemaFileName); builder.RegisterInstance(solutionService); } @@ -149,24 +167,28 @@ private void ReadConfiguration(string jsonFileName, ContainerBuilder builder) private void ReadPalettes(string palettesPath, AppSettings settings, ContainerBuilder builder) { - _paletteService = new PaletteService(); + _colorFactory = new ColorFactory(); + _paletteService = new PaletteService(_colorFactory); foreach (var paletteName in settings.GlobalPalettes) { var paletteFileName = Path.Combine(palettesPath, $"{paletteName}.json"); - _paletteService.LoadGlobalPalette(paletteFileName); + var palette = _paletteService.ReadJsonPalette(paletteFileName); + _paletteService.GlobalPalettes.Add(palette); } _paletteService.SetDefaultPalette(_paletteService.GlobalPalettes.First()); var nesPaletteFileName = Path.Combine(palettesPath, $"{settings.NesPalette}.json"); - _paletteService.LoadNesPalette(nesPaletteFileName); + var nesPalette = _paletteService.ReadJsonPalette(nesPaletteFileName); + (_colorFactory as ColorFactory).SetNesPalette(nesPalette); + (_paletteService as PaletteService).SetNesPalette(nesPalette); builder.RegisterInstance(_paletteService); } private void ReadCodecs(string codecsPath, string schemaFileName, ContainerBuilder builder) { - _codecService = new CodecService(schemaFileName, _paletteService.DefaultPalette); + _codecService = new CodecService(schemaFileName); var result = _codecService.LoadXmlCodecs(codecsPath); if (result.Value is MagitekResults.Failed fail) diff --git a/TileShop.WPF/ViewExtenders/Selectors/DocumentHeaderTemplateSelector.cs b/TileShop.WPF/ViewExtenders/Selectors/DocumentHeaderTemplateSelector.cs index 20e5c96c..a1fc7106 100644 --- a/TileShop.WPF/ViewExtenders/Selectors/DocumentHeaderTemplateSelector.cs +++ b/TileShop.WPF/ViewExtenders/Selectors/DocumentHeaderTemplateSelector.cs @@ -28,6 +28,7 @@ public override DataTemplate SelectTemplate(object item, DependencyObject contai return content switch { PaletteEditorViewModel _ => PaletteDocumentHeaderTemplate, + TablePaletteEditorViewModel _ => PaletteDocumentHeaderTemplate, ScatteredArrangerEditorViewModel _ => ScatteredArrangerDocumentHeaderTemplate, SequentialArrangerEditorViewModel _ => SequentialArrangerDocumentHeaderTemplate, DataFileEditorViewModel _ => DataFileDocumentHeaderTemplate, diff --git a/TileShop.WPF/ViewExtenders/Selectors/EditorHostTemplateSelector.cs b/TileShop.WPF/ViewExtenders/Selectors/EditorHostTemplateSelector.cs index 5f4b1dbd..d33e3817 100644 --- a/TileShop.WPF/ViewExtenders/Selectors/EditorHostTemplateSelector.cs +++ b/TileShop.WPF/ViewExtenders/Selectors/EditorHostTemplateSelector.cs @@ -21,6 +21,7 @@ public override DataTemplate SelectTemplate(object item, DependencyObject contai return item switch { PaletteEditorViewModel _ => PaletteEditorTemplate, + TablePaletteEditorViewModel _ => PaletteEditorTemplate, ScatteredArrangerEditorViewModel _ => ScatteredArrangerEditorTemplate, SequentialArrangerEditorViewModel _ => SequentialArrangerEditorTemplate, DataFileEditorViewModel _ => DataFileEditorTemplate,