This guide helps you upgrade your Stream Deck plugin from StreamDeck-Tools v6.x to v7.0, including the new cross-platform SkiaSharp API surface.
AI-assisted migration: This document is designed to work well with AI coding assistants (Cursor, Copilot, etc.). Point your AI at this file and ask it to migrate your plugin -- the tables and recipes below give it everything it needs.
Update-Package StreamDeck-Tools
Or set the version manually in your .csproj:
<PackageReference Include="StreamDeck-Tools" Version="7.0.0" />Build your project. Everything should compile. You'll see [Obsolete] warnings on System.Drawing methods -- these point you to the SkiaSharp replacements.
In files where you do image rendering, add:
using SkiaSharp;The BarRaider.SdTools namespace already contains SkiaTools, SkiaGraphicsTools, and SkiaExtensionMethods.
| Before (v6.x) | After (v7.0) |
|---|---|
netstandard2.0 + net48 |
netstandard2.0 + net48 + net8.0 + net10.0 |
The library retains netstandard2.0 for broad compatibility and net48 for .NET Framework consumers. net8.0 (LTS) and net10.0 (current) are added for modern .NET. net9.0 is not included (EOL May 2026).
The library now ships with SkiaSharp (MIT license) as a cross-platform graphics backend. SkiaSharp works on Windows, macOS, and Linux without GDI+.
Every public method that accepted or returned System.Drawing types now has a SkiaSharp equivalent in a parallel class:
| Legacy Class | SkiaSharp Replacement |
|---|---|
Tools |
SkiaTools |
GraphicsTools |
SkiaGraphicsTools |
ExtensionMethods (on Image/Graphics) |
SkiaExtensionMethods (on SKBitmap/SKCanvas) |
All public methods using System.Drawing types are now marked [Obsolete] with messages pointing to their SkiaSharp replacements. They still compile and work on Windows but will not work on macOS/Linux with .NET 8+.
ISDConnection and SDConnection now support:
| Method | Status | Platform |
|---|---|---|
SetImageAsync(SKBitmap, ...) |
New | Cross-platform |
SetImageAsync(byte[], ...) |
New | Cross-platform (raw PNG bytes) |
SetImageAsync(string, ...) |
Unchanged | Cross-platform (base64) |
SetImageAsync(Image, ...) |
[Obsolete] |
Windows-only |
TitleParameters retains its existing constructors and properties unchanged. New read-only SkiaSharp properties are added:
| Property / Method | Type | Description |
|---|---|---|
FontFamilyName |
string |
Cross-platform font family name. Replaces FontFamily.Name. |
TitleSKColor |
SKColor |
Derived from TitleColor. |
TitleTypeface |
SKTypeface |
Derived from FontFamilyName + FontStyle. Cached. |
FontStyleToSKFontStyle() |
SKFontStyle |
Converts System.Drawing.FontStyle to SKFontStyle. |
Breaking change (macOS):
TitleParameters.FontFamily(theSystem.Drawing.FontFamilyproperty) is[Obsolete]and throwsPlatformNotSupportedExceptionon non-Windows. This is a hard crash, not just a warning. Any code that accesses.FontFamily(including inOnTitleParametersDidChangehandlers) must switch to.FontFamilyNameor.TitleTypeface.
New extension method SKCanvas.DrawTextLine(string, SKFont, SKPaint, SKPoint) draws text treating Y as the top of the text (matching System.Drawing.Graphics.DrawString behavior) and returns the Y position for the next line.
Use DrawTextLine instead of raw SKCanvas.DrawText when stacking multiple lines -- DrawText uses baseline Y which leads to positioning bugs.
DrawAndMeasureString is also available on SKCanvas and delegates to DrawTextLine.
PluginBase is [Obsolete]. Use KeypadBase (keys only), EncoderBase (dials only), or KeyAndEncoderBase (both).
| System.Drawing Type | SkiaSharp Type | Notes |
|---|---|---|
Image / Bitmap |
SKBitmap |
IDisposable. Use using blocks. |
Graphics |
SKCanvas |
Created from SKBitmap. IDisposable. |
Color |
SKColor |
Struct, no disposal needed. |
Font |
SKFont |
IDisposable. Holds typeface + size. |
FontFamily |
SKTypeface |
Use SKTypeface.FromFamilyName(...). |
FontStyle |
SKFontStyle |
SKFontStyle.Bold, SKFontStyle.Normal, etc. |
SolidBrush |
SKPaint |
Set Style = SKPaintStyle.Fill and Color. |
Pen |
SKPaint |
Set Style = SKPaintStyle.Stroke and StrokeWidth. |
PointF |
SKPoint |
|
Rectangle / RectangleF |
SKRect / SKRectI |
|
ColorTranslator.FromHtml(...) |
SkiaTools.ColorFromHex(...) |
Or SKColor.TryParse(...) directly. |
Image.FromFile(...) |
SkiaTools.LoadImage(path) |
|
Image.FromStream(...) |
SkiaTools.LoadImage(stream) |
| Before (System.Drawing) | After (SkiaSharp) |
|---|---|
new SolidBrush(color) |
new SKPaint { Color = skColor, Style = SKPaintStyle.Fill } |
ColorTranslator.FromHtml("#FF0000") |
SkiaTools.ColorFromHex("#FF0000") |
new Font("Arial", 12) |
SkiaTools.CreateFont("Arial", 12) |
Image.FromFile("icon.png") |
SkiaTools.LoadImage("icon.png") |
graphics.DrawString(text, font, brush, point) |
canvas.DrawTextLine(text, font, paint, point) |
graphics.FillRectangle(brush, rect) |
canvas.DrawRect(rect, paint) |
Legacy (Tools.*) |
Replacement (SkiaTools.*) |
Notes |
|---|---|---|
GenerateKeyImage(DeviceType, out Graphics) |
GenerateKeyImage(DeviceType, out SKCanvas) |
Returns SKBitmap + SKCanvas |
GenerateGenericKeyImage(out Graphics) |
GenerateGenericKeyImage(out SKCanvas) |
Returns SKBitmap + SKCanvas |
ImageToBase64(Image, bool) |
ImageToBase64(SKBitmap, bool) |
|
Base64StringToImage(string) returns Image |
Base64StringToImage(string) returns SKBitmap |
|
FileToBase64(string, bool) |
FileToBase64(string, bool) |
Identical signature |
LoadImage(string) returns Image |
LoadImage(string) returns SKBitmap |
|
LoadImage(Stream) returns Image |
LoadImage(Stream) returns SKBitmap |
|
ImageToSHA512(Image) |
ImageToSHA512(SKBitmap) |
|
CreateFont(...) returns Font |
CreateFont(string, float, SKFontStyle?) returns SKFont |
Style defaults to Normal |
| (no equivalent) | ColorFromHex(string) returns SKColor |
New helper |
Legacy (GraphicsTools.*) |
Replacement (SkiaGraphicsTools.*) |
Notes |
|---|---|---|
ColorFromHex(string) returns Color |
ColorFromHex(string) returns SKColor |
|
GenerateColorShades(string, int, int) returns Color |
GenerateColorShades(string, int, int) returns SKColor |
|
ResizeImage(Image, int, int) |
ResizeImage(SKBitmap, int, int) |
Returns SKBitmap |
ExtractRectangle(Image, ...) |
ExtractRectangle(SKBitmap, ...) |
Returns SKBitmap |
CreateOpacityImage(Image, float) |
CreateOpacityImage(SKBitmap, float) |
Returns SKBitmap |
DrawMultiLinedText(...) with Font, Color, PointF |
DrawMultiLinedText(...) with SKFont, SKColor, SKPoint |
Returns SKBitmap[] |
WrapStringToFitImage(string, TitleParameters, leftPad, rightPad, imageWidth) |
WrapStringToFitImage(string, SKFont, imageWidth, leftPad, rightPad) |
Takes SKFont instead of TitleParameters. Parameter order changed. |
| Legacy | Replacement | Notes |
|---|---|---|
Image.ToPngByteArray() |
SKBitmap.ToPngByteArray() |
|
Image.ToBase64(bool) |
SKBitmap.ToBase64(bool) |
|
Image.ToByteArray() (BMP) |
(removed) | Use ToPngByteArray() instead |
Graphics.DrawAndMeasureString(string, Font, Brush, PointF) |
SKCanvas.DrawTextLine(string, SKFont, SKPaint, SKPoint) |
Preferred. Y = top of text. Returns next-line Y. |
Graphics.GetTextCenter(...) |
SKCanvas.GetTextCenter(...) |
Same semantics |
Graphics.GetFontSizeWhereTextFitsImage(...) |
SKCanvas.GetFontSizeWhereTextFitsImage(...) |
Same semantics |
Graphics.AddTextPath(TitleParameters, ...) |
SKCanvas.AddTextPath(TitleParameters, ...) |
Uses TitleSKColor and TitleTypeface internally |
string.SplitToFitKey(TitleParameters, ...) |
string.SplitToFitKey(TitleParameters, SKFont, ...) |
Requires explicit SKFont parameter |
Color.ToHex() |
Use SKColor directly |
|
Brush.ToHex() |
Use SKPaint.Color directly |
| Legacy | Replacement | Notes |
|---|---|---|
SetImageAsync(Image, ...) |
SetImageAsync(SKBitmap, ...) |
Direct replacement |
SetImageAsync(byte[], ...) |
Alternative: pass raw PNG bytes |
Several methods (WrapStringToFitImage, SplitToFitKey, GetTextCenter, etc.) now require an SKFont. Create one from TitleParameters:
using var font = new SKFont(titleParameters.TitleTypeface, (float)titleParameters.FontSizeInPixelsScaledToDefaultImage);The SKFont is IDisposable -- use a using statement or dispose it manually.
SKBitmap, SKCanvas, SKFont, and SKPaint are all IDisposable. Wrap them in using statements.
SetImageAsync(SKBitmap) encodes the bitmap to PNG bytes synchronously before the async send, so it is safe to dispose the bitmap immediately after the await:
using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas))
{
// ... draw on canvas ...
canvas.Dispose();
await Connection.SetImageAsync(image);
}
// image is disposed here -- safe, encoding already completedFor bitmaps stored as fields (e.g., cached key images), dispose them in your action's Dispose() method.
Before (System.Drawing):
using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics))
{
graphics.FillRectangle(new SolidBrush(Color.White), 0, 0, image.Width, image.Height);
graphics.DrawString("Hello", new Font("Arial", 20), new SolidBrush(Color.Black), new PointF(10, 50));
graphics.Dispose();
await Connection.SetImageAsync(image);
}After (SkiaSharp):
using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas))
{
canvas.Clear(SKColors.White);
using var font = SkiaTools.CreateFont("Arial", 20);
using var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true };
canvas.DrawTextLine("Hello", font, paint, new SKPoint(10, 50));
canvas.Dispose();
await Connection.SetImageAsync(image);
}Before:
TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 20, Color.White, true, TitleVerticalAlignment.Middle);
using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics))
{
graphics.AddTextPath(tp, image.Height, image.Width, "My Title");
graphics.Dispose();
await Connection.SetImageAsync(image);
}After:
TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 20, Color.White, true, TitleVerticalAlignment.Middle);
using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas))
{
canvas.AddTextPath(tp, image.Height, image.Width, "My Title");
canvas.Dispose();
await Connection.SetImageAsync(image);
}
AddTextPathonSKCanvasusesTitleSKColorandTitleTypefaceinternally -- no changes needed beyond switching fromGraphicstoSKCanvas.
Before:
Color c = ColorTranslator.FromHtml("#FF0000");
// or
Color c = GraphicsTools.ColorFromHex("#FF0000");After:
SKColor c = SkiaTools.ColorFromHex("#FF0000");
// or
SKColor c = SkiaGraphicsTools.ColorFromHex("#FF0000");
// or
SKColor.TryParse("#FF0000", out SKColor c);Before:
Image img = Tools.LoadImage("icon.png");
string base64 = Tools.ImageToBase64(img, true);
await Connection.SetImageAsync(base64);After:
using SKBitmap img = SkiaTools.LoadImage("icon.png");
string base64 = SkiaTools.ImageToBase64(img, true);
await Connection.SetImageAsync(base64);Or skip the base64 step entirely:
using SKBitmap img = SkiaTools.LoadImage("icon.png");
await Connection.SetImageAsync(img);Before:
Image resized = GraphicsTools.ResizeImage(original, 72, 72);After:
SKBitmap resized = SkiaGraphicsTools.ResizeImage(original, 72, 72);Before:
Image faded = GraphicsTools.CreateOpacityImage(original, 0.5f);After:
SKBitmap faded = SkiaGraphicsTools.CreateOpacityImage(original, 0.5f);Before:
Font font = new Font("Arial", 14, FontStyle.Bold, GraphicsUnit.Pixel);After:
using SKFont font = SkiaTools.CreateFont("Arial", 14, SKFontStyle.Bold);Before:
float x = graphics.GetTextCenter("Hello", imageWidth, font, out bool fits);After:
float x = canvas.GetTextCenter("Hello", imageWidth, skFont, out bool fits);Use DrawTextLine with layout constants tuned for the default 144x144 key image:
const int MARGIN_LEFT = 8;
const int MARGIN_TOP = 3;
const int CONTENT_TOP_OFFSET = 10;
using SKBitmap img = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas);
float y = CONTENT_TOP_OFFSET;
using var font = SkiaTools.CreateFont("Arial", 22, SKFontStyle.Bold);
using var paint = new SKPaint { Color = SKColors.White, IsAntialias = true };
y = canvas.DrawTextLine("Title", font, paint, new SKPoint(MARGIN_LEFT, y)) + MARGIN_TOP;
y = canvas.DrawTextLine("Line 2", font, paint, new SKPoint(MARGIN_LEFT, y)) + MARGIN_TOP;
y = canvas.DrawTextLine("Line 3", font, paint, new SKPoint(MARGIN_LEFT, y)) + MARGIN_TOP;
canvas.Dispose();
await Connection.SetImageAsync(img);Each DrawTextLine call returns the Y position for the next line based on font.Spacing. Adding MARGIN_TOP provides a small gap between lines.
Based on analysis of real-world plugins:
Plugins that only use library helpers (GenerateKeyImage, SetImageAsync, SplitToFitKey, etc.) without importing System.Drawing in their own code.
What to do:
- Upgrade
StreamDeck-ToolsNuGet to v7.0 - Build -- you will see
[Obsolete]warnings. Everything still works. - When ready, follow the recipes above to switch to SkiaSharp APIs and eliminate warnings.
Plugins that use SolidBrush, ColorTranslator.FromHtml, Font, Image.FromFile, or Graphics.DrawString in their own code. Migration is mechanical renaming using the mapping tables above.
What to do:
- Upgrade the NuGet package
- Add
using SkiaSharp;to files with warnings - Replace System.Drawing types with SkiaSharp equivalents using the type mapping and recipes
Plugins that use GDI+ APIs without library wrappers:
| GDI+ API | SkiaSharp Equivalent |
|---|---|
GraphicsPath, AddString, StringFormat |
SKPath, SKCanvas.DrawTextOnPath |
LockBits, BitmapData, SetPixel |
SKBitmap.GetPixels(), SKBitmap.SetPixel() |
RotateTransform, matrix operations |
SKCanvas.RotateDegrees(), SKCanvas.SetMatrix() |
Graphics.FromImage rendering pipelines |
new SKCanvas(bitmap) |
What to do:
- Upgrade the NuGet package
- Add a direct
SkiaSharpNuGet reference to your plugin project - Rewrite GDI+ drawing logic using SkiaSharp native APIs
Does your plugin import System.Drawing in its own code?
|
+-- NO --> Tier 1: Upgrade the NuGet package. Done.
|
+-- YES --> Does it use GraphicsPath, LockBits, RotateTransform, or SetPixel?
|
+-- NO --> Tier 2: Mechanical rename using the mapping tables.
|
+-- YES --> Tier 3: Rewrite GDI+ logic with SkiaSharp.
| Release | What Happens |
|---|---|
| v7.0 (current) | All System.Drawing APIs marked [Obsolete] with replacement guidance. Everything still compiles and works on Windows. SkiaSharp APIs available in parallel. |
| v7.x | Stability and feedback period. No removals. Additional helpers may be added based on community feedback. |
| v8.0 (future) | System.Drawing APIs evaluated for removal. Removal only after sufficient migration window. |
- All System.Drawing APIs continue to work on Windows across all four TFMs.
- For macOS/Linux with
net8.0/net10.0, you must use the SkiaSharp APIs. System.Drawing throwsPlatformNotSupportedException. TitleParametersconstructor and properties are unchanged. The SkiaSharp properties are purely additive.- Exact pixel parity between System.Drawing and SkiaSharp text rendering is not guaranteed. Functional parity is the target.
- SkiaSharp 3.119.2 is MIT-licensed. No licensing impact on plugin developers.
When migrating to SkiaSharp APIs, add these to your files:
using SkiaSharp;
using BarRaider.SdTools; // SkiaTools, SkiaGraphicsTools, SkiaExtensionMethodsusing BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using SkiaSharp;
using System.Threading.Tasks;
[PluginActionId("com.example.myplugin")]
public class MyAction : KeypadBase
{
public MyAction(ISDConnection connection, InitialPayload payload) : base(connection, payload) { }
public async override void KeyPressed(KeyPayload payload)
{
using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas))
{
canvas.Clear(SKColors.DarkBlue);
using var font = SkiaTools.CreateFont("Arial", 18, SKFontStyle.Bold);
using var paint = new SKPaint { Color = SKColors.White, IsAntialias = true };
float x = canvas.GetTextCenter("Pressed!", image.Width, font);
canvas.DrawTextLine("Pressed!", font, paint, new SKPoint(x, image.Height / 2f - 10));
canvas.Dispose();
await Connection.SetImageAsync(image);
}
}
public async override void KeyReleased(KeyPayload payload)
{
await Connection.SetDefaultImageAsync();
}
public override void OnTick() { }
public override void Dispose() { }
public override void ReceivedSettings(ReceivedSettingsPayload payload) { }
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) { }
}