Integrating ImageSharp with Windows and Uno Platform Applications

Earlier this week Six Labors announced the release of ImageSharp.Drawing v1.0 and it made me wonder whether this could be used as an alternative to Skia for controls that want to do custom rendering in a Windows, or multi-platform Uno Platform, application. For Skia there are already Uno (which includes Windows/WinUI) views (see source here) and there is a sample showing how to use the SKXamlCanvas. In this post we’re going to cover off a very simple prototype I did to validate that ImageSharp.Drawing can indeed be used as a backend for custom drawing in an Windows / Uno Platform application. Unlike my previous post on using ImageSharp where I used it to render bitmaps server side, all the code in this post is running in the application either in the browser or on the device.

I started with a new Uno Platform application using the Visual Studio Uno Platform Template wizard and selected the Blank template.

I added a reference to ImageSharp.Drawing to the class library. It’s not necessary to add this dependency to each of the target platforms.

I then copied across the SKXamlCanvas from the Skia repository and renamed it to ImageSharpXamlCanvas. The SKXamlCanvas is actually split into partial classes, so each of these were brought across – you’ll see that the source code for the ImageSharpXamlCanvas is still split into two partial classes.

There were a bunch of small changes I needed to make in order to get the ImageSharpXamlCanvas to compile and to use ImageSharp.Drawing. For anyone who’s interested, the full source code is available in the collapsed section at the end of this post.

Once the ImageSharpXamlCanvas was building, the next thing was to add it to the MainPage xaml.

<Page x:Class="ImageSharpApp.MainPage"
	...
	Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
	<StackPanel>
		<local:ImageSharpXamlCanvas 
			Height="300" 
			Width="300" 
			PaintSurface="PaintSurfaceHandler"/>
	</StackPanel>
</Page>

I then created the PaintSurfaceHandler callback method and used the code from the ImageSharpLogo sample, pretty much unchanged, to draw the (old) ImageSharp logo.

private void PaintSurfaceHandler(object sender, ImageSharpPaintSurfaceEventArgs e)
{
    SaveLogo(e.Surface, 300);
}

public static void SaveLogo(Image img, float size)
{
    // the point are based on a 1206x1206 shape so size requires scaling from there
    float scalingFactor = size / 1206;

    var center = new Vector2(603);

    // segment whose center of rotation should be
    var segmentOffset = new Vector2(301.16968f, 301.16974f);
    IPath segment = new Polygon(
        new LinearLineSegment(new Vector2(230.54f, 361.0261f), new Vector2(5.8641942f, 361.46031f)),
        new CubicBezierLineSegment(
            new Vector2(5.8641942f, 361.46031f),
            new Vector2(-11.715693f, 259.54052f),
            new Vector2(24.441609f, 158.17478f),
            new Vector2(78.26f, 97.0461f))).Translate(center - segmentOffset);

    // we need to create 6 of theses all rotated about the center point
    var segments = new List<IPath>();
    for (int i = 0; i < 6; i++)
    {
        float angle = i * ((float)Math.PI / 3);
        IPath s = segment.Transform(Matrix3x2.CreateRotation(angle, center));
        segments.Add(s);
    }

    var colors = new List<Color>()
        {
            Color.ParseHex("35a849"),
            Color.ParseHex("fcee21"),
            Color.ParseHex("ed7124"),
            Color.ParseHex("cb202d"),
            Color.ParseHex("5f2c83"),
            Color.ParseHex("085ba7"),
        };

    var scaler = Matrix3x2.CreateScale(scalingFactor, Vector2.Zero);

    img.Mutate(i => i.Fill(Color.Black));
    img.Mutate(i => i.Fill(Color.ParseHex("e1e1e1ff"), new EllipsePolygon(center, 600f).Transform(scaler)));
    img.Mutate(i => i.Fill(Color.White, new EllipsePolygon(center, 600f - 60).Transform(scaler)));

    for (int s = 0; s < 6; s++)
    {
        img.Mutate(i => i.Fill(colors[s], segments[s].Transform(scaler)));
    }

    img.Mutate(i => i.Fill(new Rgba32(0, 0, 0, 170), new ComplexPolygon(new EllipsePolygon(center, 161f), new EllipsePolygon(center, 61f)).Transform(scaler)));

}

