Skip to content

Commit 8cfe76a

Browse files
authored
feat(skiasharp): add a Skia bitmap loader package (#1674)
* feat(skiasharp): add a Skia bitmap loader package - Add Splat.SkiaSharp, an opt-in IBitmapLoader and IBitmap over SkiaSharp for net8.0 through net11.0, where BitmapLoader.Current has nothing to resolve today. - Take an explicit Linux native asset dependency: SkiaSharp itself only carries the macOS and Win32 assets on these target frameworks, so without it the package restores and then fails to load at run time on the hosts it exists to serve. - Preserve the aspect ratio when a decode size is requested, matching the desktop loader: both dimensions fit inside the box, one derives the other. - Apply the orientation the source recorded, and report it, so an image stored on its side decodes upright. - Add ToNative and FromNative that return the Skia bitmap rather than pretending to be a platform type, a Save overload for the formats the shared enumeration has no name for, and a choice of resampler. - Register through a module and a resolver extension, with no scanning. - Keep the native symbol files out of build and publish output, which they otherwise dominate. * build(skiasharp): leave the native asset choice to the consumer - Drop the Linux native asset reference from the library. It flowed into every consumer's output regardless of what they target, and clashed with the one an application had already chosen for itself. - Name it in the test project instead, and only when building on Linux, since the base package already carries the Windows and macOS natives.
1 parent 4639f7b commit 8cfe76a

21 files changed

Lines changed: 1788 additions & 0 deletions

‎src/Directory.Packages.props‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
2929
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.31.0.145097" />
3030
<PackageVersion Include="SimpleInjector" Version="5.6.0" />
31+
<PackageVersion Include="SkiaSharp" Version="4.151.0" />
32+
33+
<PackageVersion Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="4.151.0" />
3134
3336
<PackageVersion Include="StyleSharp.Analyzers" Version="3.40.1" />
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using SkiaSharp;
6+
7+
namespace Splat.SkiaSharp;
8+
9+
/// Converts between and the Skia bitmap behind it.
10+
///
11+
/// Skia is not the host's imaging stack, and these conversions do not pretend otherwise: what comes
12+
/// back is an , not a platform bitmap type.
13+
///
14+
public static class BitmapMixins
15+
{
16+
/// Extension members for .
17+
/// The value the extension members operate on.
18+
extension(IBitmap value)
19+
{
20+
/// Gets the Skia bitmap behind an this package produced.
21+
/// The Skia bitmap, which the still owns.
22+
/// The bitmap has been disposed.
23+
/// The bitmap came from a different loader.
24+
public SKBitmap ToNative()
25+
{
26+
ArgumentExceptionHelper.ThrowIfNull(value);
27+
28+
return ((SkiaBitmap)value).Inner ?? throw new InvalidOperationException("The bitmap has been disposed");
29+
}
30+
}
31+
32+
/// Extension members for .
33+
/// The value the extension members operate on.
34+
extension(SKBitmap value)
35+
{
36+
/// Wraps a Skia bitmap as an , taking ownership of it.
37+
/// The wrapped bitmap.
38+
public IBitmap FromNative() => new SkiaBitmap(value);
39+
}
40+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using SkiaSharp;
6+
7+
using Splat.SkiaSharp;
8+
9+
namespace Splat.Builder;
10+
11+
/// Registers Skia as the bitmap loader when the application is built.
12+
///
13+
/// Registration is a plain method call rather than assembly scanning, so it survives trimming and
14+
/// ahead-of-time compilation.
15+
///
16+
/// The sampling to resize with, or for the loader's default.
17+
public sealed class SkiaSharpSplatModule(SKSamplingOptions? sampling) : IModule
18+
{
19+
/// Initializes a new instance of the class.
20+
public SkiaSharpSplatModule()
21+
: this(null)
22+
{
23+
}
24+
25+
///
26+
public void Configure(IMutableDependencyResolver resolver)
27+
{
28+
ArgumentExceptionHelper.ThrowIfNull(resolver);
29+
30+
if (sampling is null)
31+
{
32+
resolver.UseSkiaSharpBitmapLoader();
33+
return;
34+
}
35+
36+
resolver.UseSkiaSharpBitmapLoader(sampling.Value);
37+
}
38+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
2+
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using SkiaSharp;
6+
7+
namespace Splat.SkiaSharp;
8+
9+
/// Registers Skia as the bitmap loader Splat resolves.
10+
///
11+
/// The plain .NET target frameworks register no bitmap loader of their own, so
12+
/// throws until something is registered. Registration is explicit: nothing scans assemblies for it.
13+
///
14+
public static class MutableDependencyResolverExtensions
15+
{
16+
/// Extension members for .
17+
/// An instance of Mutable Dependency Resolver.
18+
extension(IMutableDependencyResolver instance)
19+
{
20+
/// Registers as the .
21+
///
22+
/// AppLocator.CurrentMutable.UseSkiaSharpBitmapLoader();
23+
///
24+
public void UseSkiaSharpBitmapLoader()
25+
{
26+
ArgumentExceptionHelper.ThrowIfNull(instance);
27+
28+
instance.RegisterLazySingleton(static () => new SkiaBitmapLoader(), typeof(IBitmapLoader));
29+
}
30+
31+
/// Registers as the , resizing with the given sampling.
32+
/// The sampling to resize with.
33+
///
34+
/// AppLocator.CurrentMutable.UseSkiaSharpBitmapLoader(new SKSamplingOptions(SKFilterMode.Nearest));
35+
///
36+
public void UseSkiaSharpBitmapLoader(SKSamplingOptions sampling)
37+
{
38+
ArgumentExceptionHelper.ThrowIfNull(instance);
39+
40+
instance.RegisterLazySingleton(() => new SkiaBitmapLoader(sampling), typeof(IBitmapLoader));
41+
}
42+
}
43+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Splat.SkiaSharp.Tests")]
2+
namespace Splat.Builder
3+
{
4+
public sealed class SkiaSharpSplatModule : Splat.Builder.IModule
5+
{
6+
public SkiaSharpSplatModule() { }
7+
public SkiaSharpSplatModule(SkiaSharp.SKSamplingOptions? sampling) { }
8+
public void Configure(Splat.IMutableDependencyResolver resolver) { }
9+
}
10+
}
11+
namespace Splat.SkiaSharp
12+
{
13+
public static class BitmapMixins
14+
{
15+
extension(SkiaSharp.SKBitmap value)
16+
{
17+
public Splat.IBitmap FromNative() { }
18+
}
19+
extension(Splat.IBitmap value)
20+
{
21+
public SkiaSharp.SKBitmap ToNative() { }
22+
}
23+
}
24+
public static class MutableDependencyResolverExtensions
25+
{
26+
extension(Splat.IMutableDependencyResolver instance)
27+
{
28+
public void UseSkiaSharpBitmapLoader() { }
29+
public void UseSkiaSharpBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
30+
}
31+
}
32+
public sealed class SkiaBitmap : Splat.IBitmap
33+
{
34+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap) { }
35+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap, SkiaSharp.SKEncodedOrigin encodedOrigin) { }
36+
public SkiaSharp.SKEncodedOrigin EncodedOrigin { get; }
37+
public float Height { get; }
38+
public SkiaSharp.SKBitmap? Inner { get; }
39+
public float Width { get; }
40+
public void Dispose() { }
41+
public System.Threading.Tasks.Task Save(SkiaSharp.SKEncodedImageFormat format, float quality, System.IO.Stream target) { }
42+
public System.Threading.Tasks.Task Save(Splat.CompressedBitmapFormat format, float quality, System.IO.Stream target) { }
43+
}
44+
public sealed class SkiaBitmapLoader : Splat.IBitmapLoader
45+
{
46+
public SkiaBitmapLoader() { }
47+
public SkiaBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
48+
public SkiaSharp.SKSamplingOptions Sampling { get; }
49+
public Splat.IBitmap Create(float width, float height) { }
50+
public System.Threading.Tasks.Task Load(System.IO.Stream sourceStream, float? desiredWidth, float? desiredHeight) { }
51+
public System.Threading.Tasks.Task LoadFromResource(string source, float? desiredWidth, float? desiredHeight) { }
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Splat.SkiaSharp.Tests")]
2+
namespace Splat.Builder
3+
{
4+
public sealed class SkiaSharpSplatModule : Splat.Builder.IModule
5+
{
6+
public SkiaSharpSplatModule() { }
7+
public SkiaSharpSplatModule(SkiaSharp.SKSamplingOptions? sampling) { }
8+
public void Configure(Splat.IMutableDependencyResolver resolver) { }
9+
}
10+
}
11+
namespace Splat.SkiaSharp
12+
{
13+
public static class BitmapMixins
14+
{
15+
extension(SkiaSharp.SKBitmap value)
16+
{
17+
public Splat.IBitmap FromNative() { }
18+
}
19+
extension(Splat.IBitmap value)
20+
{
21+
public SkiaSharp.SKBitmap ToNative() { }
22+
}
23+
}
24+
public static class MutableDependencyResolverExtensions
25+
{
26+
extension(Splat.IMutableDependencyResolver instance)
27+
{
28+
public void UseSkiaSharpBitmapLoader() { }
29+
public void UseSkiaSharpBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
30+
}
31+
}
32+
public sealed class SkiaBitmap : Splat.IBitmap
33+
{
34+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap) { }
35+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap, SkiaSharp.SKEncodedOrigin encodedOrigin) { }
36+
public SkiaSharp.SKEncodedOrigin EncodedOrigin { get; }
37+
public float Height { get; }
38+
public SkiaSharp.SKBitmap? Inner { get; }
39+
public float Width { get; }
40+
public void Dispose() { }
41+
public System.Threading.Tasks.Task Save(SkiaSharp.SKEncodedImageFormat format, float quality, System.IO.Stream target) { }
42+
public System.Threading.Tasks.Task Save(Splat.CompressedBitmapFormat format, float quality, System.IO.Stream target) { }
43+
}
44+
public sealed class SkiaBitmapLoader : Splat.IBitmapLoader
45+
{
46+
public SkiaBitmapLoader() { }
47+
public SkiaBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
48+
public SkiaSharp.SKSamplingOptions Sampling { get; }
49+
public Splat.IBitmap Create(float width, float height) { }
50+
public System.Threading.Tasks.Task Load(System.IO.Stream sourceStream, float? desiredWidth, float? desiredHeight) { }
51+
public System.Threading.Tasks.Task LoadFromResource(string source, float? desiredWidth, float? desiredHeight) { }
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Splat.SkiaSharp.Tests")]
2+
namespace Splat.Builder
3+
{
4+
public sealed class SkiaSharpSplatModule : Splat.Builder.IModule
5+
{
6+
public SkiaSharpSplatModule() { }
7+
public SkiaSharpSplatModule(SkiaSharp.SKSamplingOptions? sampling) { }
8+
public void Configure(Splat.IMutableDependencyResolver resolver) { }
9+
}
10+
}
11+
namespace Splat.SkiaSharp
12+
{
13+
public static class BitmapMixins
14+
{
15+
extension(SkiaSharp.SKBitmap value)
16+
{
17+
public Splat.IBitmap FromNative() { }
18+
}
19+
extension(Splat.IBitmap value)
20+
{
21+
public SkiaSharp.SKBitmap ToNative() { }
22+
}
23+
}
24+
public static class MutableDependencyResolverExtensions
25+
{
26+
extension(Splat.IMutableDependencyResolver instance)
27+
{
28+
public void UseSkiaSharpBitmapLoader() { }
29+
public void UseSkiaSharpBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
30+
}
31+
}
32+
public sealed class SkiaBitmap : Splat.IBitmap
33+
{
34+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap) { }
35+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap, SkiaSharp.SKEncodedOrigin encodedOrigin) { }
36+
public SkiaSharp.SKEncodedOrigin EncodedOrigin { get; }
37+
public float Height { get; }
38+
public SkiaSharp.SKBitmap? Inner { get; }
39+
public float Width { get; }
40+
public void Dispose() { }
41+
public System.Threading.Tasks.Task Save(SkiaSharp.SKEncodedImageFormat format, float quality, System.IO.Stream target) { }
42+
public System.Threading.Tasks.Task Save(Splat.CompressedBitmapFormat format, float quality, System.IO.Stream target) { }
43+
}
44+
public sealed class SkiaBitmapLoader : Splat.IBitmapLoader
45+
{
46+
public SkiaBitmapLoader() { }
47+
public SkiaBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
48+
public SkiaSharp.SKSamplingOptions Sampling { get; }
49+
public Splat.IBitmap Create(float width, float height) { }
50+
public System.Threading.Tasks.Task Load(System.IO.Stream sourceStream, float? desiredWidth, float? desiredHeight) { }
51+
public System.Threading.Tasks.Task LoadFromResource(string source, float? desiredWidth, float? desiredHeight) { }
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Splat.SkiaSharp.Tests")]
2+
namespace Splat.Builder
3+
{
4+
public sealed class SkiaSharpSplatModule : Splat.Builder.IModule
5+
{
6+
public SkiaSharpSplatModule() { }
7+
public SkiaSharpSplatModule(SkiaSharp.SKSamplingOptions? sampling) { }
8+
public void Configure(Splat.IMutableDependencyResolver resolver) { }
9+
}
10+
}
11+
namespace Splat.SkiaSharp
12+
{
13+
public static class BitmapMixins
14+
{
15+
extension(SkiaSharp.SKBitmap value)
16+
{
17+
public Splat.IBitmap FromNative() { }
18+
}
19+
extension(Splat.IBitmap value)
20+
{
21+
public SkiaSharp.SKBitmap ToNative() { }
22+
}
23+
}
24+
public static class MutableDependencyResolverExtensions
25+
{
26+
extension(Splat.IMutableDependencyResolver instance)
27+
{
28+
public void UseSkiaSharpBitmapLoader() { }
29+
public void UseSkiaSharpBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
30+
}
31+
}
32+
public sealed class SkiaBitmap : Splat.IBitmap
33+
{
34+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap) { }
35+
public SkiaBitmap(SkiaSharp.SKBitmap bitmap, SkiaSharp.SKEncodedOrigin encodedOrigin) { }
36+
public SkiaSharp.SKEncodedOrigin EncodedOrigin { get; }
37+
public float Height { get; }
38+
public SkiaSharp.SKBitmap? Inner { get; }
39+
public float Width { get; }
40+
public void Dispose() { }
41+
public System.Threading.Tasks.Task Save(SkiaSharp.SKEncodedImageFormat format, float quality, System.IO.Stream target) { }
42+
public System.Threading.Tasks.Task Save(Splat.CompressedBitmapFormat format, float quality, System.IO.Stream target) { }
43+
}
44+
public sealed class SkiaBitmapLoader : Splat.IBitmapLoader
45+
{
46+
public SkiaBitmapLoader() { }
47+
public SkiaBitmapLoader(SkiaSharp.SKSamplingOptions sampling) { }
48+
public SkiaSharp.SKSamplingOptions Sampling { get; }
49+
public Splat.IBitmap Create(float width, float height) { }
50+
public System.Threading.Tasks.Task Load(System.IO.Stream sourceStream, float? desiredWidth, float? desiredHeight) { }
51+
public System.Threading.Tasks.Task LoadFromResource(string source, float? desiredWidth, float? desiredHeight) { }
52+
}
53+
}

0 commit comments

Comments
 (0)