|
| 1 | +# Migrating to StreamDeck-Tools v7.0 |
| 2 | + |
| 3 | +This guide helps you upgrade your Stream Deck plugin from StreamDeck-Tools v6.x to v7.0. |
| 4 | + |
| 5 | +> **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. |
| 6 | +
|
| 7 | +## What's New in v7.0 |
| 8 | + |
| 9 | +- **Cross-platform support** -- plugins can now run on both Windows and macOS |
| 10 | +- **SkiaSharp graphics** -- new cross-platform drawing APIs alongside the existing System.Drawing ones |
| 11 | +- **.NET 10 support** -- library targets `netstandard2.0`, `net48`, `net8.0`, and `net10.0` |
| 12 | +- **Self-contained deployment** -- ship your plugin with the runtime included, no user install required |
| 13 | + |
| 14 | +## Do I Need to Change My Code? |
| 15 | + |
| 16 | +Maybe not! Here's a quick check: |
| 17 | + |
| 18 | +``` |
| 19 | +Does your plugin use System.Drawing directly in its own code? |
| 20 | + │ |
| 21 | + ├── NO → Just upgrade the NuGet package. Build. Done. |
| 22 | + │ You'll see [Obsolete] warnings -- these are informational. |
| 23 | + │ Everything still works on Windows. |
| 24 | + │ |
| 25 | + └── YES → Does it use GraphicsPath, LockBits, RotateTransform, or SetPixel? |
| 26 | + │ |
| 27 | + ├── NO → Straightforward rename using the tables below. |
| 28 | + │ |
| 29 | + └── YES → Rewrite those sections using SkiaSharp directly. |
| 30 | +``` |
| 31 | + |
| 32 | +Most plugins (roughly half) fall into the "just upgrade" category. |
| 33 | + |
| 34 | +## Step 1: Upgrade the NuGet Package |
| 35 | + |
| 36 | +``` |
| 37 | +Update-Package StreamDeck-Tools |
| 38 | +``` |
| 39 | + |
| 40 | +Or set the version manually in your `.csproj`: |
| 41 | + |
| 42 | +```xml |
| 43 | +<PackageReference Include="StreamDeck-Tools" Version="7.0.0-beta.2" /> |
| 44 | +``` |
| 45 | + |
| 46 | +Build your project. Everything should compile. You'll see `[Obsolete]` warnings on System.Drawing methods -- these point you to the SkiaSharp replacements. |
| 47 | + |
| 48 | +## Step 2: Add SkiaSharp Usings |
| 49 | + |
| 50 | +In files where you do image rendering, add: |
| 51 | + |
| 52 | +```csharp |
| 53 | +using SkiaSharp; |
| 54 | +``` |
| 55 | + |
| 56 | +The `BarRaider.SdTools` namespace already contains `SkiaTools`, `SkiaGraphicsTools`, and `SkiaExtensionMethods`. |
| 57 | + |
| 58 | +## Step 3: Replace System.Drawing Calls |
| 59 | + |
| 60 | +### Class Mapping |
| 61 | + |
| 62 | +| Legacy Class | Replacement | |
| 63 | +|---|---| |
| 64 | +| `Tools` (image/font methods) | `SkiaTools` | |
| 65 | +| `GraphicsTools` | `SkiaGraphicsTools` | |
| 66 | +| `ExtensionMethods` (on `Image`/`Graphics`) | `SkiaExtensionMethods` (on `SKBitmap`/`SKCanvas`) | |
| 67 | + |
| 68 | +### Type Mapping |
| 69 | + |
| 70 | +| System.Drawing | SkiaSharp | Notes | |
| 71 | +|---|---|---| |
| 72 | +| `Image` / `Bitmap` | `SKBitmap` | Disposable | |
| 73 | +| `Graphics` | `SKCanvas` | Created from `SKBitmap` | |
| 74 | +| `Color` | `SKColor` | Struct, no disposal | |
| 75 | +| `Font` | `SKFont` | Disposable | |
| 76 | +| `FontFamily` | `SKTypeface` | `SKTypeface.FromFamilyName(...)` | |
| 77 | +| `FontStyle` | `SKFontStyle` | `SKFontStyle.Bold`, `.Normal`, etc. | |
| 78 | +| `SolidBrush` | `SKPaint` | `Style = SKPaintStyle.Fill` | |
| 79 | +| `Pen` | `SKPaint` | `Style = SKPaintStyle.Stroke` | |
| 80 | +| `PointF` | `SKPoint` | | |
| 81 | +| `Rectangle` / `RectangleF` | `SKRect` / `SKRectI` | | |
| 82 | +| `ColorTranslator.FromHtml(...)` | `SkiaTools.ColorFromHex(...)` | | |
| 83 | +| `Image.FromFile(...)` | `SkiaTools.LoadImage(path)` | | |
| 84 | + |
| 85 | +### Common Patterns |
| 86 | + |
| 87 | +| Before (System.Drawing) | After (SkiaSharp) | |
| 88 | +|---|---| |
| 89 | +| `new SolidBrush(color)` | `new SKPaint { Color = skColor, Style = SKPaintStyle.Fill }` | |
| 90 | +| `ColorTranslator.FromHtml("#FF0000")` | `SkiaTools.ColorFromHex("#FF0000")` | |
| 91 | +| `new Font("Arial", 12)` | `SkiaTools.CreateFont("Arial", 12)` | |
| 92 | +| `Image.FromFile("icon.png")` | `SkiaTools.LoadImage("icon.png")` | |
| 93 | +| `graphics.DrawString(text, font, brush, point)` | `canvas.DrawText(text, x, y, font, paint)` | |
| 94 | +| `graphics.FillRectangle(brush, rect)` | `canvas.DrawRect(rect, paint)` | |
| 95 | + |
| 96 | +### Method Migration |
| 97 | + |
| 98 | +| Legacy | Replacement | |
| 99 | +|---|---| |
| 100 | +| `Tools.GenerateKeyImage(type, out Graphics)` | `SkiaTools.GenerateKeyImage(type, out SKCanvas)` | |
| 101 | +| `Tools.GenerateGenericKeyImage(out Graphics)` | `SkiaTools.GenerateGenericKeyImage(out SKCanvas)` | |
| 102 | +| `Tools.ImageToBase64(Image, bool)` | `SkiaTools.ImageToBase64(SKBitmap, bool)` | |
| 103 | +| `Tools.Base64StringToImage(string)` → `Image` | `SkiaTools.Base64StringToImage(string)` → `SKBitmap` | |
| 104 | +| `Tools.FileToBase64(string, bool)` | `SkiaTools.FileToBase64(string, bool)` | |
| 105 | +| `Tools.LoadImage(string/Stream)` → `Image` | `SkiaTools.LoadImage(string/Stream)` → `SKBitmap` | |
| 106 | +| `Tools.CreateFont(...)` → `Font` | `SkiaTools.CreateFont(name, size, SKFontStyle?)` → `SKFont` | |
| 107 | +| `GraphicsTools.ResizeImage(Image, w, h)` | `SkiaGraphicsTools.ResizeImage(SKBitmap, w, h)` | |
| 108 | +| `GraphicsTools.CreateOpacityImage(Image, f)` | `SkiaGraphicsTools.CreateOpacityImage(SKBitmap, f)` | |
| 109 | +| `Connection.SetImageAsync(Image)` | `Connection.SetImageAsync(SKBitmap)` | |
| 110 | +| `graphics.AddTextPath(tp, h, w, text)` | `canvas.AddTextPath(tp, h, w, text)` | |
| 111 | +| `graphics.GetTextCenter(text, w, font)` | `canvas.GetTextCenter(text, w, skFont)` | |
| 112 | +| `text.SplitToFitKey(tp, ...)` | `text.SplitToFitKey(tp, skFont, ...)` | |
| 113 | + |
| 114 | +> **Coordinate difference:** `Graphics.DrawString` positions text from the top-left corner. `SKCanvas.DrawText` uses the text **baseline**. You may need to adjust Y coordinates when migrating (add the font ascent to the Y value). |
| 115 | +
|
| 116 | +> **SKFont from TitleParameters:** Some methods now require an `SKFont`. Create one from `TitleParameters` like this: `new SKFont(titleParameters.TitleTypeface, (float)titleParameters.FontSizeInPixelsScaledToDefaultImage)`. Remember to dispose it. |
| 117 | +
|
| 118 | +## Code Recipes |
| 119 | + |
| 120 | +### Key Image Rendering |
| 121 | + |
| 122 | +**Before:** |
| 123 | +```csharp |
| 124 | +using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics)) |
| 125 | +{ |
| 126 | + graphics.FillRectangle(new SolidBrush(Color.White), 0, 0, image.Width, image.Height); |
| 127 | + graphics.DrawString("Hello", new Font("Arial", 20), new SolidBrush(Color.Black), new PointF(10, 50)); |
| 128 | + graphics.Dispose(); |
| 129 | + await Connection.SetImageAsync(image); |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +**After:** |
| 134 | +```csharp |
| 135 | +using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas)) |
| 136 | +{ |
| 137 | + canvas.Clear(SKColors.White); |
| 138 | + using (var font = SkiaTools.CreateFont("Arial", 20)) |
| 139 | + using (var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true }) |
| 140 | + { |
| 141 | + canvas.DrawText("Hello", 10, 50, font, paint); |
| 142 | + } |
| 143 | + canvas.Dispose(); |
| 144 | + await Connection.SetImageAsync(image); |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +### Title Text with TitleParameters |
| 149 | + |
| 150 | +**Before:** |
| 151 | +```csharp |
| 152 | +TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 20, Color.White, true, TitleVerticalAlignment.Middle); |
| 153 | +using (Image image = Tools.GenerateGenericKeyImage(out Graphics graphics)) |
| 154 | +{ |
| 155 | + graphics.AddTextPath(tp, image.Height, image.Width, "My Title"); |
| 156 | + graphics.Dispose(); |
| 157 | + await Connection.SetImageAsync(image); |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +**After:** |
| 162 | +```csharp |
| 163 | +TitleParameters tp = new TitleParameters(new FontFamily("Arial"), FontStyle.Bold, 20, Color.White, true, TitleVerticalAlignment.Middle); |
| 164 | +using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas)) |
| 165 | +{ |
| 166 | + canvas.AddTextPath(tp, image.Height, image.Width, "My Title"); |
| 167 | + canvas.Dispose(); |
| 168 | + await Connection.SetImageAsync(image); |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +### Color Parsing |
| 173 | + |
| 174 | +**Before:** `Color c = ColorTranslator.FromHtml("#FF0000");` |
| 175 | + |
| 176 | +**After:** `SKColor c = SkiaTools.ColorFromHex("#FF0000");` |
| 177 | + |
| 178 | +### Image Loading |
| 179 | + |
| 180 | +**Before:** |
| 181 | +```csharp |
| 182 | +Image img = Tools.LoadImage("icon.png"); |
| 183 | +string base64 = Tools.ImageToBase64(img, true); |
| 184 | +await Connection.SetImageAsync(base64); |
| 185 | +``` |
| 186 | + |
| 187 | +**After:** |
| 188 | +```csharp |
| 189 | +using (SKBitmap img = SkiaTools.LoadImage("icon.png")) |
| 190 | +{ |
| 191 | + string base64 = SkiaTools.ImageToBase64(img, true); |
| 192 | + await Connection.SetImageAsync(base64); |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +### Font Creation |
| 197 | + |
| 198 | +**Before:** `Font font = new Font("Arial", 14, FontStyle.Bold, GraphicsUnit.Pixel);` |
| 199 | + |
| 200 | +**After:** |
| 201 | +```csharp |
| 202 | +using (SKFont font = SkiaTools.CreateFont("Arial", 14, SKFontStyle.Bold)) |
| 203 | +{ |
| 204 | + // use font for drawing |
| 205 | +} |
| 206 | +``` |
| 207 | + |
| 208 | +## Going Cross-Platform (Optional) |
| 209 | + |
| 210 | +If you want your plugin to run on both Windows and macOS: |
| 211 | + |
| 212 | +### 1. Target .NET 10 with Self-Contained Deployment |
| 213 | + |
| 214 | +```xml |
| 215 | +<PropertyGroup> |
| 216 | + <TargetFramework>net10.0</TargetFramework> |
| 217 | + <SelfContained>true</SelfContained> |
| 218 | + <PublishTrimmed>true</PublishTrimmed> |
| 219 | + <TrimMode>partial</TrimMode> |
| 220 | + <RuntimeIdentifiers>win-x64;osx-x64</RuntimeIdentifiers> |
| 221 | +</PropertyGroup> |
| 222 | +``` |
| 223 | + |
| 224 | +`osx-x64` is recommended for macOS -- it runs natively on Intel and via Rosetta 2 on Apple Silicon, covering all Mac users with a single binary. |
| 225 | + |
| 226 | +### 2. Update manifest.json |
| 227 | + |
| 228 | +```json |
| 229 | +{ |
| 230 | + "CodePath": "com.your.plugin", |
| 231 | + "CodePathWin": "win-x64/com.your.plugin.exe", |
| 232 | + "CodePathMac": "osx-x64/com.your.plugin", |
| 233 | + "OS": [ |
| 234 | + { "Platform": "windows", "MinimumVersion": "10" }, |
| 235 | + { "Platform": "mac", "MinimumVersion": "13" } |
| 236 | + ] |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +### 3. Publish for Both Platforms |
| 241 | + |
| 242 | +```powershell |
| 243 | +dotnet publish -c Release -r win-x64 |
| 244 | +dotnet publish -c Release -r osx-x64 |
| 245 | +``` |
| 246 | + |
| 247 | +Assemble both outputs into your `.sdPlugin` folder with `win-x64/` and `osx-x64/` subdirectories, alongside shared assets (manifest, images, Property Inspector). |
| 248 | + |
| 249 | +### Known Gotcha: System.Management and Trimming |
| 250 | + |
| 251 | +`System.Management` (WMI) does not work in trimmed .NET 10 builds due to COM interop incompatibility. If your plugin or any dependency uses WMI, replace those calls with alternatives (e.g., Windows Registry reads). This only affects trimmed self-contained builds. |
| 252 | + |
| 253 | +## Deprecation Timeline |
| 254 | + |
| 255 | +| Release | What Happens | |
| 256 | +|---|---| |
| 257 | +| **v7.0** (current) | System.Drawing APIs marked `[Obsolete]` with guidance. Everything still works on Windows. SkiaSharp APIs available in parallel. | |
| 258 | +| **v7.x** | Stability period. No removals. | |
| 259 | +| **v8.0** (future) | System.Drawing APIs evaluated for removal based on adoption. | |
| 260 | + |
| 261 | +## Compatibility Notes |
| 262 | + |
| 263 | +- System.Drawing APIs continue to work on **Windows** across all TFMs. |
| 264 | +- For **macOS/Linux**, you must use the SkiaSharp APIs. System.Drawing throws `PlatformNotSupportedException` on non-Windows. |
| 265 | +- `TitleParameters` is unchanged. The new SkiaSharp properties (`TitleSKColor`, `TitleTypeface`) are additive. |
| 266 | +- SkiaSharp 3.x is MIT-licensed. No licensing impact. |
| 267 | + |
| 268 | +## Complete Example: Cross-Platform Plugin Action |
| 269 | + |
| 270 | +```csharp |
| 271 | +using BarRaider.SdTools; |
| 272 | +using BarRaider.SdTools.Wrappers; |
| 273 | +using SkiaSharp; |
| 274 | +using System.Threading.Tasks; |
| 275 | + |
| 276 | +[PluginActionId("com.example.myplugin")] |
| 277 | +public class MyAction : KeypadBase |
| 278 | +{ |
| 279 | + public MyAction(ISDConnection connection, InitialPayload payload) : base(connection, payload) { } |
| 280 | + |
| 281 | + public async override void KeyPressed(KeyPayload payload) |
| 282 | + { |
| 283 | + using (SKBitmap image = SkiaTools.GenerateGenericKeyImage(out SKCanvas canvas)) |
| 284 | + { |
| 285 | + canvas.Clear(SKColors.DarkBlue); |
| 286 | + |
| 287 | + using (var font = SkiaTools.CreateFont("Arial", 18, SKFontStyle.Bold)) |
| 288 | + using (var paint = new SKPaint { Color = SKColors.White, IsAntialias = true }) |
| 289 | + { |
| 290 | + float x = canvas.GetTextCenter("Pressed!", image.Width, font); |
| 291 | + canvas.DrawText("Pressed!", x, image.Height / 2f, font, paint); |
| 292 | + } |
| 293 | + |
| 294 | + canvas.Dispose(); |
| 295 | + await Connection.SetImageAsync(image); |
| 296 | + } |
| 297 | + } |
| 298 | + |
| 299 | + public async override void KeyReleased(KeyPayload payload) |
| 300 | + { |
| 301 | + await Connection.SetDefaultImageAsync(); |
| 302 | + } |
| 303 | + |
| 304 | + public override void OnTick() { } |
| 305 | + public override void Dispose() { } |
| 306 | + public override void ReceivedSettings(ReceivedSettingsPayload payload) { } |
| 307 | + public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) { } |
| 308 | +} |
| 309 | +``` |
0 commit comments