Prechádzať zdrojové kódy

Merge branch 'refs/heads/avalonia-rewrite' into beta-version

# Conflicts:
#	src/PixiEditor.AvaloniaUI/Data/Localization/Languages/en.json
CPKreuz 1 rok pred
rodič
commit
1d4e0d6b07
28 zmenil súbory, kde vykonal 501 pridanie a 141 odobranie
  1. 4 2
      src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs
  2. 4 0
      src/PixiEditor.AnimationRenderer.Core/PixiEditor.AnimationRenderer.Core.csproj
  3. 51 29
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  4. 55 0
      src/PixiEditor.AnimationRenderer.FFmpeg/ImgFrame.cs
  5. 2 0
      src/PixiEditor.AvaloniaUI/Data/Localization/Languages/en.json
  6. 1 1
      src/PixiEditor.AvaloniaUI/Helpers/SerializationUtil.cs
  7. 13 3
      src/PixiEditor.AvaloniaUI/Helpers/StructureHelpers.cs
  8. 16 4
      src/PixiEditor.AvaloniaUI/Models/Files/VideoFileType.cs
  9. 2 0
      src/PixiEditor.AvaloniaUI/Models/IO/Importer.cs
  10. 1 1
      src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs
  11. 1 1
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorMatrixSerializationFactory.cs
  12. 13 8
      src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml
  13. 4 13
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  14. 4 4
      src/PixiEditor.AvaloniaUI/Views/Nodes/ConnectionLine.cs
  15. 149 14
      src/PixiEditor.AvaloniaUI/Views/Nodes/NodeGraphView.cs
  16. 13 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/NodeView.cs
  17. 6 0
      src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs
  18. 28 10
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs
  19. 47 19
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  20. 10 5
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  21. 3 0
      src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IBitmapImplementation.cs
  22. 1 0
      src/PixiEditor.DrawingApi.Core/Bridge/Operations/IImageImplementation.cs
  23. 2 0
      src/PixiEditor.DrawingApi.Core/Surfaces/Bitmap.cs
  24. 2 0
      src/PixiEditor.DrawingApi.Core/Surfaces/ImageData/Image.cs
  25. 13 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs
  26. 6 0
      src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageImplementation.cs
  27. 41 1
      src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.Impl.cs
  28. 9 26
      src/PixiEditor.Numerics/ColorMatrix.cs

+ 4 - 2
src/PixiEditor.AnimationRenderer.Core/IAnimationRenderer.cs

@@ -1,6 +1,8 @@
-namespace PixiEditor.AnimationRenderer.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+
+namespace PixiEditor.AnimationRenderer.Core;
 
 public interface IAnimationRenderer
 {
-    public Task<bool> RenderAsync(string framesPath, string outputPath);
+    public Task<bool> RenderAsync(List<Image> imageStream, string outputPath);
 }

+ 4 - 0
src/PixiEditor.AnimationRenderer.Core/PixiEditor.AnimationRenderer.Core.csproj

@@ -6,4 +6,8 @@
         <Nullable>enable</Nullable>
     </PropertyGroup>
 
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.DrawingApi.Core\PixiEditor.DrawingApi.Core.csproj" />
+    </ItemGroup>
+
 </Project>

+ 51 - 29
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -3,7 +3,9 @@ using System.Reflection;
 using FFMpegCore;
 using FFMpegCore.Arguments;
 using FFMpegCore.Enums;
+using FFMpegCore.Pipes;
 using PixiEditor.AnimationRenderer.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.AnimationRenderer.FFmpeg;
@@ -14,24 +16,8 @@ public class FFMpegRenderer : IAnimationRenderer
     public string OutputFormat { get; set; } = "mp4";
     public VecI Size { get; set; }
 
