Linux support for color extraction (Closes #1447) (#1448)

This commit is contained in:
2026-02-18 11:50:15 -08:00
parent fe9178c66d
commit 85593ea94b
3 changed files with 112 additions and 30 deletions
+1
View File
@@ -65,6 +65,7 @@
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Debug" />
<PackageReference Include="SharpCompress" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
</ItemGroup>
@@ -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<string, ScreenshotEntry> EntryCache = [];
private readonly ILogger<ScreenshotService> 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<Rgba32>(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<Rgba32> 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<byte>.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<long>.Zero;
var sumG = Vector<long>.Zero;
var sumB = Vector<long>.Zero;
int simdPixelCount = 0;
// Collect sampled pixels for SIMD processing
while (x + (Vector<int>.Count * SampleQuality) <= rowSpan.Length)
{
// Gather sampled pixels into arrays for vectorization
Span<int> rValues = stackalloc int[Vector<int>.Count];
Span<int> gValues = stackalloc int[Vector<int>.Count];
Span<int> bValues = stackalloc int[Vector<int>.Count];
for (int i = 0; i < Vector<int>.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<int>(rValues);
var gVec = new Vector<int>(gValues);
var bVec = new Vector<int>(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<long>.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
}
+1
View File
@@ -54,6 +54,7 @@
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="SevenZipExtractor" Version="1.0.19" />
<PackageVersion Include="SharpCompress" Version="0.46.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.10" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="10.1.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.1.2" />