Browse Source

failed to implment dirty rects

Krzysztof Krysiński 1 year ago
parent
commit
8250278d01

+ 13 - 0
src/ChunkyImageLib/Surface.cs

@@ -17,6 +17,8 @@ public class Surface : IDisposable
     public int BytesPerPixel { get; }
     public VecI Size { get; }
 
+    public RectI DirtyRect { get; private set; }
+
     private Paint drawingPaint = new Paint() { BlendMode = BlendMode.Src };
     private Paint nearestNeighborReplacingPaint = new() { BlendMode = BlendMode.Src, FilterQuality = FilterQuality.None };
 
@@ -197,6 +199,17 @@ public class Surface : IDisposable
     {
         Marshal.FreeHGlobal(PixelBuffer);
     }
+
+    public void AddDirtyRect(RectI dirtyRect)
+    {
+        // TODO: yeah well this is probably wrong lol, since the name even says "Add" and not "Set"
+        DirtyRect = dirtyRect;
+    }
+
+    public void ClearDirtyRects()
+    {
+        DirtyRect = default;
+    }
 }
 
 public enum ResizeMethod

+ 28 - 11
src/PixiEditor.AvaloniaUI/Models/DocumentModels/ActionAccumulator.cs

@@ -10,6 +10,7 @@ using PixiEditor.AvaloniaUI.Models.Rendering.RenderInfos;
 using PixiEditor.ChangeableDocument.Actions;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos;
+using PixiEditor.DrawingApi.Core.Numerics;
 
 namespace PixiEditor.AvaloniaUI.Models.DocumentModels;
 #nullable enable
@@ -68,9 +69,13 @@ internal class ActionAccumulator
             // pass them to changeabledocument for processing
             List<IChangeInfo?> changes;
             if (AreAllPassthrough(toExecute))
+            {
                 changes = toExecute.Select(a => (IChangeInfo?)a).ToList();
+            }
             else
+            {
                 changes = await internals.Tracker.ProcessActions(toExecute);
+            }
 
             // update viewmodels based on changes
             List<IChangeInfo> optimizedChanges = ChangeInfoListOptimizer.Optimize(changes);
@@ -89,6 +94,11 @@ internal class ActionAccumulator
             renderResult.AddRange(await canvasUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed || viewportRefreshRequest));
             renderResult.AddRange(await previewUpdater.UpdateGatheredChunks(affectedAreas, undoBoundaryPassed));
 
+            if (undoBoundaryPassed)
+            {
+                ClearDirtyRects();
+            }
+
             // add dirty rectangles
             AddDirtyRects(renderResult);
 
@@ -106,6 +116,14 @@ internal class ActionAccumulator
         executing = false;
     }
 
+    private void ClearDirtyRects()
+    {
+        foreach (var surface in document.Surfaces)
+        {
+            surface.Value.ClearDirtyRects();
+        }
+    }
+
     private bool AreAllPassthrough(List<IAction> actions)
     {
         foreach (var action in actions)
@@ -118,42 +136,41 @@ internal class ActionAccumulator
 
     private void AddDirtyRects(List<IRenderInfo> changes)
     {
-        //TODO: Avalonia doesn't seem to have a way to add dirty rects to bitmaps
-        /*foreach (IRenderInfo renderInfo in changes)
+        foreach (IRenderInfo renderInfo in changes)
         {
             switch (renderInfo)
             {
                 case DirtyRect_RenderInfo info:
                     {
-                        var bitmap = document.LazyBitmaps[info.Resolution];
-                        RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelSize.Width, bitmap.PixelSize.Height));
+                        var bitmap = document.Surfaces[info.Resolution];
+                        RectI finalRect = new RectI(VecI.Zero, new(bitmap.Size.X, bitmap.Size.Y));
 
                         RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);
-                        bitmap.AddDirtyRect(new(dirtyRect.Left, dirtyRect.Top, dirtyRect.Width, dirtyRect.Height));
+                        bitmap.AddDirtyRect(dirtyRect);
                     }
                     break;
                 case PreviewDirty_RenderInfo info:
                     {
-                        var bitmap = document.StructureHelper.Find(info.GuidValue)?.PreviewBitmap;
+                        var bitmap = document.StructureHelper.Find(info.GuidValue)?.PreviewSurface;
                         if (bitmap is null)
                             continue;
-                        bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelSize.Width, bitmap.PixelSize.Height));
+                        bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
                     }
                     break;
                 case MaskPreviewDirty_RenderInfo info:
                     {
-                        var bitmap = document.StructureHelper.Find(info.GuidValue)?.MaskPreviewBitmap;
+                        var bitmap = document.StructureHelper.Find(info.GuidValue)?.MaskPreviewSurface;
                         if (bitmap is null)
                             continue;
-                        bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelSize.Width, bitmap.PixelSize.Height));
+                        bitmap.AddDirtyRect(new RectI(0, 0, bitmap.Size.X, bitmap.Size.Y));
                     }
                     break;
                 case CanvasPreviewDirty_RenderInfo:
                     {
-                        document.PreviewBitmap.AddDirtyRect(new Int32Rect(0, 0, document.PreviewBitmap.PixelSize.Width, document.PreviewBitmap.PixelSize.Height));
+                        document.PreviewSurface.AddDirtyRect(new RectI(0, 0, document.PreviewSurface.Size.X, document.PreviewSurface.Size.Y));
                     }
                     break;
             }
-        }*/
+        }
     }
 }

