From 835e0d34fc6079b5b7009df703916aed19f61b4e Mon Sep 17 00:00:00 2001 From: Raspberry-Monster Date: Thu, 23 Oct 2025 22:38:31 +0800 Subject: [PATCH] chores: Bump Dependencies, Remove ImageSharp --- .../BetterLyrics.WinUI3 (Package).wapproj | 294 ++++++------- .../BetterLyrics.WinUI3.csproj | 31 +- .../BetterLyrics.WinUI3/Helper/ImageHelper.cs | 180 +++++--- .../Helper/PaletteHelper.cs | 36 +- .../AlbumArtSearchService.cs | 13 +- .../IAlbumArtSearchService.cs | 3 +- .../MediaSessionsService.AlbumArtUpdater.cs | 23 +- .../MediaSessionsService.cs | 13 +- BetterLyrics.sln | 14 + ColorThief.WinUI3/CMap.cs | 111 +++++ ColorThief.WinUI3/Color.cs | 94 +++++ ColorThief.WinUI3/ColorThief.WinUI3.cs | 84 ++++ ColorThief.WinUI3/ColorThief.WinUI3.csproj | 14 + ColorThief.WinUI3/ColorThief.cs | 77 ++++ ColorThief.WinUI3/HslColor.cs | 28 ++ ColorThief.WinUI3/Mmcq.cs | 387 ++++++++++++++++++ ColorThief.WinUI3/QuantizedColor.cs | 23 ++ ColorThief.WinUI3/VBox.cs | 163 ++++++++ 18 files changed, 1311 insertions(+), 277 deletions(-) create mode 100644 ColorThief.WinUI3/CMap.cs create mode 100644 ColorThief.WinUI3/Color.cs create mode 100644 ColorThief.WinUI3/ColorThief.WinUI3.cs create mode 100644 ColorThief.WinUI3/ColorThief.WinUI3.csproj create mode 100644 ColorThief.WinUI3/ColorThief.cs create mode 100644 ColorThief.WinUI3/HslColor.cs create mode 100644 ColorThief.WinUI3/Mmcq.cs create mode 100644 ColorThief.WinUI3/QuantizedColor.cs create mode 100644 ColorThief.WinUI3/VBox.cs diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj index 2dfc48a..8bc7a88 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3 (Package)/BetterLyrics.WinUI3 (Package).wapproj @@ -1,150 +1,150 @@ - - 15.0 - - - - Debug - x86 - - - Release - x86 - - - Debug - x64 - - - Release - x64 - - - Debug - ARM64 - - - Release - ARM64 - - - - $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ - BetterLyrics.WinUI3\ - - - - 6576cd19-ef92-4099-b37d-e2d8ebdb6bf5 - 10.0.26100.0 - 10.0.17763.0 - net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) - zh-CN - True - ..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj - False - SHA256 - False - True - x86|x64 - True - 0 - BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx - - - Always - en-US - - - Always - en-US - - - Always - en-US - - - Always - en-US - - - Always - en-US - - - Always - en-US - - - - Designer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true -True - Properties\PublishProfiles\win-$(Platform).pubxml - - - - - - - + + 15.0 + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + BetterLyrics.WinUI3\ + + + + 6576cd19-ef92-4099-b37d-e2d8ebdb6bf5 + 10.0.26100.0 + 10.0.17763.0 + net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback) + zh-CN + True + ..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj + False + SHA256 + False + True + x86|x64 + True + 0 + BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + Always + en-US + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + True + Properties\PublishProfiles\win-$(Platform).pubxml + + + + + + + \ No newline at end of file diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj index 7a44072..ffff3cf 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/BetterLyrics.WinUI3.csproj @@ -49,7 +49,6 @@ - @@ -66,14 +65,14 @@ - + - - + + - - + + @@ -82,20 +81,20 @@ - - - + + - - - - - - - + + + + + + + + diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs index 0d635ac..a4039f3 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/ImageHelper.cs @@ -9,12 +9,6 @@ using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Text; using Microsoft.UI; using Microsoft.UI.Xaml.Media.Imaging; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; using System; using System.Collections.Generic; using System.IO; @@ -52,7 +46,7 @@ namespace BetterLyrics.WinUI3.Helper return RandomAccessStreamReference.CreateFromStream(stream); } - public static async Task CreateTextPlaceholderBytesAsync(int width, int height) + public static async Task CreateTextPlaceholderBytesAsync(int width, int height) { using var device = CanvasDevice.GetSharedDevice(); using var renderTarget = new CanvasRenderTarget(device, width, height, 96); @@ -82,34 +76,29 @@ namespace BetterLyrics.WinUI3.Helper } // 保存为 PNG 并转为 byte[] - using var stream = new InMemoryRandomAccessStream(); + var stream = new InMemoryRandomAccessStream(); await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png); - var buffer = new byte[stream.Size]; - using (var reader = new DataReader(stream.GetInputStreamAt(0))) - { - await reader.LoadAsync((uint)stream.Size); - reader.ReadBytes(buffer); - } - return buffer; + stream.Seek(0); + return stream; } - public static Task GetAccentColorFromByteAsync(byte[] bytes, PaletteGeneratorType generatorType) + public static Task GetAccentColorFromByteAsync(BitmapDecoder decoder, PaletteGeneratorType generatorType) { return generatorType switch { - PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorFromByteAsync(bytes), - PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorFromByteAsync(bytes), - _ => throw new ArgumentOutOfRangeException("generatorType"), + PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorFromByteAsync(decoder), + PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorFromByteAsync(decoder), + _ => throw new ArgumentOutOfRangeException(nameof(generatorType)), }; } - public static Task GetAccentColorsFromByteAsync(byte[] bytes, int count, PaletteGeneratorType generatorType, bool? isDark = null) + public static Task GetAccentColorsFromByteAsync(BitmapDecoder decoder, int count, PaletteGeneratorType generatorType, bool? isDark = null) { return generatorType switch { - PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorsFromByteAsync(bytes, count, isDark), - PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorsFromByteAsync(bytes, count, isDark), - _ => throw new ArgumentOutOfRangeException("generatorType"), + PaletteGeneratorType.OctTree => PaletteHelper.OctTreeGetAccentColorsFromByteAsync(decoder, count, isDark), + PaletteGeneratorType.MedianCut => PaletteHelper.MedianCutGetAccentColorsFromByteAsync(decoder, count, isDark), + _ => throw new ArgumentOutOfRangeException(nameof(generatorType)), }; } @@ -166,13 +155,12 @@ namespace BetterLyrics.WinUI3.Helper // return stream; //} - public static async Task ToByteArrayAsync(IRandomAccessStreamReference streamRef) + public static async Task ToBufferAsync(IRandomAccessStreamReference streamRef) { using IRandomAccessStream stream = await streamRef.OpenReadAsync(); - using var reader = new DataReader(stream); - await reader.LoadAsync((uint)stream.Size); - byte[] buffer = new byte[stream.Size]; - reader.ReadBytes(buffer); + stream.Seek(0); + var buffer = new Windows.Storage.Streams.Buffer((uint)stream.Size); + await stream.ReadAsync(buffer, (uint)stream.Size, InputStreamOptions.None); return buffer; } @@ -193,55 +181,111 @@ namespace BetterLyrics.WinUI3.Helper return (double)(sum / (pixels.Length / 4)); } - public static async Task MakeSquareWithThemeColor(byte[] imageBytes, PaletteGeneratorType generatorType) + public static async Task MakeSquareWithThemeColor(IBuffer buffer, PaletteGeneratorType generatorType) { - using var image = Image.Load(imageBytes); - - if (image.Width == image.Height) + try { - // 已经是正方形,直接返回 - return imageBytes; + using var stream = new InMemoryRandomAccessStream(); + await stream.WriteAsync(buffer); + var decoder = await BitmapDecoder.CreateAsync(stream); + + if (decoder.PixelWidth == decoder.PixelHeight) + { + // 已经是正方形,直接返回 + return buffer; + } + + using var device = CanvasDevice.GetSharedDevice(); + using var canvasBitmap = await CanvasBitmap.LoadAsync(device, stream); + var size = Math.Max(decoder.PixelWidth, decoder.PixelHeight); + + var result = await GetAccentColorFromByteAsync(decoder, generatorType); + var color = Windows.UI.Color.FromArgb(255, (byte)result.Color.X, (byte)result.Color.Y, (byte)result.Color.Z); + using var renderTarget = new CanvasRenderTarget(device, size, size, 96); + + int offsetX = (int)(size - decoder.PixelWidth) / 2; + int offsetY = (int)(size - decoder.PixelHeight) / 2; + using (var ds = renderTarget.CreateDrawingSession()) + { + ds.FillRectangle(0, 0, size, size, color); + ds.DrawImage(canvasBitmap, offsetX, offsetY); + } + + // 保存为 PNG 并转为 byte[] + stream.Seek(0); + stream.Size = 0; + await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png); + var newBuffer = new Windows.Storage.Streams.Buffer((uint)stream.Size); + + await stream.ReadAsync(newBuffer, (uint)stream.Size, InputStreamOptions.None); + return newBuffer; + } + catch(Exception e) + { + throw e; } - - int size = Math.Max(image.Width, image.Height); - - var result = await GetAccentColorFromByteAsync(imageBytes, generatorType); - var color = Windows.UI.Color.FromArgb(255, (byte)result.Color.X, (byte)result.Color.Y, (byte)result.Color.Z); - var themeColor = Rgba32.ParseHex(color.ToHex()); - - using var square = new Image(size, size, themeColor); - - int offsetX = (size - image.Width) / 2; - int offsetY = (size - image.Height) / 2; - - square.Mutate(ctx => ctx.DrawImage(image, new Point(offsetX, offsetY), 1f)); - - using var ms = new MemoryStream(); - square.Save(ms, new PngEncoder()); - return ms.ToArray(); } - public static byte[] Resize(byte[] imageBytes, int size) + public static async Task Resize(IBuffer buffer, int size) { - using (Image image = Image.Load(imageBytes)) + using var stream = new InMemoryRandomAccessStream(); + await stream.WriteAsync(buffer); + var decoder = await BitmapDecoder.CreateAsync(stream); + + var factor = Math.Max((double)size / decoder.PixelWidth, (double)size / decoder.PixelHeight); + + var width = (uint)(decoder.PixelWidth * factor); + var height = (uint)(decoder.PixelHeight * factor); + + if (factor > 1) { - var factor = Math.Max((double)size / image.Width, (double)size / image.Height); - - int width = (int)(image.Width * factor); - int height = (int)(image.Height * factor); - - if (factor > 1) + var transform = new BitmapTransform() { - image.Mutate(x => x.Resize(width, height, KnownResamplers.Welch)); - } - else - { - image.Mutate(x => x.Resize(width, height, KnownResamplers.NearestNeighbor)); - } + ScaledWidth = width, + ScaledHeight = height, + InterpolationMode = BitmapInterpolationMode.Fant + }; + var pixelData = await decoder.GetPixelDataAsync( + BitmapPixelFormat.Rgba8, + BitmapAlphaMode.Straight, + transform, ExifOrientationMode.RespectExifOrientation, + ColorManagementMode.ColorManageToSRgb); + var pixels = pixelData.DetachPixelData(); - using var ms = new MemoryStream(); - image.Save(ms, new JpegEncoder()); - return ms.ToArray(); + stream.Seek(0); + stream.Size = 0; + var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream); + encoder.SetPixelData(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Straight, width, height, 96, 96, pixels); + await encoder.FlushAsync(); + var output = new Windows.Storage.Streams.Buffer((uint)stream.Size); + stream.Seek(0); + await stream.ReadAsync(output, (uint)stream.Size, InputStreamOptions.None); + return output; + } + else + { + var transform = new BitmapTransform() + { + ScaledWidth = (uint)width, + ScaledHeight = (uint)height, + InterpolationMode = BitmapInterpolationMode.NearestNeighbor + }; + var pixelData = await decoder.GetPixelDataAsync( + BitmapPixelFormat.Rgba8, + BitmapAlphaMode.Straight, + transform, ExifOrientationMode.RespectExifOrientation, + ColorManagementMode.ColorManageToSRgb); + var pixels = pixelData.DetachPixelData(); + + stream.Seek(0); + stream.Size = 0; + var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream); + encoder.SetPixelData(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Straight, width, height, 96, 96, pixels); + await encoder.FlushAsync(); + var output = new Windows.Storage.Streams.Buffer((uint)stream.Size); + stream.Seek(0); + await stream.ReadAsync(output, (uint)stream.Size, InputStreamOptions.None); + return output; } } diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/PaletteHelper.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/PaletteHelper.cs index 179f755..8c91863 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/PaletteHelper.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Helper/PaletteHelper.cs @@ -1,7 +1,6 @@ -using Impressionist.Abstractions; +using ColorThiefDotNet; +using Impressionist.Abstractions; using Impressionist.Implementations; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using System; using System.Collections.Generic; using System.Linq; @@ -16,44 +15,33 @@ namespace BetterLyrics.WinUI3.Helper { public static class PaletteHelper { - public static async Task OctTreeGetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null) + private static ColorThief colorThief = new(); + public static async Task OctTreeGetAccentColorsFromByteAsync(BitmapDecoder decoder, int count, bool? isDark = null) { - using var stream = new InMemoryRandomAccessStream(); - await stream.WriteAsync(bytes.AsBuffer()); - stream.Seek(0); - var decoder = await BitmapDecoder.CreateAsync(stream); var colors = await GetPixelColor(decoder); var palette = await PaletteGenerators.OctTreePaletteGenerator.CreatePalette(colors, count, false, isDark); return palette; } - public static async Task OctTreeGetAccentColorFromByteAsync(byte[] bytes) + public static async Task OctTreeGetAccentColorFromByteAsync(BitmapDecoder decoder) { - using var stream = new InMemoryRandomAccessStream(); - await stream.WriteAsync(bytes.AsBuffer()); - stream.Seek(0); - var decoder = await BitmapDecoder.CreateAsync(stream); var colors = await GetPixelColor(decoder); var theme = await PaletteGenerators.OctTreePaletteGenerator.CreateThemeColor(colors, false); return theme; } - public static Task MedianCutGetAccentColorFromByteAsync(byte[] bytes) + public static async Task MedianCutGetAccentColorFromByteAsync(BitmapDecoder decoder) { - using var image = Image.Load(bytes); - var colorThief = new ColorThief.ImageSharp.ColorThief(); - var mainColor = colorThief.GetColor(image, 10, false); + var mainColor = await colorThief.GetColor(decoder, 10, false); var theme = new ThemeColorResult(new Vector3(mainColor.Color.R, mainColor.Color.G, mainColor.Color.B), mainColor.IsDark); - return Task.FromResult(theme); + return theme; } - public static Task MedianCutGetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null) + public static async Task MedianCutGetAccentColorsFromByteAsync(BitmapDecoder decoder, int count, bool? isDark = null) { - using var image = Image.Load(bytes); - var colorThief = new ColorThief.ImageSharp.ColorThief(); - var mainColor = colorThief.GetColor(image, 10, false); + var mainColor = await colorThief.GetColor(decoder, 10, false); var theme = new ThemeColorResult(new Vector3(mainColor.Color.R, mainColor.Color.G, mainColor.Color.B), mainColor.IsDark); - var palette = colorThief.GetPalette(image, 255, 10, false); + var palette = await colorThief.GetPalette(decoder, 255, 10, false); var topColors = palette .Where(x => x.IsDark == (isDark ?? mainColor.IsDark)) .OrderByDescending(x => x.Population) @@ -62,7 +50,7 @@ namespace BetterLyrics.WinUI3.Helper .ToList(); var paletteResult = new PaletteResult(topColors, mainColor.IsDark, theme); - return Task.FromResult(paletteResult); + return paletteResult; } public static async Task> GetPixelColor(BitmapDecoder bitmapDecoder) diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs index 1ac9a89..3ba6551 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/AlbumArtSearchService.cs @@ -11,9 +11,11 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Runtime.InteropServices.WindowsRuntime; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Windows.Storage.Streams; namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService { @@ -31,9 +33,9 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService _iTunesHttpClinet = new(); } - public async Task SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token) + public async Task SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token) { - byte[]? result = null; + IBuffer? result = null; try { @@ -47,15 +49,16 @@ namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService switch (provider.Provider) { case AlbumArtSearchProvider.Local: - result = SearchFile(artist, title); + result = SearchFile(artist, title)?.AsBuffer(); break; case AlbumArtSearchProvider.SMTC: - result = bytesFromSMTC; + result = bufferFromSMTC; break; case AlbumArtSearchProvider.iTunes: foreach (string countryCode in new List() { "us", "cn", "jp", "kr" }) { - result = await SearchiTunesAsync(artist, album, title, countryCode); + var byteArray = await SearchiTunesAsync(artist, album, title, countryCode); + result = byteArray?.AsBuffer(); if (token.IsCancellationRequested) return result; if (result != null) break; } diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/IAlbumArtSearchService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/IAlbumArtSearchService.cs index faee086..a505280 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/IAlbumArtSearchService.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/AlbumArtSearchService/IAlbumArtSearchService.cs @@ -4,11 +4,12 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Windows.Storage.Streams; namespace BetterLyrics.WinUI3.Services.AlbumArtSearchService { public interface IAlbumArtSearchService { - Task SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token); + Task SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token); } } diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs index bfce206..4563067 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.AlbumArtUpdater.cs @@ -33,38 +33,41 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService return; } - byte[]? bytes = await Task.Run(async () => await _albumArtSearchService.SearchAsync( + IBuffer? buffer = await Task.Run(async () => await _albumArtSearchService.SearchAsync( SongInfo?.PlayerId ?? "", _cachedSongInfo.Title, _cachedSongInfo.Artist, _cachedSongInfo?.Album ?? string.Empty, - _SMTCAlbumArtBytes, + _SMTCAlbumArtBuffer, token ), token); if (token.IsCancellationRequested) return; + BitmapDecoder? decoder = null; - if (bytes == null) + if (buffer == null) { - bytes = await ImageHelper.CreateTextPlaceholderBytesAsync(500, 500); + using var placeHolderStream = await ImageHelper.CreateTextPlaceholderBytesAsync(500, 500); + var tempBuffer = new Windows.Storage.Streams.Buffer((uint)placeHolderStream.Size); + await placeHolderStream.ReadAsync(tempBuffer, (uint)placeHolderStream.Size, InputStreamOptions.None); + buffer = tempBuffer; token.ThrowIfCancellationRequested(); } - - bytes = await ImageHelper.MakeSquareWithThemeColor(bytes, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType); + buffer = await ImageHelper.MakeSquareWithThemeColor(buffer, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType); using var stream = new InMemoryRandomAccessStream(); - await stream.WriteAsync(bytes.AsBuffer()); + await stream.WriteAsync(buffer); token.ThrowIfCancellationRequested(); - var decoder = await BitmapDecoder.CreateAsync(stream); + decoder = await BitmapDecoder.CreateAsync(stream); token.ThrowIfCancellationRequested(); var albumArtSwBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Rgba8, BitmapAlphaMode.Premultiplied); albumArtSwBitmap = SoftwareBitmap.Copy(albumArtSwBitmap); token.ThrowIfCancellationRequested(); - var albumArtLightAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, false); + var albumArtLightAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, false); var lightColorBytes = albumArtLightAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList(); - var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(bytes, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, true); + var albumArtDarkAccentColors = await ImageHelper.GetAccentColorsFromByteAsync(decoder, 4, _liveStatesService.LiveStates.LyricsWindowStatus.LyricsBackgroundSettings.PaletteGeneratorType, true); var darkColorBytes = albumArtDarkAccentColors.Palette.Select(t => Windows.UI.Color.FromArgb(255, (byte)t.X, (byte)t.Y, (byte)t.Z)).ToList(); AlbumArtChanged?.Invoke(this, new AlbumArtChangedEventArgs(null, albumArtSwBitmap, lightColorBytes, darkColorBytes)); } diff --git a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.cs b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.cs index 377314f..362a1db 100644 --- a/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.cs +++ b/BetterLyrics.WinUI3/BetterLyrics.WinUI3/Services/MediaSessionsService/MediaSessionsService.cs @@ -23,6 +23,7 @@ using Microsoft.UI.Dispatching; using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; using System.Text.Json; using System.Threading.Tasks; using Windows.Media.Control; @@ -58,7 +59,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService private readonly MediaManager _mediaManager = new(); private SongInfo? _cachedSongInfo; - private byte[]? _SMTCAlbumArtBytes = null; + private IBuffer? _SMTCAlbumArtBuffer = null; public event EventHandler? IsPlayingChanged; public event EventHandler? TimelineChanged; @@ -303,7 +304,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService StopSSE(); } - _SMTCAlbumArtBytes = null; + _SMTCAlbumArtBuffer = null; } else { @@ -352,15 +353,15 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService if (sessionId == Constants.PlayerID.LXMusic && _lxMusicAlbumArtBytes != null) { - _SMTCAlbumArtBytes = _lxMusicAlbumArtBytes; + _SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer(); } else if (mediaProperties.Thumbnail is IRandomAccessStreamReference streamReference) { - _SMTCAlbumArtBytes = await ImageHelper.ToByteArrayAsync(streamReference); + _SMTCAlbumArtBuffer = await ImageHelper.ToBufferAsync(streamReference); } else { - _SMTCAlbumArtBytes = null; + _SMTCAlbumArtBuffer = null; } } @@ -532,7 +533,7 @@ namespace BetterLyrics.WinUI3.Services.MediaSessionsService { _logger.LogInformation("LX Music Album Art URL: {url}", picUrl); _lxMusicAlbumArtBytes = await ImageHelper.GetImageBytesFromUrlAsync(picUrl); - _SMTCAlbumArtBytes = _lxMusicAlbumArtBytes; + _SMTCAlbumArtBuffer = _lxMusicAlbumArtBytes.AsBuffer(); UpdateAlbumArt(); } } diff --git a/BetterLyrics.sln b/BetterLyrics.sln index 1bb41bd..5bdbb14 100644 --- a/BetterLyrics.sln +++ b/BetterLyrics.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BetterLyrics.WinUI3", "Bett EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impressionist", "Impressionist\Impressionist\Impressionist.csproj", "{A678BCA5-03DE-71E4-73C1-388B7550E4E3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorThief.WinUI3", "ColorThief.WinUI3\ColorThief.WinUI3.csproj", "{8F2FE667-2D91-428E-0630-05E6330F9625}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -61,6 +63,18 @@ Global {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x64.Build.0 = Release|Any CPU {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.ActiveCfg = Release|Any CPU {A678BCA5-03DE-71E4-73C1-388B7550E4E3}.Release|x86.Build.0 = Release|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|ARM64.Build.0 = Debug|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x64.ActiveCfg = Debug|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x64.Build.0 = Debug|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x86.ActiveCfg = Debug|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Debug|x86.Build.0 = Debug|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Release|ARM64.ActiveCfg = Release|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Release|ARM64.Build.0 = Release|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.ActiveCfg = Release|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x64.Build.0 = Release|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.ActiveCfg = Release|Any CPU + {8F2FE667-2D91-428E-0630-05E6330F9625}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ColorThief.WinUI3/CMap.cs b/ColorThief.WinUI3/CMap.cs new file mode 100644 index 0000000..04459bb --- /dev/null +++ b/ColorThief.WinUI3/CMap.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ColorThiefDotNet +{ + /// + /// Color map + /// + internal class CMap + { + private readonly List vboxes = new List(); + private List palette; + + public void Push(VBox box) + { + palette = null; + vboxes.Add(box); + } + + public List GeneratePalette() + { + if(palette == null) + { + palette = (from vBox in vboxes + let rgb = vBox.Avg(false) + let color = FromRgb(rgb[0], rgb[1], rgb[2]) + select new QuantizedColor(color, vBox.Count(false))).ToList(); + } + + return palette; + } + + public int Size() + { + return vboxes.Count; + } + + public int[] Map(int[] color) + { + foreach(var vbox in vboxes.Where(vbox => vbox.Contains(color))) + { + return vbox.Avg(false); + } + return Nearest(color); + } + + public int[] Nearest(int[] color) + { + var d1 = double.MaxValue; + int[] pColor = null; + + foreach(var t in vboxes) + { + var vbColor = t.Avg(false); + var d2 = Math.Sqrt(Math.Pow(color[0] - vbColor[0], 2) + + Math.Pow(color[1] - vbColor[1], 2) + + Math.Pow(color[2] - vbColor[2], 2)); + if(d2 < d1) + { + d1 = d2; + pColor = vbColor; + } + } + return pColor; + } + + public VBox FindColor(double targetLuma, double minLuma, double maxLuma, double targetSaturation, double minSaturation, double maxSaturation) + { + VBox max = null; + double maxValue = 0; + var highestPopulation = vboxes.Select(p => p.Count(false)).Max(); + + foreach(var swatch in vboxes) + { + var avg = swatch.Avg(false); + var hsl = FromRgb(avg[0], avg[1], avg[2]).ToHsl(); + var sat = hsl.S; + var luma = hsl.L; + + if(sat >= minSaturation && sat <= maxSaturation && + luma >= minLuma && luma <= maxLuma) + { + var thisValue = Mmcq.CreateComparisonValue(sat, targetSaturation, luma, targetLuma, + swatch.Count(false), highestPopulation); + + if(max == null || thisValue > maxValue) + { + max = swatch; + maxValue = thisValue; + } + } + } + + return max; + } + + public Color FromRgb(int red, int green, int blue) + { + var color = new Color + { + A = 255, + R = (byte)red, + G = (byte)green, + B = (byte)blue + }; + + return color; + } + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/Color.cs b/ColorThief.WinUI3/Color.cs new file mode 100644 index 0000000..dffd499 --- /dev/null +++ b/ColorThief.WinUI3/Color.cs @@ -0,0 +1,94 @@ +using System; + +namespace ColorThiefDotNet +{ + /// + /// Defines a color in RGB space. + /// + public struct Color + { + /// + /// Get or Set the Alpha component value for sRGB. + /// + public byte A; + + /// + /// Get or Set the Blue component value for sRGB. + /// + public byte B; + + /// + /// Get or Set the Green component value for sRGB. + /// + public byte G; + + /// + /// Get or Set the Red component value for sRGB. + /// + public byte R; + + /// + /// Get HSL color. + /// + /// + public HslColor ToHsl() + { + const double toDouble = 1.0 / 255; + var r = toDouble * R; + var g = toDouble * G; + var b = toDouble * B; + var max = Math.Max(Math.Max(r, g), b); + var min = Math.Min(Math.Min(r, g), b); + var chroma = max - min; + double h1; + + // ReSharper disable CompareOfFloatsByEqualityOperator + if(chroma == 0) + { + h1 = 0; + } + else if(max == r) + { + h1 = (g - b) / chroma % 6; + } + else if(max == g) + { + h1 = 2 + (b - r) / chroma; + } + else //if (max == b) + { + h1 = 4 + (r - g)/chroma; + } + + var lightness = 0.5 * (max - min); + var saturation = chroma == 0 ? 0 : chroma / (1 - Math.Abs(2*lightness - 1)); + HslColor ret; + ret.H = 60 * h1; + ret.S = saturation; + ret.L = lightness; + ret.A = toDouble * A; + return ret; + // ReSharper restore CompareOfFloatsByEqualityOperator + } + + public string ToHexString() + { + return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2"); + } + + public string ToHexAlphaString() + { + return "#" + A.ToString("X2") + R.ToString("X2") + G.ToString("X2") + B.ToString("X2"); + } + + public override string ToString() + { + if(A == 255) + { + return ToHexString(); + } + + return ToHexAlphaString(); + } + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/ColorThief.WinUI3.cs b/ColorThief.WinUI3/ColorThief.WinUI3.cs new file mode 100644 index 0000000..a2f9dcc --- /dev/null +++ b/ColorThief.WinUI3/ColorThief.WinUI3.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Windows.Graphics.Imaging; + +namespace ColorThiefDotNet +{ + public partial class ColorThief + { + /// + /// Use the median cut algorithm to cluster similar colors and return the base color from the largest cluster. + /// + /// The source image. + /// + /// 1 is the highest quality settings. 10 is the default. There is + /// a trade-off between quality and speed. The bigger the number, + /// the faster a color will be returned but the greater the + /// likelihood that it will not be the visually most dominant color. + /// + /// if set to true [ignore white]. + /// + public async Task GetColor(BitmapDecoder sourceImage, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite) + { + var palette = await GetPalette(sourceImage, 3, quality, ignoreWhite); + + var dominantColor = new QuantizedColor(new Color + { + A = Convert.ToByte(palette.Average(a => a.Color.A)), + R = Convert.ToByte(palette.Average(a => a.Color.R)), + G = Convert.ToByte(palette.Average(a => a.Color.G)), + B = Convert.ToByte(palette.Average(a => a.Color.B)) + }, Convert.ToInt32(palette.Average(a => a.Population))); + + return dominantColor; + } + + /// + /// Use the median cut algorithm to cluster similar colors. + /// + /// The source image. + /// The color count. + /// + /// 1 is the highest quality settings. 10 is the default. There is + /// a trade-off between quality and speed. The bigger the number, + /// the faster a color will be returned but the greater the + /// likelihood that it will not be the visually most dominant color. + /// + /// if set to true [ignore white]. + /// + /// true + public async Task> GetPalette(BitmapDecoder sourceImage, int colorCount = DefaultColorCount, int quality = DefaultQuality, bool ignoreWhite = DefaultIgnoreWhite) + { + var pixelArray = await GetPixelsFast(sourceImage, quality, ignoreWhite); + var cmap = GetColorMap(pixelArray, colorCount); + if(cmap != null) + { + var colors = cmap.GeneratePalette(); + return colors; + } + return new List(); + } + + private async Task GetIntFromPixel(BitmapDecoder decoder) + { + var pixelsData = await decoder.GetPixelDataAsync(); + var pixels = pixelsData.DetachPixelData(); + return pixels; + } + + private async Task GetPixelsFast(BitmapDecoder sourceImage, int quality, bool ignoreWhite) + { + if(quality < 1) + { + quality = DefaultQuality; + } + + var pixels = await GetIntFromPixel(sourceImage); + var pixelCount = sourceImage.PixelWidth*sourceImage.PixelHeight; + + return ConvertPixels(pixels, Convert.ToInt32(pixelCount), quality, ignoreWhite); + } + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/ColorThief.WinUI3.csproj b/ColorThief.WinUI3/ColorThief.WinUI3.csproj new file mode 100644 index 0000000..14fe016 --- /dev/null +++ b/ColorThief.WinUI3/ColorThief.WinUI3.csproj @@ -0,0 +1,14 @@ + + + net8.0-windows10.0.19041.0 + 10.0.17763.0 + ColorThief.WinUI3 + win-x86;win-x64;win-arm64 + true + + + + + + + \ No newline at end of file diff --git a/ColorThief.WinUI3/ColorThief.cs b/ColorThief.WinUI3/ColorThief.cs new file mode 100644 index 0000000..5025fbe --- /dev/null +++ b/ColorThief.WinUI3/ColorThief.cs @@ -0,0 +1,77 @@ +using System; + +namespace ColorThiefDotNet +{ + public partial class ColorThief + { + public const int DefaultColorCount = 5; + public const int DefaultQuality = 10; + public const bool DefaultIgnoreWhite = true; + public const int ColorDepth = 4; + + /// + /// Use the median cut algorithm to cluster similar colors. + /// + /// Pixel array. + /// The color count. + /// + private CMap GetColorMap(byte[][] pixelArray, int colorCount) + { + // Send array to quantize function which clusters values using median + // cut algorithm + + if (colorCount > 0) + { + --colorCount; + } + + var cmap = Mmcq.Quantize(pixelArray, colorCount); + return cmap; + } + + private byte[][] ConvertPixels(byte[] pixels, int pixelCount, int quality, bool ignoreWhite) + { + + + var expectedDataLength = pixelCount * ColorDepth; + if(expectedDataLength != pixels.Length) + { + throw new ArgumentException("(expectedDataLength = " + + expectedDataLength + ") != (pixels.length = " + + pixels.Length + ")"); + } + + // Store the RGB values in an array format suitable for quantize + // function + + // numRegardedPixels must be rounded up to avoid an + // ArrayIndexOutOfBoundsException if all pixels are good. + + var numRegardedPixels = (pixelCount + quality - 1) / quality; + + var numUsedPixels = 0; + var pixelArray = new byte[numRegardedPixels][]; + + for(var i = 0; i < pixelCount; i += quality) + { + var offset = i * ColorDepth; + var b = pixels[offset]; + var g = pixels[offset + 1]; + var r = pixels[offset + 2]; + var a = pixels[offset + 3]; + + // If pixel is mostly opaque and not white + if(a >= 125 && !(ignoreWhite && r > 250 && g > 250 && b > 250)) + { + pixelArray[numUsedPixels] = new[] {r, g, b}; + numUsedPixels++; + } + } + + // Remove unused pixels from the array + var copy = new byte[numUsedPixels][]; + Array.Copy(pixelArray, copy, numUsedPixels); + return copy; + } + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/HslColor.cs b/ColorThief.WinUI3/HslColor.cs new file mode 100644 index 0000000..db681fd --- /dev/null +++ b/ColorThief.WinUI3/HslColor.cs @@ -0,0 +1,28 @@ +namespace ColorThiefDotNet +{ + /// + /// Defines a color in Hue/Saturation/Lightness (HSL) space. + /// + public struct HslColor + { + /// + /// The Alpha/opacity in 0..1 range. + /// + public double A; + + /// + /// The Hue in 0..360 range. + /// + public double H; + + /// + /// The Lightness in 0..1 range. + /// + public double L; + + /// + /// The Saturation in 0..1 range. + /// + public double S; + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/Mmcq.cs b/ColorThief.WinUI3/Mmcq.cs new file mode 100644 index 0000000..1294b86 --- /dev/null +++ b/ColorThief.WinUI3/Mmcq.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; + +namespace ColorThiefDotNet +{ + internal static class Mmcq + { + public const int Sigbits = 5; + public const int Rshift = 8 - Sigbits; + public const int Mult = 1 << Rshift; + public const int Histosize = 1 << (3 * Sigbits); + public const int VboxLength = 1 << Sigbits; + public const double FractByPopulation = 0.75; + public const int MaxIterations = 1000; + public const double WeightSaturation = 3d; + public const double WeightLuma = 6d; + public const double WeightPopulation = 1d; + private static readonly VBoxComparer ComparatorProduct = new VBoxComparer(); + private static readonly VBoxCountComparer ComparatorCount = new VBoxCountComparer(); + + public static int GetColorIndex(int r, int g, int b) + { + return (r << (2 * Sigbits)) + (g << Sigbits) + b; + } + + /// + /// Gets the histo. + /// + /// The pixels. + /// Histo (1-d array, giving the number of pixels in each quantized region of color space), or null on error. + private static int[] GetHisto(IEnumerable pixels) + { + var histo = new int[Histosize]; + + foreach(var pixel in pixels) + { + var rval = pixel[0] >> Rshift; + var gval = pixel[1] >> Rshift; + var bval = pixel[2] >> Rshift; + var index = GetColorIndex(rval, gval, bval); + histo[index]++; + } + return histo; + } + + private static VBox VboxFromPixels(IList pixels, int[] histo) + { + int rmin = 1000000, rmax = 0; + int gmin = 1000000, gmax = 0; + int bmin = 1000000, bmax = 0; + + // find min/max + var numPixels = pixels.Count; + for(var i = 0; i < numPixels; i++) + { + var pixel = pixels[i]; + var rval = pixel[0] >> Rshift; + var gval = pixel[1] >> Rshift; + var bval = pixel[2] >> Rshift; + + if(rval < rmin) + { + rmin = rval; + } + else if(rval > rmax) + { + rmax = rval; + } + + if(gval < gmin) + { + gmin = gval; + } + else if(gval > gmax) + { + gmax = gval; + } + + if(bval < bmin) + { + bmin = bval; + } + else if(bval > bmax) + { + bmax = bval; + } + } + + return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo); + } + + private static VBox[] DoCut(char color, VBox vbox, IList partialsum, IList lookaheadsum, int total) + { + int vboxDim1; + int vboxDim2; + + switch(color) + { + case 'r': + vboxDim1 = vbox.R1; + vboxDim2 = vbox.R2; + break; + case 'g': + vboxDim1 = vbox.G1; + vboxDim2 = vbox.G2; + break; + default: + vboxDim1 = vbox.B1; + vboxDim2 = vbox.B2; + break; + } + + for(var i = vboxDim1; i <= vboxDim2; i++) + { + if(partialsum[i] > total / 2) + { + var vbox1 = vbox.Clone(); + var vbox2 = vbox.Clone(); + + var left = i - vboxDim1; + var right = vboxDim2 - i; + + var d2 = left <= right + ? Math.Min(vboxDim2 - 1, Math.Abs(i + right / 2)) + : Math.Max(vboxDim1, Math.Abs(Convert.ToInt32(i - 1 - left / 2.0))); + + // avoid 0-count boxes + while(d2 < 0 || partialsum[d2] <= 0) + { + d2++; + } + var count2 = lookaheadsum[d2]; + while(count2 == 0 && d2 > 0 && partialsum[d2 - 1] > 0) + { + count2 = lookaheadsum[--d2]; + } + + // set dimensions + switch(color) + { + case 'r': + vbox1.R2 = d2; + vbox2.R1 = d2 + 1; + break; + case 'g': + vbox1.G2 = d2; + vbox2.G1 = d2 + 1; + break; + default: + vbox1.B2 = d2; + vbox2.B1 = d2 + 1; + break; + } + + return new[] {vbox1, vbox2}; + } + } + + throw new Exception("VBox can't be cut"); + } + + private static VBox[] MedianCutApply(IList histo, VBox vbox) + { + if(vbox.Count(false) == 0) + { + return null; + } + if(vbox.Count(false) == 1) + { + return new[] {vbox.Clone(), null}; + } + + // only one pixel, no split + + var rw = vbox.R2 - vbox.R1 + 1; + var gw = vbox.G2 - vbox.G1 + 1; + var bw = vbox.B2 - vbox.B1 + 1; + var maxw = Math.Max(Math.Max(rw, gw), bw); + + // Find the partial sum arrays along the selected axis. + var total = 0; + var partialsum = new int[VboxLength]; + // -1 = not set / 0 = 0 + for(var l = 0; l < partialsum.Length; l++) + { + partialsum[l] = -1; + } + + // -1 = not set / 0 = 0 + var lookaheadsum = new int[VboxLength]; + for(var l = 0; l < lookaheadsum.Length; l++) + { + lookaheadsum[l] = -1; + } + + int i, j, k, sum, index; + + if(maxw == rw) + { + for(i = vbox.R1; i <= vbox.R2; i++) + { + sum = 0; + for(j = vbox.G1; j <= vbox.G2; j++) + { + for(k = vbox.B1; k <= vbox.B2; k++) + { + index = GetColorIndex(i, j, k); + sum += histo[index]; + } + } + total += sum; + partialsum[i] = total; + } + } + else if(maxw == gw) + { + for(i = vbox.G1; i <= vbox.G2; i++) + { + sum = 0; + for(j = vbox.R1; j <= vbox.R2; j++) + { + for(k = vbox.B1; k <= vbox.B2; k++) + { + index = GetColorIndex(j, i, k); + sum += histo[index]; + } + } + total += sum; + partialsum[i] = total; + } + } + else /* maxw == bw */ + { + for(i = vbox.B1; i <= vbox.B2; i++) + { + sum = 0; + for(j = vbox.R1; j <= vbox.R2; j++) + { + for(k = vbox.G1; k <= vbox.G2; k++) + { + index = GetColorIndex(j, k, i); + sum += histo[index]; + } + } + total += sum; + partialsum[i] = total; + } + } + + for(i = 0; i < VboxLength; i++) + { + if(partialsum[i] != -1) + { + lookaheadsum[i] = total - partialsum[i]; + } + } + + // determine the cut planes + return maxw == rw ? DoCut('r', vbox, partialsum, lookaheadsum, total) : maxw == gw + ? DoCut('g', vbox, partialsum, lookaheadsum, total) : DoCut('b', vbox, partialsum, lookaheadsum, total); + } + + /// + /// Inner function to do the iteration. + /// + /// The lh. + /// The comparator. + /// The target. + /// The histo. + /// vbox1 not defined; shouldn't happen! + private static void Iter(List lh, IComparer comparator, int target, IList histo) + { + var ncolors = 1; + var niters = 0; + + while(niters < MaxIterations) + { + var vbox = lh[lh.Count - 1]; + if(vbox.Count(false) == 0) + { + lh.Sort(comparator); + niters++; + continue; + } + + lh.RemoveAt(lh.Count - 1); + + // do the cut + var vboxes = MedianCutApply(histo, vbox); + var vbox1 = vboxes[0]; + var vbox2 = vboxes[1]; + + if(vbox1 == null) + { + throw new Exception( + "vbox1 not defined; shouldn't happen!"); + } + + lh.Add(vbox1); + if(vbox2 != null) + { + lh.Add(vbox2); + ncolors++; + } + lh.Sort(comparator); + + if(ncolors >= target) + { + return; + } + if(niters++ > MaxIterations) + { + return; + } + } + } + + public static CMap Quantize(byte[][] pixels, int maxcolors) + { + // short-circuit + if(pixels.Length == 0 || maxcolors < 2 || maxcolors > 256) + { + return null; + } + + var histo = GetHisto(pixels); + + // get the beginning vbox from the colors + var vbox = VboxFromPixels(pixels, histo); + var pq = new List {vbox}; + + // Round up to have the same behaviour as in JavaScript + var target = (int)Math.Ceiling(FractByPopulation * maxcolors); + + // first set of colors, sorted by population + Iter(pq, ComparatorCount, target, histo); + + // Re-sort by the product of pixel occupancy times the size in color + // space. + pq.Sort(ComparatorProduct); + + // next set - generate the median cuts using the (npix * vol) sorting. + Iter(pq, ComparatorProduct, maxcolors - pq.Count, histo); + + // Reverse to put the highest elements first into the color map + pq.Reverse(); + + // calculate the actual colors + var cmap = new CMap(); + foreach(var vb in pq) + { + cmap.Push(vb); + } + + return cmap; + } + + public static double CreateComparisonValue(double saturation, double targetSaturation, double luma, double targetLuma, int population, int highestPopulation) + { + return WeightedMean(InvertDiff(saturation, targetSaturation), WeightSaturation, + InvertDiff(luma, targetLuma), WeightLuma, + population / (double)highestPopulation, WeightPopulation); + } + + private static double WeightedMean(params double[] values) + { + double sum = 0; + double sumWeight = 0; + + for(var i = 0; i < values.Length; i += 2) + { + var value = values[i]; + var weight = values[i + 1]; + + sum += value * weight; + sumWeight += weight; + } + + return sum / sumWeight; + } + + private static double InvertDiff(double value, double targetValue) + { + return 1 - Math.Abs(value - targetValue); + } + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/QuantizedColor.cs b/ColorThief.WinUI3/QuantizedColor.cs new file mode 100644 index 0000000..7561be8 --- /dev/null +++ b/ColorThief.WinUI3/QuantizedColor.cs @@ -0,0 +1,23 @@ +using System; + +namespace ColorThiefDotNet +{ + public class QuantizedColor + { + public QuantizedColor(Color color, int population) + { + Color = color; + Population = population; + IsDark = CalculateYiqLuma(color) < 128; + } + + public Color Color { get; private set; } + public int Population { get; private set; } + public bool IsDark { get; private set; } + + public int CalculateYiqLuma(Color color) + { + return Convert.ToInt32(Math.Round((299 * color.R + 587 * color.G + 114 * color.B) / 1000f)); + } + } +} \ No newline at end of file diff --git a/ColorThief.WinUI3/VBox.cs b/ColorThief.WinUI3/VBox.cs new file mode 100644 index 0000000..8b0ff74 --- /dev/null +++ b/ColorThief.WinUI3/VBox.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; + +namespace ColorThiefDotNet +{ + /// + /// 3D color space box. + /// + internal class VBox + { + private readonly int[] histo; + private int[] avg; + public int B1; + public int B2; + private int? count; + public int G1; + public int G2; + public int R1; + public int R2; + private int? volume; + + public VBox(int r1, int r2, int g1, int g2, int b1, int b2, int[] histo) + { + R1 = r1; + R2 = r2; + G1 = g1; + G2 = g2; + B1 = b1; + B2 = b2; + + this.histo = histo; + } + + public int Volume(bool force) + { + if(volume == null || force) + { + volume = (R2 - R1 + 1) * (G2 - G1 + 1) * (B2 - B1 + 1); + } + + return volume.Value; + } + + public int Count(bool force) + { + if(count == null || force) + { + var npix = 0; + int i; + + for(i = R1; i <= R2; i++) + { + int j; + for(j = G1; j <= G2; j++) + { + int k; + for(k = B1; k <= B2; k++) + { + var index = Mmcq.GetColorIndex(i, j, k); + npix += histo[index]; + } + } + } + + count = npix; + } + + return count.Value; + } + + public VBox Clone() + { + return new VBox(R1, R2, G1, G2, B1, B2, histo); + } + + public int[] Avg(bool force) + { + if(avg == null || force) + { + var ntot = 0; + + var rsum = 0; + var gsum = 0; + var bsum = 0; + + int i; + + for(i = R1; i <= R2; i++) + { + int j; + for(j = G1; j <= G2; j++) + { + int k; + for(k = B1; k <= B2; k++) + { + var histoindex = Mmcq.GetColorIndex(i, j, k); + var hval = histo[histoindex]; + ntot += hval; + rsum += Convert.ToInt32((hval * (i + 0.5) * Mmcq.Mult)); + gsum += Convert.ToInt32((hval * (j + 0.5) * Mmcq.Mult)); + bsum += Convert.ToInt32((hval * (k + 0.5) * Mmcq.Mult)); + } + } + } + + if(ntot > 0) + { + avg = new[] + { + Math.Abs(rsum / ntot), Math.Abs(gsum / ntot), + Math.Abs(bsum / ntot) + }; + } + else + { + avg = new[] + { + Math.Abs(Mmcq.Mult * (R1 + R2 + 1) / 2), + Math.Abs(Mmcq.Mult * (G1 + G2 + 1) / 2), + Math.Abs(Mmcq.Mult * (B1 + B2 + 1) / 2) + }; + } + } + + return avg; + } + + public bool Contains(int[] pixel) + { + var rval = pixel[0] >> Mmcq.Rshift; + var gval = pixel[1] >> Mmcq.Rshift; + var bval = pixel[2] >> Mmcq.Rshift; + + return rval >= R1 && rval <= R2 && gval >= G1 && gval <= G2 && bval >= B1 && bval <= B2; + } + } + + internal class VBoxCountComparer : IComparer + { + public int Compare(VBox x, VBox y) + { + var a = x.Count(false); + var b = y.Count(false); + return a < b ? -1 : (a > b ? 1 : 0); + } + } + + internal class VBoxComparer : IComparer + { + public int Compare(VBox x, VBox y) + { + var aCount = x.Count(false); + var bCount = y.Count(false); + var aVolume = x.Volume(false); + var bVolume = y.Volume(false); + + // Otherwise sort by products + var a = aCount * aVolume; + var b = bCount * bVolume; + return a < b ? -1 : (a > b ? 1 : 0); + } + } +} \ No newline at end of file