chores: Bump Dependencies, Remove ImageSharp

This commit is contained in:
Raspberry-Monster
2025-10-23 22:38:31 +08:00
parent 7ab833a53a
commit 835e0d34fc
18 changed files with 1311 additions and 277 deletions

View File

@@ -1,150 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
<PathToXAMLWinRTImplementations>BetterLyrics.WinUI3\</PathToXAMLWinRTImplementations>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>6576cd19-ef92-4099-b37d-e2d8ebdb6bf5</ProjectGuid>
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
<DefaultLanguage>zh-CN</DefaultLanguage>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<PackageCertificateKeyFile>BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx</PackageCertificateKeyFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<None Include="BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx" />
<Content Include="Images\LargeTile.scale-100.png" />
<Content Include="Images\LargeTile.scale-125.png" />
<Content Include="Images\LargeTile.scale-150.png" />
<Content Include="Images\LargeTile.scale-200.png" />
<Content Include="Images\LargeTile.scale-400.png" />
<Content Include="Images\SmallTile.scale-100.png" />
<Content Include="Images\SmallTile.scale-125.png" />
<Content Include="Images\SmallTile.scale-150.png" />
<Content Include="Images\SmallTile.scale-200.png" />
<Content Include="Images\SmallTile.scale-400.png" />
<Content Include="Images\SplashScreen.scale-100.png" />
<Content Include="Images\SplashScreen.scale-125.png" />
<Content Include="Images\SplashScreen.scale-150.png" />
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\SplashScreen.scale-400.png" />
<Content Include="Images\Square150x150Logo.scale-100.png" />
<Content Include="Images\Square150x150Logo.scale-125.png" />
<Content Include="Images\Square150x150Logo.scale-150.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.scale-100.png" />
<Content Include="Images\Square44x44Logo.scale-125.png" />
<Content Include="Images\Square44x44Logo.scale-150.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.targetsize-16.png" />
<Content Include="Images\Square44x44Logo.targetsize-24.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\Square44x44Logo.targetsize-256.png" />
<Content Include="Images\Square44x44Logo.targetsize-32.png" />
<Content Include="Images\Square44x44Logo.targetsize-48.png" />
<Content Include="Images\StoreLogo.scale-100.png" />
<Content Include="Images\StoreLogo.scale-125.png" />
<Content Include="Images\StoreLogo.scale-150.png" />
<Content Include="Images\StoreLogo.scale-200.png" />
<Content Include="Images\StoreLogo.scale-400.png" />
<Content Include="Images\Wide310x150Logo.scale-100.png" />
<Content Include="Images\Wide310x150Logo.scale-125.png" />
<Content Include="Images\Wide310x150Logo.scale-150.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<Content Include="Images\Wide310x150Logo.scale-400.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
<EnableMsixTooling>true</EnableMsixTooling>
<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" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
<PathToXAMLWinRTImplementations>BetterLyrics.WinUI3\</PathToXAMLWinRTImplementations>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>6576cd19-ef92-4099-b37d-e2d8ebdb6bf5</ProjectGuid>
<TargetPlatformVersion>10.0.26100.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<AssetTargetFallback>net8.0-windows$(TargetPlatformVersion);$(AssetTargetFallback)</AssetTargetFallback>
<DefaultLanguage>zh-CN</DefaultLanguage>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<EntryPointProjectUniqueName>..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj</EntryPointProjectUniqueName>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
<HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<PackageCertificateKeyFile>BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx</PackageCertificateKeyFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<AppxBundle>Always</AppxBundle>
<DefaultLanguage>en-US</DefaultLanguage>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<None Include="BetterLyrics.WinUI3 %28Package%29_TemporaryKey.pfx" />
<Content Include="Images\LargeTile.scale-100.png" />
<Content Include="Images\LargeTile.scale-125.png" />
<Content Include="Images\LargeTile.scale-150.png" />
<Content Include="Images\LargeTile.scale-200.png" />
<Content Include="Images\LargeTile.scale-400.png" />
<Content Include="Images\SmallTile.scale-100.png" />
<Content Include="Images\SmallTile.scale-125.png" />
<Content Include="Images\SmallTile.scale-150.png" />
<Content Include="Images\SmallTile.scale-200.png" />
<Content Include="Images\SmallTile.scale-400.png" />
<Content Include="Images\SplashScreen.scale-100.png" />
<Content Include="Images\SplashScreen.scale-125.png" />
<Content Include="Images\SplashScreen.scale-150.png" />
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\SplashScreen.scale-400.png" />
<Content Include="Images\Square150x150Logo.scale-100.png" />
<Content Include="Images\Square150x150Logo.scale-125.png" />
<Content Include="Images\Square150x150Logo.scale-150.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
<Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
<Content Include="Images\Square44x44Logo.scale-100.png" />
<Content Include="Images\Square44x44Logo.scale-125.png" />
<Content Include="Images\Square44x44Logo.scale-150.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-400.png" />
<Content Include="Images\Square44x44Logo.targetsize-16.png" />
<Content Include="Images\Square44x44Logo.targetsize-24.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\Square44x44Logo.targetsize-256.png" />
<Content Include="Images\Square44x44Logo.targetsize-32.png" />
<Content Include="Images\Square44x44Logo.targetsize-48.png" />
<Content Include="Images\StoreLogo.scale-100.png" />
<Content Include="Images\StoreLogo.scale-125.png" />
<Content Include="Images\StoreLogo.scale-150.png" />
<Content Include="Images\StoreLogo.scale-200.png" />
<Content Include="Images\StoreLogo.scale-400.png" />
<Content Include="Images\Wide310x150Logo.scale-100.png" />
<Content Include="Images\Wide310x150Logo.scale-125.png" />
<Content Include="Images\Wide310x150Logo.scale-150.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<Content Include="Images\Wide310x150Logo.scale-400.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BetterLyrics.WinUI3\BetterLyrics.WinUI3.csproj">
<EnableMsixTooling>true</EnableMsixTooling>
<SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
<PublishProfile>Properties\PublishProfiles\win-$(Platform).pubxml</PublishProfile>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<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>

View File

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

View File

@@ -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);
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<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();
}
public static byte[] Resize(byte[] imageBytes, int size)
public static async Task<IBuffer> 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;
}
}

View File

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

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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));
}

View File

@@ -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();
}
}

View File

@@ -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
View 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;
}
}
}

View 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();
}
}
}

View 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);
}
}
}

View 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>

View 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;
}
}
}

View 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
View 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);
}
}
}

View 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
View 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);
}
}
}