+ 3 - 4
src/PixiEditor.AvaloniaUI/Models/Handlers/IStructureMemberHandler.cs

@@ -1,4 +1,5 @@
 using Avalonia.Media.Imaging;
+using ChunkyImageLib;
 using PixiEditor.AvaloniaUI.Models.Layers;
 using PixiEditor.DrawingApi.Core.Surface;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
@@ -10,10 +11,8 @@ internal interface IStructureMemberHandler : IHandler
     public bool HasMaskBindable { get; }
     public Guid GuidValue { get; }
     public string NameBindable { get; set; }
-    public DrawingSurface? MaskPreviewSurface { get; set; }
-    public DrawingSurface? PreviewSurface { get; set; }
-    public WriteableBitmap? PreviewBitmap { get; set; }
-    public WriteableBitmap? MaskPreviewBitmap { get; set; }
+    public Surface? MaskPreviewSurface { get; set; }
+    public Surface? PreviewSurface { get; set; }
     public bool MaskIsVisibleBindable { get; set; }
     public StructureMemberSelectionType Selection { get; set; }
     public float OpacityBindable { get; set; }

+ 24 - 28
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -212,19 +212,17 @@ internal class MemberPreviewUpdater
             {
                 member.PreviewSurface?.Dispose();
                 member.PreviewSurface = null;
-                member.PreviewBitmap = null;
             }
             else
             {
-                if (member.PreviewBitmap is not null && member.PreviewBitmap.PixelSize.Width == newSize.Value.previewSize.X && member.PreviewBitmap.PixelSize.Height == newSize.Value.previewSize.Y)
+                if (member.PreviewSurface is not null && member.PreviewSurface.Size.X == newSize.Value.previewSize.X && member.PreviewSurface.Size.Y == newSize.Value.previewSize.Y)
                 {
-                    member.PreviewSurface!.Canvas.Clear();
+                    member.PreviewSurface!.DrawingSurface.Canvas.Clear();
                 }
                 else
                 {
                     member.PreviewSurface?.Dispose();
-                    member.PreviewSurface = DrawingSurface.Create(new ImageInfo(newSize.Value.previewSize.X,
-                        newSize.Value.previewSize.Y, ColorType.Bgra8888, AlphaType.Premul));
+                    member.PreviewSurface = new Surface(newSize.Value.previewSize); // TODO: premul bgra8888 was here
                 }
             }
 
@@ -241,12 +239,10 @@ internal class MemberPreviewUpdater
             if (newSize is null)
             {
                 member.MaskPreviewSurface = null;
-                member.MaskPreviewBitmap = null;
             }
             else
             {
-                member.MaskPreviewSurface = DrawingSurface.Create(new ImageInfo(newSize.Value.previewSize.X,
-                    newSize.Value.previewSize.Y, ColorType.Bgra8888, AlphaType.Premul));
+                member.MaskPreviewSurface = new Surface(newSize.Value.previewSize); // TODO: premul bgra8888 was here
             }
 
             //TODO: Make sure MaskPreviewBitmap implementation raises PropertyChanged