-    public async Task<bool> RenderAsync(string framesPath, string outputPath)
+    public async Task<bool> RenderAsync(List<Image> rawFrames, string outputPath)
     {
-        string[] frames = Directory.GetFiles(framesPath, "*.png");
-        if (frames.Length == 0)
-        {
-            return false;
-        }
-
-        string[] finalFrames = new string[frames.Length];
-
-        for (int i = 0; i < frames.Length; i++)
-        {
-            if (int.TryParse(Path.GetFileNameWithoutExtension(frames[i]), out int frameNumber))
-            {
-                finalFrames[frameNumber - 1] = frames[i];
-            }
-        }
-        
         string path = "ThirdParty/{0}/ffmpeg/bin";
 #if WINDOWS
         path = string.Format(path, "Windows");
@@ -48,19 +34,47 @@ public class FFMpegRenderer : IAnimationRenderer
 
         try
         {
+            List<ImgFrame> frames = new();
+            
+            foreach (var frame in rawFrames)
+            {
+                frames.Add(new ImgFrame(frame));
+            }
+
+            RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
+            
+            string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
+            
+            if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
+            {
+                Directory.CreateDirectory(Path.GetDirectoryName(paletteTempPath));
+            }
+            
             if (RequiresPaletteGeneration())
             {
-                GeneratePalette(finalFrames, framesPath);
+                GeneratePalette(streamPipeSource, paletteTempPath);
             }
+            
+            streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
             var args = FFMpegArguments
-                .FromConcatInput(finalFrames, options =>
+                .FromPipeInput(streamPipeSource, options =>
                 {
                     options.WithFramerate(FrameRate);
                 });
 
-            var outputArgs = GetProcessorForFormat(args, framesPath, outputPath);
-            return await outputArgs.ProcessAsynchronously();
+            var outputArgs = GetProcessorForFormat(args, outputPath, paletteTempPath);
+            var result = await outputArgs.ProcessAsynchronously();
+            
+            if (RequiresPaletteGeneration())
+            {
+                File.Delete(paletteTempPath);
+                Directory.Delete(Path.GetDirectoryName(paletteTempPath));
+            }
+            
+            DisposeStream(frames);
+            
+            return result;
         }
         catch (Exception e)
         {
@@ -69,20 +83,29 @@ public class FFMpegRenderer : IAnimationRenderer
         }
     }
 
-    private FFMpegArgumentProcessor GetProcessorForFormat(FFMpegArguments args, string framesPath, string outputPath)
+    private void DisposeStream(List<ImgFrame> frames)
+    {
+        foreach (var frame in frames)
+        {
+            frame.Dispose();
+        }
+    }
+
+    private FFMpegArgumentProcessor GetProcessorForFormat(FFMpegArguments args, string outputPath,
+        string paletteTempPath)
     {
         return OutputFormat switch
         {
-            "gif" => GetGifArguments(args, framesPath, outputPath),
+            "gif" => GetGifArguments(args, outputPath, paletteTempPath),
             "mp4" => GetMp4Arguments(args, outputPath),
             _ => throw new NotSupportedException($"Output format {OutputFormat} is not supported")
         };
     }
 
-    private FFMpegArgumentProcessor GetGifArguments(FFMpegArguments args, string framesPath, string outputPath)
+    private FFMpegArgumentProcessor GetGifArguments(FFMpegArguments args, string outputPath, string paletteTempPath)
     {
         return args
-            .AddFileInput(Path.Combine(framesPath, "palette.png"))
+            .AddFileInput(paletteTempPath)
             .OutputToFile(outputPath, true, options =>
             {
                 options.WithCustomArgument(
@@ -108,15 +131,14 @@ public class FFMpegRenderer : IAnimationRenderer
         return OutputFormat == "gif";
     }
 
-    private void GeneratePalette(string[] frames, string path)
+    private void GeneratePalette(IPipeSource imageStream, string path)
     {
-        string palettePath = Path.Combine(path, "palette.png");
         FFMpegArguments
-            .FromConcatInput(frames, options =>
+            .FromPipeInput(imageStream, options =>
             {
                 options.WithFramerate(FrameRate);
             })
-            .OutputToFile(palettePath, true, options =>
+            .OutputToFile(path, true, options =>
             {
                 options
                     .WithCustomArgument($"-vf \"palettegen\"");

+ 55 - 0
src/PixiEditor.AnimationRenderer.FFmpeg/ImgFrame.cs

@@ -0,0 +1,55 @@
+using FFMpegCore.Pipes;
+using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
+
+namespace PixiEditor.AnimationRenderer.FFmpeg;
+
+public class ImgFrame : IVideoFrame, IDisposable
+{
+    public Image Image { get; }
+
+    public int Width => Image.Width;
+    public int Height => Image.Height;
+    public string Format => ToStreamFormat(); 
+
+    private Bitmap encoded;
+    
+    public ImgFrame(Image image)
+    {
+        Image = image;
+        encoded = Bitmap.FromImage(image);
+    }
+
+    public void Serialize(Stream pipe)
+    {
+        var bytes = encoded.Bytes;
+        pipe.Write(bytes, 0, bytes.Length);
+    }
+
+    public async Task SerializeAsync(Stream pipe, CancellationToken token)
+    {
+        await pipe.WriteAsync(encoded.Bytes, 0, encoded.Bytes.Length, token).ConfigureAwait(false); 
+    }
+    
+    private string ToStreamFormat()
+    {
+        switch (encoded.Info.ColorType)
+        {
+            case ColorType.Gray8:
+                return "gray8";
+            case ColorType.Bgra8888:
+                return "bgra";
+            case ColorType.Rgba8888:
+                return "rgba";
+            case ColorType.Rgb565:
+                return "rgb565";
+            default:
+                throw new NotSupportedException($"Color type {Image.Info.ColorType} is not supported.");
+        } 
+    }
+
+    public void Dispose()
+    {
+        encoded.Dispose();
+    }
+}

+ 2 - 0
src/PixiEditor.AvaloniaUI/Data/Localization/Languages/en.json

@@ -672,6 +672,8 @@
   "OCTAVES": "Octaves",
   "ACTIVE_FRAME": "Active Frame",
   "NORMALIZED_TIME": "Normalized Time",
+  "WITHOUT_FILTERS": "Without filters",
+  "RAW_LAYER_OUTPUT": "Raw",
   
   "BETA_EXAMPLE_FILES": "Beta Example Files",
   "POND_EXAMPLE": "Pond"

+ 1 - 1
src/PixiEditor.AvaloniaUI/Helpers/SerializationUtil.cs

@@ -28,7 +28,7 @@ public static class SerializationUtil
             return factory.Serialize(value);
         }
 
-        if (value.GetType().IsValueType || value is string)
+        if (value.GetType().IsValueType || value is string || value is object[])
         {
             return value;
         }

+ 13 - 3
src/PixiEditor.AvaloniaUI/Helpers/StructureHelpers.cs

@@ -6,6 +6,7 @@ namespace PixiEditor.AvaloniaUI.Helpers;
 public static class StructureHelpers
 {
     public const int PreviewSize = 48;
+
     /// <summary>
     /// Calculates the size of a scaled-down preview for a given size of layer tight bounds.
     /// </summary>
@@ -13,8 +14,17 @@ public static class StructureHelpers
     {
         double proportions = tightBoundsSize.Y / (double)tightBoundsSize.X;
         const int prSize = PreviewSize;
-        return proportions > 1 ?
-            new VecI(Math.Max((int)Math.Round(prSize / proportions), 1), prSize) :
-            new VecI(prSize, Math.Max((int)Math.Round(prSize * proportions), 1));
+        return proportions > 1
+            ? new VecI(Math.Max((int)Math.Round(prSize / proportions), 1), prSize)
+            : new VecI(prSize, Math.Max((int)Math.Round(prSize * proportions), 1));
+    }
+
+    public static VecI CalculatePreviewSize(VecI tightBoundsSize, int previewSize)
+    {
+        double proportions = tightBoundsSize.Y / (double)tightBoundsSize.X;
+        int prSize = previewSize;
+        return proportions > 1
+            ? new VecI(Math.Max((int)Math.Round(prSize / proportions), 1), prSize)
+            : new VecI(prSize, Math.Max((int)Math.Round(prSize * proportions), 1));
     }
 }

+ 16 - 4
src/PixiEditor.AvaloniaUI/Models/Files/VideoFileType.cs

@@ -1,17 +1,23 @@
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 
 namespace PixiEditor.AvaloniaUI.Models.Files;
 
 internal abstract class VideoFileType : IoFileType
 {
     public override FileTypeDialogDataSet.SetKind SetKind { get; } = FileTypeDialogDataSet.SetKind.Video;
-    public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document, ExportConfig config)
+
+    public override async Task<SaveResult> TrySave(string pathWithExtension, DocumentViewModel document,
+        ExportConfig config)
     {
         if (config.AnimationRenderer is null)
             return SaveResult.UnknownError;
-        
-        document.RenderFrames(Paths.TempRenderingPath, surface =>
+
+        List<Image> frames = new(); 
+
+        document.RenderFrames(frames, surface =>
         {
             if (config.ExportSize != surface.Size)
             {
@@ -20,8 +26,14 @@ internal abstract class VideoFileType : IoFileType
 
             return surface;
         });
+
+        var result = await config.AnimationRenderer.RenderAsync(frames, pathWithExtension);
         
-        var result = await config.AnimationRenderer.RenderAsync(Paths.TempRenderingPath, pathWithExtension);
+        foreach (var frame in frames)
+        {
+            frame.Dispose();
+        } 
+
         return result ? SaveResult.Success : SaveResult.UnknownError;
     }
 }

+ 2 - 0
src/PixiEditor.AvaloniaUI/Models/IO/Importer.cs

@@ -92,6 +92,7 @@ internal class Importer : ObservableObject
             {
                 Document v5 => v5.ToDocument(),
                 DocumentV4 v4 => v4.ToDocument()
+                // TODO: Default handling
             };
 
             if (associatePath)
@@ -132,6 +133,7 @@ internal class Importer : ObservableObject
             {
                 Document v5 => v5.ToDocument(),
                 DocumentV4 v4 => v4.ToDocument()
+                // TODO: Default handling
             };
 
             document.FullFilePath = originalFilePath;

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -739,7 +739,7 @@ internal class MemberPreviewUpdater
             if (nodeVm.ResultPreview == null)
             {
                 nodeVm.ResultPreview =
-                    new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size));
+                    new Surface(StructureHelpers.CalculatePreviewSize(internals.Tracker.Document.Size, 150));
             }
 
             float scalingX = (float)nodeVm.ResultPreview.Size.X / node.CachedResult.Size.X;

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorMatrixSerializationFactory.cs

@@ -12,7 +12,7 @@ internal class ColorMatrixSerializationFactory : SerializationFactory<Serializab
         {
             Width = 4,
             Height = 5,
-            Values = original.AsSpan().ToArray()
+            Values = original.ToArray()
         };    
     }
 

+ 13 - 8
src/PixiEditor.AvaloniaUI/Styles/Templates/NodeSocket.axaml

@@ -6,14 +6,19 @@
             <ControlTemplate>
                 <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                     <Grid Name="PART_ConnectPort">
-                        <Ellipse Width="10" Height="10" 
-                                 Fill="{TemplateBinding SocketBrush}" 
-                                 IsVisible="{Binding !IsFunc, RelativeSource={RelativeSource TemplatedParent}}"/>
-                        <Rectangle Width="10" Height="10"
-                                   RadiusX="2" RadiusY="2"
-                                   Fill="{TemplateBinding SocketBrush}"
-                                   RenderTransform="rotate(45deg) scale(0.89)"
-                                   IsVisible="{Binding IsFunc, RelativeSource={RelativeSource TemplatedParent}}"/>
+                        <Panel Width="20" Height="20" Margin="-5, 0" Background="Transparent"
+                               IsVisible="{Binding !IsFunc, RelativeSource={RelativeSource TemplatedParent}}">
+                            <Ellipse Width="10" Height="10"
+                                     Fill="{TemplateBinding SocketBrush}" />
+                        </Panel>
+                        <Panel Margin="-5, 0" Width="20" Height="20" Background="Transparent"
+                               IsVisible="{Binding IsFunc, RelativeSource={RelativeSource TemplatedParent}}">
+                            <Rectangle Width="10" Height="10"
+                                       RadiusX="2" RadiusY="2"
+                                       Fill="{TemplateBinding SocketBrush}"
+                                       RenderTransform="rotate(45deg) scale(0.89)" />
+                        </Panel>
+
                     </Grid>
                 </StackPanel>
             </ControlTemplate>

+ 4 - 13
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -887,20 +887,11 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         }
     }
 
-    public bool RenderFrames(string tempRenderingPath, Func<Surface, Surface> processFrameAction = null)
+    public bool RenderFrames(List<Image> frames, Func<Surface, Surface> processFrameAction = null)
     {
         if (AnimationDataViewModel.KeyFrames.Count == 0)
             return false;
 
-        if (!Directory.Exists(tempRenderingPath))
-        {
-            Directory.CreateDirectory(tempRenderingPath);
-        }
-        else
-        {
-            ClearTempFolder(tempRenderingPath);
-        }
-
         var keyFrames = AnimationDataViewModel.KeyFrames;
         var firstFrame = keyFrames.Min(x => x.StartFrameBindable);
         var lastFrame = keyFrames.Max(x => x.StartFrameBindable + x.DurationBindable);
@@ -919,9 +910,9 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 surface = processFrameAction(surface.AsT1);
             }
 
-            using var stream = new FileStream(Path.Combine(tempRenderingPath, $"{i}.png"), FileMode.Create);
-            surface.AsT1.DrawingSurface.Snapshot().Encode().SaveTo(stream);
-            stream.Position = 0;
+
+            var snapshot = surface.AsT1.DrawingSurface.Snapshot();
+            frames.Add(snapshot);
         }
 
         return true;

+ 4 - 4
src/PixiEditor.AvaloniaUI/Views/Nodes/ConnectionLine.cs

@@ -8,15 +8,15 @@ public class ConnectionLine : Control
 {
     private Pen pen = new() { LineCap = PenLineCap.Round };
     
-    public static readonly StyledProperty<LinearGradientBrush> ColorProperty = AvaloniaProperty.Register<ConnectionLine, LinearGradientBrush>("Color");
+    public static readonly StyledProperty<LinearGradientBrush> LineBrushProperty = AvaloniaProperty.Register<ConnectionLine, LinearGradientBrush>("LineBrush");
     public static readonly StyledProperty<double> ThicknessProperty = AvaloniaProperty.Register<ConnectionLine, double>("Thickness");
     public static readonly StyledProperty<Point> StartPointProperty = AvaloniaProperty.Register<ConnectionLine, Point>("StartPoint");
     public static readonly StyledProperty<Point> EndPointProperty = AvaloniaProperty.Register<ConnectionLine, Point>("EndPoint");
 
     public LinearGradientBrush LineBrush
     {
-        get { return GetValue(ColorProperty); }
-        set { SetValue(ColorProperty, value); }
+        get { return GetValue(LineBrushProperty); }
+        set { SetValue(LineBrushProperty, value); }
     }
 
     public double Thickness
@@ -39,7 +39,7 @@ public class ConnectionLine : Control
     
     static ConnectionLine()
     {
-        AffectsRender<ConnectionLine>(ColorProperty, ThicknessProperty, StartPointProperty, EndPointProperty);
+        AffectsRender<ConnectionLine>(LineBrushProperty, ThicknessProperty, StartPointProperty, EndPointProperty);
     }
 
     public override void Render(DrawingContext context)

+ 149 - 14
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeGraphView.cs

@@ -6,6 +6,8 @@ using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.VisualTree;
 using CommunityToolkit.Mvvm.Input;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.Handlers;
@@ -13,6 +15,7 @@ using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Nodes;
 using PixiEditor.AvaloniaUI.Views.Nodes.Properties;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.Changes.NodeGraph;
 using PixiEditor.Numerics;
 using Point = Avalonia.Point;
 
@@ -48,19 +51,24 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             nameof(EndChangeNodePosCommand));
 
-    public static readonly StyledProperty<string> SearchQueryProperty = AvaloniaProperty.Register<NodeGraphView, string>(
-        nameof(SearchQuery));
+    public static readonly StyledProperty<string> SearchQueryProperty =
+        AvaloniaProperty.Register<NodeGraphView, string>(
+            nameof(SearchQuery));
 
-    public static readonly StyledProperty<ObservableCollection<Type>> AllNodeTypesProperty = AvaloniaProperty.Register<NodeGraphView, ObservableCollection<Type>>(
-        nameof(AllNodeTypes));
+    public static readonly StyledProperty<ObservableCollection<Type>> AllNodeTypesProperty =
+        AvaloniaProperty.Register<NodeGraphView, ObservableCollection<Type>>(
+            nameof(AllNodeTypes));
 
-    public static readonly StyledProperty<ICommand> SocketDropCommandProperty = AvaloniaProperty.Register<NodeGraphView, ICommand>(
-        nameof(SocketDropCommand));
+    public static readonly StyledProperty<ICommand> SocketDropCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            nameof(SocketDropCommand));
 
-    public static readonly StyledProperty<ICommand> CreateNodeCommandProperty = AvaloniaProperty.Register<NodeGraphView, ICommand>("CreateNodeCommand");
+    public static readonly StyledProperty<ICommand> CreateNodeCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>("CreateNodeCommand");
 
-    public static readonly StyledProperty<ICommand> ConnectPropertiesCommandProperty = AvaloniaProperty.Register<NodeGraphView, ICommand>(
-        "ConnectPropertiesCommand");
+    public static readonly StyledProperty<ICommand> ConnectPropertiesCommandProperty =
+        AvaloniaProperty.Register<NodeGraphView, ICommand>(
+            "ConnectPropertiesCommand");
 
     public ICommand ConnectPropertiesCommand
     {
@@ -150,6 +158,11 @@ internal class NodeGraphView : Zoombox.Zoombox
     private INodeHandler startConnectionNode;
     private INodeHandler endConnectionNode;
 
+    private Point startDragConnectionPoint;
+    private ConnectionLine _previewConnectionLine;
+    private NodeConnectionViewModel? _hiddenConnection;
+    private Color _startingPropColor;
+
     public NodeGraphView()
     {
         SelectNodeCommand = new RelayCommand<PointerPressedEventArgs>(SelectNode);
@@ -171,13 +184,81 @@ internal class NodeGraphView : Zoombox.Zoombox
         }
     }
 
+    protected override void OnPointerMoved(PointerEventArgs e)
+    {
+        if (isDraggingConnection)
+        {
+            UpdateConnectionEnd(e);
+        }
+    }
+
+    private void UpdateConnectionEnd(PointerEventArgs e)
+    {
+        Point pos = e.GetPosition(this);
+        VecD currentPoint = ToZoomboxSpace(new VecD(pos.X, pos.Y));
+
+        NodeSocket? nodeSocket = e.Source as NodeSocket;
+        
+        if (nodeSocket != null)
+        {
+            Canvas canvas = nodeSocket.FindAncestorOfType<Canvas>();
+            pos = nodeSocket.ConnectPort.TranslatePoint(
+                new Point(nodeSocket.ConnectPort.Bounds.Width / 2, nodeSocket.ConnectPort.Bounds.Height / 2),
+                canvas) ?? default;
+            currentPoint = new VecD(pos.X, pos.Y);
+        }
+
+
+        if (_previewConnectionLine != null)
+        {
+            Point endPoint = new Point(currentPoint.X, currentPoint.Y);
+
+            Color gradientStopFirstColor = _startingPropColor;
+            Color gradientStopSecondColor =
+                ((SolidColorBrush)nodeSocket?.SocketBrush)?.Color ?? gradientStopFirstColor;
+
+            if (endPoint.X > startDragConnectionPoint.X)
+            {
+                _previewConnectionLine.StartPoint = endPoint;
+                _previewConnectionLine.EndPoint = startDragConnectionPoint;
+                (gradientStopFirstColor, gradientStopSecondColor) =
+                    (gradientStopSecondColor, gradientStopFirstColor);
+            }
+            else
+            {
+                _previewConnectionLine.StartPoint = startDragConnectionPoint;
+                _previewConnectionLine.EndPoint = endPoint;
+            }
+
+            _previewConnectionLine.LineBrush = new LinearGradientBrush()
+            {
+                GradientStops = new GradientStops()
+                {
+                    new GradientStop(gradientStopFirstColor, 0),
+                    new GradientStop(gradientStopSecondColor, 1),
+                }
+            };
+        }
+    }
+
     protected override void OnPointerReleased(PointerReleasedEventArgs e)
     {
         base.OnPointerReleased(e);
-        if (startConnectionProperty != null)
+        if (startConnectionProperty is { IsInput: true } && e.Source is not NodeSocket)
         {
             SocketDrop(null);
         }
+
+        if (isDraggingConnection)
+        {
+            if (_previewConnectionLine != null)
+            {
+                _previewConnectionLine.IsVisible = false;
+            }
+
+            isDraggingConnection = false;
+            _hiddenConnection = null;
+        }
     }
 
     private IEnumerable<Type> GatherAssemblyTypes<T>()
@@ -196,6 +277,20 @@ internal class NodeGraphView : Zoombox.Zoombox
                 startConnectionProperty = nodeSocket.Property;
                 startConnectionNode = nodeSocket.Node;
                 isDraggingConnection = true;
+
+                if (nodeSocket is { IsInput: true, Property.ConnectedOutput: not null })
+                {
+                    var conn = NodeGraph.Connections.FirstOrDefault(x => x.InputProperty == nodeSocket.Property);
+                    if (conn != null)
+                    {
+                        _hiddenConnection = conn;
+                        NodeGraph.Connections.Remove(conn);
+                        NodeView view = FindNodeView(conn.OutputNode);
+                        nodeSocket = view.GetSocket(conn.OutputProperty);
+                    }
+                }
+
+                UpdatePreviewLine(nodeSocket);
             }
             else
             {
@@ -207,12 +302,46 @@ internal class NodeGraphView : Zoombox.Zoombox
         }
     }
 
