Browse Source

Exporter and encoded Image Format Encoder

Krzysztof Krysiński 2 years ago
parent
commit
4303a42e6a

+ 0 - 80
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/ClipboardHelper.cs

@@ -1,80 +0,0 @@
-using System.Threading.Tasks;
-using System.Windows;
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Input;
-using Avalonia.Input.Platform;
-using PixiEditor.DrawingApi.Core.Numerics;
-
-namespace PixiEditor.Helpers;
-
-internal static class ClipboardHelper
-{
-    public static async Task<bool> TrySetDataObject(DataObject obj)
-    {
-        try
-        {
-            if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-            {
-                 await desktop.MainWindow.Clipboard.SetDataObjectAsync(obj);
-                 return true;
-            }
-
-            return false;
-        }
-        catch
-        {
-            return false;
-        }
-    }
-
-    public static async Task<object> TryGetDataObject(string format)
-    {
-        try
-        {
-            if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop){
-                return await desktop.MainWindow.Clipboard.GetDataAsync(format);
-            }
-
-            return null;
-        }
-        catch
-        {
-            return null;
-        }
-    }
-
-    public static async Task<bool> TryClear()
-    {
-        try
-        {
-            if(Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-            {
-                await desktop.MainWindow.Clipboard.ClearAsync();
-                return true;
-            }
-
-            return true;
-        }
-        catch
-        {
-            return false;
-        }
-    }
-    
-    public static VecI GetVecI(this DataObject data, string format)
-    {
-        if (!data.Contains(format))
-            return VecI.NegativeOne;
-
-        byte[] bytes = (byte[])data.Get(format);
-
-        if (bytes is { Length: < 8 })
-            return VecI.NegativeOne;
-
-        return VecI.FromBytes(bytes);
-    }
-
-    public static void SetVecI(this DataObject data, string format, VecI value) => data.Set(format, value.ToByteArray());
-}

+ 7 - 3
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Extensions/BitmapExtensions.cs

@@ -7,6 +7,11 @@ namespace PixiEditor.Avalonia.Helpers.Extensions;
 public static class BitmapExtensions
 {
     public static byte[] ExtractPixels(this Bitmap source)
+    {
+        return ExtractPixels(source, out _);
+    }
+
+    public static byte[] ExtractPixels(this Bitmap source, out IntPtr address)
     {
         var size = source.PixelSize;
         var stride = size.Width * 4;
@@ -14,10 +19,9 @@ public static class BitmapExtensions
 
         byte[] target = new byte[bufferSize];
 
-        IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(target, 0);
-
-        source.CopyPixels(new PixelRect(0, 0, size.Width, size.Height), ptr, bufferSize, stride);
+        address = Marshal.UnsafeAddrOfPinnedArrayElement(target, 0);
 
+        source.CopyPixels(new PixelRect(0, 0, size.Width, size.Height), address, bufferSize, stride);
         return target;
     }
 }

+ 22 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Extensions/DataObjectExtensions.cs

@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using Avalonia.Input;
+
+namespace PixiEditor.Avalonia.Helpers.Extensions;
+
+public static class DataObjectExtensions
+{
+    /// <summary>
+    ///     Clears the data object and sets the specified data.
+    /// </summary>
+    /// <param name="data">The data object to set the data on.</param>
+    /// <param name="files">File paths to set.</param>
+    public static void SetFileDropList(this DataObject data, IEnumerable<string> files)
+    {
+        data.Set(DataFormats.Files, files);
+    }
+
+    public static string[] GetFileDropList(this DataObject data)
+    {
+        return (string[])data.Get(DataFormats.Files);
+    }
+}

+ 19 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Extensions/FileTypeExtensions.cs

@@ -0,0 +1,19 @@
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Models.Files;
+
+namespace PixiEditor.Avalonia.Helpers.Extensions;
+
+public static class FileTypeExtensions
+{
+    public static EncodedImageFormat ToEncodedImageFormat(this FileType fileType)
+    {
+        return fileType switch
+        {
+            FileType.Png => EncodedImageFormat.Png,
+            FileType.Jpeg => EncodedImageFormat.Jpeg,
+            FileType.Bmp => EncodedImageFormat.Bmp,
+            FileType.Gif => EncodedImageFormat.Gif,
+            _ => EncodedImageFormat.Unknown
+        };
+    }
+}

+ 74 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Extensions/PixelFormatHelper.cs

