mirror of
https://github.com/jayfunc/BetterLyrics.git
synced 2026-01-12 10:54:55 +08:00
chores: Bump Dependencies, Remove ImageSharp
This commit is contained in:
@@ -138,13 +138,13 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
|
||||
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
|
||||
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
|
||||
</Project>
|
||||
@@ -49,7 +49,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="3v.EvtSource" Version="2.0.0" />
|
||||
<PackageReference Include="ColorThief.ImageSharp" Version="1.0.0" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.MarqueeText" Version="0.1.230830" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.OpacityMaskView" Version="0.1.250703-build.2173" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Shimmer" Version="0.1.250703-build.2173" />
|
||||
@@ -66,14 +65,14 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||
<PackageReference Include="csharp-pinyin" Version="1.0.1" />
|
||||
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
|
||||
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
|
||||
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.2" />
|
||||
<PackageReference Include="Hqub.Last.fm" Version="2.5.1" />
|
||||
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.7.250606001" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
|
||||
<PackageReference Include="NAudio.Wasapi" Version="2.2.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
@@ -82,20 +81,20 @@
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.3-dev-02320" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="ShadowViewer.Controls.Notification" Version="1.2.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.8" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.10" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageReference Include="Ude.NetStandard" Version="1.2.0" />
|
||||
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.1.6" />
|
||||
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.6" />
|
||||
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.1.6" />
|
||||
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.1.6" />
|
||||
<PackageReference Include="Vanara.PInvoke.User32" Version="4.1.6" />
|
||||
<PackageReference Include="WinUIEx" Version="2.6.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="7.2.0" />
|
||||
<PackageReference Include="Vanara.PInvoke.CoreAudio" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.PInvoke.Gdi32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.PInvoke.Shell32" Version="4.2.1" />
|
||||
<PackageReference Include="Vanara.PInvoke.User32" Version="4.2.1" />
|
||||
<PackageReference Include="WinUIEx" Version="2.9.0" />
|
||||
<PackageReference Include="z440.atl.core" Version="7.5.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\ColorThief.WinUI3\ColorThief.WinUI3.csproj" />
|
||||
<ProjectReference Include="..\..\Impressionist\Impressionist\Impressionist.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<byte[]> CreateTextPlaceholderBytesAsync(int width, int height)
|
||||
public static async Task<IRandomAccessStream> 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<ThemeColorResult> GetAccentColorFromByteAsync(byte[] bytes, PaletteGeneratorType generatorType)
|
||||
public static Task<ThemeColorResult> 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<PaletteResult> GetAccentColorsFromByteAsync(byte[] bytes, int count, PaletteGeneratorType generatorType, bool? isDark = null)
|
||||
public static Task<PaletteResult> 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<byte[]> ToByteArrayAsync(IRandomAccessStreamReference streamRef)
|
||||
public static async Task<IBuffer> 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<byte[]> MakeSquareWithThemeColor(byte[] imageBytes, PaletteGeneratorType generatorType)
|
||||
public static async Task<IBuffer> MakeSquareWithThemeColor(IBuffer buffer, PaletteGeneratorType generatorType)
|
||||
{
|
||||
using var image = Image.Load<Rgba32>(imageBytes);
|
||||
try
|
||||
{
|
||||
using var stream = new InMemoryRandomAccessStream();
|
||||
await stream.WriteAsync(buffer);
|
||||
var decoder = await BitmapDecoder.CreateAsync(stream);
|
||||
|
||||
if (image.Width == image.Height)
|
||||
if (decoder.PixelWidth == decoder.PixelHeight)
|
||||
{
|
||||
// 已经是正方形,直接返回
|
||||
return imageBytes;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
int size = Math.Max(image.Width, image.Height);
|
||||
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(imageBytes, generatorType);
|
||||
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);
|
||||
var themeColor = Rgba32.ParseHex(color.ToHex());
|
||||
using var renderTarget = new CanvasRenderTarget(device, size, size, 96);
|
||||
|
||||
using var square = new Image<Rgba32>(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();
|
||||
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);
|
||||
}
|
||||
|
||||
public static byte[] Resize(byte[] imageBytes, int size)
|
||||
{
|
||||
using (Image image = Image.Load(imageBytes))
|
||||
{
|
||||
var factor = Math.Max((double)size / image.Width, (double)size / image.Height);
|
||||
// 保存为 PNG 并转为 byte[]
|
||||
stream.Seek(0);
|
||||
stream.Size = 0;
|
||||
await renderTarget.SaveAsync(stream, CanvasBitmapFileFormat.Png);
|
||||
var newBuffer = new Windows.Storage.Streams.Buffer((uint)stream.Size);
|
||||
|
||||
int width = (int)(image.Width * factor);
|
||||
int height = (int)(image.Height * factor);
|
||||
await stream.ReadAsync(newBuffer, (uint)stream.Size, InputStreamOptions.None);
|
||||
return newBuffer;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<IBuffer> Resize(IBuffer buffer, int size)
|
||||
{
|
||||
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)
|
||||
{
|
||||
image.Mutate(x => x.Resize(width, height, KnownResamplers.Welch));
|
||||
var transform = new BitmapTransform()
|
||||
{
|
||||
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();
|
||||
|
||||
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
|
||||
{
|
||||
image.Mutate(x => x.Resize(width, height, KnownResamplers.NearestNeighbor));
|
||||
}
|
||||
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();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<PaletteResult> OctTreeGetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null)
|
||||
private static ColorThief colorThief = new();
|
||||
public static async Task<PaletteResult> 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<ThemeColorResult> OctTreeGetAccentColorFromByteAsync(byte[] bytes)
|
||||
public static async Task<ThemeColorResult> 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<ThemeColorResult> MedianCutGetAccentColorFromByteAsync(byte[] bytes)
|
||||
public static async Task<ThemeColorResult> MedianCutGetAccentColorFromByteAsync(BitmapDecoder decoder)
|
||||
{
|
||||
using var image = Image.Load<Rgba32>(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<PaletteResult> MedianCutGetAccentColorsFromByteAsync(byte[] bytes, int count, bool? isDark = null)
|
||||
public static async Task<PaletteResult> MedianCutGetAccentColorsFromByteAsync(BitmapDecoder decoder, int count, bool? isDark = null)
|
||||
{
|
||||
using var image = Image.Load<Rgba32>(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<Dictionary<Vector3, int>> GetPixelColor(BitmapDecoder bitmapDecoder)
|
||||
|
||||
@@ -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<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token)
|
||||
public async Task<IBuffer?> 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<string>() { "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;
|
||||
}
|
||||
|
||||
@@ -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<byte[]?> SearchAsync(string mediaSessionId, string title, string artist, string album, byte[]? bytesFromSMTC, CancellationToken token);
|
||||
Task<IBuffer?> SearchAsync(string mediaSessionId, string title, string artist, string album, IBuffer? bufferFromSMTC, CancellationToken token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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<IsPlayingChangedEventArgs>? IsPlayingChanged;
|
||||
public event EventHandler<TimelineChangedEventArgs>? 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
111
ColorThief.WinUI3/CMap.cs
Normal file
111
ColorThief.WinUI3/CMap.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ColorThiefDotNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Color map
|
||||
/// </summary>
|
||||
internal class CMap
|
||||
{
|
||||
private readonly List<VBox> vboxes = new List<VBox>();
|
||||
private List<QuantizedColor> palette;
|
||||
|
||||
public void Push(VBox box)
|
||||
{
|
||||
palette = null;
|
||||
vboxes.Add(box);
|
||||
}
|
||||
|
||||
public List<QuantizedColor> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
ColorThief.WinUI3/Color.cs
Normal file
94
ColorThief.WinUI3/Color.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
|
||||
namespace ColorThiefDotNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a color in RGB space.
|
||||
/// </summary>
|
||||
public struct Color
|
||||
{
|
||||
/// <summary>
|
||||
/// Get or Set the Alpha component value for sRGB.
|
||||
/// </summary>
|
||||
public byte A;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the Blue component value for sRGB.
|
||||
/// </summary>
|
||||
public byte B;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the Green component value for sRGB.
|
||||
/// </summary>
|
||||
public byte G;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the Red component value for sRGB.
|
||||
/// </summary>
|
||||
public byte R;
|
||||
|
||||
/// <summary>
|
||||
/// Get HSL color.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
84
ColorThief.WinUI3/ColorThief.WinUI3.cs
Normal file
84
ColorThief.WinUI3/ColorThief.WinUI3.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Use the median cut algorithm to cluster similar colors and return the base color from the largest cluster.
|
||||
/// </summary>
|
||||
/// <param name="sourceImage">The source image.</param>
|
||||
/// <param name="quality">
|
||||
/// 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.
|
||||
/// </param>
|
||||
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
|
||||
/// <returns></returns>
|
||||
public async Task<QuantizedColor> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use the median cut algorithm to cluster similar colors.
|
||||
/// </summary>
|
||||
/// <param name="sourceImage">The source image.</param>
|
||||
/// <param name="colorCount">The color count.</param>
|
||||
/// <param name="quality">
|
||||
/// 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.
|
||||
/// </param>
|
||||
/// <param name="ignoreWhite">if set to <c>true</c> [ignore white].</param>
|
||||
/// <returns></returns>
|
||||
/// <code>true</code>
|
||||
public async Task<List<QuantizedColor>> 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<QuantizedColor>();
|
||||
}
|
||||
|
||||
private async Task<byte[]> GetIntFromPixel(BitmapDecoder decoder)
|
||||
{
|
||||
var pixelsData = await decoder.GetPixelDataAsync();
|
||||
var pixels = pixelsData.DetachPixelData();
|
||||
return pixels;
|
||||
}
|
||||
|
||||
private async Task<byte[][]> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
ColorThief.WinUI3/ColorThief.WinUI3.csproj
Normal file
14
ColorThief.WinUI3/ColorThief.WinUI3.csproj
Normal file
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>ColorThief.WinUI3</RootNamespace>
|
||||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251003001" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
77
ColorThief.WinUI3/ColorThief.cs
Normal file
77
ColorThief.WinUI3/ColorThief.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Use the median cut algorithm to cluster similar colors.
|
||||
/// </summary>
|
||||
/// <param name="pixelArray">Pixel array.</param>
|
||||
/// <param name="colorCount">The color count.</param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
ColorThief.WinUI3/HslColor.cs
Normal file
28
ColorThief.WinUI3/HslColor.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
namespace ColorThiefDotNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a color in Hue/Saturation/Lightness (HSL) space.
|
||||
/// </summary>
|
||||
public struct HslColor
|
||||
{
|
||||
/// <summary>
|
||||
/// The Alpha/opacity in 0..1 range.
|
||||
/// </summary>
|
||||
public double A;
|
||||
|
||||
/// <summary>
|
||||
/// The Hue in 0..360 range.
|
||||
/// </summary>
|
||||
public double H;
|
||||
|
||||
/// <summary>
|
||||
/// The Lightness in 0..1 range.
|
||||
/// </summary>
|
||||
public double L;
|
||||
|
||||
/// <summary>
|
||||
/// The Saturation in 0..1 range.
|
||||
/// </summary>
|
||||
public double S;
|
||||
}
|
||||
}
|
||||
387
ColorThief.WinUI3/Mmcq.cs
Normal file
387
ColorThief.WinUI3/Mmcq.cs
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the histo.
|
||||
/// </summary>
|
||||
/// <param name="pixels">The pixels.</param>
|
||||
/// <returns>Histo (1-d array, giving the number of pixels in each quantized region of color space), or null on error.</returns>
|
||||
private static int[] GetHisto(IEnumerable<byte[]> 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<byte[]> 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<int> partialsum, IList<int> 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<int> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inner function to do the iteration.
|
||||
/// </summary>
|
||||
/// <param name="lh">The lh.</param>
|
||||
/// <param name="comparator">The comparator.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="histo">The histo.</param>
|
||||
/// <exception cref="System.Exception">vbox1 not defined; shouldn't happen!</exception>
|
||||
private static void Iter(List<VBox> lh, IComparer<VBox> comparator, int target, IList<int> 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> {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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
ColorThief.WinUI3/QuantizedColor.cs
Normal file
23
ColorThief.WinUI3/QuantizedColor.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
163
ColorThief.WinUI3/VBox.cs
Normal file
163
ColorThief.WinUI3/VBox.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ColorThiefDotNet
|
||||
{
|
||||
/// <summary>
|
||||
/// 3D color space box.
|
||||
/// </summary>
|
||||
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<VBox>
|
||||
{
|
||||
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<VBox>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user