And then we run it! Here it is running on WASM, Windows (WinUI/WinAppSdk) and Gtk.

Disclaimer: The mobile targets build and run but don’t render correctly (iOS) or at all (Android), so further work is required to bring across specific platform support from the SKXamlCanvas implementation to get these to work.

You might be wondering why you’d want to use ImageSharp.Drawing over the existing Skia implementation. For a wide variety of scenarios there’s probably no reason to prefer one over the other. However, you should check out the insane font support that’s available in the ImageSharp.Fonts library, which can be used in conjunction with ImageSharp.Drawing.

ImageSharpXamlCanvas – Source Code

public partial class ImageSharpXamlCanvas : Canvas
{
    private const float DpiBase = 96.0f;

    private static readonly DependencyProperty ProxyVisibilityProperty =
        DependencyProperty.Register(
            "ProxyVisibility",
            typeof(Visibility),
            typeof(ImageSharpXamlCanvas),
            new PropertyMetadata(Visibility.Visible, OnVisibilityChanged));

    private static bool designMode = DesignMode.DesignModeEnabled;

    private bool ignorePixelScaling;
    private bool isVisible = true;

    // workaround for https://github.com/mono/SkiaSharp/issues/1118
    private int loadUnloadCounter = 0;

    private void Initialize()
    {
        if (designMode)
            return;

#if !WINDOWS
        var display = DisplayInformation.GetForCurrentView();
        OnDpiChanged(display);
#else
        Invalidate();
#endif

        Loaded += OnLoaded;
        Unloaded += OnUnloaded;
        SizeChanged += OnSizeChanged;

        var binding = new Binding
        {
            Path = new PropertyPath(nameof(Visibility)),
            Source = this
        };
        SetBinding(ProxyVisibilityProperty, binding);
    }

    public Size CanvasSize { get; private set; }

    public bool IgnorePixelScaling
    {
        get => ignorePixelScaling;
        set
        {
            ignorePixelScaling = value;
            Invalidate();
        }
    }

    public double Dpi { get; private set; } = 1;

    public event EventHandler<ImageSharpPaintSurfaceEventArgs> PaintSurface;

    protected virtual void OnPaintSurface(ImageSharpPaintSurfaceEventArgs e)
    {
        PaintSurface?.Invoke(this, e);
    }

    private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is ImageSharpXamlCanvas canvas && e.NewValue is Visibility visibility)
        {
            canvas.isVisible = visibility == Visibility.Visible;
            canvas.Invalidate();
        }
    }

    private void OnDpiChanged(DisplayInformation sender, object args = null)
    {
        Dpi = sender.LogicalDpi / DpiBase;
        Invalidate();
    }

    private void OnSizeChanged(object sender, SizeChangedEventArgs e)
    {
        Invalidate();
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        loadUnloadCounter++;
        if (loadUnloadCounter != 1)
            return;

        DoLoaded();

#if !WINDOWS
        var display = DisplayInformation.GetForCurrentView();
        display.DpiChanged += OnDpiChanged;

        OnDpiChanged(display);
#else
        Invalidate();
#endif
    }

    private void OnUnloaded(object sender, RoutedEventArgs e)
    {
        loadUnloadCounter--;
        if (loadUnloadCounter != 0)
            return;

        DoUnloaded();

        var display = DisplayInformation.GetForCurrentView();
        display.DpiChanged -= OnDpiChanged;
    }

    public void Invalidate()
    {
        if (DispatcherQueue.HasThreadAccess)
            DoInvalidate();
        else
            DispatcherQueue.TryEnqueue(DoInvalidate);
    }

    partial void DoLoaded();

    partial void DoUnloaded();

    private Size CreateSize(out Size unscaledSize, out float dpi)
    {
        unscaledSize = Size.Empty;
        dpi = (float)Dpi;

        var w = ActualWidth;
        var h = ActualHeight;

        if (!IsPositive(w) || !IsPositive(h))
            return Size.Empty;

        unscaledSize = new Size((int)w, (int)h);
        return new Size((int)(w * dpi), (int)(h * dpi));

        static bool IsPositive(double value)
        {
            return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0;
        }
    }

}