@@ -0,0 +1,74 @@
+using System.Windows.Media;
+using Avalonia.Platform;
+using PixiEditor.DrawingApi.Core.Surface.ImageData;
+
+namespace PixiEditor.Helpers.Extensions;
+
+internal static class PixelFormatHelper
+{
+    public static ColorType ToColorType(this PixelFormat format, out AlphaType alphaType)
+    {
+        if (TryConvertToColorType(format, out var color, out alphaType))
+        {
+            return color;
+        }
+
+        throw new NotImplementedException($"Skia does not support the '{format}' format");
+    }
+
+    public static bool TryConvertToColorType(this PixelFormat format, out ColorType colorType, out AlphaType alphaType)
+    {
+        if (format == PixelFormats.Rgba64)
+        {
+            alphaType = AlphaType.Unpremul;
+            colorType = ColorType.Rgba16161616;
+            return true;
+        }
+
+        /*if (format == PixelFormats.Bgra32)
+        {
+            alphaType = AlphaType.Unpremul;
+            colorType = ColorType.Bgra8888;
+            return true;
+        }*/
+
+        /*if (format == PixelFormats.Default)
+        {
+            alphaType = AlphaType.Unpremul;
+            colorType = ColorType.RgbaF16;
+            return true;
+        }*/
+
+        if (format == PixelFormats.Gray8)
+        {
+            alphaType = AlphaType.Opaque;
+            colorType = ColorType.Gray8;
+            return true;
+        }
+
+        /*if (format == PixelFormats.Pbgra32)
+        {
+            alphaType = AlphaType.Premul;
+            colorType = ColorType.Bgra8888;
+            return true;
+        }*/
+
+        if (format == PixelFormats.Bgr24 || format == PixelFormats.BlackWhite ||
+            format == PixelFormats.Gray16 ||
+            format == PixelFormats.Gray2 || format == PixelFormats.Gray32Float || format == PixelFormats.Gray4
+            || format == PixelFormats.Rgb24)
+        {
+            alphaType = AlphaType.Unknown;
+            colorType = ColorType.Unknown;
+            return false;
+        }
+
+        throw new NotImplementedException(
+            $"'{format}' has not been implemented by {nameof(PixelFormatHelper)}.{nameof(TryConvertToColorType)}()");
+    }
+
+    public static bool IsSkiaSupported(this PixelFormat format)
+    {
+        return TryConvertToColorType(format, out _, out _);
+    }
+}

+ 10 - 7
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/SurfaceHelpers.cs

@@ -1,6 +1,7 @@
 using System.Windows;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
+using Avalonia;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 using ChunkyImageLib;
@@ -8,25 +9,27 @@ using PixiEditor.Avalonia.Helpers;
 using PixiEditor.Avalonia.Helpers.Extensions;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Helpers.Extensions;
 
 namespace PixiEditor.Helpers;
 
 public static class SurfaceHelpers
 {
-    /*public static Surface FromBitmapSource(BitmapSource original)
+    public static Surface FromBitmap(Bitmap original)
     {
-        ColorType color = original.Format.ToColorType(out AlphaType alpha);
+        if(original.Format == null) throw new ArgumentException("Bitmap format must be non-null");
+
+        ColorType color = original.Format.Value.ToColorType(out AlphaType alpha);
         if (original.PixelSize.Width <= 0 || original.PixelSize.Height <= 0)
             throw new ArgumentException("Surface dimensions must be non-zero");
 
-        int stride = (original.PixelSize.Width * original.Format.BitsPerPixel + 7) / 8;
-        byte[] pixels = new byte[stride * original.PixelSize.Height];
-        original.CopyPixels(pixels, stride, 0);
+        int stride = (original.PixelSize.Width * original.Format.Value.BitsPerPixel + 7) / 8;
+        byte[] pixels = original.ExtractPixels();
 
         Surface surface = new Surface(new VecI(original.PixelSize.Width, original.PixelSize.Height));
         surface.DrawBytes(surface.Size, pixels, color, alpha);
         return surface;
-    }*/
+    }
 
     public static WriteableBitmap ToWriteableBitmap(this Surface surface)
     {
@@ -34,7 +37,7 @@ public static class SurfaceHelpers
         using var framebuffer = result.Lock();
         var dirty = new RectI(0, 0, surface.Size.X, surface.Size.Y);
         framebuffer.WritePixels(dirty, ToByteArray(surface));
-        //result.AddDirtyRect(dirty);
+        //result.AddDirtyRect(dirty); //TODO: Look at this later, no DirtyRect in Avalonia
         return result;
     }
 