+    private NodeView FindNodeView(INodeHandler node)
+    {
+        return this.GetVisualDescendants().OfType<NodeView>().FirstOrDefault(x => x.Node == node);
+    }
+
+    private void UpdatePreviewLine(NodeSocket nodeSocket)
+    {
+        Canvas canvas = nodeSocket.FindAncestorOfType<Canvas>();
+        if (_previewConnectionLine == null)
+        {
+            _previewConnectionLine = new ConnectionLine();
+            _previewConnectionLine.Thickness = 2;
+
+            canvas.Children.Insert(0, _previewConnectionLine);
+        }
+
+        _previewConnectionLine.IsVisible = true;
+        _startingPropColor = ((SolidColorBrush)nodeSocket.SocketBrush).Color;
+        _previewConnectionLine.LineBrush = new LinearGradientBrush()
+        {
+            GradientStops = new GradientStops()
+            {
+                new GradientStop(_startingPropColor, 1),
+            }
+        };
+
+        _previewConnectionLine.StartPoint = nodeSocket.ConnectPort.TranslatePoint(
+            new Point(nodeSocket.ConnectPort.Bounds.Width / 2, nodeSocket.ConnectPort.Bounds.Height / 2),
+            canvas) ?? default;
+        _previewConnectionLine.EndPoint = _previewConnectionLine.StartPoint;
+        startDragConnectionPoint = _previewConnectionLine.StartPoint;
+    }
+
     private void Dragged(PointerEventArgs e)
     {
         if (isDraggingNodes)
         {
             Point pos = e.GetPosition(this);
             VecD currentPoint = ToZoomboxSpace(new VecD(pos.X, pos.Y));
+
             VecD delta = currentPoint - clickPointOffset;
             foreach (var node in SelectedNodes)
             {
@@ -232,11 +361,11 @@ internal class NodeGraphView : Zoombox.Zoombox
 
     private void SocketDrop(NodeSocket socket)
     {
-        if(startConnectionProperty == null)
+        if (startConnectionProperty == null)
         {
             return;
         }
-        
+
         (INodePropertyHandler, INodePropertyHandler) connection = (startConnectionProperty, null);
         if (socket != null)
         {
@@ -253,15 +382,21 @@ internal class NodeGraphView : Zoombox.Zoombox
 
             if (startConnectionNode == endConnectionNode)
             {
+                if (_hiddenConnection != null)
+                {
+                    NodeGraph.Connections.Add(_hiddenConnection);
+                    _hiddenConnection = null;
+                }
+
                 return;
             }
         }
 
-        if(ConnectPropertiesCommand != null && ConnectPropertiesCommand.CanExecute(connection))
+        if (ConnectPropertiesCommand != null && ConnectPropertiesCommand.CanExecute(connection))
         {
             ConnectPropertiesCommand.Execute(connection);
         }
-        
+
         startConnectionProperty = null;
         endConnectionProperty = null;
         startConnectionNode = null;

+ 13 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/NodeView.cs

@@ -200,6 +200,19 @@ public class NodeView : TemplatedControl
         e.Handled = true;
     }
 
+    public NodeSocket GetSocket(INodePropertyHandler property)
+    {
+        NodePropertyView propertyView = this.GetVisualDescendants().OfType<NodePropertyView>()
+            .FirstOrDefault(x => x.DataContext == property);
+
+        if (propertyView is null)
+        {
+            return default;
+        }
+
+        return property.IsInput ? propertyView.InputSocket : propertyView.OutputSocket;
+    }
+
     public Point GetSocketPoint(INodePropertyHandler property, Canvas canvas)
     {
         NodePropertyView propertyView = this.GetVisualDescendants().OfType<NodePropertyView>()

+ 6 - 0
src/PixiEditor.AvaloniaUI/Views/Nodes/Properties/NodeSocket.cs

@@ -51,6 +51,7 @@ public class NodeSocket : TemplatedControl
         ConnectPort = e.NameScope.Find<Control>("PART_ConnectPort");
         ConnectPort.PointerPressed += ConnectPortOnPointerPressed;
         ConnectPort.PointerReleased += ConnectPortOnPointerReleased;
+        ConnectPort.PointerMoved += ConnectPortOnPointerMoved;
     }
 
     private void ConnectPortOnPointerPressed(object? sender, PointerPressedEventArgs e)
@@ -58,6 +59,11 @@ public class NodeSocket : TemplatedControl
         e.Source = this;
         e.Pointer.Capture(null);
     }
+    
+    private void ConnectPortOnPointerMoved(object? sender, PointerEventArgs e)
+    {
+        e.Source = this;
+    }
 
     private void ConnectPortOnPointerReleased(object? sender, PointerEventArgs e)
     {

+ 28 - 10
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/FolderNode.cs

@@ -39,48 +39,66 @@ public class FolderNode : StructureNode, IReadOnlyFolderNode
 
         VecI size = Content.Value?.Size ?? Background.Value?.Size ?? VecI.Zero;
         
-        var workingSurface = TryInitWorkingSurface(size, context);
+        var outputWorkingSurface = TryInitWorkingSurface(size, context, 0);
+        var filterlessWorkingSurface = TryInitWorkingSurface(size, context, 1);
+        
+        if (Background.Value != null)
+        {
+            DrawBackground(filterlessWorkingSurface, context);
+            blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
+        }
+
+        if (Content.Value != null)
+        {
+            blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255)); 
+            DrawSurface(filterlessWorkingSurface, Content.Value, context, null);
+        }
+
+        FilterlessOutput.Value = filterlessWorkingSurface;
 
         if (!HasOperations())
         {
             if (Background.Value != null)
             {
-                DrawBackground(workingSurface, context);
+                blendPaint.Color = new Color(255, 255, 255, 255);
+                blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
+                DrawBackground(outputWorkingSurface, context);
                 blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
             }
             
             if (Content.Value != null)
             {
                 blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255)); 
-                DrawSurface(workingSurface, Content.Value, context, Filters.Value);
+                DrawSurface(outputWorkingSurface, Content.Value, context, Filters.Value);
             }
             
