diff --git a/Daybreak.Core/Daybreak.Core.csproj b/Daybreak.Core/Daybreak.Core.csproj
index 80865133..4ddde62c 100644
--- a/Daybreak.Core/Daybreak.Core.csproj
+++ b/Daybreak.Core/Daybreak.Core.csproj
@@ -65,6 +65,7 @@
+
diff --git a/Daybreak.Core/Services/Screenshots/ScreenshotService.cs b/Daybreak.Core/Services/Screenshots/ScreenshotService.cs
index 4ba200fd..7a495b54 100644
--- a/Daybreak.Core/Services/Screenshots/ScreenshotService.cs
+++ b/Daybreak.Core/Services/Screenshots/ScreenshotService.cs
@@ -1,13 +1,15 @@
using System.Collections.Concurrent;
-#if WINDOWS
-using System.Drawing;
-#endif
using System.Extensions.Core;
+using System.Numerics;
+using System.Runtime.InteropServices;
using Daybreak.Shared.Models;
using Daybreak.Shared.Models.ColorPalette;
using Daybreak.Shared.Services.Screenshots;
using Microsoft.Extensions.Logging;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
using static Daybreak.Shared.Models.Themes.Theme;
+using Color = System.Drawing.Color;
namespace Daybreak.Services.Screenshots;
@@ -17,11 +19,9 @@ public sealed class ScreenshotService(
{
private const string GuildWarsFolder = "Guild Wars";
private const string ScreensFolder = "Screens";
+ private const int SampleQuality = 10; // Sample every Nth pixel for performance
private static readonly string ScreenshotsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), GuildWarsFolder, ScreensFolder, "");
-#if WINDOWS
- private static readonly ColorThiefDotNet.ColorThief ColorThief = new();
-#endif
private static readonly ConcurrentDictionary EntryCache = [];
private readonly ILogger logger = logger;
@@ -62,14 +62,14 @@ public sealed class ScreenshotService(
return cachedEntry;
}
-#if WINDOWS
try
{
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
- using var bitmap = new Bitmap(fileStream);
- var dominantColor = ColorThief.GetColor(bitmap, quality: 2);
- var closestAccent = GetClosestAccentColor(Color.FromArgb(dominantColor.Color.A, dominantColor.Color.R, dominantColor.Color.G, dominantColor.Color.B));
- var entry = new ScreenshotEntry(path, closestAccent, dominantColor.IsDark ? LightDarkMode.Dark : LightDarkMode.Light);
+ using var image = Image.Load(fileStream);
+
+ var (dominantColor, isDark) = GetDominantColor(image);
+ var closestAccent = GetClosestAccentColor(dominantColor);
+ var entry = new ScreenshotEntry(path, closestAccent, isDark ? LightDarkMode.Dark : LightDarkMode.Light);
EntryCache[path] = entry;
scopedLogger.LogDebug("Screenshot entry created and cached for path {path}", path);
return entry;
@@ -79,15 +79,107 @@ public sealed class ScreenshotService(
scopedLogger.LogError(e, "Failed to get screenshot entry from path {path}", path);
return default;
}
-#else
- // On non-Windows platforms, return a default entry without color extraction
- var entry = new ScreenshotEntry(path, AccentColor.Accents[0], LightDarkMode.Dark);
- EntryCache[path] = entry;
- return entry;
-#endif
}
-#if WINDOWS
+ private static (Color Color, bool IsDark) GetDominantColor(Image image)
+ {
+ long totalR = 0, totalG = 0, totalB = 0;
+ int pixelCount = 0;
+
+ image.ProcessPixelRows(accessor =>
+ {
+ for (int y = 0; y < accessor.Height; y += SampleQuality)
+ {
+ var rowSpan = accessor.GetRowSpan(y);
+ var byteSpan = MemoryMarshal.AsBytes(rowSpan);
+
+ // Process using SIMD where possible
+ // Each Rgba32 pixel is 4 bytes: R, G, B, A
+ var vectorSize = Vector.Count;
+ var pixelsPerVector = vectorSize / 4; // 4 bytes per pixel
+
+ int x = 0;
+
+ // SIMD path: process multiple sampled pixels at once
+ if (Vector.IsHardwareAccelerated && rowSpan.Length >= pixelsPerVector * SampleQuality)
+ {
+ var sumR = Vector.Zero;
+ var sumG = Vector.Zero;
+ var sumB = Vector.Zero;
+ int simdPixelCount = 0;
+
+ // Collect sampled pixels for SIMD processing
+ while (x + (Vector.Count * SampleQuality) <= rowSpan.Length)
+ {
+ // Gather sampled pixels into arrays for vectorization
+ Span rValues = stackalloc int[Vector.Count];
+ Span gValues = stackalloc int[Vector.Count];
+ Span bValues = stackalloc int[Vector.Count];
+
+ for (int i = 0; i < Vector.Count && x < rowSpan.Length; i++, x += SampleQuality)
+ {
+ var pixel = rowSpan[x];
+ rValues[i] = pixel.R;
+ gValues[i] = pixel.G;
+ bValues[i] = pixel.B;
+ simdPixelCount++;
+ }
+
+ var rVec = new Vector(rValues);
+ var gVec = new Vector(gValues);
+ var bVec = new Vector(bValues);
+
+ // Widen to long and accumulate
+ Vector.Widen(rVec, out var rLow, out var rHigh);
+ Vector.Widen(gVec, out var gLow, out var gHigh);
+ Vector.Widen(bVec, out var bLow, out var bHigh);
+
+ sumR += rLow + rHigh;
+ sumG += gLow + gHigh;
+ sumB += bLow + bHigh;
+ }
+
+ // Sum up the vector lanes
+ for (int i = 0; i < Vector.Count; i++)
+ {
+ totalR += sumR[i];
+ totalG += sumG[i];
+ totalB += sumB[i];
+ }
+ pixelCount += simdPixelCount;
+ }
+
+ // Scalar fallback for remaining pixels
+ for (; x < rowSpan.Length; x += SampleQuality)
+ {
+ var pixel = rowSpan[x];
+ totalR += pixel.R;
+ totalG += pixel.G;
+ totalB += pixel.B;
+ pixelCount++;
+ }
+ }
+ });
+
+ if (pixelCount == 0)
+ {
+ return (Color.Black, true);
+ }
+
+ var avgR = (byte)(totalR / pixelCount);
+ var avgG = (byte)(totalG / pixelCount);
+ var avgB = (byte)(totalB / pixelCount);
+
+ var color = Color.FromArgb(255, avgR, avgG, avgB);
+
+ // Calculate perceived brightness using standard formula
+ // Values > 128 are generally considered "light"
+ var brightness = (0.299 * avgR) + (0.587 * avgG) + (0.114 * avgB);
+ var isDark = brightness < 128;
+
+ return (color, isDark);
+ }
+
private static AccentColor GetClosestAccentColor(Color color)
{
var closest = AccentColor.Accents[0];
@@ -112,16 +204,4 @@ public sealed class ScreenshotService(
var bDiff = c1.B - c2.B;
return (rDiff * rDiff) + (gDiff * gDiff) + (bDiff * bDiff);
}
-
- private static Bitmap? GetBitmap(string path)
- {
- var image = Bitmap.FromFile(path);
- if (image is not Bitmap bitmapImage)
- {
- return default;
- }
-
- return bitmapImage;
- }
-#endif
}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 9c582f48..837d4007 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -54,6 +54,7 @@
+