+ 39 - 27
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Controllers/ClipboardController.cs

@@ -6,26 +6,39 @@ using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
+using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 using ChunkyImageLib;
 using PixiEditor.Avalonia.Helpers;
+using PixiEditor.Avalonia.Helpers.Extensions;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+using PixiEditor.Models.IO.FileEncoders;
 using PixiEditor.Parser;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.ViewModels.SubViewModels.Document;
+using Bitmap = Avalonia.Media.Imaging.Bitmap;
 
 namespace PixiEditor.Models.Controllers;
 
 #nullable enable
 internal static class ClipboardController
 {
+    public static IClipboard Clipboard { get; private set; }
     private const string PositionFormat = "PixiEditor.Position";
+
+    public static void Initialize(IClipboard clipboard)
+    {
+        Clipboard = clipboard;
+    }
     
     public static readonly string TempCopyFilePath = Path.Join(
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@@ -64,7 +77,7 @@ internal static class ClipboardController
             Directory.CreateDirectory(Path.GetDirectoryName(TempCopyFilePath)!);
             using FileStream fileStream = new FileStream(TempCopyFilePath, FileMode.Create, FileAccess.Write);
             await pngStream.CopyToAsync(fileStream);
-            data.SetFileDropList(new StringCollection() { TempCopyFilePath });
+            data.SetFileDropList(new [] { TempCopyFilePath });
         }
 
         WriteableBitmap finalBitmap = actuallySurface.ToWriteableBitmap();
@@ -146,7 +159,7 @@ internal static class ClipboardController
             return surfaces;
         }
 
-        if (!data.GetDataPresent(DataFormats.FileDrop))
+        if (!data.Contains(DataFormats.Files))
         {
             return surfaces;
         }
@@ -228,32 +241,38 @@ internal static class ClipboardController
             return false;
         }
 
-        return HasData(dataObject, "PNG", DataFormats.Dib, DataFormats.Bitmap);
+        return HasData(dataObject, "PNG", ClipboardDataFormats.Dib, ClipboardDataFormats.Bitmap);
     }
 
-    private static BitmapSource FromPNG(DataObject data)
+    private static Bitmap FromPNG(DataObject data)
     {
-        MemoryStream pngStream = (MemoryStream)data.GetData("PNG");
-        PngBitmapDecoder decoder = new PngBitmapDecoder(pngStream, BitmapCreateOptions.IgnoreImageCache, BitmapCacheOption.OnLoad);
-
-        return decoder.Frames[0];
+        MemoryStream pngStream = (MemoryStream)data.Get("PNG");
+        Bitmap bitmap = new Bitmap(pngStream);
+        return bitmap;
     }
 
-    private static bool HasData(DataObject dataObject, params string[] formats) => formats.Any(dataObject.GetDataPresent);
+    private static bool HasData(DataObject dataObject, params string[] formats) => formats.Any(dataObject.Contains);
     
     private static bool TryExtractSingleImage(DataObject data, [NotNullWhen(true)] out Surface? result)
     {
         try
         {
-            BitmapSource source;
-
-            if (data.GetDataPresent("PNG"))
+            Bitmap source;
+            if (data.Contains("PNG"))
             {
                 source = FromPNG(data);
             }
-            else if (HasData(data, DataFormats.Dib, DataFormats.Bitmap))
+            else if (HasData(data, ClipboardDataFormats.Dib, ClipboardDataFormats.Bitmap))
             {
-                source = Clipboard.GetImage();
+                var imgs = GetImage(data);
+                if (imgs == null || imgs.Count == 0)
+                {
+                    result = null;
+                    return false;
+                }
+
+                result = imgs[0].image;
+                return true;
             }
             else
             {
@@ -261,19 +280,17 @@ internal static class ClipboardController
                 return false;
             }
 
-            if (source.Format.IsSkiaSupported())
+            if (source.Format.Value.IsSkiaSupported())
             {
-                result = SurfaceHelpers.FromBitmapSource(source);
+                result = SurfaceHelpers.FromBitmap(source);
             }
             else
             {
-                FormatConvertedBitmap newFormat = new FormatConvertedBitmap();
-                newFormat.BeginInit();
-                newFormat.Source = source;
-                newFormat.DestinationFormat = PixelFormats.Bgra32;
-                newFormat.EndInit();
+                source.ExtractPixels(out IntPtr address);
+                Bitmap newFormat = new Bitmap(PixelFormats.Bgra8888, AlphaFormat.Premul, address, source.PixelSize,
+                    source.Dpi, source.PixelSize.Width * 4);
 
-                result = SurfaceHelpers.FromBitmapSource(newFormat);
+                result = SurfaceHelpers.FromBitmap(newFormat);
             }
 
             return true;
@@ -283,9 +300,4 @@ internal static class ClipboardController
         result = null;
         return false;
     }
-
-    public record struct DataImage(string? name, Surface image, VecI position)
-    {
-        public DataImage(Surface image, VecI position) : this(null, image, position) { }
-    }
 }

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -12,6 +12,7 @@ using PixiEditor.DrawingApi.Core.Surface.Vector;
 using PixiEditor.Extensions.Palettes;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Containers;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DocumentModels.UpdateableChangeExecutors;
 using PixiEditor.Models.DocumentPassthroughActions;
 using PixiEditor.Models.Enums;