-            Output.Value = workingSurface;
+            Output.Value = outputWorkingSurface;
             return Output.Value;
         }
         
         if (Content.Value != null)
         {
-            DrawSurface(workingSurface, Content.Value, context, Filters.Value);
+            DrawSurface(outputWorkingSurface, Content.Value, context, Filters.Value);
+            
+            ApplyMaskIfPresent(outputWorkingSurface, context);
+            ApplyRasterClip(outputWorkingSurface, context);
             
-            ApplyMaskIfPresent(workingSurface, context);
-            ApplyRasterClip(workingSurface, context);
         }
         
         if (Background.Value != null)
         {
-            Surface tempSurface = new Surface(workingSurface.Size);
+            Surface tempSurface = new Surface(outputWorkingSurface.Size);
             DrawBackground(tempSurface, context);
             
             blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255));
             blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
-            tempSurface.DrawingSurface.Canvas.DrawSurface(workingSurface.DrawingSurface, 0, 0, blendPaint);
+            tempSurface.DrawingSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
 
             Output.Value = tempSurface;
             return tempSurface;
         }
 
-        Output.Value = workingSurface;
+        Output.Value = outputWorkingSurface;
         return Output.Value;
     }
 

+ 47 - 19
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -16,6 +16,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     public const string ImageFramesKey = "Frames";
     public const string ImageLayerKey = "LayerImage";
 
