Browse Source

WIP Extensions

CPKreuz 4 years ago
parent
commit
1469a60a4e
41 changed files with 1329 additions and 37 deletions
  1. 77 0
      PixiEditor.ExtensionExample/ExampleDocumentParser.cs
  2. 20 0
      PixiEditor.ExtensionExample/PixiEditor.ExtensionExample.csproj
  3. 27 0
      PixiEditor.ExtensionExample/SomeExtension.cs
  4. 3 0
      PixiEditor.SDK/AssemblySettings.cs
  5. 47 0
      PixiEditor.SDK/Exceptions/ExtensionException.cs
  6. 23 0
      PixiEditor.SDK/Exceptions/ExtensionLoadingException.cs
  7. 28 0
      PixiEditor.SDK/Extension.cs
  8. 57 0
      PixiEditor.SDK/ExtensionLoadingInformation.cs
  9. 15 0
      PixiEditor.SDK/FileParsers/Attributes/FileParserAttribute.cs
  10. 20 0
      PixiEditor.SDK/FileParsers/DocumentFileFeatures.cs
  11. 13 0
      PixiEditor.SDK/FileParsers/DocumentParserInfo.cs
  12. 25 0
      PixiEditor.SDK/FileParsers/Exceptions/ParserException.cs
  13. 29 0
      PixiEditor.SDK/FileParsers/FileInfo.cs
  14. 53 0
      PixiEditor.SDK/FileParsers/FileParserInfo.cs
  15. 62 0
      PixiEditor.SDK/FileParsers/FileParserList.cs
  16. 13 0
      PixiEditor.SDK/FileParsers/ImageParserInfo.cs
  17. 8 0
      PixiEditor.SDK/FileParsers/ParserType.cs
  18. 8 0
      PixiEditor.SDK/FileParsers/ParserTypes/DocumentParser.cs
  19. 132 0
      PixiEditor.SDK/FileParsers/ParserTypes/FileParser.cs
  20. 9 0
      PixiEditor.SDK/FileParsers/ParserTypes/ImageParser.cs
  21. 12 0
      PixiEditor.SDK/Internal/ExtensionLoadingResult.cs
  22. 14 0
      PixiEditor.SDK/Internal/PreferenceStorageLocation.cs
  23. 118 0
      PixiEditor.SDK/Internal/SDKManager.cs
  24. 54 0
      PixiEditor.SDK/ListDictionary.cs
  25. 13 0
      PixiEditor.SDK/PixiEditor.SDK.csproj
  26. 15 0
      PixiEditor.SDK/PixiEditorExtensionAttribute.cs
  27. 144 0
      PixiEditor.SDK/Preferences.cs
  28. 52 0
      PixiEditor.sln
  29. 33 0
      PixiEditor/Helpers/SDKHelper.cs
  30. 31 0
      PixiEditor/Models/BaseExtension.cs
  31. 2 2
      PixiEditor/Models/DataHolders/Document/Document.IO.cs
  32. 51 8
      PixiEditor/Models/IO/Exporter.cs
  33. 12 21
      PixiEditor/Models/IO/Importer.cs
  34. 55 0
      PixiEditor/Models/IO/Parsers/ImageParsers.cs
  35. 19 0
      PixiEditor/Models/IO/Parsers/PixiParser.cs
  36. 1 1
      PixiEditor/Models/Undo/StorageBasedChange.cs
  37. 1 0
      PixiEditor/PixiEditor.csproj
  38. 23 0
      PixiEditor/ViewModels/SubViewModels/Main/ExtensionViewModel.cs
  39. 1 1
      PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  40. 7 2
      PixiEditor/ViewModels/ViewModelMain.cs
  41. 2 2
      PixiEditorTests/ModelsTests/IO/ExporterTests.cs

+ 77 - 0
PixiEditor.ExtensionExample/ExampleDocumentParser.cs

@@ -0,0 +1,77 @@
+using PixiEditor.Parser;
+using PixiEditor.SDK.FileParsers;
+using System.ComponentModel;
+using System.IO;
+using System.Text;
+
+namespace PixiEditor.ExtensionExample
+{
+    [FileParser(".nopixi")]
+    [Description("A example file type called .noPixi.\n Can only save layer images, name and is heavier than a regular .pixi file so you shouldn't actually use it")]
+    public class ExampleDocumentParser : DocumentParser
+    {
+        public override bool UseBigEndian { get; } = true;
+
+        public override Encoding Encoding { get; } = Encoding.UTF8;
+
+        public override SerializableDocument Parse()
+        {
+            int width = ReadInt32();
+            int height = ReadInt32();
+
+            SerializableDocument document = new SerializableDocument(width, height);
+
+            for (int i = 0; true; i++)
+            {
+                try
+                {
+                    int layerWidth = ReadInt32();
+                    int layerHeight = ReadInt32();
+
+                    int offsetX = ReadInt32();
+                    int offsetY = ReadInt32();
+
+                    string layerName = ReadString();
+
+                    SerializableLayer layer = new SerializableLayer()
+                    {
+                        Width = layerWidth,
+                        Height = layerHeight,
+                        OffsetX = offsetX,
+                        OffsetY = offsetY,
+                        Name = layerName,
+                        BitmapBytes = ReadBytes(ReadInt32())
+                    };
+
+                    document.Layers.Add(layer);
+                }
+                catch (EndOfStreamException)
+                {
+                    break;
+                }
+            }
+
+            return document;
+        }
+
+        public override void Save(SerializableDocument document)
+        {
+            WriteInt32(document.Width);
+            WriteInt32(document.Height);
+
+            foreach (SerializableLayer layer in document)
+            {
+                WriteInt32(layer.Width);
+                WriteInt32(layer.Height);
+
+                WriteInt32(layer.OffsetX);
+                WriteInt32(layer.OffsetY);
+
+                WriteString(layer.Name, true);
+
+                WriteInt32(layer.BitmapBytes.Length);
+                WriteBytes(layer.BitmapBytes);
+            }
+        }
+    }
+}

+ 20 - 0
PixiEditor.ExtensionExample/PixiEditor.ExtensionExample.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0-windows7</TargetFramework>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <DebugType>embedded</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+
+  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+    <Exec Command="copy /Y /B $(TargetPath) /B %25LocalAppData%25\PixiEditor\Extensions\$(TargetFileName)" />
+  </Target>
+
+  <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.SDK\PixiEditor.SDK.csproj" />
+  </ItemGroup>
+
+</Project>

+ 27 - 0
PixiEditor.ExtensionExample/SomeExtension.cs