+ 12 - 12
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/IO/Exporter.cs

@@ -8,11 +8,14 @@ using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform.Storage;
 using ChunkyImageLib;
+using PixiEditor.Avalonia.Helpers.Extensions;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Files;
+using PixiEditor.Models.IO.FileEncoders;
 using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Models.IO;
@@ -117,12 +120,16 @@ internal class Exporter
                 return SaveResult.ConcurrencyError;
             var bitmap = maybeBitmap.AsT1;
 
-            if (!encodersFactory.ContainsKey(typeFromPath))
+            EncodedImageFormat mappedFormat = typeFromPath.ToEncodedImageFormat();
+
+            if (mappedFormat == EncodedImageFormat.Unknown)
             {
                 return SaveResult.UnknownError;
             }
 
-            return TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize);
+            UniversalFileEncoder encoder = new(mappedFormat);
+
+            return TrySaveAs(encoder, pathWithExtension, bitmap, exportSize);
         }
         else
         {
@@ -132,14 +139,8 @@ internal class Exporter
         return SaveResult.Success;
     }
 
-    static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
-
     static Exporter()
     {
-        encodersFactory[FileType.Png] = () => new PngBitmapEncoder();
-        encodersFactory[FileType.Jpeg] = () => new JpegBitmapEncoder();
-        encodersFactory[FileType.Bmp] = () => new BmpBitmapEncoder();
-        encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
     }
 
     public static void SaveAsGZippedBytes(string path, Surface surface)
@@ -173,19 +174,18 @@ internal class Exporter
     /// <summary>
     /// Saves image to PNG file. Messes with the passed bitmap.
     /// </summary>