+    public OutputProperty<Surface> RawOutput { get; }
+
     public InputProperty<bool> LockTransparency { get; }
 
     private VecI size;
@@ -36,6 +38,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public ImageLayerNode(VecI size)
     {
+        RawOutput = CreateOutput<Surface>(nameof(RawOutput), "RAW_LAYER_OUTPUT", null);
+
         LockTransparency = CreateInput<bool>("LockTransparency", "LOCK_TRANSPARENCY", false);
 
         if (keyFrames.Count == 0)
@@ -73,49 +77,72 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     private Surface RenderImage(ChunkyImage frameImage, RenderingContext context)
     {
-        var workingSurface = TryInitWorkingSurface(frameImage.LatestSize, context);
+        var outputWorkingSurface = TryInitWorkingSurface(frameImage.LatestSize, context, 0);
+        var filterlessWorkingSurface = TryInitWorkingSurface(frameImage.LatestSize, context, 1);
+        var rawWorkingSurface = TryInitWorkingSurface(frameImage.LatestSize, context, 3);
+
+        bool shouldClear = Background.Value == null;
+        // Draw filterless
+        if (Background.Value != null)
+        {
+            DrawBackground(filterlessWorkingSurface, context);
+            blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
+        }
 
+        DrawLayer(frameImage, context, filterlessWorkingSurface, shouldClear, useFilters: false);
+        blendPaint.BlendMode = DrawingApi.Core.Surfaces.BlendMode.Src;
+        
+        FilterlessOutput.Value = filterlessWorkingSurface;
+        
+        // Draw raw
+        DrawLayer(frameImage, context, rawWorkingSurface, true, useFilters: false);
+
+        RawOutput.Value = rawWorkingSurface;
+        
+        // Draw output
         if (!HasOperations())
         {
-            bool canClear = Background.Value == null;
             if (Background.Value != null)
             {
-                DrawBackground(workingSurface, context);
+                DrawBackground(outputWorkingSurface, context);
                 blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
             }
 
-            DrawLayer(frameImage, context, workingSurface, canClear);
-            Output.Value = workingSurface;
-            return workingSurface;
+            DrawLayer(frameImage, context, outputWorkingSurface, shouldClear);
+            
+            Output.Value = outputWorkingSurface;
+            
+            return outputWorkingSurface;
         }
 
-        DrawLayer(frameImage, context, workingSurface, true);
+        DrawLayer(frameImage, context, outputWorkingSurface, true);
 
         // shit gets downhill with mask on big canvases, TODO: optimize
-        ApplyMaskIfPresent(workingSurface, context);
-        ApplyRasterClip(workingSurface, context);
-
+        ApplyMaskIfPresent(outputWorkingSurface, context);
+        ApplyRasterClip(outputWorkingSurface, context);
+        
         if (Background.Value != null)
         {
-            Surface tempSurface = new Surface(workingSurface.Size);
+            Surface tempSurface = new Surface(outputWorkingSurface.Size);
             DrawBackground(tempSurface, context);
             blendPaint.BlendMode = RenderingContext.GetDrawingBlendMode(BlendMode.Value);
-            tempSurface.DrawingSurface.Canvas.DrawSurface(workingSurface.DrawingSurface, 0, 0, blendPaint);
+            tempSurface.DrawingSurface.Canvas.DrawSurface(outputWorkingSurface.DrawingSurface, 0, 0, blendPaint);
 
             Output.Value = tempSurface;
             return tempSurface;
         }
 
-        Output.Value = workingSurface;
-        return workingSurface;
+        Output.Value = outputWorkingSurface;
+        
+        return outputWorkingSurface;
     }
 
-    private void DrawLayer(ChunkyImage frameImage, RenderingContext context, Surface workingSurface, bool shouldClear)
+    private void DrawLayer(ChunkyImage frameImage, RenderingContext context, Surface workingSurface, bool shouldClear, bool useFilters = true)
     {
-        blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255)); 
-        
-        blendPaint.SetFilters(Filters.Value);
-        
+        blendPaint.Color = blendPaint.Color.WithAlpha((byte)Math.Round(Opacity.Value * 255));
+
+        blendPaint.SetFilters(useFilters ? Filters.Value : null);
+
         if (!frameImage.DrawMostUpToDateChunkOn(
                 context.ChunkToUpdate,
                 context.ChunkResolution,
@@ -124,6 +151,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                 blendPaint) && shouldClear)
         {
             workingSurface.DrawingSurface.Canvas.DrawRect(CalculateDestinationRect(context), clearPaint);
+            workingSurface.DrawingSurface.Canvas.Flush();
         }
     }
 

