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 @@ +