diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 1751d863e6..ce13834a0b 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -19,6 +19,18 @@
baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,System.Nullable{System.Int32})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Platform.IBitmapImpl.Save(System.String,System.Nullable{System.Int32})
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double,Avalonia.Size)
@@ -79,6 +91,18 @@
baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+ CP0002
+ M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,System.Nullable{System.Int32})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0002
+ M:Avalonia.Platform.IBitmapImpl.Save(System.String,System.Nullable{System.Int32})
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0002
M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double,Avalonia.Size)
@@ -121,6 +145,12 @@
baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll
current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll
+
+ CP0006
+ M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions)
+ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll
+ current/Avalonia/lib/net10.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)
@@ -139,6 +169,12 @@
baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll
current/Avalonia/lib/net10.0/Avalonia.Controls.dll
+
+ CP0006
+ M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
CP0006
M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)
diff --git a/samples/GpuInterop/VulkanDemo/VulkanImage.cs b/samples/GpuInterop/VulkanDemo/VulkanImage.cs
index 216cb3f815..15d2952cfe 100644
--- a/samples/GpuInterop/VulkanDemo/VulkanImage.cs
+++ b/samples/GpuInterop/VulkanDemo/VulkanImage.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Vulkan;
using Silk.NET.Core.Native;
@@ -355,7 +356,7 @@ public unsafe class VulkanImage : IDisposable
using var b = new Avalonia.Media.Imaging.Bitmap(PixelFormat.Bgra8888,
AlphaFormat.Premul, pSurface, new PixelSize(w, h),
new Vector(96, 96), (int)sstride);
- b.Save(path);
+ b.Save(path, PngBitmapEncoderOptions.Default);
NativeMethods.IOSurfaceUnlock(surf, 0, IntPtr.Zero);
return;
diff --git a/src/Android/Avalonia.Android/PlatformIconLoader.cs b/src/Android/Avalonia.Android/PlatformIconLoader.cs
index f557685dd2..5b5705753e 100644
--- a/src/Android/Avalonia.Android/PlatformIconLoader.cs
+++ b/src/Android/Avalonia.Android/PlatformIconLoader.cs
@@ -1,4 +1,5 @@
using System.IO;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Android
@@ -9,7 +10,7 @@ namespace Avalonia.Android
{
using (var stream = new MemoryStream())
{
- bitmap.Save(stream);
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
return LoadIcon(stream);
}
}
diff --git a/src/Android/Avalonia.Android/Stubs.cs b/src/Android/Avalonia.Android/Stubs.cs
index 0620919e4d..57de1b1b10 100644
--- a/src/Android/Avalonia.Android/Stubs.cs
+++ b/src/Android/Avalonia.Android/Stubs.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Android
@@ -22,7 +23,7 @@ namespace Avalonia.Android
{
using (var stream = new MemoryStream())
{
- bitmap.Save(stream);
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
return LoadIcon(stream);
}
}
diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
index 87ef0a234f..e04603914c 100644
--- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs
@@ -147,7 +147,7 @@ namespace Avalonia.Media.Imaging
IRef IBitmap.PlatformImpl => PlatformImpl;
///
- /// Saves the bitmap to a file.
+ /// Saves the bitmap to a file, in PNG format.
///
/// The filename.
///
@@ -155,23 +155,44 @@ namespace Avalonia.Media.Imaging
/// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting is applied.
///
+ [Obsolete($"Use the overload accepting {nameof(BitmapEncoderOptions)} instead.")]
public void Save(string fileName, int? quality = null)
+ => Save(fileName, PngBitmapEncoderOptions.Default);
+
+ ///
+ /// Saves the bitmap to a file with the specified options.
+ ///
+ /// The filename.
+ ///
+ /// The options specifying the format and settings to use.
+ /// Typical usages include and .
+ ///
+ public void Save(string fileName, BitmapEncoderOptions options)
{
- PlatformImpl.Item.Save(fileName, quality);
+ using var stream = File.Create(fileName);
+
+ Save(stream, options);
}
///
- /// Saves the bitmap to a stream.
+ /// Saves the bitmap to a stream, in PNG format.
///
/// The stream.
///
- /// The optional quality for compression.
- /// The quality value is interpreted from 0 - 100. If quality is null the default quality
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 - 100. If quality is null the default quality
/// setting is applied.
///
+ [Obsolete($"Use the overload accepting {nameof(BitmapEncoderOptions)} instead.")]
public void Save(Stream stream, int? quality = null)
{
- PlatformImpl.Item.Save(stream, quality);
+ PlatformImpl.Item.Save(stream, PngBitmapEncoderOptions.Default);
+ }
+
+ ///
+ public void Save(Stream stream, BitmapEncoderOptions options)
+ {
+ PlatformImpl.Item.Save(stream, options);
}
public virtual PixelFormat? Format => (PlatformImpl.Item as IReadableBitmapImpl)?.Format;
diff --git a/src/Avalonia.Base/Media/Imaging/BitmapEncoderOptions.cs b/src/Avalonia.Base/Media/Imaging/BitmapEncoderOptions.cs
new file mode 100644
index 0000000000..e75e2ab65c
--- /dev/null
+++ b/src/Avalonia.Base/Media/Imaging/BitmapEncoderOptions.cs
@@ -0,0 +1,14 @@
+using System.IO;
+
+namespace Avalonia.Media.Imaging;
+
+///
+/// Represents the options used while saving a bitmap using .
+/// Common implementations are , .
+///
+public abstract class BitmapEncoderOptions
+{
+ internal BitmapEncoderOptions()
+ {
+ }
+}
diff --git a/src/Avalonia.Base/Media/Imaging/IBitmap.cs b/src/Avalonia.Base/Media/Imaging/IBitmap.cs
index f2cbc7a11e..e2e1611383 100644
--- a/src/Avalonia.Base/Media/Imaging/IBitmap.cs
+++ b/src/Avalonia.Base/Media/Imaging/IBitmap.cs
@@ -32,25 +32,13 @@ namespace Avalonia.Media.Imaging
IRef PlatformImpl { get; }
///
- /// Saves the bitmap to a file.
- ///
- /// The filename.
- ///
- /// The optional quality for compression if supported by the specific backend.
- /// The quality value is interpreted from 0 - 100. If quality is null the default quality
- /// setting of the backend is applied.
- ///
- void Save(string fileName, int? quality = null);
-
- ///
- /// Saves the bitmap to a stream in png format.
+ /// Saves the bitmap to a stream with the specified options.
///
/// The stream.
- ///
- /// The optional quality for compression if supported by the specific backend.
- /// The quality value is interpreted from 0 - 100. If quality is null the default quality
- /// setting of the backend is applied.
+ ///
+ /// The options specifying the format and settings to use.
+ /// Typical usages include and .
///
- void Save(Stream stream, int? quality = null);
+ void Save(Stream stream, BitmapEncoderOptions options);
}
}
diff --git a/src/Avalonia.Base/Media/Imaging/JpegBitmapEncoderOptions.cs b/src/Avalonia.Base/Media/Imaging/JpegBitmapEncoderOptions.cs
new file mode 100644
index 0000000000..9745c60f92
--- /dev/null
+++ b/src/Avalonia.Base/Media/Imaging/JpegBitmapEncoderOptions.cs
@@ -0,0 +1,18 @@
+namespace Avalonia.Media.Imaging;
+
+///
+/// Represents the options used to save a bitmap in the JPEG format.
+///
+public sealed class JpegBitmapEncoderOptions : BitmapEncoderOptions
+{
+ ///
+ /// Gets the default JPEG encoder options.
+ ///
+ public static JpegBitmapEncoderOptions Default { get; } = new();
+
+ ///
+ /// Gets or sets the quality to use, from 0 (lowest) to 100 (highest).
+ /// Defaults to 100.
+ ///
+ public int Quality { get; init; } = 100;
+}
diff --git a/src/Avalonia.Base/Media/Imaging/PngBitmapEncoderOptions.cs b/src/Avalonia.Base/Media/Imaging/PngBitmapEncoderOptions.cs
new file mode 100644
index 0000000000..0931ec9b8c
--- /dev/null
+++ b/src/Avalonia.Base/Media/Imaging/PngBitmapEncoderOptions.cs
@@ -0,0 +1,20 @@
+using System.IO.Compression;
+
+namespace Avalonia.Media.Imaging;
+
+///
+/// Represents the options used to save a bitmap in the PNG format.
+///
+public sealed class PngBitmapEncoderOptions : BitmapEncoderOptions
+{
+ ///
+ /// Gets the default PNG encoder options.
+ ///
+ public static PngBitmapEncoderOptions Default { get; } = new();
+
+ ///
+ /// Gets or sets the compression level to use.
+ /// Defaults to .
+ ///
+ public CompressionLevel CompressionLevel { get; init; } = CompressionLevel.Optimal;
+}
diff --git a/src/Avalonia.Base/Platform/IBitmapImpl.cs b/src/Avalonia.Base/Platform/IBitmapImpl.cs
index 586ac76a3a..bec184e6aa 100644
--- a/src/Avalonia.Base/Platform/IBitmapImpl.cs
+++ b/src/Avalonia.Base/Platform/IBitmapImpl.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using Avalonia.Media.Imaging;
using Avalonia.Metadata;
namespace Avalonia.Platform
@@ -26,25 +27,13 @@ namespace Avalonia.Platform
int Version { get; }
///
- /// Saves the bitmap to a file.
- ///
- /// The filename.
- ///
- /// The optional quality for compression if supported by the specific backend.
- /// The quality value is interpreted from 0 - 100. If quality is null the default quality
- /// setting of the backend is applied.
- ///
- void Save(string fileName, int? quality = null);
-
- ///
- /// Saves the bitmap to a stream in png format.
+ /// Saves the bitmap to a stream with the specified options.
///
/// The stream.
- ///
- /// The optional quality for compression if supported by the specific backend.
- /// The quality value is interpreted from 0 - 100. If quality is null the default quality
- /// setting of the backend is applied.
+ ///
+ /// The options specifying the format and settings to use.
+ /// Typical usages include and .
///
- void Save(Stream stream, int? quality = null);
+ void Save(Stream stream, BitmapEncoderOptions options);
}
}
diff --git a/src/Avalonia.Native/Cursor.cs b/src/Avalonia.Native/Cursor.cs
index 58306b5698..37bb18feb6 100644
--- a/src/Avalonia.Native/Cursor.cs
+++ b/src/Avalonia.Native/Cursor.cs
@@ -45,7 +45,7 @@ namespace Avalonia.Native
{
using(var ms = new MemoryStream())
{
- cursor.Save(ms);
+ cursor.Save(ms, PngBitmapEncoderOptions.Default);
var imageData = ms.ToArray();
diff --git a/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs b/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs
index 4b659a3cc4..ccaae66379 100644
--- a/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs
+++ b/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using Avalonia.Input;
using Avalonia.Logging;
+using Avalonia.Media.Imaging;
using Avalonia.Native.Interop;
namespace Avalonia.Native;
@@ -35,7 +36,7 @@ internal sealed class DataTransferItemToAvnClipboardDataItemWrapper(IDataTransfe
if (_item.TryGetValue(DataFormat.Bitmap) is { } bitmap)
{
var memoryStream = new MemoryStream();
- bitmap.Save(memoryStream);
+ bitmap.Save(memoryStream, PngBitmapEncoderOptions.Default);
memoryStream.Seek(0, SeekOrigin.Begin);
return new StreamValue(memoryStream);
}
diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs
index 441b974df2..65b8563055 100644
--- a/src/Avalonia.Native/IAvnMenuItem.cs
+++ b/src/Avalonia.Native/IAvnMenuItem.cs
@@ -56,7 +56,7 @@ namespace Avalonia.Native.Interop.Impl
{
using(var ms = new MemoryStream())
{
- icon.Save(ms);
+ icon.Save(ms, PngBitmapEncoderOptions.Default);
var imageData = ms.ToArray();
diff --git a/src/Avalonia.Native/IconLoader.cs b/src/Avalonia.Native/IconLoader.cs
index dbc6379bd8..104ec2078c 100644
--- a/src/Avalonia.Native/IconLoader.cs
+++ b/src/Avalonia.Native/IconLoader.cs
@@ -1,4 +1,5 @@
using System.IO;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Native
@@ -20,7 +21,7 @@ namespace Avalonia.Native
public void Save(Stream outputStream)
{
- _bitmap.Save(outputStream);
+ _bitmap.Save(outputStream, PngBitmapEncoderOptions.Default);
}
}
@@ -39,7 +40,7 @@ namespace Avalonia.Native
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
{
var ms = new MemoryStream();
- bitmap.Save(ms);
+ bitmap.Save(ms, PngBitmapEncoderOptions.Default);
ms.Seek(0, SeekOrigin.Begin);
return LoadIcon(ms);
}
diff --git a/src/Avalonia.Wayland/Clipboard/WaylandClipboardImpl.cs b/src/Avalonia.Wayland/Clipboard/WaylandClipboardImpl.cs
index 5c82470ee6..70a7c6d315 100644
--- a/src/Avalonia.Wayland/Clipboard/WaylandClipboardImpl.cs
+++ b/src/Avalonia.Wayland/Clipboard/WaylandClipboardImpl.cs
@@ -109,7 +109,7 @@ class WaylandClipboardImpl : IOwnedClipboardImpl
await Task.Run(() =>
{
using (bitmapRef)
- bitmapRef.Item.Save(stream);
+ bitmapRef.Item.Save(stream, PngBitmapEncoderOptions.Default);
}).ConfigureAwait(false);
return;
}
diff --git a/src/Avalonia.X11/Selections/SelectionDataProvider.cs b/src/Avalonia.X11/Selections/SelectionDataProvider.cs
index c1a7be87fb..846a25bb6e 100644
--- a/src/Avalonia.X11/Selections/SelectionDataProvider.cs
+++ b/src/Avalonia.X11/Selections/SelectionDataProvider.cs
@@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Logging;
+using Avalonia.Media.Imaging;
using Avalonia.X11.Selections.Clipboard;
using static Avalonia.X11.XLib;
@@ -154,7 +155,7 @@ internal abstract class SelectionDataProvider : IDisposable
return null;
using var stream = new MemoryStream();
- bitmap.Save(stream);
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
return stream.ToArray();
}
diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs
index ab0946f531..caa5662e00 100644
--- a/src/Avalonia.X11/X11IconLoader.cs
+++ b/src/Avalonia.X11/X11IconLoader.cs
@@ -22,7 +22,7 @@ namespace Avalonia.X11
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
{
var ms = new MemoryStream();
- bitmap.Save(ms);
+ bitmap.Save(ms, PngBitmapEncoderOptions.Default);
ms.Position = 0;
return LoadIcon(ms);
}
@@ -81,7 +81,7 @@ namespace Avalonia.X11
fbp[fbr + x] = Data[r + x + 2].ToUInt32();
}
}
- wr.Save(outputStream);
+ wr.Save(outputStream, PngBitmapEncoderOptions.Default);
}
}
}
diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs
index 50cec8721c..1c39f62742 100644
--- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs
+++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs
@@ -6,6 +6,7 @@ using Avalonia.Browser.Interop;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Logging;
+using Avalonia.Media.Imaging;
using static Avalonia.Browser.BrowserDataFormatHelper;
using static Avalonia.Browser.Interop.InputHelper;
@@ -78,7 +79,7 @@ internal sealed class ClipboardImpl : IClipboardImpl
if (bitmap != null)
{
using var stream = new MemoryStream();
- bitmap.Save(stream);
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
writeableItem ??= CreateWriteableClipboardItem(source);
AddBytesToWriteableClipboardItem(writeableItem, formatString, stream.ToArray());
diff --git a/src/Browser/Avalonia.Browser/Cursor.cs b/src/Browser/Avalonia.Browser/Cursor.cs
index bf782f2126..39db876376 100644
--- a/src/Browser/Avalonia.Browser/Cursor.cs
+++ b/src/Browser/Avalonia.Browser/Cursor.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using Avalonia.Input;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Browser
@@ -79,7 +80,7 @@ namespace Avalonia.Browser
public ICursorImpl CreateCursor(Avalonia.Media.Imaging.Bitmap cursor, PixelPoint hotSpot)
{
using var imageStream = new MemoryStream();
- cursor.Save(imageStream);
+ cursor.Save(imageStream, PngBitmapEncoderOptions.Default);
//not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser.
var base64String = Convert.ToBase64String(imageStream.ToArray());
diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 1c152d0fe4..0d8114f7f5 100644
--- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -428,17 +428,10 @@ namespace Avalonia.Headless
public AlphaFormat? AlphaFormat => Platform.AlphaFormat.Premul;
public int Version { get; set; }
- public void Save(string fileName, int? quality = null)
+ public void Save(Stream stream, BitmapEncoderOptions options)
{
-
- }
-
- public void Save(Stream stream, int? quality = null)
- {
-
}
-
public ILockedFramebuffer Lock()
{
Version++;
diff --git a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
index 6adfc01951..89c40186d0 100644
--- a/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
+++ b/src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
@@ -1,5 +1,7 @@
using System;
using System.IO;
+using System.IO.Compression;
+using Avalonia.Media.Imaging;
using SkiaSharp;
namespace Avalonia.Skia.Helpers
@@ -10,57 +12,116 @@ namespace Avalonia.Skia.Helpers
public static class ImageSavingHelper
{
///
- /// Save Skia image to a file.
+ /// Saves a Skia image to a file in the PNG format.
///
- /// Image to save
- /// Target file.
+ /// The image to save
+ /// The output file to save the image to.
///
- /// The optional quality for PNG compression.
- /// The quality value is interpreted from 0 - 100. If quality is null
- /// the encoder applies the default quality value.
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 to 100. When null, 100 is used.
///
+ [Obsolete($"Use the overload accepting {nameof(BitmapEncoderOptions)} instead.")]
public static void SaveImage(SKImage image, string fileName, int? quality = null)
{
- if (image == null) throw new ArgumentNullException(nameof(image));
- if (fileName == null) throw new ArgumentNullException(nameof(fileName));
+ SaveImage(image, fileName, PngBitmapEncoderOptions.Default);
+ }
- using (var stream = File.Create(fileName))
- {
- SaveImage(image, stream, quality);
- }
+ ///
+ /// Saves a Skia image to a file in the specified format.
+ ///
+ /// The image to save
+ /// The output file to save the image to.
+ ///
+ /// The options specifying the format and settings to use.
+ /// Typical usages include and .
+ ///
+ public static void SaveImage(SKImage image, string fileName, BitmapEncoderOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(image);
+ ArgumentNullException.ThrowIfNull(fileName);
+
+ using var stream = File.Create(fileName);
+
+ SaveImage(image, stream, options);
}
///
- /// Save Skia image to a stream.
+ /// Saves a Skia image to a stream in the PNG format.
///
- /// Image to save
- /// The output stream to save the image.
+ /// The image to save
+ /// The output stream to save the image to.
///
- /// The optional quality for PNG compression.
- /// The quality value is interpreted from 0 - 100. If quality is null
- /// the encoder applies the default quality value.
+ /// The optional quality for compression.
+ /// The quality value is interpreted from 0 to 100. When null, 100 is used.
///
+ [Obsolete($"Use the overload accepting {nameof(BitmapEncoderOptions)} instead.")]
public static void SaveImage(SKImage image, Stream stream, int? quality = null)
{
- if (image == null) throw new ArgumentNullException(nameof(image));
- if (stream == null) throw new ArgumentNullException(nameof(stream));
+ SaveImage(image, stream, PngBitmapEncoderOptions.Default);
+ }
- if (quality == null)
+ ///
+ /// Saves a Skia image to a stream in the specified format.
+ ///
+ /// The image to save
+ /// The output stream to save the image to.
+ ///
+ /// The options specifying the format and settings to use.
+ /// Typical usages include and .
+ ///
+ public static void SaveImage(SKImage image, Stream stream, BitmapEncoderOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(image);
+ ArgumentNullException.ThrowIfNull(stream);
+ ArgumentNullException.ThrowIfNull(options);
+
+ var raster = image.ToRasterImage(true);
+
+ try
{
- using (var data = image.Encode())
+ using var pixmap = raster.PeekPixels() ?? throw new InvalidOperationException("Could not get image pixels");
+
+ using var data = options switch
{
- data.SaveTo(stream);
- }
+ PngBitmapEncoderOptions pngOptions => pixmap.Encode(ToSkia(pngOptions)),
+ JpegBitmapEncoderOptions jpegOptions => pixmap.Encode(ToSkia(jpegOptions)),
+ _ => throw new ArgumentOutOfRangeException(nameof(options), options, "Unknown encoder options type")
+ };
+
+ if (data is null)
+ throw new InvalidOperationException("Could not encode image");
+
+ data.SaveTo(stream);
}
- else
+ finally
{
- using (var data = image.Encode(SKEncodedImageFormat.Png, (int)quality))
- {
- data.SaveTo(stream);
- }
+ if (image != raster)
+ raster.Dispose();
}
}
+ private static SKPngEncoderOptions ToSkia(PngBitmapEncoderOptions options)
+ {
+ var zLibLevel = options.CompressionLevel switch
+ {
+ CompressionLevel.Optimal => 6,
+ CompressionLevel.Fastest => 1,
+ CompressionLevel.NoCompression => 0,
+ CompressionLevel.SmallestSize => 9,
+ _ => throw new ArgumentOutOfRangeException(nameof(options), "Unknown compression level")
+ };
+
+ return new SKPngEncoderOptions(SKPngEncoderFilterFlags.AllFilters, zLibLevel);
+ }
+
+ private static SKJpegEncoderOptions ToSkia(JpegBitmapEncoderOptions options)
+ {
+ if (options.Quality is < 0 or > 100)
+ throw new ArgumentOutOfRangeException(nameof(options), "Unknown quality");
+
+ return new SKJpegEncoderOptions(options.Quality);
+ }
+
// This method is here mostly for debugging purposes
internal static void SavePicture(SKPicture picture, float scale, string path)
{
@@ -68,7 +129,7 @@ namespace Avalonia.Skia.Helpers
(int)Math.Ceiling(picture.CullRect.Height * scale));
using var snap =
SKImage.FromPicture(picture, snapshotSize, SKMatrix.CreateScale(scale, scale));
- SaveImage(snap, path);
+ SaveImage(snap, path, PngBitmapEncoderOptions.Default);
}
}
}
diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
index c168394768..e2994f7b64 100644
--- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
+++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs
@@ -173,16 +173,9 @@ namespace Avalonia.Skia
_bitmap?.Dispose();
}
- ///
- public void Save(string fileName, int? quality = null)
- {
- ImageSavingHelper.SaveImage(_image, fileName, quality);
- }
-
- ///
- public void Save(Stream stream, int? quality = null)
+ public void Save(Stream stream, BitmapEncoderOptions options)
{
- ImageSavingHelper.SaveImage(_image, stream, quality);
+ ImageSavingHelper.SaveImage(_image, stream, options);
}
///
diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
index 3e54394541..889abb9d8d 100644
--- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
+++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using Avalonia.Media.Imaging;
using Avalonia.Reactive;
using Avalonia.Platform;
using Avalonia.Skia.Helpers;
@@ -126,23 +127,12 @@ namespace Avalonia.Skia
public PixelSize PixelSize { get; }
public int Version { get; private set; } = 1;
-
- ///
- public void Save(string fileName, int? quality = null)
+
+ public void Save(Stream stream, BitmapEncoderOptions options)
{
- using (var image = SnapshotImage())
- {
- ImageSavingHelper.SaveImage(image, fileName, quality);
- }
- }
+ using var image = SnapshotImage();
- ///
- public void Save(Stream stream, int? quality = null)
- {
- using (var image = SnapshotImage())
- {
- ImageSavingHelper.SaveImage(image, stream, quality);
- }
+ ImageSavingHelper.SaveImage(image, stream, options);
}
public void Blit(IDrawingContextImpl contextImpl)
diff --git a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
index f57f84b168..3fd7664dca 100644
--- a/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
+++ b/src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
@@ -149,24 +149,13 @@ namespace Avalonia.Skia
}
}
- ///
- public void Save(Stream stream, int? quality = null)
+ public void Save(Stream stream, BitmapEncoderOptions options)
{
- using (var image = GetSnapshot())
- {
- ImageSavingHelper.SaveImage(image, stream, quality);
- }
- }
+ using var image = GetSnapshot();
- ///
- public void Save(string fileName, int? quality = null)
- {
- using (var image = GetSnapshot())
- {
- ImageSavingHelper.SaveImage(image, fileName, quality);
- }
+ ImageSavingHelper.SaveImage(image, stream, options);
}
-
+
public PixelFormat? Format => _bitmap.ColorType.ToAvalonia();
public AlphaFormat? AlphaFormat => _bitmap.AlphaType.ToAlphaFormat();
diff --git a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
index 198c6fca07..c69fba5f10 100644
--- a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
+++ b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
@@ -445,7 +445,7 @@ internal static class OleDataObjectHelper
if (bitmap != null)
{
using var stream = new MemoryStream();
- bitmap.Save(stream);
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
return WriteBytesToHGlobal(ref hGlobal, stream.ToArray().AsSpan());
}
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 5eaee60448..c7cd651ca5 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -1,24 +1,22 @@
using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
-using Avalonia.Reactive;
using System.Runtime.InteropServices;
-using System.Threading;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.Win32.Input;
-using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods;
-using System.Collections.Generic;
namespace Avalonia
{
@@ -282,7 +280,7 @@ namespace Avalonia.Win32
{
using (var memoryStream = new MemoryStream())
{
- bitmap.Save(memoryStream);
+ bitmap.Save(memoryStream, PngBitmapEncoderOptions.Default);
memoryStream.Seek(0, SeekOrigin.Begin);
return new IconImpl(memoryStream);
}
diff --git a/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs
index 57ff3c9043..5f02c24604 100644
--- a/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs
+++ b/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Logging;
+using Avalonia.Media.Imaging;
using Foundation;
using UIKit;
using static Avalonia.iOS.Clipboard.ClipboardDataFormatHelper;
@@ -100,7 +101,7 @@ internal sealed class ClipboardImpl(UIPasteboard pasteboard)
if (bitmap is null)
return null;
using var memoryStream = new MemoryStream();
- bitmap.Save(memoryStream, 100);
+ bitmap.Save(memoryStream, PngBitmapEncoderOptions.Default);
memoryStream.Seek(0, SeekOrigin.Begin);
using var data = NSData.FromStream(memoryStream)!;
return UIImage.LoadFromData(data);
diff --git a/src/iOS/Avalonia.iOS/Stubs.cs b/src/iOS/Avalonia.iOS/Stubs.cs
index c78823cd81..913b8ba2fd 100644
--- a/src/iOS/Avalonia.iOS/Stubs.cs
+++ b/src/iOS/Avalonia.iOS/Stubs.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using Avalonia.Input;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.iOS
@@ -34,11 +35,10 @@ namespace Avalonia.iOS
{
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
{
- using (var stream = new MemoryStream())
- {
- bitmap.Save(stream);
- return LoadIcon(stream);
- }
+ using var stream = new MemoryStream();
+
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
+ return LoadIcon(stream);
}
public IWindowIconImpl LoadIcon(Stream stream)
diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs
index 1db7556b46..12f6becd8f 100644
--- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs
+++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs
@@ -94,7 +94,7 @@ namespace Avalonia.Skia.RenderTests
var rc = new Rect(0, 0, 60, 60);
ctx.DrawBitmap(bmp.PlatformImpl, 1, rc, rc);
}
- rtb.Save(Path.Combine(OutputPath, testName + ".out.png"));
+ rtb.Save(Path.Combine(OutputPath, testName + ".out.png"), PngBitmapEncoderOptions.Default);
}
CompareImagesNoRenderer(testName);
}
@@ -123,7 +123,7 @@ namespace Avalonia.Skia.RenderTests
var name = nameof(WriteableBitmapShouldBeUsable) + "_" + fmt;
- writeableBitmap.Save(Path.Combine(OutputPath, name + ".out.png"));
+ writeableBitmap.Save(Path.Combine(OutputPath, name + ".out.png"), PngBitmapEncoderOptions.Default);
CompareImagesNoRenderer(name);
}
@@ -215,7 +215,7 @@ namespace Avalonia.Skia.RenderTests
Assert.Equal(data.ToArray(), copyTo);
}
- b.Save(path);
+ b.Save(path, PngBitmapEncoderOptions.Default);
CompareImagesNoRenderer(testName, expectedName);
}
finally
diff --git a/tests/Avalonia.RenderTests/TestRenderHelper.cs b/tests/Avalonia.RenderTests/TestRenderHelper.cs
index 2bd12cfa81..7b3a94d6b7 100644
--- a/tests/Avalonia.RenderTests/TestRenderHelper.cs
+++ b/tests/Avalonia.RenderTests/TestRenderHelper.cs
@@ -69,7 +69,7 @@ static class TestRenderHelper
target.Measure(size);
target.Arrange(new Rect(size));
bitmap.Render(target);
- bitmap.Save(path);
+ bitmap.Save(path, PngBitmapEncoderOptions.Default);
}
}
else
@@ -91,7 +91,8 @@ static class TestRenderHelper
renderer.Paint(new Rect(root.Bounds.Size), false);
}
- writableBitmap.Save(path);
+ using var fileStream = File.Create(path);
+ writableBitmap.Save(fileStream, PngBitmapEncoderOptions.Default);
}
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/BitmapSaveTests.cs b/tests/Avalonia.Skia.UnitTests/Media/BitmapSaveTests.cs
new file mode 100644
index 0000000000..5107e778a9
--- /dev/null
+++ b/tests/Avalonia.Skia.UnitTests/Media/BitmapSaveTests.cs
@@ -0,0 +1,108 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.InteropServices;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+using SkiaSharp;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests.Media
+{
+ public class BitmapSaveTests
+ {
+ [Fact]
+ public void Save_With_Null_Options_Throws()
+ {
+ using var app = Start();
+
+ using var bitmap = CreateBitmap(SKColors.Red, 16, 16);
+ using var stream = new MemoryStream();
+
+ Assert.Throws(() => bitmap.Save(stream, (BitmapEncoderOptions)null!));
+ }
+
+ [Fact]
+ public void Save_With_Invalid_Png_CompressionLevel_Throws()
+ {
+ using var app = Start();
+
+ using var bitmap = CreateBitmap(SKColors.Red, 16, 16);
+ using var stream = new MemoryStream();
+ var options = new PngBitmapEncoderOptions { CompressionLevel = (CompressionLevel)42 };
+
+ Assert.Throws(() => bitmap.Save(stream, options));
+ }
+
+ [Fact]
+ public void Save_With_Invalid_Jpeg_Quality_Throws()
+ {
+ using var app = Start();
+
+ using var bitmap = CreateBitmap(SKColors.Red, 16, 16);
+ using var stream = new MemoryStream();
+ var options = new JpegBitmapEncoderOptions { Quality = -1 };
+
+ Assert.Throws(() => bitmap.Save(stream, options));
+ }
+
+ [Fact]
+ public void Save_With_Png_Options_Produces_Png()
+ {
+ using var app = Start();
+
+ using var bitmap = CreateBitmap(SKColors.Red, 16, 16);
+ using var stream = new MemoryStream();
+
+ bitmap.Save(stream, PngBitmapEncoderOptions.Default);
+
+ stream.Position = 0;
+ using var codec = SKCodec.Create(stream);
+
+ Assert.Equal(SKEncodedImageFormat.Png, codec.EncodedFormat);
+ }
+
+ [Fact]
+ public void Save_With_Jpeg_Options_Produces_Jpeg()
+ {
+ using var app = Start();
+
+ using var bitmap = CreateBitmap(SKColors.Red, 16, 16);
+ using var stream = new MemoryStream();
+
+ bitmap.Save(stream, JpegBitmapEncoderOptions.Default);
+
+ stream.Position = 0;
+ using var codec = SKCodec.Create(stream);
+
+ Assert.Equal(SKEncodedImageFormat.Jpeg, codec.EncodedFormat);
+ }
+
+ private static WriteableBitmap CreateBitmap(SKColor color, int width, int height)
+ {
+ var pixel = (color.Alpha << 24) | (color.Red << 16) | (color.Green << 8) | color.Blue;
+
+ var data = new int[width * height];
+ data.AsSpan().Fill(pixel);
+
+ return CreateBitmap(width, height, data);
+ }
+
+ private static WriteableBitmap CreateBitmap(int width, int height, int[] data)
+ {
+ var bitmap = new WriteableBitmap(new PixelSize(width, height), new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Premul);
+
+ using var fb = bitmap.Lock();
+
+ for (var y = 0; y < height; y++)
+ Marshal.Copy(data, y * width, fb.Address + y * fb.RowBytes, width);
+
+ return bitmap;
+ }
+
+ private static IDisposable Start()
+ => UnitTestApplication.Start(
+ TestServices.MockPlatformRenderInterface.With(renderInterface: new PlatformRenderInterface()));
+ }
+}