public partial class ImageSharpXamlCanvas : Canvas
{
    private byte[] pixels;
    private GCHandle pixelsHandle;
    private int pixelWidth;
    private int pixelHeight;
    private WriteableBitmap bitmap;

    public ImageSharpXamlCanvas()
    {
        Initialize();
    }

    partial void DoUnloaded() =>
        FreeBitmap();

    private void DoInvalidate()
    {
        if (designMode)
            return;

        if (!isVisible)
            return;

        if (ActualWidth <= 0 || ActualHeight <= 0)
        {
            CanvasSize = Size.Empty;
            return;
        }

        var info = CreateBitmap(out var unscaledSize, out var dpi);

        //using (var surface = SKSurface.Create(info, pixelsHandle.AddrOfPinnedObject(), info.RowBytes))
        using (var surface = new Image<Bgra32>(info.Width, info.Height)) // SKSurface.Create(info, pixelsHandle.AddrOfPinnedObject(), info.RowBytes))
        {
            var userVisibleSize = IgnorePixelScaling ? unscaledSize : info.Size;
            CanvasSize = userVisibleSize;

            //if (IgnorePixelScaling)
            //{
            //    var canvas = surface.Canvas;
            //    canvas.Scale(dpi);
            //    canvas.Save();
            //}

            OnPaintSurface(new ImageSharpPaintSurfaceEventArgs(surface, info with { Width = userVisibleSize.Width, Height = userVisibleSize.Height }, info));

            //surface.sa


            surface.CopyPixelDataTo(pixels);
        }



        // This implementation is not fast enough, and providing the original pixel buffer
        // is needed, yet the internal `IBufferByteAccess` interface is not yet available in Uno.
        // Once it is, we can replace this implementation and provide the pinned array directly
        // to skia.
        using (var data = bitmap.PixelBuffer.AsStream())
        {
            data.Write(pixels, 0, pixels.Length);
            data.Flush();
        }

        bitmap.Invalidate();
    }

    private ImageInfo CreateBitmap(out Size unscaledSize, out float dpi)
    {
        var size = CreateSize(out unscaledSize, out dpi);
        var info = new ImageInfo(size.Width, size.Height);//, SKImageInfo.PlatformColorType, SKAlphaType.Premul);

        if (bitmap?.PixelWidth != info.Width || bitmap?.PixelHeight != info.Height)
            FreeBitmap();

        if (bitmap == null && info.Width > 0 && info.Height > 0)
        {
            bitmap = new WriteableBitmap(info.Width, info.Height);

            var brush = new Microsoft.UI.Xaml.Media.ImageBrush
            {
                ImageSource = bitmap,
                AlignmentX = AlignmentX.Left,
                AlignmentY = AlignmentY.Top,
                Stretch = Stretch.Fill
            };

            Background = brush;
        }

        if (pixels == null || pixelWidth != info.Width || pixelHeight != info.Height)
        {
            FreeBitmap();

            pixels = new byte[info.BytesSize];
            pixelsHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned);

            pixelWidth = info.Width;
            pixelHeight = info.Height;
        }

        return info;
    }

    private void FreeBitmap()
    {
        if (pixels != null)
        {
            pixelsHandle.Free();
            pixels = null;
            bitmap = null;
        }
    }
}


public class ImageSharpPaintSurfaceEventArgs : EventArgs
{
    public ImageSharpPaintSurfaceEventArgs(Image surface, ImageInfo info)
           : this(surface, info, info)
    {
    }

    public ImageSharpPaintSurfaceEventArgs(Image surface, ImageInfo info, ImageInfo rawInfo)
    {
        Surface = surface;
        Info = info;
        RawInfo = rawInfo;
    }

    public Image Surface { get; }

    public ImageInfo Info { get; }

    public ImageInfo RawInfo { get; }
}

public record ImageInfo(int Width, int Height)
{
    public int BytesSize => Width * Height * 4;// BytesPerPixel;
    public Size Size => new Size(Width, Height);
}

Leave a comment