+ 10 - 5
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using PixiEditor.DrawingApi.Core;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.Numerics;
 
@@ -23,6 +24,8 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     public OutputProperty<Surface?> Output { get; }
 
+    public OutputProperty<Surface?> FilterlessOutput { get; }
+
     public string MemberName { get; set; } = "New Element"; // would be good to add localization here, it is set if node is created via node graph
     
     public override string DisplayName
@@ -31,7 +34,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         set => MemberName = value;
     }
 
-    protected Dictionary<ChunkResolution, Surface> workingSurfaces = new Dictionary<ChunkResolution, Surface>();
+    protected Dictionary<(ChunkResolution, int), Surface> workingSurfaces = new Dictionary<(ChunkResolution, int), Surface>();
     private Paint maskPaint = new Paint() { BlendMode = DrawingApi.Core.Surfaces.BlendMode.DstIn };
     protected Paint blendPaint = new Paint();
 
@@ -47,20 +50,21 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
         Filters = CreateInput<Filter>(nameof(Filters), "FILTERS", null);
 
         Output = CreateOutput<Surface?>("Output", "OUTPUT", null);
+        FilterlessOutput = CreateOutput<Surface?>(nameof(FilterlessOutput), "WITHOUT_FILTERS", null);
     }
 
     protected abstract override Surface? OnExecute(RenderingContext context);
 