@@ -0,0 +1,27 @@
+using PixiEditor.ExtensionExample;
+using PixiEditor.SDK;
+using System;
+
+[assembly: PixiEditorExtension(typeof(SomeExtension))]
+
+namespace PixiEditor.ExtensionExample
+{
+    public class SomeExtension : Extension
+    {
+        public override string Name { get; } = "PixiEditor.ExampleExtension";
+
+        public override string DisplayName { get; } = "Example extension";
+
+        public override string Description { get; } = "A exmaple extension showing how extensions work";
+
+        public override Version Version { get; } = new Version(1, 0, 0, 0);
+
+        public override bool IsVersionSupported(Version pixiEditorVersion) => true;
+
+        public override void Load(ExtensionLoadingInformation information)
+        {
+            information
+                .AddDocumentParser<ExampleDocumentParser>();
+        }
+    }
+}

+ 3 - 0
PixiEditor.SDK/AssemblySettings.cs

@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("PixiEditor")]

+ 47 - 0
PixiEditor.SDK/Exceptions/ExtensionException.cs

@@ -0,0 +1,47 @@
+using System;
+
+namespace PixiEditor.SDK
+{
+
+    [Serializable]
+    public class ExtensionException : Exception
+    {
+        public string ExtensionPath { get; set; }
+
+        public Extension Extension { get; set; }
+
+        public ExtensionException(string extensionPath) : base()
+        {
+            ExtensionPath = extensionPath;
+        }
+
+        public ExtensionException(string extensionPath, string message) : base(message)
+        {
+            ExtensionPath = extensionPath;
+        }
+
+        public ExtensionException(string extensionPath, string message, Exception inner) : base(message, inner)
+        {
+            ExtensionPath = extensionPath;
+        }
+
+        public ExtensionException(Extension extension)
+        {
+            Extension = extension;
+        }
+
+        public ExtensionException(Extension extension, string message) : base(message)
+        {
+            Extension = extension;
+        }
+
+        public ExtensionException(Extension extension, string message, Exception inner) : base(message, inner) 
+        {
+            Extension = extension;
+        }
+
+        protected ExtensionException(
+          System.Runtime.Serialization.SerializationInfo info,
+          System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+    }
+}

+ 23 - 0
PixiEditor.SDK/Exceptions/ExtensionLoadingException.cs

@@ -0,0 +1,23 @@
+using System;
+
+namespace PixiEditor.SDK
+{
+    public class ExtensionLoadingException : ExtensionException
+    {
+        public ExtensionLoadingException(string extensionPath) : base(extensionPath, "Error while trying to load extension") { }
+
+        public ExtensionLoadingException(string extensionPath, string message) : base(extensionPath, message) { }
+
+        public ExtensionLoadingException(string extensionPath, string message, Exception inner) : base(extensionPath, message, inner) { }
+
+        public ExtensionLoadingException(Extension extension) : base(extension, "Error while trying to load extension") { }
+
+        public ExtensionLoadingException(Extension extension, string message) : base(extension, message) { }
+
+        public ExtensionLoadingException(Extension extension, string message, Exception inner) : base(extension, message, inner) { }
+
+        protected ExtensionLoadingException(
+          System.Runtime.Serialization.SerializationInfo info,
+          System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+    }
+}

+ 28 - 0
PixiEditor.SDK/Extension.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace PixiEditor.SDK
+{
+    [DebuggerDisplay("{DisplayName} ({Name})")]
+    public abstract class Extension
+    {
+        public abstract string Name { get; }
+
+        public abstract string DisplayName { get; }
+
+        public abstract string Description { get; }
+
+        public string ExtensionPath { get; internal set; }
+
+        public abstract Version Version { get; }
+
+        internal List<string> SupportedDocumentFileExtensions { get; set; } = new List<string>();
+
+        internal List<string> SupportedImageFileExtensions { get; set; } = new List<string>();
+
+        public abstract bool IsVersionSupported(Version pixiEditorVersion);
+
+        public abstract void Load(ExtensionLoadingInformation information);
+    }
+}

+ 57 - 0
PixiEditor.SDK/ExtensionLoadingInformation.cs

@@ -0,0 +1,57 @@
+using PixiEditor.SDK.FileParsers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.SDK
+{
+    public class ExtensionLoadingInformation
+    {
+        internal Extension Extension { get; }
+
+        internal List<DocumentParserInfo> DocumentParsers { get; set; } = new List<DocumentParserInfo>();
+
+        internal List<ImageParserInfo> ImageParsers { get; set; } = new List<ImageParserInfo>();
+
+        internal ExtensionLoadingInformation(Extension extension)
+        {
+            Extension = extension;
+        }
+
+        public ExtensionLoadingInformation AddDocumentParser(Type documentParserType)
+        {
+            DocumentParserInfo parserInfo = new DocumentParserInfo(documentParserType);
+
+            DocumentParsers.Add(parserInfo);
+
+            foreach (string s in parserInfo.SupportedFileExtensions)
+            {
+                Extension.SupportedDocumentFileExtensions.Add(s);
+            }
+
+            return this;
+        }
+
+        public ExtensionLoadingInformation AddDocumentParser<TParser>()
+            where TParser : DocumentParser => AddDocumentParser(typeof(TParser));
+
+        public ExtensionLoadingInformation AddImageParser(Type documentParserType)
+        {
+            ImageParserInfo parserInfo = new ImageParserInfo(documentParserType);
+
+            ImageParsers.Add(parserInfo);
+
+            foreach (string s in parserInfo.SupportedFileExtensions)
+            {
+                Extension.SupportedImageFileExtensions.Add(s);
+            }
+
+            return this;
+        }
+
+        public ExtensionLoadingInformation AddImageParser<TParser>()
+            where TParser : ImageParser => AddImageParser(typeof(TParser));
+    }
+}

+ 15 - 0
PixiEditor.SDK/FileParsers/Attributes/FileParserAttribute.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    [AttributeUsage(AttributeTargets.Class)]
+    public class FileParserAttribute : Attribute
+    {
+        public string[] FileExtensions { get; set; }
+
+        public FileParserAttribute(params string[] fileExtensions)
+        {
+            FileExtensions = fileExtensions;
+        }
+    }
+}

+ 20 - 0
PixiEditor.SDK/FileParsers/DocumentFileFeatures.cs

@@ -0,0 +1,20 @@
+using System;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    [Flags]
+    public enum DocumentFileFeatures
+    {
+        /// <summary>
+        /// Supports all features that a .pixi file supports
+        /// </summary>
+        All = Basic | Layers | Swatches,
+
+        /// <summary>
+        /// Supports saving image, store document size, ... <para>This is required</para>
+        /// </summary>
+        Basic = 0,
+        Layers = 1,
+        Swatches = 2
+    }
+}

