using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using SkiaSharp.Views.Blazor.Internal; namespace SkiaSharp.Views.Blazor { public partial class SKCanvasView : IDisposable { private SKHtmlCanvasInterop interop = null!; private SizeWatcherInterop sizeWatcher = null!; private DpiWatcherInterop dpiWatcher = null!; private ElementReference htmlCanvas; private SKSizeI pixelSize; private byte[]? pixels; private GCHandle pixelsHandle; private bool ignorePixelScaling; private double dpi; private SKSize canvasSize; private bool enableRenderLoop; [Inject] IJSRuntime JS { get; set; } = null!; [Parameter] public Action? OnPaintSurface { get; set; } [Parameter] public bool EnableRenderLoop { get => enableRenderLoop; set { if (enableRenderLoop != value) { enableRenderLoop = value; Invalidate(); } } } [Parameter] public bool IgnorePixelScaling { get => ignorePixelScaling; set { if (ignorePixelScaling != value) { ignorePixelScaling = value; Invalidate(); } } } [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? AdditionalAttributes { get; set; } public double Dpi => dpi; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { interop = await SKHtmlCanvasInterop.ImportAsync(JS, htmlCanvas, OnRenderFrame); interop.InitRaster(); sizeWatcher = await SizeWatcherInterop.ImportAsync(JS, htmlCanvas, OnSizeChanged); dpiWatcher = await DpiWatcherInterop.ImportAsync(JS, OnDpiChanged); } } public void Invalidate() { if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0) return; interop.RequestAnimationFrame(EnableRenderLoop, (int)(canvasSize.Width * dpi), (int)(canvasSize.Height * dpi)); } private void OnRenderFrame() { if (canvasSize.Width <= 0 || canvasSize.Height <= 0 || dpi <= 0) return; var info = CreateBitmap(out var unscaledSize); var userVisibleSize = IgnorePixelScaling ? unscaledSize : info.Size; using (var surface = SKSurface.Create(info, pixelsHandle.AddrOfPinnedObject(), info.RowBytes)) { if (IgnorePixelScaling) { var canvas = surface.Canvas; canvas.Scale((float)dpi); canvas.Save(); } OnPaintSurface?.Invoke(new SKPaintSurfaceEventArgs(surface, info.WithSize(userVisibleSize), info)); } interop.PutImageData(pixelsHandle.AddrOfPinnedObject(), info.Size); } private SKImageInfo CreateBitmap(out SKSizeI unscaledSize) { var size = CreateSize(out unscaledSize); var info = new SKImageInfo(size.Width, size.Height, SKImageInfo.PlatformColorType, SKAlphaType.Opaque); if (pixels == null || pixelSize.Width != info.Width || pixelSize.Height != info.Height) { FreeBitmap(); pixels = new byte[info.BytesSize]; pixelsHandle = GCHandle.Alloc(pixels, GCHandleType.Pinned); pixelSize = info.Size; } return info; } private SKSizeI CreateSize(out SKSizeI unscaledSize) { unscaledSize = SKSizeI.Empty; var w = canvasSize.Width; var h = canvasSize.Height; if (!IsPositive(w) || !IsPositive(h)) return SKSizeI.Empty; unscaledSize = new SKSizeI((int)w, (int)h); return new SKSizeI((int)(w * dpi), (int)(h * dpi)); static bool IsPositive(double value) { return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0; } } private void FreeBitmap() { if (pixels != null) { pixelsHandle.Free(); pixels = null; } } private void OnDpiChanged(double newDpi) { dpi = newDpi; Invalidate(); } private void OnSizeChanged(SKSize newSize) { if ((int)(canvasSize.Width * dpi) == newSize.Width && (int)(canvasSize.Height * dpi) == newSize.Height) return; canvasSize = newSize; Invalidate(); } public void Dispose() { dpiWatcher?.Unsubscribe(OnDpiChanged); sizeWatcher?.Dispose(); interop?.Dispose(); FreeBitmap(); } } }