@@ -496,10 +492,10 @@ internal class MemberPreviewUpdater
     /// </summary>
     private void RenderFolderMainPreview(IReadOnlyFolder folder, IStructureMemberHandler memberVM, AffectedArea area, VecI position, float scaling)
     {
-        memberVM.PreviewSurface.Canvas.Save();
-        memberVM.PreviewSurface.Canvas.Scale(scaling);
-        memberVM.PreviewSurface.Canvas.Translate(-position);
-        memberVM.PreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea);
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Save();
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Translate(-position);
+        memberVM.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
         foreach (var chunk in area.Chunks)
         {
             var pos = chunk * ChunkResolution.Full.PixelSize();
@@ -508,15 +504,15 @@ internal class MemberPreviewUpdater
             OneOf<Chunk, EmptyChunk> rendered = ChunkRenderer.MergeWholeStructure(chunk, ChunkResolution.Full, folder);
             if (rendered.IsT0)
             {
-                memberVM.PreviewSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
+                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
                 rendered.AsT0.Dispose();
             }
             else
             {
-                memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(), ChunkResolution.Full.PixelSize(), ClearPaint);
+                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkResolution.Full.PixelSize(), ChunkResolution.Full.PixelSize(), ClearPaint);
             }
         }
-        memberVM.PreviewSurface.Canvas.Restore();
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
     }
 
     /// <summary>
@@ -524,19 +520,19 @@ internal class MemberPreviewUpdater
     /// </summary>
     private void RenderLayerMainPreview(IReadOnlyLayer layer, IStructureMemberHandler memberVM, AffectedArea area, VecI position, float scaling)
     {
-        memberVM.PreviewSurface.Canvas.Save();
-        memberVM.PreviewSurface.Canvas.Scale(scaling);
-        memberVM.PreviewSurface.Canvas.Translate(-position);
-        memberVM.PreviewSurface.Canvas.ClipRect((RectD)area.GlobalArea);
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Save();
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Translate(-position);
+        memberVM.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
 
         foreach (var chunk in area.Chunks)
         {
             var pos = chunk * ChunkResolution.Full.PixelSize();
-            if (!layer.Rasterize().DrawCommittedChunkOn(chunk, ChunkResolution.Full, memberVM.PreviewSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
-                memberVM.PreviewSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
+            if (!layer.Rasterize().DrawCommittedChunkOn(chunk, ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
+                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize, ClearPaint);
         }
 
-        memberVM.PreviewSurface.Canvas.Restore();
+        memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
     }
 
     private void RenderMaskPreviews(
@@ -579,18 +575,18 @@ internal class MemberPreviewUpdater
 
             var member = internals.Tracker.Document.FindMemberOrThrow(guid);
 
-            memberVM.MaskPreviewSurface!.Canvas.Save();
-            memberVM.MaskPreviewSurface.Canvas.Scale(scaling);
-            memberVM.MaskPreviewSurface.Canvas.Translate(-position);
-            memberVM.MaskPreviewSurface.Canvas.ClipRect((RectD)affArea.Value.GlobalArea);
+            memberVM.MaskPreviewSurface!.DrawingSurface.Canvas.Save();
+            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Scale(scaling);
+            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Translate(-position);
+            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)affArea.Value.GlobalArea);
             foreach (var chunk in affArea.Value.Chunks)
             {
                 var pos = chunk * ChunkResolution.Full.PixelSize();
                 member.Mask!.DrawMostUpToDateChunkOn
-                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
+                    (chunk, ChunkResolution.Full, memberVM.MaskPreviewSurface.DrawingSurface, pos, scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint);
             }
 
-            memberVM.MaskPreviewSurface.Canvas.Restore();
+            memberVM.MaskPreviewSurface.DrawingSurface.Canvas.Restore();
             infos.Add(new MaskPreviewDirty_RenderInfo(guid));
         }
     }

+ 3 - 18
src/PixiEditor.AvaloniaUI/ViewModels/Document/StructureMemberViewModel.cs

@@ -1,4 +1,5 @@
 using Avalonia.Media.Imaging;
+using ChunkyImageLib;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
@@ -143,24 +144,9 @@ internal abstract class StructureMemberViewModel : ObservableObject, IStructureM
         set => SetProperty(ref selection, value);
     }
 
-    private WriteableBitmap? previewBitmap;
+    public Surface? PreviewSurface { get; set; }
 
