Browse Source

Implement Bitmap.Save with a format (#21455)

* Implement Bitmap.Save with format options

* Rename BitmapEncoderOptions.cs to match its type

* Update API suppressions
pull/20993/head
Julien Lebosquain 3 months ago
committed by GitHub
parent
commit
6ef2d0b708
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 36
      api/Avalonia.nupkg.xml
  2. 3
      samples/GpuInterop/VulkanDemo/VulkanImage.cs
  3. 3
      src/Android/Avalonia.Android/PlatformIconLoader.cs
  4. 3
      src/Android/Avalonia.Android/Stubs.cs
  5. 33
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  6. 14
      src/Avalonia.Base/Media/Imaging/BitmapEncoderOptions.cs
  7. 22
      src/Avalonia.Base/Media/Imaging/IBitmap.cs
  8. 18
      src/Avalonia.Base/Media/Imaging/JpegBitmapEncoderOptions.cs
  9. 20
      src/Avalonia.Base/Media/Imaging/PngBitmapEncoderOptions.cs
  10. 23
      src/Avalonia.Base/Platform/IBitmapImpl.cs
  11. 2
      src/Avalonia.Native/Cursor.cs
  12. 3
      src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs
  13. 2
      src/Avalonia.Native/IAvnMenuItem.cs
  14. 5
      src/Avalonia.Native/IconLoader.cs
  15. 2
      src/Avalonia.Wayland/Clipboard/WaylandClipboardImpl.cs
  16. 3
      src/Avalonia.X11/Selections/SelectionDataProvider.cs
  17. 4
      src/Avalonia.X11/X11IconLoader.cs
  18. 3
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  19. 3
      src/Browser/Avalonia.Browser/Cursor.cs
  20. 9
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  21. 121
      src/Skia/Avalonia.Skia/Helpers/ImageSavingHelper.cs
  22. 11
      src/Skia/Avalonia.Skia/ImmutableBitmap.cs
  23. 20
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  24. 19
      src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs
  25. 2
      src/Windows/Avalonia.Win32/OleDataObjectHelper.cs
  26. 8
      src/Windows/Avalonia.Win32/Win32Platform.cs
  27. 3
      src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs
  28. 10
      src/iOS/Avalonia.iOS/Stubs.cs
  29. 6
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  30. 5
      tests/Avalonia.RenderTests/TestRenderHelper.cs
  31. 108
      tests/Avalonia.Skia.UnitTests/Media/BitmapSaveTests.cs

36
api/Avalonia.nupkg.xml

@ -19,6 +19,18 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.String,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double,Avalonia.Size)</Target>
@ -79,6 +91,18 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.String,System.Nullable{System.Int32})</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Platform.IRenderTarget.RenderTargetSceneInfo.#ctor(Avalonia.PixelSize,System.Double,Avalonia.Size)</Target>
@ -121,6 +145,12 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)</Target>
@ -139,6 +169,12 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Controls.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageProvider.OpenFilePickerWithResultAsync(Avalonia.Platform.Storage.FilePickerOpenOptions)</Target>

3
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;

3
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);
}
}

3
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);
}
}

33
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -147,7 +147,7 @@ namespace Avalonia.Media.Imaging
IRef<IBitmapImpl> IBitmap.PlatformImpl => PlatformImpl;
/// <summary>
/// Saves the bitmap to a file.
/// Saves the bitmap to a file, in PNG format.
/// </summary>
/// <param name="fileName">The filename.</param>
/// <param name="quality">
@ -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.
/// </param>
[Obsolete($"Use the overload accepting {nameof(BitmapEncoderOptions)} instead.")]
public void Save(string fileName, int? quality = null)
=> Save(fileName, PngBitmapEncoderOptions.Default);
/// <summary>
/// Saves the bitmap to a file with the specified options.
/// </summary>
/// <param name="fileName">The filename.</param>
/// <param name="options">
/// The options specifying the format and settings to use.
/// Typical usages include <see cref="PngBitmapEncoderOptions"/> and <see cref="JpegBitmapEncoderOptions"/>.
/// </param>
public void Save(string fileName, BitmapEncoderOptions options)
{
PlatformImpl.Item.Save(fileName, quality);
using var stream = File.Create(fileName);
Save(stream, options);
}
/// <summary>
/// Saves the bitmap to a stream.
/// Saves the bitmap to a stream, in PNG format.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="quality">
/// 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.
/// </param>
[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);
}
/// <inheritdoc />
public void Save(Stream stream, BitmapEncoderOptions options)
{
PlatformImpl.Item.Save(stream, options);
}
public virtual PixelFormat? Format => (PlatformImpl.Item as IReadableBitmapImpl)?.Format;

14
src/Avalonia.Base/Media/Imaging/BitmapEncoderOptions.cs

@ -0,0 +1,14 @@
using System.IO;
namespace Avalonia.Media.Imaging;
/// <summary>
/// Represents the options used while saving a bitmap using <see cref="Bitmap.Save(Stream, BitmapEncoderOptions)"/>.
/// Common implementations are <see cref="PngBitmapEncoderOptions"/>, <see cref="JpegBitmapEncoderOptions"/>.
/// </summary>
public abstract class BitmapEncoderOptions
{
internal BitmapEncoderOptions()
{
}
}

22
src/Avalonia.Base/Media/Imaging/IBitmap.cs