+ 13 - 0
PixiEditor.SDK/FileParsers/DocumentParserInfo.cs

@@ -0,0 +1,13 @@
+using System;
+using PixiEditor.Parser;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    internal class DocumentParserInfo : FileParserInfo<DocumentParser, SerializableDocument>
+    {
+        public DocumentParserInfo(Type documentParserType)
+            : base(documentParserType)
+        {
+        }
+    }
+}

+ 25 - 0
PixiEditor.SDK/FileParsers/Exceptions/ParserException.cs

@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.SDK.FileParsers
+{
+
+    [Serializable]
+    public class ParserException : Exception
+    {
+        public Type ParserType { get; }
+
+        public ParserException(Type parserType) : base("A parser threw an exception") => ParserType = parserType;
+
+        public ParserException(Type parserType, string message) : base(message) => ParserType = parserType;
+
+        public ParserException(Type parserType, string message, Exception inner) : base(message, inner) => ParserType = parserType;
+
+        protected ParserException(
+          System.Runtime.Serialization.SerializationInfo info,
+          System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+    }
+}

+ 29 - 0
PixiEditor.SDK/FileParsers/FileInfo.cs

@@ -0,0 +1,29 @@
+using System.IO;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    public class FileInfo
+    {
+        /// <summary>
+        /// Gets the file name without the file extension
+        /// </summary>
+        public string FileName { get; private set; }
+
+        /// <summary>
+        /// Gets the file extension of the file, including the dot
+        /// </summary>
+        public string FileExtension { get; private set; }
+
+        internal FileInfo(string path)
+        {
+            FileName = Path.GetFileNameWithoutExtension(path);
+            FileExtension = Path.GetExtension(path);
+        }
+
+        internal FileInfo(string name, string extension)
+        {
+            FileName = name;
+            FileExtension = extension;
+        }
+    }
+}

+ 53 - 0
PixiEditor.SDK/FileParsers/FileParserInfo.cs

@@ -0,0 +1,53 @@
+using System;
+using System.IO;
+using System.Reflection;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    internal abstract class FileParserInfo<TParser, T>
+        where TParser : FileParser<T>
+    {
+        public Type ParserType { get; }
+
+        public bool Enabled { get; set; }
+
+        public string[] SupportedFileExtensions { get; set; }
+
+        private ConstructorInfo Constructor { get; }
+
+        public TParser Create(Stream stream)
+        {
+            TParser parser = (TParser)Constructor.Invoke(null);
+            parser.Stream = stream;
+
+            return parser;
+        }
+
+        public TParser Create(Stream stream, string path)
+        {
+            TParser parser = Create(stream);
+            parser.FileInfo = new FileInfo(path);
+
+            return parser;
+        }
+
+        public FileParserInfo(Type parserType)
+        {
+            FileParserAttribute fileParserAttribute;
+
+            if ((fileParserAttribute = parserType.GetCustomAttribute<FileParserAttribute>()) is null)
+            {
+                throw new ParserException(parserType, $"'{parserType}' needs an {nameof(FileParserAttribute)}");
+            }
+
+            Constructor = parserType.GetConstructor(Type.EmptyTypes);
+
+            if (Constructor is null)
+            {
+                throw new ParserException(parserType, $"'{parserType}' needs an constructor that no parameters");
+            }
+
+            ParserType = parserType;
+        }
+    }
+}

+ 62 - 0
PixiEditor.SDK/FileParsers/FileParserList.cs