-    public WriteableBitmap? PreviewBitmap
-    {
-        get => previewBitmap;
-        set => SetProperty(ref previewBitmap, value);
-    }
-    public DrawingSurface? PreviewSurface { get; set; }
-
-    private WriteableBitmap? maskPreviewBitmap;
-
-    public WriteableBitmap? MaskPreviewBitmap
-    {
-        get => maskPreviewBitmap;
-        set => SetProperty(ref maskPreviewBitmap, value);
-    }
-
-    public DrawingSurface? MaskPreviewSurface { get; set; }
+    public Surface? MaskPreviewSurface { get; set; }
 
     IDocument IStructureMemberHandler.Document => Document;
 
@@ -182,7 +168,6 @@ internal abstract class StructureMemberViewModel : ObservableObject, IStructureM
         Internals = internals;
 
         this.guidValue = guidValue;
-        PreviewBitmap = null;
         PreviewSurface = null;
     }
 }

+ 5 - 2
src/PixiEditor.AvaloniaUI/Views/Layers/LayerControl.axaml

@@ -11,6 +11,7 @@
              xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
              xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
              xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
+             xmlns:visuals="clr-namespace:PixiEditor.AvaloniaUI.Views.Visuals"
              mc:Ignorable="d" 
              Focusable="True"
              d:DesignHeight="35" d:DesignWidth="250" Name="uc"
@@ -72,7 +73,9 @@
                                 <Binding ElementName="uc" Path="Layer.HasMaskBindable"/>
                             </MultiBinding>
                         </Border.BorderBrush>
-                        <Image Source="{Binding Layer.PreviewBitmap, ElementName=uc}" Stretch="Uniform" Width="30" Height="30"
+                        <visuals:SurfaceControl Surface="{Binding Layer.PreviewSurface, ElementName=uc}"
+                                                Stretch="Uniform" Width="30"
+                                                Height="30"
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                     </Border>
                     <Border
@@ -95,7 +98,7 @@
                             </MultiBinding>
                         </Border.BorderBrush>
                         <Grid IsHitTestVisible="False">
-                            <Image Source="{Binding Layer.MaskPreviewBitmap,ElementName=uc}" Stretch="Uniform" Width="30" Height="30"
+                            <visuals:SurfaceControl Surface="{Binding Layer.MaskPreviewSurface,ElementName=uc}" Stretch="Uniform" Width="30" Height="30"
                            RenderOptions.BitmapInterpolationMode="None" IsHitTestVisible="False"/>
                             <Path
                                 Data="M 2 0 L 10 8 L 18 0 L 20 2 L 12 10 L 20 18 L 18 20 L 10 12 L 2 20 L 0 18 L 8 10 L 0 2 Z"

+ 3 - 2
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/FixedViewport.axaml

@@ -13,12 +13,13 @@
              VerticalAlignment="Center"
              d:DesignHeight="450" d:DesignWidth="800">
 
-    <!--TODO there was Stretch="Uniform" below-->
     <visuals:SurfaceControl
         x:Name="mainImage"
         Focusable="True"
         Surface="{Binding TargetBitmap, ElementName=uc}"
-        HorizontalAlignment="Stretch"
+        Width="{Binding Document.Width, ElementName=uc}"
+        Height="{Binding Document.Height, ElementName=uc}"
+        Stretch="Uniform"
         SizeChanged="OnImageSizeChanged">
         <ui:RenderOptionsBindable.BitmapInterpolationMode>
             <MultiBinding Converter="{converters1:WidthToBitmapScalingModeConverter}">

+ 33 - 1
src/PixiEditor.AvaloniaUI/Views/Visuals/SurfaceControl.cs

@@ -6,6 +6,7 @@ using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Skia;
 using ChunkyImageLib;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using Image = PixiEditor.DrawingApi.Core.Surface.ImageData.Image;
@@ -37,10 +38,12 @@ public class SurfaceControl : Control
 
     static SurfaceControl()
     {
-        AffectsRender<SurfaceControl>(SurfaceProperty, StretchProperty);
+        AffectsRender<SurfaceControl>(StretchProperty);
         SurfaceProperty.Changed.AddClassHandler<SurfaceControl>(OnSurfaceChanged);
         BoundsProperty.Changed.AddClassHandler<SurfaceControl>(BoundsChanged);
         StretchProperty.Changed.AddClassHandler<SurfaceControl>(StretchChanged);
+        WidthProperty.Changed.AddClassHandler<SurfaceControl>(BoundsChanged);
+        HeightProperty.Changed.AddClassHandler<SurfaceControl>(BoundsChanged);
     }
 
     public override void Render(DrawingContext context)
