using SkiaSharp;
namespace Sandbox;
public sealed partial class Bitmap : IDisposable, IValid
{
private SKBitmap _bitmap;
private SKCanvas _canvas;
public int Width => _bitmap.Width;
public int Height => _bitmap.Height;
public int BytesPerPixel => _bitmap.BytesPerPixel;
public int ByteCount => _bitmap.ByteCount;
public Rect Rect => new( 0, 0, Width, Height );
///
/// The width and height of the bitmap
///
public Vector2Int Size => new( Width, Height );
public Vector2 Center => new Vector2( Width, Height ) * 0.5f;
public bool IsFloatingPoint { get; init; }
public bool IsValid => _bitmap is not null && _canvas is not null;
private const int MaxDimension = 16384;
public Bitmap( int width, int height, bool floatingPoint = false )
{
if ( width <= 0 || height <= 0 )
throw new ArgumentOutOfRangeException( "Dimensions must be positive." );
if ( width > MaxDimension || height > MaxDimension )
throw new ArgumentOutOfRangeException( $"Dimensions cannot exceed {MaxDimension}." );
IsFloatingPoint = floatingPoint;
var colorType = IsFloatingPoint ? SKColorType.RgbaF16 : SKColorType.Rgba8888;
var info = new SKImageInfo( width, height, colorType, SKAlphaType.Unpremul );
_bitmap = new SKBitmap( info );
_canvas = new SKCanvas( _bitmap );
}
///
/// Used internally for resizing operations
///
internal Bitmap( SKBitmap bitmap )
{
IsFloatingPoint = bitmap.ColorType == SKColorType.RgbaF16;
_bitmap = bitmap;
_canvas = new SKCanvas( _bitmap );
}
public void Dispose()
{
_canvas?.Dispose();
_canvas = default;
_bitmap?.Dispose();
_bitmap = default;
}
///
/// Clears the bitmap to the specified color.
///
/// The color to fill the bitmap with.
public void Clear( Color color )
{
_canvas.Clear( color.ToSkF() );
}
///
/// Retrieves the pixel data of the bitmap as an array of colors.
///
public Color[] GetPixels()
{
if ( IsFloatingPoint )
{
unsafe
{
int pixelCount = _bitmap.Width * _bitmap.Height;
var raw = new Span( (void*)_bitmap.GetPixels(), pixelCount );
// Allocate the final Color array
var colors = new Color[pixelCount];
// Convert each HalfColor to Color
for ( int i = 0; i < pixelCount; i++ )
{
colors[i] = raw[i].ToColor();
}
return colors;
}
}
else
{
return _bitmap.Pixels.Select( p => p.FromSk() ).ToArray();
}
}
///
/// Retrieves the pixel data of the bitmap as an array of colors.
///
public Color.Rgba16[] GetPixels16()
{
if ( IsFloatingPoint )
{
unsafe
{
int pixelCount = _bitmap.Width * _bitmap.Height;
var raw = new Span( (void*)_bitmap.GetPixels(), pixelCount );
return raw.ToArray();
}
}
else
{
return _bitmap.Pixels.Select( p => (Color.Rgba16)p.FromSk() ).ToArray();
}
}
///
/// Retrieves the pixel data of the bitmap as an array of colors.
///
public Color32[] GetPixels32()
{
if ( IsFloatingPoint )
{
unsafe
{
int pixelCount = _bitmap.Width * _bitmap.Height;
var raw = new Span( (void*)_bitmap.GetPixels(), pixelCount );
// Allocate the final Color array
var colors = new Color32[pixelCount];
// Convert each HalfColor to Color
for ( int i = 0; i < pixelCount; i++ )
{
colors[i] = raw[i].ToColor();
}
return colors;
}
}
else
{
return _bitmap.Pixels.Select( p => (Color32)p.FromSk() ).ToArray();
}
}
public void SetPixels( Color[] colors )
{
if ( colors is null || colors.Length != _bitmap.Width * _bitmap.Height )
{
throw new ArgumentException( "Colors array must match the size of the bitmap." );
}
if ( IsFloatingPoint )
{
unsafe
{
int pixelCount = _bitmap.Width * _bitmap.Height;
var rawPixels = new Span( (void*)_bitmap.GetPixels(), pixelCount );
for ( int i = 0; i < pixelCount; i++ )
{
rawPixels[i] = new Color.Rgba16( colors[i] );
}
}
}
else
{
var skColors = new SKColor[colors.Length];
for ( int i = 0; i < colors.Length; i++ )
{
skColors[i] = colors[i].ToSk();
}
_bitmap.Pixels = skColors;
}
}
///
/// Retrieves the color of a specific pixel in the bitmap.
///
/// The x-coordinate of the pixel.
/// The y-coordinate of the pixel.
/// The color of the pixel at the specified coordinates.
public Color GetPixel( int x, int y )
{
AssertBounds( x, y, 1, 1 );
return _bitmap.GetPixel( x, y ).FromSk();
}
///
/// Sets the color of a specific pixel in the bitmap.
///
/// The x-coordinate of the pixel.
/// The y-coordinate of the pixel.
/// The color to set the pixel to.
public void SetPixel( int x, int y, Color color )
{
AssertBounds( x, y, 1, 1 );
if ( _bitmap.ColorType == SKColorType.RgbaF16 )
{
unsafe
{
int index = y * _bitmap.Width + x;
var rawPixels = new Span( (void*)_bitmap.GetPixels(), _bitmap.Width * _bitmap.Height );
rawPixels[index] = new Color.Rgba16( color );
}
}
else
{
_bitmap.SetPixel( x, y, color.ToSk() );
}
}
///
/// Low level, get a span of the bitmap data.
///
internal unsafe Span GetBuffer()
{
return new Span( (void*)_bitmap.GetPixels(), ByteCount );
}
///
/// Super low level, get a pointer to the bitmap data.
///
internal unsafe void* GetPointer()
{
return (void*)_bitmap.GetPixels();
}
///
/// Asserts that the specified region is within the bounds of the bitmap.
/// Throws an exception if the bounds are out of range.
///
/// The x-coordinate of the starting point.
/// The y-coordinate of the starting point.
/// The width of the region to check.
/// The height of the region to check.
private void AssertBounds( int x, int y, int width, int height )
{
if ( x < 0 || y < 0 || x + width > _bitmap.Width || y + height > _bitmap.Height )
{
throw new ArgumentOutOfRangeException( nameof( x ), "Specified region is out of bounds." );
}
}
///
/// Copy the bitmap to a new one without any changes.
///
public Bitmap Clone()
{
var newBitmap = _bitmap.Copy();
return new Bitmap( newBitmap );
}
///
/// Returns true if this bitmap is completely opaque (no alpha)
/// This does a pixel by pixel search, so it's not the fastest.
///
public unsafe bool IsOpaque()
{
if ( _bitmap.AlphaType == SKAlphaType.Opaque )
return true;
int height = _bitmap.Height;
int width = _bitmap.Width;
int rowBytes = _bitmap.RowBytes;
IntPtr pixels = _bitmap.GetPixels();
if ( _bitmap.ColorType == SKColorType.RgbaF16 )
{
float* ptr = (float*)pixels;
bool opaque = true;
Parallel.For( 0, height, ( y, state ) =>
{
float* row = ptr + (y * (rowBytes / sizeof( float )));
for ( int x = 0; x < width; x++ )
{
float alpha = row[x * 4 + 3]; // Alpha is the 4th channel
if ( alpha < 1.0f ) // Check if alpha is not fully opaque
{
opaque = false;
state.Stop(); // Stop all threads
return;
}
}
} );
return opaque;
}
if ( _bitmap.ColorType == SKColorType.Rgba8888 )
{
bool opaque = true;
Parallel.For( 0, height, ( y, state ) =>
{
byte* ptr = (byte*)pixels + (y * rowBytes);
for ( int x = 0; x < width; x++ )
{
if ( ptr[(x * 4) + 3] != 255 )
{
opaque = false;
state.Stop(); // Stop all threads
return;
}
}
} );
return opaque;
}
// we can't determin that it's opaque so say it's not
return false;
}
}