Editor utility for applying photo edits and layered composition to a Bitmap for the SuperShot editor. It applies transforms, color adjustments, filters, vignette, grain, borders, watermarks, and draws text/image/adjustment layers from a SuperShotDocument.
using System;
using System.IO;
using Sandbox;
namespace Editor.SuperShot;
public static class SuperShotEdit
{
public static Bitmap RenderDocument( Bitmap source, SuperShotDocument document, SuperShotSettings settings )
{
if ( source is null )
return null;
if ( document is null )
return Apply( source, settings?.Edit ?? new EditSettings() );
var working = source.Clone();
try
{
if ( settings?.FindFilterPreset( document.SelectedFilterPresetId ) is { } preset )
ApplyEditPass( working, preset.PhotoEditSettings );
ApplyEditPass( working, document.BaseEdit );
if ( document.Layers is not null )
{
foreach ( var layer in document.Layers )
ApplyLayer( working, layer );
}
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Document render failed: {e.Message}" );
}
return working;
}
public static Bitmap Apply( Bitmap source, EditSettings edit )
{
if ( source is null )
return null;
var working = source.Clone();
try
{
working = ApplyFullEdit( working, edit );
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Edit pass failed: {e.Message}" );
}
return working;
}
static Bitmap ApplyFullEdit( Bitmap bmp, EditSettings edit )
{
bmp = ApplyTransform( bmp, edit );
ApplyEditPass( bmp, edit );
bmp = ApplyBorder( bmp, edit );
ApplyWatermark( bmp, edit );
return bmp;
}
static void ApplyEditPass( Bitmap bmp, EditSettings edit )
{
if ( bmp is null || edit is null )
return;
ApplyColor( bmp, edit );
ApplyFilter( bmp, edit.Filter );
ApplyDetail( bmp, edit );
ApplyVignette( bmp, edit.Vignette );
ApplyGrain( bmp, edit.Grain );
}
static void ApplyLayer( Bitmap canvas, SuperShotLayer layer )
{
if ( canvas is null || layer is null || !layer.Visible || layer.Opacity <= 0f )
return;
switch ( layer.Kind )
{
case SuperShotLayerKind.Text:
DrawTextLayer( canvas, layer );
break;
case SuperShotLayerKind.Image:
DrawImageLayer( canvas, layer );
break;
case SuperShotLayerKind.Adjustment:
ApplyEditPass( canvas, layer.Adjustment?.Edit );
break;
}
}
static void DrawTextLayer( Bitmap canvas, SuperShotLayer layer )
{
var text = layer.Text;
if ( text is null || string.IsNullOrWhiteSpace( text.Text ) )
return;
var rect = layer.PixelRect( canvas.Width, canvas.Height );
int lines = CountLines( text.Text );
float fontPx = layer.FontPixels( canvas.Height, lines );
var flags = AnchorToTextFlag( text.Anchor ) | TextFlag.WordWrap;
if ( text.Shadow )
{
var shadow = new TextRendering.Scope( text.Text, text.ShadowColor.WithAlpha( text.ShadowColor.a * layer.Opacity ), fontPx );
bmpDrawText( canvas, shadow, OffsetRect( rect, text.ShadowOffset ), flags );
}
var color = text.Color.WithAlpha( text.Color.a * layer.Opacity );
var scope = new TextRendering.Scope( text.Text, color, fontPx );
bmpDrawText( canvas, scope, rect, flags );
}
static void DrawImageLayer( Bitmap canvas, SuperShotLayer layer )
{
var path = layer.Image?.Path;
if ( string.IsNullOrWhiteSpace( path ) || !File.Exists( path ) )
return;
try
{
using var image = Bitmap.CreateFromBytes( File.ReadAllBytes( path ) );
if ( image is null || !image.IsValid )
return;
canvas.DrawBitmap( image, layer.PixelRect( canvas.Width, canvas.Height ) );
}
catch ( Exception e )
{
Log.Warning( $"[Supershot] Couldn't draw image layer '{path}': {e.Message}" );
}
}
static int CountLines( string text )
{
if ( string.IsNullOrEmpty( text ) )
return 1;
int lines = 1;
foreach ( var c in text )
{
if ( c == '\n' )
lines++;
}
return lines;
}
static Rect OffsetRect( Rect rect, Vector2 offset )
{
return new Rect( rect.Left + offset.x, rect.Top + offset.y, rect.Width, rect.Height );
}
static TextFlag AnchorToTextFlag( ShotAnchor anchor )
{
return anchor switch
{
ShotAnchor.TopLeft => TextFlag.LeftTop,
ShotAnchor.TopRight => TextFlag.RightTop,
ShotAnchor.BottomLeft => TextFlag.LeftBottom,
ShotAnchor.BottomRight => TextFlag.RightBottom,
_ => TextFlag.Center
};
}
static void bmpDrawText( Bitmap bmp, TextRendering.Scope scope, Rect rect, TextFlag flags )
{
bmp.DrawText( scope, rect, flags );
}
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 );
}
}