-    protected Surface TryInitWorkingSurface(VecI imageSize, RenderingContext context)
+    protected Surface TryInitWorkingSurface(VecI imageSize, RenderingContext context, int id)
     {
         ChunkResolution targetResolution = context.ChunkResolution;
-        bool hasSurface = workingSurfaces.TryGetValue(targetResolution, out Surface workingSurface);
+        bool hasSurface = workingSurfaces.TryGetValue((targetResolution, id), out Surface workingSurface);
         VecI targetSize = (VecI)(imageSize * targetResolution.Multiplier());
 
         if (!hasSurface || workingSurface.Size != targetSize || workingSurface.IsDisposed)
         {
-            workingSurfaces[targetResolution] = new Surface(targetSize);
-            workingSurface = workingSurfaces[targetResolution];
+            workingSurfaces[(targetResolution, id)] = new Surface(targetSize);
+            workingSurface = workingSurfaces[(targetResolution, id)];
         }
 
         return workingSurface;
@@ -105,6 +109,7 @@ public abstract class StructureNode : Node, IReadOnlyStructureNode, IBackgroundI
 
     protected void DrawBackground(Surface workingSurface, RenderingContext context)
     {
+        blendPaint.Color = Colors.White;
         DrawSurface(workingSurface, Background.Value, context, null); 
     }
 

+ 3 - 0
src/PixiEditor.DrawingApi.Core/Bridge/NativeObjectsImpl/IBitmapImplementation.cs

@@ -1,5 +1,6 @@
 using System;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 
 namespace PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
@@ -11,4 +12,6 @@ public interface IBitmapImplementation
     public object GetNativeBitmap(IntPtr objectPointer);
     public Bitmap FromImage(IntPtr snapshot);
     public VecI GetSize(IntPtr objectPointer);
+    public byte[] GetBytes(IntPtr objectPointer);
+    public ImageInfo GetInfo(IntPtr objectPointer);
 }