@@ -0,0 +1,62 @@
+using PixiEditor.Parser;
+using System.Collections.Generic;
+using System.IO;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    internal class FileParserList
+    {
+        public ListDictionary<string, ImageParserInfo> ImageParsers { get; set; } = new();
+
+        public ListDictionary<string, DocumentParserInfo> DocumentParsers { get; set; } = new();
+
+        public void AddImageParser(ImageParserInfo info)
+        {
+            foreach (string ext in info.SupportedFileExtensions)
+            {
+                ImageParsers.Add(ext, info);
+            }
+        }
+
+        public void AddDocumentParser(DocumentParserInfo info)
+        {
+            foreach (string ext in info.SupportedFileExtensions)
+            {
+                DocumentParsers.Add(ext, info);
+            }
+        }
+
+        public ImageParser CreateImageParser(string extensions, Stream stream) => 
+            Create<ImageParserInfo, ImageParser, WriteableBitmap>(ImageParsers, extensions, stream);
+
+        public DocumentParser CreateDocumentParser(string extension, Stream stream) =>
+            Create<DocumentParserInfo, DocumentParser, SerializableDocument>(DocumentParsers, extension, stream);
+
+        private static TParser Create<TParserInfo, TParser, T>(
+            ListDictionary<string, TParserInfo> dict,
+            string extension,
+            Stream stream)
+
+            where TParserInfo : FileParserInfo<TParser, T>
+            where TParser : FileParser<T>
+        {
+            if (!dict.ContainsKey(extension))
+            {
+                return null;
+            }
+
+            var parserInfos = dict[extension];
+
+            foreach (var fileParserInfo in parserInfos)
+            {
+                if (fileParserInfo.Enabled)
+                {
+                    return fileParserInfo.Create(stream);
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 13 - 0
PixiEditor.SDK/FileParsers/ImageParserInfo.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    internal class ImageParserInfo : FileParserInfo<ImageParser, WriteableBitmap>
+    {
+        public ImageParserInfo(Type imageParserType)
+            : base(imageParserType)
+        {
+        }
+    }
+}

+ 8 - 0
PixiEditor.SDK/FileParsers/ParserType.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.SDK.FileParsers
+{
+    internal enum ParserType
+    {
+        Document,
+        Image
+    }
+}

+ 8 - 0
PixiEditor.SDK/FileParsers/ParserTypes/DocumentParser.cs

@@ -0,0 +1,8 @@
+using PixiEditor.Parser;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    public abstract class DocumentParser : FileParser<SerializableDocument>
+    {
+    }
+}

+ 132 - 0
PixiEditor.SDK/FileParsers/ParserTypes/FileParser.cs

@@ -0,0 +1,132 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    public abstract class FileParser<T>
+    {
+        public abstract bool UseBigEndian { get; }
+
+        public virtual Encoding Encoding { get => Encoding.UTF8; }
+
+        public Stream Stream { get; internal set; }
+
+        public FileInfo FileInfo { get; internal set; }
+
+        protected long TotalLenght => Stream.Length;
+
+        protected long Position => Stream.Position;
+
+        internal FileParser() { }
+
+        public abstract T Parse();
+
+        public abstract void Save(T value);
+
+        public void WriteByte(byte value) => Stream.WriteByte(value);
+
+        public void WriteSByte(sbyte value) => Stream.WriteByte((byte)value);
+
+        public void WriteInt16(short value) => WriteToStreamRBIN(BitConverter.GetBytes(value));
+
+        public void WriteUInt16(ushort value) => WriteToStreamRBIN(BitConverter.GetBytes(value));
+
+        public void WriteInt32(int value) => WriteToStreamRBIN(BitConverter.GetBytes(value));
+
+        public void WriteUInt32(uint value) => WriteToStreamRBIN(BitConverter.GetBytes(value));
+
+        public void WriteInt64(long value) => WriteToStreamRBIN(BitConverter.GetBytes(value));
+
+        public void WriteUInt64(ulong value) => WriteToStreamRBIN(BitConverter.GetBytes(value));
+
+        public void WriteString(string value, bool includeLenght = false) => WriteString(value, Encoding, includeLenght);
+
+        public void WriteString(string value, Encoding encoding, bool includeLenght = false)
+        {
+            byte[] buffer = encoding.GetBytes(value);
+
+            if (includeLenght)
+            {
+                WriteInt32(buffer.Length);
+            }
+
+            WriteBytes(buffer);
+        }
+
+        public void WriteBytes(byte[] value) => Stream.Write(value, 0, value.Length);
+
+        public byte ReadByte() => (byte)Stream.ReadByte();
+
+        public sbyte ReadSByte() => (sbyte)Stream.ReadByte();
+
+        public short ReadInt16() => BitConverter.ToInt16(ReadFromStreamRBIN(2), 0);
+
+        public ushort ReadUInt16() => BitConverter.ToUInt16(ReadFromStreamRBIN(2), 0);
+
+        public int ReadInt32() => BitConverter.ToInt32(ReadFromStreamRBIN(4), 0);
+
+        public uint ReadUInt32() => BitConverter.ToUInt32(ReadFromStreamRBIN(4), 0);
+
+        public long ReadInt64() => BitConverter.ToInt64(ReadFromStreamRBIN(8), 0);
+
+        public ulong ReadUInt64() => BitConverter.ToUInt64(ReadFromStreamRBIN(8), 0);
+
+        public string ReadString()
+        {
+            int stringLenght = ReadInt32();
+            return ReadString(stringLenght);
+        }
+
+        public string ReadString(int lenght) => Encoding.GetString(ReadBytes(lenght));
+
+        public string ReadString(int lenght, Encoding encoding) => encoding.GetString(ReadBytes(lenght));
+
+        public byte[] ReadBytes()
+        {
+            int lenght = ReadInt32();
+            return ReadBytes(lenght);
+        }
+
+        protected byte[] ReadBytes(int lenght)
+        {
+            byte[] buffer = new byte[lenght];
+
+            int read = Stream.Read(buffer, 0, lenght);
+
+            if (read < lenght)
+            {
+                throw new EndOfStreamException();
+            }
+
+            return buffer;
+        }
+
+        private byte[] ReadFromStreamRBIN(int lenght)
+        {
+            byte[] buffer = ReadBytes(lenght);
+
+            ReverseBufferIfNeeded(buffer);
+
+            return buffer;
+        }
+
+        private void WriteToStreamRBIN(byte[] toWrite)
+        {
+            ReverseBufferIfNeeded(toWrite);
+
+            Stream.Write(toWrite, 0, toWrite.Length);
+        }
+
+        private byte[] ReverseBufferIfNeeded(byte[] buffer)
+        {
+            if (UseBigEndian == BitConverter.IsLittleEndian)
+            {
+                return buffer.Reverse().ToArray();
+            }
+
+            return buffer;
+        }
+    }
+}

+ 9 - 0
PixiEditor.SDK/FileParsers/ParserTypes/ImageParser.cs

@@ -0,0 +1,9 @@
+using System.Windows.Media.Imaging;
+
+namespace PixiEditor.SDK.FileParsers
+{
+    public abstract class ImageParser : FileParser<WriteableBitmap>
+    {
+        public ImageParser() { }
+    }
+}

+ 12 - 0
PixiEditor.SDK/Internal/ExtensionLoadingResult.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.SDK
+{
+    internal struct ExtensionLoadingResult
+    {
+        public ExtensionLoadingException[] LoadingExceptions { get; }
+
+        public ExtensionLoadingResult(ExtensionLoadingException[] exceptions)
+        {
+            LoadingExceptions = exceptions;
+        }
+    }
+}

+ 14 - 0
PixiEditor.SDK/Internal/PreferenceStorageLocation.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.SDK
+{
+    internal enum PreferenceStorageLocation
+    {
+        Local,
+        Roaming
+    }
+}

+ 118 - 0
PixiEditor.SDK/Internal/SDKManager.cs

@@ -0,0 +1,118 @@
+using PixiEditor.SDK.FileParsers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+namespace PixiEditor.SDK
+{
+    internal class SDKManager
+    {
+        public List<Extension> Extensions { get; } = new();
+
+        public FileParserList Parsers { get; } = new();
+
+        public List<string> SupportedFileExtensions { get; } = new();
+
+        public void AddExtension(Extension extension)
+        {
+            Extensions.Add(extension);
+        }
+
+        public ExtensionLoadingResult LoadExtensions(string extensionLocation)
+        {
+            List<ExtensionLoadingException> extensionExceptions = new();
+            List<Assembly> loadedAssemblies = new();
+
+            if (!Directory.Exists(extensionLocation))
+            {
+                Directory.CreateDirectory(extensionLocation);
+            }
+
+            foreach (string path in Directory.EnumerateFiles(extensionLocation, "*.dll"))
+            {
+                try
+                {
+                    loadedAssemblies.Add(Assembly.LoadFrom(path));
+                }
+                catch (Exception e)
+                {
+                    extensionExceptions.Add(new ExtensionLoadingException(path, "Error while trying to load extension", e));
+                }
+            }
+
+            foreach (Assembly assembly in loadedAssemblies)
+            {
+                try
+                {
+                    Extension extension = LoadExtensionFromAssembly(assembly);
+                    extension.ExtensionPath = assembly.Location;
+
+                    Extensions.Add(extension);
+                }
+                catch (Exception e)
+                {
+                    extensionExceptions.Add(new ExtensionLoadingException(assembly.Location, "Error while trying to initialize extension", e));
+                }
+            }
+
+            return new ExtensionLoadingResult(extensionExceptions.ToArray());
+        }
+
+        public void SetupExtensions()
+        {
+            List<Extension> extensions = new List<Extension>(Extensions);
+
+            foreach (Extension extension in extensions)
+            {
+                try
+                {
+                    ExtensionLoadingInformation information = new(extension);
+                    extension.Load(information);
+
+                    foreach (DocumentParserInfo fileParserInformation in information.DocumentParsers)
+                    {
+                        foreach (string fileExtension in fileParserInformation.SupportedFileExtensions)
+                        {
+                            Parsers.DocumentParsers.Add(fileExtension, fileParserInformation);
+
+                            if (!SupportedFileExtensions.Contains(fileExtension))
+                            {
+                                SupportedFileExtensions.Add(fileExtension);
+                            }
+                        }
+                    }
+
+                    foreach (ImageParserInfo fileParserInformation in information.ImageParsers)
+                    {
+                        foreach (string fileExtension in fileParserInformation.SupportedFileExtensions)
+                        {
+                            Parsers.ImageParsers.Add(fileExtension, fileParserInformation);
+
+                            if (!SupportedFileExtensions.Contains(fileExtension))
+                            {
+                                SupportedFileExtensions.Add(fileExtension);
+                            }
+                        }
+                    }
+                }
+                catch
+                {
+                    Extensions.Remove(extension);
+                }
+            }
+        }
+
+        private static Extension LoadExtensionFromAssembly(Assembly assembly)
+        {
+            PixiEditorExtensionAttribute attribute = assembly.GetCustomAttribute<PixiEditorExtensionAttribute>();
+
+            ConstructorInfo info = attribute.ExtensionType.GetConstructor(Type.EmptyTypes);
+
+            Extension extension = info.Invoke(null) as Extension;
+            extension.ExtensionPath = assembly.Location;
+
+            return extension;
+        }
+    }
+}

+ 54 - 0
PixiEditor.SDK/ListDictionary.cs

@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+
+namespace PixiEditor.SDK
+{
+    internal class ListDictionary<TKey, TValue>
+    {
+        private readonly Dictionary<TKey, List<TValue>> dictionary;
+
+        public ListDictionary()
+        {
+            dictionary = new();
+        }
+
+        public ListDictionary(ListDictionary<TKey, TValue> listDictionary)
+        {
+            dictionary = new Dictionary<TKey, List<TValue>>(listDictionary.dictionary);
+        }
+
+        public ListDictionary(IEnumerable<KeyValuePair<TKey, IEnumerable<TValue>>> enumerable)
+        {
+            dictionary = new();
+
+            foreach (var value in enumerable)
+            {
+                dictionary.Add(value.Key, new List<TValue>(value.Value));
+            }
+        }
+
+        public List<TValue> this[TKey key]
+        {
+            get => dictionary[key];
+            set => dictionary[key] = value;
+        }
+
+        public void Add(TKey key, TValue value)
+        {
+            if (dictionary.ContainsKey(key))
+            {
+                dictionary[key].Add(value);
+                return;
+            }
+
+            dictionary.Add(key, new() { value });
+        }
+
+        public void Clear(TKey key) => this[key].Clear();
+
+        public bool ContainsKey(TKey key) => dictionary.ContainsKey(key);
+
+        public bool ContainsValue(TKey key) => this[key].Count != 0;
+
+        public int LenghtOf(TKey key) => this[key].Count;
+    }
+}

+ 13 - 0
PixiEditor.SDK/PixiEditor.SDK.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0-windows7.0</TargetFramework>
+    <OutputType>Library</OutputType>
+    <UseWPF>true</UseWPF>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="PixiEditor.Parser" Version="1.1.2.1" />
+  </ItemGroup>
+
+</Project>

+ 15 - 0
PixiEditor.SDK/PixiEditorExtensionAttribute.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace PixiEditor.SDK
+{
+    [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+    public class PixiEditorExtensionAttribute : Attribute
+    {
+        public PixiEditorExtensionAttribute(Type extensionType)
+        {
+            ExtensionType = extensionType;
+        }
+
+        public Type ExtensionType { get; set; }
+    }
+}

+ 144 - 0
PixiEditor.SDK/Preferences.cs

@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.SDK
+{
+    public class Preferences
+    {
+        private static Action<Extension, string, object, PreferenceStorageLocation> SavePreferenceCallback { get; set; }
+
+        private static Func<Extension, string, PreferenceStorageLocation, object> LoadPreferenceCallback { get; set; }
+
+        private Extension Extension { get; set; }
+
+        internal static void Init(Action<Extension, string, object, PreferenceStorageLocation> savePreference, Func<Extension, string, PreferenceStorageLocation, object> loadPreference)
+        {
+            SavePreferenceCallback = savePreference;
+        }
+
+        internal Preferences(Extension extension)
+        {
+            Extension = extension;
+        }
+
+        public void UpdatePreference<T>(string name, T value)
+        {
+            SavePreferenceCallback(Extension, name, value, PreferenceStorageLocation.Local);
+
+            if (Callbacks.ContainsKey((name, PreferenceStorageLocation.Roaming)))
+            {
+                foreach (var action in Callbacks[(name, PreferenceStorageLocation.Roaming)])
+                {
+                    action.Invoke(value);
+                }
+            }
+        }
+
+        public void UpdateLocalPreference<T>(string name, T value)
+        {
+            SavePreferenceCallback(Extension, name, value, PreferenceStorageLocation.Local);
+
+            if (Callbacks.ContainsKey((name, PreferenceStorageLocation.Local)))
+            {
+                foreach (var action in Callbacks[(name, PreferenceStorageLocation.Local)])
+                {
+                    action.Invoke(value);
+                }
+            }
+        }
+
+        private Dictionary<(string, PreferenceStorageLocation), List<Action<object>>> Callbacks { get; set; } = new Dictionary<(string, PreferenceStorageLocation), List<Action<object>>>();
+
+        public void AddCallback(string name, Action<object> action)
+        {
+            if (action == null)
+            {
+                throw new ArgumentNullException(nameof(action));
+            }
+
+            if (Callbacks.ContainsKey((name, PreferenceStorageLocation.Local)))
+            {
+                Callbacks[(name, PreferenceStorageLocation.Roaming)].Add(action);
+                return;
+            }
+
+            Callbacks.Add((name, PreferenceStorageLocation.Roaming), new List<Action<object>>() { action });
+        }
+
+        public void AddCallback<T>(string name, Action<T> action)
+        {
+            if (action == null)
+            {
+                throw new ArgumentNullException(nameof(action));
+            }
+
+            AddCallback(name, new Action<object>(o => action((T)o)));
+        }
+
+        public void AddLocalCallback(string name, Action<object> action)
+        {
+            if (action == null)
+            {
+                throw new ArgumentNullException(nameof(action));
+            }
+
+            if (Callbacks.ContainsKey((name, PreferenceStorageLocation.Local)))
+            {
+                Callbacks[(name, PreferenceStorageLocation.Local)].Add(action);
+                return;
+            }
+
+            Callbacks.Add((name, PreferenceStorageLocation.Local), new List<Action<object>>() { action });
+        }
+
+        public void AddLocalCallback<T>(string name, Action<T> action)
+        {
+            if (action == null)
+            {
+                throw new ArgumentNullException(nameof(action));
+            }
+
+            AddLocalCallback(name, new Action<object>(o => action((T)o)));
+        }
+
+#nullable enable
+
+        public T? GetPreference<T>(string name)
+        {
+            return GetLocalPreference(name, default(T));
+        }
+
+        public T? GetPreference<T>(string name, T? fallbackValue)
+        {
+            T? obj = (T?)Convert.ChangeType(LoadPreferenceCallback(Extension, name, PreferenceStorageLocation.Roaming), typeof(T));
+
+            if (obj == null)
+            {
+                return fallbackValue;
+            }
+
+            return obj;
+        }
+
+        public T? GetLocalPreference<T>(string name)
+        {
+            return GetLocalPreference(name, default(T));
+        }
+
+        public T? GetLocalPreference<T>(string name, T? fallbackValue)
+        {
+            T? obj = (T?)Convert.ChangeType(LoadPreferenceCallback(Extension, name, PreferenceStorageLocation.Local), typeof(T));
+
+            if (obj == null)
+            {
+                return fallbackValue;
+            }
+
+            return obj;
+        }
+
+    }
+}

+ 52 - 0
PixiEditor.sln

@@ -20,6 +20,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildConfiguration", "Build
 EndProject
 Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PixiEditor.MSIX", "PixiEditor.MSIX\PixiEditor.MSIX.wapproj", "{1F97F972-F9E8-4F35-A8B5-3F71408D2230}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.SDK", "PixiEditor.SDK\PixiEditor.SDK.csproj", "{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.ExtensionExaple", "PixiEditor.ExtensionExample\PixiEditor.ExtensionExample.csproj", "{757887F7-7EEC-4248-8F89-A35327576559}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -164,6 +168,54 @@ Global
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Release|Any CPU.Deploy.0 = Release|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Release|x64.ActiveCfg = Release|x64
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Release|x86.ActiveCfg = Release|x86
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Debug|x64.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Debug|x86.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX|x64.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.MSIX|x86.Build.0 = Debug|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Release|x64.ActiveCfg = Release|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Release|x64.Build.0 = Release|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Release|x86.ActiveCfg = Release|Any CPU
+		{1C8DA92D-FEDE-4137-A99E-0B3C6733413D}.Release|x86.Build.0 = Release|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Debug|x64.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Debug|x86.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX|x64.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.MSIX|x86.Build.0 = Debug|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Release|Any CPU.Build.0 = Release|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Release|x64.ActiveCfg = Release|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Release|x64.Build.0 = Release|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Release|x86.ActiveCfg = Release|Any CPU
+		{757887F7-7EEC-4248-8F89-A35327576559}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 33 - 0
PixiEditor/Helpers/SDKHelper.cs

@@ -0,0 +1,33 @@
+using PixiEditor.SDK;
+using PixiEditor.SDK.FileParsers;
+using PixiEditor.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Helpers
+{
+    public static class SDKHelper
+    {
+        internal static SDKManager GetCurrentManager()
+        {
+            return ViewModelMain.Current?.ExtensionSubViewModel?.SDKManager;
+        }
+
+        public static class FileParsers
+        {
+            public static DocumentParser CreateDocumentParser(string extension, Stream stream)
+            {
+                return GetCurrentManager().Parsers.CreateDocumentParser(extension, stream);
+            }
+
+            public static ImageParser CreateImageParser(string extension, Stream stream)
+            {
+                return GetCurrentManager().Parsers.CreateImageParser(extension, stream);
+            }
+        }
+    }
+}

+ 31 - 0
PixiEditor/Models/BaseExtension.cs

@@ -0,0 +1,31 @@
+using PixiEditor.Models.IO.Parsers;
+using PixiEditor.SDK;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models
+{
+    class BaseExtension : Extension
+    {
+        public override string Name => "PixiEditor.PixiEditorBase";
+
+        public override string DisplayName => "PixiEditor";
+
+        public override string Description => "Extension containing parser's for .pixi, .png and .jpg files";
+
+        public override Version Version => new Version(1, 0, 0, 0);
+
+        public override bool IsVersionSupported(Version pixiEditorVersion) => true;
+
+        public override void Load(ExtensionLoadingInformation information)
+        {
+            information
+                .AddDocumentParser<PixiParser>()
+                .AddImageParser<PngParser>()
+                .AddImageParser<JpegParser>();
+        }
+    }
+}

+ 2 - 2
PixiEditor/Models/DataHolders/Document/Document.IO.cs

@@ -36,7 +36,7 @@ namespace PixiEditor.Models.DataHolders
 
         public void SaveWithDialog()
         {
-            bool savedSuccessfully = Exporter.SaveAsEditableFileWithDialog(this, out string path);
+            bool savedSuccessfully = Exporter.SaveAsDocumentWithDialog(this, out string path);
             DocumentFilePath = path;
             ChangesSaved = savedSuccessfully;
         }
@@ -48,7 +48,7 @@ namespace PixiEditor.Models.DataHolders
 
         public void Save(string path)
         {
-            DocumentFilePath = Exporter.SaveAsEditableFile(this, path);
+            DocumentFilePath = Exporter.SaveAsDocument(this, path);
             ChangesSaved = true;
         }
 

+ 51 - 8
PixiEditor/Models/IO/Exporter.cs

@@ -1,12 +1,15 @@
 using System;
 using System.IO;
+using System.Text;
 using System.Windows;
 using System.Windows.Media.Imaging;
 using Microsoft.Win32;
+using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Parser;
+using PixiEditor.SDK;
 
 namespace PixiEditor.Models.IO
 {
@@ -17,16 +20,36 @@ namespace PixiEditor.Models.IO
         /// </summary>
         /// <param name="document">Document to save.</param>
         /// <param name="path">Path where file was saved.</param>
-        public static bool SaveAsEditableFileWithDialog(Document document, out string path)
+        public static bool SaveAsDocumentWithDialog(Document document, out string path)
         {
+            StringBuilder filter = new StringBuilder();
+
+            foreach (Extension extension in SDKHelper.GetCurrentManager().Extensions)
+            {
+                if (filter.Length != 0)
+                {
+                    filter.Append('|');
+                }
+
+                filter.Append(extension.DisplayName);
+                filter.Append('|');
+
+                foreach (string ext in extension.SupportedDocumentFileExtensions)
+                {
+                    filter.Append('*');
+                    filter.Append(ext);
+                    filter.Append(';');
+                }
+            }
+
             SaveFileDialog dialog = new SaveFileDialog
             {
-                Filter = "PixiEditor Files | *.pixi",
-                DefaultExt = "pixi"
+                Filter = filter.ToString(),
+                DefaultExt = ".pixi"
             };
             if ((bool)dialog.ShowDialog())
             {
-                path = SaveAsEditableFile(document, dialog.FileName);
+                path = SaveAsDocument(document, dialog.FileName);
                 return true;
             }
 
@@ -40,7 +63,7 @@ namespace PixiEditor.Models.IO
         /// <param name="document">Document to be saved.</param>
         /// <param name="path">Path where to save file.</param>
         /// <returns>Path.</returns>
-        public static string SaveAsEditableFile(Document document, string path)
+        public static string SaveAsDocument(Document document, string path)
         {
             PixiParser.Serialize(document.ToSerializable(), path);
             return path;
@@ -51,8 +74,28 @@ namespace PixiEditor.Models.IO
         /// </summary>
         /// <param name="bitmap">Bitmap to be saved as file.</param>
         /// <param name="fileDimensions">Size of file.</param>
-        public static void Export(WriteableBitmap bitmap, Size fileDimensions)
+        public static void SaveAsImage(WriteableBitmap bitmap, Size fileDimensions)
         {
+            StringBuilder filter = new StringBuilder();
+
+            foreach (Extension extension in SDKHelper.GetCurrentManager().Extensions)
+            {
+                if (filter.Length != 0)
+                {
+                    filter.Append('|');
+                }
+
+                filter.Append(extension.DisplayName);
+                filter.Append('|');
+
+                foreach (string ext in extension.SupportedImageFileExtensions)
+                {
+                    filter.Append('*');
+                    filter.Append(ext);
+                    filter.Append(';');
+                }
+            }
+
             ExportFileDialog info = new ExportFileDialog(fileDimensions);
 
             // If OK on dialog has been clicked
@@ -65,7 +108,7 @@ namespace PixiEditor.Models.IO
                     return;
                 }
 
-                SaveAsPng(info.FilePath, info.FileWidth, info.FileHeight, bitmap);
+                SaveAsImage(info.FilePath, info.FileWidth, info.FileHeight, bitmap);
             }
         }
 
@@ -76,7 +119,7 @@ namespace PixiEditor.Models.IO
         /// <param name="exportWidth">File width.</param>
         /// <param name="exportHeight">File height.</param>
         /// <param name="bitmap">Bitmap to save.</param>
-        public static void SaveAsPng(string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
+        public static void SaveAsImage(string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
         {
             try
             {

+ 12 - 21
PixiEditor/Models/IO/Importer.cs

@@ -22,6 +22,7 @@ namespace PixiEditor.Models.IO
         public static WriteableBitmap ImportImage(string path, int width, int height)
         {
             WriteableBitmap wbmp = ImportImage(path);
+
             if (wbmp.PixelWidth != width || wbmp.PixelHeight != height)
             {
                 return wbmp.Resize(width, height, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
@@ -36,45 +37,35 @@ namespace PixiEditor.Models.IO
         /// <param name="path">Path of image.</param>
         public static WriteableBitmap ImportImage(string path)
         {
-            try
-            {
-                Uri uri = new Uri(path, UriKind.RelativeOrAbsolute);
-                BitmapImage bitmap = new BitmapImage();
-                bitmap.BeginInit();
-                bitmap.UriSource = uri;
-                bitmap.CacheOption = BitmapCacheOption.OnLoad;
-                bitmap.EndInit();
+            FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read);
 
-                return BitmapFactory.ConvertToPbgra32Format(bitmap);
-            }
-            catch (NotSupportedException)
+            try
             {
-                throw new CorruptedFileException();
+                return SDKHelper.FileParsers.CreateImageParser(Path.GetExtension(path), stream).Parse();
             }
-            catch (FileFormatException)
+            catch (Exception e)
             {
-                throw new CorruptedFileException();
+                throw new CorruptedFileException("Selected file is invalid or corrupted.", e);
             }
         }
 
         public static Document ImportDocument(string path)
         {
+            FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read);
+
             try
             {
-                Document doc = PixiParser.Deserialize(path).ToDocument();
-                doc.DocumentFilePath = path;
-                return doc;
+                return SDKHelper.FileParsers.CreateDocumentParser(Path.GetExtension(path), stream).Parse().ToDocument();
             }
-            catch (InvalidFileException)
+            catch (Exception e)
             {
-                throw new CorruptedFileException();
+                throw new CorruptedFileException("Selected file is invalid or corrupted.", e);
             }
         }
 
         public static bool IsSupportedFile(string path)
         {
-            path = path.ToLower();
-            return path.EndsWith(".pixi") || path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg");
+            return SDKHelper.GetCurrentManager().SupportedFileExtensions.Contains(path);
         }
     }
 }

+ 55 - 0
PixiEditor/Models/IO/Parsers/ImageParsers.cs

@@ -0,0 +1,55 @@
+using PixiEditor.SDK.FileParsers;
+using System.Windows.Media.Imaging;
+
+#pragma warning disable SA1402 // File may only contain a single type
+
+namespace PixiEditor.Models.IO.Parsers
+{
+    [FileParser(".png")]
+    class PngParser : ImageParser
+    {
+        public override bool UseBigEndian => false;
+
+        public override WriteableBitmap Parse()
+        {
+            BitmapImage bitmap = new BitmapImage();
+            bitmap.BeginInit();
+            bitmap.StreamSource = Stream;
+            bitmap.CacheOption = BitmapCacheOption.OnLoad;
+            bitmap.EndInit();
+
+            return BitmapFactory.ConvertToPbgra32Format(bitmap);
+        }
+
+        public override void Save(WriteableBitmap value)
+        {
+            PngBitmapEncoder encoder = new PngBitmapEncoder();
+            encoder.Frames.Add(BitmapFrame.Create(value));
+            encoder.Save(Stream);
+        }
+    }
+
+    [FileParser(".jpg", ".jpeg")]
+    class JpegParser : ImageParser
+    {
+        public override bool UseBigEndian => false;
+
+        public override WriteableBitmap Parse()
+        {
+            BitmapImage bitmap = new BitmapImage();
+            bitmap.BeginInit();
+            bitmap.StreamSource = Stream;
+            bitmap.CacheOption = BitmapCacheOption.OnLoad;
+            bitmap.EndInit();
+
+            return BitmapFactory.ConvertToPbgra32Format(bitmap);
+        }
+
+        public override void Save(WriteableBitmap value)
+        {
+            JpegBitmapEncoder encoder = new JpegBitmapEncoder();
+            encoder.Frames.Add(BitmapFrame.Create(value));
+            encoder.Save(Stream);
+        }
+    }
+}

+ 19 - 0
PixiEditor/Models/IO/Parsers/PixiParser.cs

@@ -0,0 +1,19 @@
+using PixiEditor.Parser;
+using PixiEditor.SDK.FileParsers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PixiEditor.Models.IO.Parsers
+{
+    public class PixiParser : DocumentParser
+    {
+        public override bool UseBigEndian => true;
+
+        public override SerializableDocument Parse() => Parser.PixiParser.Deserialize(Stream);
+
+        public override void Save(SerializableDocument value) => Parser.PixiParser.Serialize(value);
+    }
+}

+ 1 - 1
PixiEditor/Models/Undo/StorageBasedChange.cs

@@ -60,7 +60,7 @@ namespace PixiEditor.Models.Undo
                 UndoLayer storedLayer = StoredLayers[i];
                 if (Directory.Exists(Path.GetDirectoryName(storedLayer.StoredPngLayerName)))
                 {
-                    Exporter.SaveAsPng(storedLayer.StoredPngLayerName, storedLayer.Width, storedLayer.Height, layer.LayerBitmap);
+                    Exporter.SaveAsImage(storedLayer.StoredPngLayerName, storedLayer.Width, storedLayer.Height, layer.LayerBitmap);
                 }
 
                 i++;

+ 1 - 0
PixiEditor/PixiEditor.csproj

@@ -184,6 +184,7 @@
     </None>
   </ItemGroup>
   <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.SDK\PixiEditor.SDK.csproj" />
     <ProjectReference Include="..\PixiEditor.UpdateModule\PixiEditor.UpdateModule.csproj" />
   </ItemGroup>
   <ItemGroup>

+ 23 - 0
PixiEditor/ViewModels/SubViewModels/Main/ExtensionViewModel.cs

@@ -0,0 +1,23 @@
+using PixiEditor.Models;
+using PixiEditor.SDK;
+using System;
+using System.IO;
+
+namespace PixiEditor.ViewModels.SubViewModels.Main
+{
+    public class ExtensionViewModel : SubViewModel<ViewModelMain>
+    {
+        internal SDKManager SDKManager { get; set; }
+
+        public ExtensionViewModel(ViewModelMain owner)
+            : base(owner)
+        {
+            SDKManager = new SDKManager();
+
+            SDKManager.AddExtension(new BaseExtension());
+
+            SDKManager.LoadExtensions(Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PixiEditor", "Extensions"));
+            SDKManager.SetupExtensions();
+        }
+    }
+}

+ 1 - 1
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -265,7 +265,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         private void ExportFile(object parameter)
         {
             WriteableBitmap bitmap = Owner.BitmapManager.GetCombinedLayersBitmap();
-            Exporter.Export(bitmap, new Size(bitmap.PixelWidth, bitmap.PixelHeight));
+            Exporter.SaveAsImage(bitmap, new Size(bitmap.PixelWidth, bitmap.PixelHeight));
         }
 
         /// <summary>

+ 7 - 2
PixiEditor/ViewModels/ViewModelMain.cs

@@ -1,8 +1,8 @@
 using System;
-using System.Collections.Generic;
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Linq;
+using System.Runtime.Versioning;
 using System.Windows;
 using System.Windows.Input;
 using Microsoft.Extensions.DependencyInjection;
@@ -18,7 +18,6 @@ using PixiEditor.Models.Tools;
 using PixiEditor.Models.Tools.Tools;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels.SubViewModels.Main;
-using PixiEditor.Views.Dialogs;
 
 namespace PixiEditor.ViewModels
 {
@@ -37,6 +36,8 @@ namespace PixiEditor.ViewModels
 
         public RelayCommand CloseWindowCommand { get; set; }
 
+        public ExtensionViewModel ExtensionSubViewModel { get; set; }
+
         public FileViewModel FileSubViewModel { get; set; }
 
         public UpdateViewModel UpdateSubViewModel { get; set; }
@@ -117,6 +118,8 @@ namespace PixiEditor.ViewModels
 #endif
         }
 
+        [UnsupportedOSPlatform("windows")]
+        [SupportedOSPlatform("windows7")]
         public ViewModelMain(IServiceProvider services)
         {
             Current = this;
@@ -157,6 +160,8 @@ namespace PixiEditor.ViewModels
             AddDebugOnlyViewModels();
             AddReleaseOnlyViewModels();
 
+            ExtensionSubViewModel = new ExtensionViewModel(this);
+
             ShortcutController = new ShortcutController(
                     new ShortcutGroup(
                         "Tools",

+ 2 - 2
PixiEditorTests/ModelsTests/IO/ExporterTests.cs

@@ -16,7 +16,7 @@ namespace PixiEditorTests.ModelsTests.IO
         [Fact]
         public void TestThatSaveAsPngSavesFile()
         {
-            Exporter.SaveAsPng(FilePath, 10, 10, BitmapFactory.New(10, 10));
+            Exporter.SaveAsImage(FilePath, 10, 10, BitmapFactory.New(10, 10));
             Assert.True(File.Exists(FilePath));
 
             File.Delete(FilePath);
@@ -34,7 +34,7 @@ namespace PixiEditorTests.ModelsTests.IO
 
             document.Swatches.Add(Colors.White);
 
-            Exporter.SaveAsEditableFile(document, filePath);
+            Exporter.SaveAsDocument(document, filePath);
 
             Assert.True(File.Exists(filePath));