@ -32,25 +32,13 @@ namespace Avalonia.Media.Imaging
IRef<IBitmapImpl> PlatformImpl { get; }
/// <summary>
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
/// <param name="quality">
/// 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.
/// </param>
void Save(string fileName, int? quality = null);
/// <summary>
/// Saves the bitmap to a stream in png format.
/// Saves the bitmap to a stream with the specified options.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="quality">
/// 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.
/// <param name="options">
/// The options specifying the format and settings to use.
/// Typical usages include <see cref="PngBitmapEncoderOptions"/> and <see cref="JpegBitmapEncoderOptions"/>.
/// </param>
void Save(Stream stream, int? quality = null);
void Save(Stream stream, BitmapEncoderOptions options);
}
}

18
src/Avalonia.Base/Media/Imaging/JpegBitmapEncoderOptions.cs

@ -0,0 +1,18 @@
namespace Avalonia.Media.Imaging;
/// <summary>
/// Represents the options used to save a bitmap in the JPEG format.
/// </summary>
public sealed class JpegBitmapEncoderOptions : BitmapEncoderOptions
{
/// <summary>
/// Gets the default JPEG encoder options.
/// </summary>
public static JpegBitmapEncoderOptions Default { get; } = new();
/// <summary>
/// Gets or sets the quality to use, from 0 (lowest) to 100 (highest).
/// Defaults to 100.
/// </summary>
public int Quality { get; init; } = 100;
}

20
src/Avalonia.Base/Media/Imaging/PngBitmapEncoderOptions.cs

@ -0,0 +1,20 @@
using System.IO.Compression;
namespace Avalonia.Media.Imaging;
/// <summary>
/// Represents the options used to save a bitmap in the PNG format.
/// </summary>
public sealed class PngBitmapEncoderOptions : BitmapEncoderOptions
{
/// <summary>
/// Gets the default PNG encoder options.
/// </summary>
public static PngBitmapEncoderOptions Default { get; } = new();
/// <summary>
/// Gets or sets the compression level to use.
/// Defaults to <see cref="System.IO.Compression.CompressionLevel.Optimal"/>.
/// </summary>
public CompressionLevel CompressionLevel { get; init; } = CompressionLevel.Optimal;
}

23
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; }
/// <summary>
/// Saves the bitmap to a file.
/// </summary>
/// <param name="fileName">The filename.</param>
/// <param name="quality">
/// 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.
/// </param>
void Save(string fileName, int? quality = null);
/// <summary>
/// Saves the bitmap to a stream in png format.
/// Saves the bitmap to a stream with the specified options.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="quality">
/// 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.
/// <param name="options">
/// The options specifying the format and settings to use.
/// Typical usages include <see cref="PngBitmapEncoderOptions"/> and <see cref="JpegBitmapEncoderOptions"/>.
/// </param>
void Save(Stream stream, int? quality = null);
void Save(Stream stream, BitmapEncoderOptions options);
}
}

2
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();

3
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);
}

2
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();

5
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);
}

2
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;
}

3
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();
}

4
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);
}
}
}

3
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());

3
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());

9
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++;

121
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
{
/// <summary>
/// Save Skia image to a file.
/// Saves a Skia image to a file in the PNG format.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="fileName">Target file.</param>
/// <param name="image">The image to save</param>
/// <param name="fileName">The output file to save the image to.</param>
/// <param name="quality">
/// 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.
/// </param>
[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);
}
/// <summary>
/// Saves a Skia image to a file in the specified format.
/// </summary>
/// <param name="image">The image to save</param>
/// <param name="fileName">The output file to save the image to.</param>
/// <param name="options">
/// The options specifying the format and settings to use.
/// Typical usages include <see cref="PngBitmapEncoderOptions"/> and <see cref="JpegBitmapEncoderOptions"/>.
/// </param>
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);
}
/// <summary>
/// Save Skia image to a stream.
/// Saves a Skia image to a stream in the PNG format.
/// </summary>
/// <param name="image">Image to save</param>
/// <param name="stream">The output stream to save the image.</param>
/// <param name="image">The image to save</param>
/// <param name="stream">The output stream to save the image to.</param>
/// <param name="quality">
/// 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.
/// </param>
[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)
/// <summary>
/// Saves a Skia image to a stream in the specified format.
/// </summary>
/// <param name="image">The image to save</param>
/// <param name="stream">The output stream to save the image to.</param>
/// <param name="options">
/// The options specifying the format and settings to use.
/// Typical usages include <see cref="PngBitmapEncoderOptions"/> and <see cref="JpegBitmapEncoderOptions"/>.
/// </param>
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);
}
}
}

11
src/Skia/Avalonia.Skia/ImmutableBitmap.cs

@ -173,16 +173,9 @@ namespace Avalonia.Skia
_bitmap?.Dispose();
}
/// <inheritdoc />
public void Save(string fileName, int? quality = null)
{
ImageSavingHelper.SaveImage(_image, fileName, quality);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />

20
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;
/// <inheritdoc />
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();
/// <inheritdoc />
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)

19
src/Skia/Avalonia.Skia/WriteableBitmapImpl.cs

@ -149,24 +149,13 @@ namespace Avalonia.Skia
}
}
/// <inheritdoc />
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();
/// <inheritdoc />
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();

2
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());
}

8
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);
}

3
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);

10
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)

6
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

5
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);
}
}

108
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<ArgumentNullException>(() => 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<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => 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()));
}
}
Loading…
Cancel
Save