+ 1 - 0
src/PixiEditor.DrawingApi.Core/Bridge/Operations/IImageImplementation.cs

@@ -23,5 +23,6 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
         public object GetNativeImage(IntPtr objectPointer);
         public Image Clone(Image image);
         public Pixmap PeekPixels(IntPtr objectPointer);
+        public ImageInfo GetImageInfo(IntPtr objectPointer);
     }
 }

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/Bitmap.cs

@@ -16,6 +16,8 @@ public class Bitmap : NativeObject
     }
 
     public override object Native => DrawingBackendApi.Current.BitmapImplementation.GetNativeBitmap(ObjectPointer);
+    public byte[] Bytes => DrawingBackendApi.Current.BitmapImplementation.GetBytes(ObjectPointer);
+    public ImageInfo Info => DrawingBackendApi.Current.BitmapImplementation.GetInfo(ObjectPointer);
 
     public override void Dispose()
     {

+ 2 - 0
src/PixiEditor.DrawingApi.Core/Surfaces/ImageData/Image.cs

@@ -20,6 +20,8 @@ namespace PixiEditor.DrawingApi.Core.Surfaces.ImageData
         
         public int Height => DrawingBackendApi.Current.ImageImplementation.GetHeight(ObjectPointer);
         
+        public ImageInfo Info => DrawingBackendApi.Current.ImageImplementation.GetImageInfo(ObjectPointer);
+        
         public VecI Size => new VecI(Width, Height);
         
         public Image(IntPtr objPtr) : base(objPtr)

+ 13 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaBitmapImplementation.cs

@@ -1,6 +1,7 @@
 using System;
 using PixiEditor.DrawingApi.Core.Bridge.NativeObjectsImpl;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 using SkiaSharp;
 
@@ -43,6 +44,18 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return new VecI(bitmap.Width, bitmap.Height);
         }
 
+        public byte[] GetBytes(IntPtr objectPointer)
+        {
+            SKBitmap bitmap = ManagedInstances[objectPointer];
+            return bitmap.Bytes; 
+        }
+        
+        public ImageInfo GetInfo(IntPtr objectPointer)
+        {
+            SKBitmap bitmap = ManagedInstances[objectPointer];
+            return bitmap.Info.ToImageInfo();
+        }
+
         public object GetNativeBitmap(IntPtr objectPointer)
         {
             return ManagedInstances[objectPointer];

+ 6 - 0
src/PixiEditor.DrawingApi.Skia/Implementations/SkiaImageImplementation.cs

@@ -137,6 +137,12 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return _pixmapImplementation.CreateFrom(nativePixmap);
         }
 
+        public ImageInfo GetImageInfo(IntPtr objectPointer)
+        {
+            var info = ManagedInstances[objectPointer].Info;
+            return info.ToImageInfo();
+        }
+
         public object GetNativeImage(IntPtr objectPointer)
         {
             return ManagedInstances[objectPointer];

+ 41 - 1
src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.Impl.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.Extensions.CommonApi.Palettes;
 
-public partial class PaletteColor
+public partial class PaletteColor : IEquatable<PaletteColor>
 {
     public static PaletteColor Empty => new PaletteColor(0, 0, 0);
     public static PaletteColor Black => new PaletteColor(0, 0, 0);
@@ -79,4 +79,44 @@ public partial class PaletteColor
 
         return new PaletteColor(r, g, b);
     }
+
+    public bool Equals(PaletteColor other)
+    {
+        if (ReferenceEquals(null, other))
+        {
+            return false;
+        }
+
+        if (ReferenceEquals(this, other))
+        {
+            return true;
+        }
+
+        return RValue == other.RValue && GValue == other.GValue && BValue == other.BValue;
+    }
+
+    public override bool Equals(object obj)
+    {
+        if (ReferenceEquals(null, obj))
+        {
+            return false;
+        }
+
+        if (ReferenceEquals(this, obj))
+        {
+            return true;
+        }
+
+        if (obj.GetType() != this.GetType())
+        {
+            return false;
+        }
+
+        return Equals((PaletteColor)obj);
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(RValue, GValue, BValue);
+    }
 }

+ 9 - 26
src/PixiEditor.Numerics/ColorMatrix.cs

@@ -233,6 +233,15 @@ public record struct ColorMatrix
         (M41, M42, M43, M44, M45) = row4;
     }
 
+    public float[] ToArray()
+    {
+        var buffer = new float[Width * Height];
+
+        TryGetMembers(buffer);
+        
+        return buffer;
+    }
+
     public ColorMatrix(float[] values)
     {
         if (values.Length != 20)
@@ -413,30 +422,4 @@ public record struct ColorMatrix
 
     public static int Width { get => 5; }
     public static int Height { get => 4; }
-
-    public Span<float> AsSpan()
-    {
-        float[] values = new float[20];
-        values[0] = M11;
-        values[1] = M12;
-        values[2] = M13;
-        values[3] = M14;
-        values[4] = M15;
-        values[5] = M21;
-        values[6] = M22;
-        values[7] = M23;
-        values[8] = M24;
-        values[9] = M25;
-        values[10] = M31;
-        values[11] = M32;
-        values[12] = M33;
-        values[13] = M34;
-        values[14] = M35;
-        values[15] = M41;
-        values[16] = M42;
-        values[17] = M43;
-        values[18] = M44;
-        values[19] = M45;
-        return values;
-    }
 }