-    private static SaveResult TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
+    private static SaveResult TrySaveAs(IFileEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
     {
         try
         {
             if (exportSize is not null && exportSize != bitmap.Size)
                 bitmap = bitmap.ResizeNearestNeighbor((VecI)exportSize);
 
-            if (encoder is (JpegBitmapEncoder or BmpBitmapEncoder))
+            if (!encoder.SupportsTransparency)
                 bitmap.DrawingSurface.Canvas.DrawColor(Colors.White, DrawingApi.Core.Surface.BlendMode.Multiply);
 
             using var stream = new FileStream(savePath, FileMode.Create);
-            encoder.Frames.Add(BitmapFrame.Create(bitmap.ToWriteableBitmap()));
-            encoder.Save(stream);
+            encoder.Save(stream, bitmap);
         }
         catch (SecurityException)
         {

+ 11 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/IO/FileEncoders/IFileEncoder.cs

@@ -0,0 +1,11 @@
+using System.IO;
+using System.Threading.Tasks;
+using ChunkyImageLib;
+
+namespace PixiEditor.Models.IO.FileEncoders;
+
+public interface IFileEncoder
+{
+    public bool SupportsTransparency { get; }
+    public Task Save(Stream stream, Surface bitmap);
+}

+ 59 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/IO/FileEncoders/UniversalFileEncoder.cs

@@ -0,0 +1,59 @@
+using System.IO;
+using System.Threading.Tasks;
+using ChunkyImageLib;
+using PixiEditor.DrawingApi.Core.Surface;
+
+namespace PixiEditor.Models.IO.FileEncoders;
+
+public class UniversalFileEncoder : IFileEncoder
+{
+    public bool SupportsTransparency { get; }
+    public EncodedImageFormat Format { get; set; }
+
+    //TODO: Check if this is correct and exclude unsupported formats
+    public UniversalFileEncoder(EncodedImageFormat format)
+    {
+        Format = format;
+        SupportsTransparency = FormatSupportsTransparency(format);
+    }
+
+    public async Task Save(Stream stream, Surface bitmap)
+    {
+        await bitmap.DrawingSurface.Snapshot().Encode(Format).AsStream().CopyToAsync(stream);
+    }
+
+    private bool FormatSupportsTransparency(EncodedImageFormat format)
+    {
+        switch (format)
+        {
+            case EncodedImageFormat.Bmp:
+                return false;
+            case EncodedImageFormat.Gif:
+                return true;
+            case EncodedImageFormat.Ico:
+                return true;
+            case EncodedImageFormat.Jpeg:
+                return false;
+            case EncodedImageFormat.Png:
+                return true;
+            case EncodedImageFormat.Wbmp:
+                return false;
+            case EncodedImageFormat.Webp:
+                return true;
+            case EncodedImageFormat.Pkm:
+                return false;
+            case EncodedImageFormat.Ktx:
+                return false;
+            case EncodedImageFormat.Astc:
+                return false; // not sure if astc is supported at all
+            case EncodedImageFormat.Dng:
+                return false; // not sure
+            case EncodedImageFormat.Heif:
+                return true;
+            case EncodedImageFormat.Unknown:
+                return false;
+            default:
+                throw new ArgumentOutOfRangeException(nameof(format), format, null);
+        }
+    }
+}

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

@@ -12,6 +12,7 @@ namespace PixiEditor.DrawingApi.Core.Bridge.Operations
         public Image FromEncodedData(byte[] dataBytes);
         public void GetColorShifts(ref int platformColorAlphaShift, ref int platformColorRedShift, ref int platformColorGreenShift, ref int platformColorBlueShift);
         public ImgData Encode(Image image);
+        public ImgData Encode(Image image, EncodedImageFormat format, int quality);
         public int GetWidth(IntPtr objectPointer);
         public int GetHeight(IntPtr objectPointer);
     }

+ 30 - 0
src/PixiEditor.DrawingApi.Core/Surface/EncodedImageFormat.cs

@@ -0,0 +1,30 @@
+namespace PixiEditor.DrawingApi.Core.Surface;
+
+public enum EncodedImageFormat
+{
+    /// <summary>The BMP image format.</summary>
+    Bmp,
+    /// <summary>The GIF image format.</summary>
+    Gif,
+    /// <summary>The ICO image format.</summary>
+    Ico,
+    /// <summary>The JPEG image format.</summary>
+    Jpeg,
+    /// <summary>The PNG image format.</summary>
+    Png,
+    /// <summary>The WBMP image format.</summary>
+    Wbmp,
+    /// <summary>The WEBP image format.</summary>
+    Webp,
+    /// <summary>The PKM image format.</summary>
+    Pkm,
+    /// <summary>The KTX image format.</summary>
+    Ktx,
+    /// <summary>The ASTC image format.</summary>
+    Astc,
+    /// <summary>The Adobe DNG image format.</summary>
+    Dng,
+    /// <summary>The HEIF or High Efficiency Image File format.</summary>
+    Heif,
+    Unknown,
+}

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

@@ -40,5 +40,10 @@ namespace PixiEditor.DrawingApi.Core.Surface.ImageData
         {
             return DrawingBackendApi.Current.ImageImplementation.Encode(this);
         }
+
+        public ImgData Encode(EncodedImageFormat format, int quality = 100)
+        {
+            return DrawingBackendApi.Current.ImageImplementation.Encode(this, format, quality);
+        }
     }
 }

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

@@ -69,6 +69,14 @@ namespace PixiEditor.DrawingApi.Skia.Implementations
             return new ImgData(encoded.Handle);
         }
 
+        public ImgData Encode(Image image, EncodedImageFormat format, int quality)
+        {
+            var native = ManagedInstances[image.ObjectPointer];
+            var encoded = native.Encode((SKEncodedImageFormat)format, quality);
+            _imgImplementation.ManagedInstances[encoded.Handle] = encoded;
+            return new ImgData(encoded.Handle);
+        }
+
         public int GetWidth(IntPtr objectPointer)
         {
             return ManagedInstances[objectPointer].Width;