Editor/SuperShotEdit.cs

Editor image editing utility for the SuperShot feature. It applies transforms, color adjustments, filters, detail (sharpen/blur), vignette, film grain, border and a text watermark to a Bitmap according to EditSettings.

Native Interop
using System;
using Sandbox;

namespace Editor.SuperShot;

public static class SuperShotEdit
{
	public static Bitmap Apply( Bitmap source, EditSettings edit )
	{
		if ( source is null )
			return null;

		var working = source.Clone();

		try
		{
			working = ApplyTransform( working, edit );
			ApplyColor( working, edit );
			ApplyFilter( working, edit.Filter );
			ApplyDetail( working, edit );
			ApplyVignette( working, edit.Vignette );
			ApplyGrain( working, edit.Grain );
			working = ApplyBorder( working, edit );
			ApplyWatermark( working, edit );
		}
		catch ( Exception e )
		{
			Log.Warning( $"[Supershot] Edit pass failed: {e.Message}" );
		}

		return working;
	}

	static Bitmap ApplyTransform( Bitmap bmp, EditSettings edit )
	{
		float degrees = edit.Rotate switch
		{
			ShotRotate.Cw90 => 90f,
			ShotRotate.Half => 180f,
			ShotRotate.Ccw90 => 270f,
			_ => 0f
		};

		if ( degrees != 0f )
		{
			var rotated = bmp.Rotate( degrees );
			bmp.Dispose();
			bmp = rotated;
		}

		if ( edit.FlipH )
		{
			var f = bmp.FlipHorizontal();
			bmp.Dispose();
			bmp = f;
		}

		if ( edit.FlipV )
		{
			var f = bmp.FlipVertical();
			bmp.Dispose();
			bmp = f;
		}

		return bmp;
	}

	static void ApplyColor( Bitmap bmp, EditSettings edit )
	{
		float brightness = edit.Brightness * MathF.Pow( 2f, edit.Exposure );
		bool changed = brightness != 1f || edit.Contrast != 1f || edit.Saturation != 1f || edit.Hue != 0f;
		if ( changed )
			bmp.Adjust( brightness, edit.Contrast, edit.Saturation, edit.Hue );
	}

	static void ApplyFilter( Bitmap bmp, ShotFilter filter )
	{
		switch ( filter )
		{
			case ShotFilter.Warm:
				bmp.Tint( new Color( 1f, 0.92f, 0.82f ) );
				break;
			case ShotFilter.Cool:
				bmp.Tint( new Color( 0.84f, 0.92f, 1f ) );
				break;
			case ShotFilter.Vintage:
				bmp.Adjust( 1.02f, 0.92f, 0.7f, 0f );
				bmp.Tint( new Color( 1f, 0.9f, 0.75f ) );
				break;
			case ShotFilter.Cinematic:
				bmp.Adjust( 1f, 1.15f, 0.9f, 0f );
				bmp.Tint( new Color( 0.92f, 0.97f, 1f ) );
				break;
			case ShotFilter.BlackAndWhite:
				bmp.Adjust( 1f, 1.05f, 0f, 0f );
				break;
			case ShotFilter.HighContrast:
				bmp.Adjust( 1f, 1.35f, 1.05f, 0f );
				break;
		}
	}

	static void ApplyDetail( Bitmap bmp, EditSettings edit )
	{
		if ( edit.Sharpen > 0f )
			bmp.Sharpen( edit.Sharpen );

		if ( edit.Blur > 0f )
			bmp.Blur( edit.Blur );
	}

	static void ApplyVignette( Bitmap bmp, float intensity )
	{
		if ( intensity <= 0f )
			return;

		var center = bmp.Center;
		float radius = MathF.Sqrt( center.x * center.x + center.y * center.y );

		var g = new Gradient();
		g.AddColor( 0f, Color.Black );
		g.AddColor( 1f, Color.Black );
		g.AddAlpha( 0f, 0f );
		g.AddAlpha( 0.55f, 0f );
		g.AddAlpha( 1f, MathX.Clamp( intensity, 0f, 1f ) );

		bmp.SetRadialGradient( center, radius, g );
		bmp.DrawRect( bmp.Rect );
	}

	static void ApplyGrain( Bitmap bmp, float intensity )
	{
		if ( intensity <= 0f )
			return;

		int nw = Math.Clamp( bmp.Width / 2, 8, 1024 );
		int nh = Math.Clamp( bmp.Height / 2, 8, 1024 );

		using var noise = new Bitmap( nw, nh );
		var pixels = new Color[nw * nh];
		float a = MathX.Clamp( intensity, 0f, 1f ) * 0.5f;
		for ( int i = 0; i < pixels.Length; i++ )
		{
			float v = System.Random.Shared.Float( 0f, 1f );
			pixels[i] = new Color( v, v, v, a );
		}
		noise.SetPixels( pixels );

		bmp.SetAntialias( true );
		bmp.DrawBitmap( noise, bmp.Rect );
	}

	static Bitmap ApplyBorder( Bitmap bmp, EditSettings edit )
	{
		if ( edit.Border <= 0 )
			return bmp;

		int b = Math.Min( edit.Border, Math.Min( bmp.Width, bmp.Height ) / 2 - 1 );
		if ( b <= 0 )
			return bmp;

		var bordered = new Bitmap( bmp.Width, bmp.Height );
		bordered.Clear( edit.BorderColor );
		bordered.DrawBitmap( bmp, new Rect( b, b, bmp.Width - b * 2, bmp.Height - b * 2 ) );
		bmp.Dispose();
		return bordered;
	}

	static void ApplyWatermark( Bitmap bmp, EditSettings edit )
	{
		if ( !edit.Watermark || string.IsNullOrWhiteSpace( edit.WatermarkText ) )
			return;

		float margin = MathF.Max( 8f, edit.WatermarkSize * 0.4f );
		var rect = new Rect( margin, margin, bmp.Width - margin * 2f, bmp.Height - margin * 2f );

		var flags = edit.WatermarkAnchor switch
		{
			ShotAnchor.TopLeft => TextFlag.LeftTop,
			ShotAnchor.TopRight => TextFlag.RightTop,
			ShotAnchor.BottomLeft => TextFlag.LeftBottom,
			ShotAnchor.BottomRight => TextFlag.RightBottom,
			_ => TextFlag.Center
		};

		var scope = new TextRendering.Scope( edit.WatermarkText, edit.WatermarkColor, edit.WatermarkSize );
		bmp.DrawText( scope, rect, flags );
	}
}