@@ -86,6 +89,11 @@ public class DrawingSurfaceOp : ICustomDrawOperation
 
     private SKPaint _paint = new SKPaint();
 
+    //TODO: Implement dirty rect handling
+    /*private RectI? _lastDirtyRect;
+    private SKImage? _lastImage;
+    private SKImage? _lastFullImage;*/
+
     public DrawingSurfaceOp(Surface surface, Rect bounds, Stretch stretch)
     {
         Surface = surface;
@@ -100,8 +108,32 @@ public class DrawingSurfaceOp : ICustomDrawOperation
             using var lease = skiaSurface.Lease();
             var canvas = lease.SkCanvas;
             canvas.Save();
+
             ScaleCanvas(canvas);
             canvas.DrawSurface((SKSurface)Surface.DrawingSurface.Native, 0, 0, _paint);
+            /*if(_lastDirtyRect != Surface.DirtyRect)
+            {
+                RectI dirtyRect = Surface.DirtyRect;
+                if (dirtyRect.IsZeroOrNegativeArea)
+                {
+                    dirtyRect = new RectI(0, 0, Surface.Size.X, Surface.Size.Y);
+                    _lastFullImage = (SKImage)Surface.DrawingSurface.Snapshot().Native;
+                }
+
+                _lastImage = (SKImage)Surface.DrawingSurface.Snapshot(dirtyRect).Native;
+                _lastDirtyRect = Surface.DirtyRect;
+            }
+
+            if (_lastFullImage != null)
+            {
+                canvas.DrawImage(_lastFullImage, new SKPoint(0, 0), _paint);
+            }
+
+            if (_lastImage != null)
+            {
+                canvas.DrawImage(_lastImage, new SKPoint(_lastDirtyRect.Value.X, _lastDirtyRect.Value.Y), _paint);
+            }*/
+
             canvas.Restore();
         }
     }

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

@@ -1,4 +1,5 @@
 using System;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 
@@ -7,6 +8,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
     public interface IImageImplementation
     {
         public Image Snapshot(DrawingSurface drawingSurface);
+        public Image Snapshot(DrawingSurface drawingSurface, RectI bounds);
         public void DisposeImage(Image image);
         public Image? FromEncodedData(string path);
         public Image? FromEncodedData(byte[] dataBytes);

+ 5 - 0
src/PixiEditor.DrawingApi.Core/Surface/DrawingSurface.cs

@@ -31,6 +31,11 @@ namespace PixiEditor.DrawingApi.Core.Surface
             return DrawingBackendApi.Current.ImageImplementation.Snapshot(this);
         }
 
+        public Image Snapshot(RectI bounds)
+        {
+            return DrawingBackendApi.Current.ImageImplementation.Snapshot(this, bounds);
+        }
+
         public Pixmap PeekPixels()
         {
             return DrawingBackendApi.Current.SurfaceImplementation.PeekPixels(this);

+ 5 - 0
src/PixiEditor.DrawingApi.Skia/ConversionExtensions.cs

@@ -45,6 +45,11 @@ namespace PixiEditor.DrawingApi.Skia
         {
             return new SKRect(rect.Left, rect.Top, rect.Right, rect.Bottom);
         }
+
+        public static SKRectI ToSkRectI(this RectI rect)
+        {
+            return new SKRectI(rect.Left, rect.Top, rect.Right, rect.Bottom);
+        }
         
         public static SKImageInfo ToSkImageInfo(this ImageInfo info)
         {

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

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using PixiEditor.DrawingApi.Core.Bridge.Operations;
+using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using SkiaSharp;
@@ -30,6 +31,15 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             ManagedInstances[snapshot.Handle] = snapshot;
             return new Image(snapshot.Handle);
         }
+
+        public Image Snapshot(DrawingSurface drawingSurface, RectI bounds)
+        {
+            var surface = _surfaceImplementation![drawingSurface.ObjectPointer];
+            SKImage snapshot = surface.Snapshot(bounds.ToSkRectI());
+
+            ManagedInstances[snapshot.Handle] = snapshot;
+            return new Image(snapshot.Handle);
+        }
         
         public Image? FromEncodedData(byte[] dataBytes)
         {