.editorconfig
+19
-0
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..2e0cc88
@@ -0,0 +1,19 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.cs]
indent_style = space
indent_size = 4
max_line_length = 120
[*.md]
trim_trailing_whitespace = false
[*.{json,xml,csproj,props}]
indent_style = space
indent_size = 2
.gitignore
+490
-0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..28b1b6e
@@ -0,0 +1,490 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from `dotnet new gitignore`
# dotenv files
.env
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
# but not Directory.Build.rsp, as it configures directory-level build defaults
!Directory.Build.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# but not directories ending in .e2e
!*.e2e/
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Official VS Code C# Dev Kit Extension exclusion
*.lscache
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea/
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# Vim temporary swap files
*.swp
# SixLabors license file
sixlabors.lic
Colors/example/Mosaic.Colors.Example.csproj
+10
-0
diff --git a/Colors/example/Mosaic.Colors.Example.csproj b/Colors/example/Mosaic.Colors.Example.csproj
new file mode 100644
index 0000000..c09b8e9
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Mosaic.Colors.Example</RootNamespace>
<AssemblyName>Mosaic.Colors.Example</AssemblyName>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\Mosaic.Colors.csproj" />
</ItemGroup>
</Project>
Colors/example/Program.cs
+21
-0
diff --git a/Colors/example/Program.cs b/Colors/example/Program.cs
new file mode 100644
index 0000000..5251dde
@@ -0,0 +1,21 @@
using System;
using System.Linq;
using Mosaic.Colors;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using var image = Image.Load<Rgba32>(args.Length > 0 ? args[0] : @".\images\example1.jpg");
image.ConvertToScaledOklch();
var palette = image.GetPalette();
Console.WriteLine(
" "
+ string.Join(
" │ ",
palette
.Select(p => p.color.FromScaledOklch())
.Select(color => $"\e[38;2;{color.R};{color.G};{color.B}m████\e[39m")
)
);
Console.WriteLine(" " + string.Join(" │ ", palette.Select(p => $"{p.proportion * 100, 3:0}%")));
Colors/example/images/LICENSE
+21
-0
diff --git a/Colors/example/images/LICENSE b/Colors/example/images/LICENSE
new file mode 100644
index 0000000..0b34e12
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Lokesh Dhakar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Colors/example/images/example1.jpg
+0
-0
diff --git a/Colors/example/images/example1.jpg b/Colors/example/images/example1.jpg
new file mode 100644
index 0000000..ee92327
Binary files /dev/null and b/Colors/example/images/example1.jpg differ
Colors/example/images/example2.jpg
+0
-0
diff --git a/Colors/example/images/example2.jpg b/Colors/example/images/example2.jpg
new file mode 100644
index 0000000..f9b4ea0
Binary files /dev/null and b/Colors/example/images/example2.jpg differ
Colors/example/images/example3.jpg
+0
-0
diff --git a/Colors/example/images/example3.jpg b/Colors/example/images/example3.jpg
new file mode 100644
index 0000000..2e38e37
Binary files /dev/null and b/Colors/example/images/example3.jpg differ
Colors/example/packages.lock.json
+33
-0
diff --git a/Colors/example/packages.lock.json b/Colors/example/packages.lock.json
new file mode 100644
index 0000000..73f011e
@@ -0,0 +1,33 @@
{
"version": 2,
"dependencies": {
"net11.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.3.0, )",
"resolved": "1.3.0",
"contentHash": "jzzLpmz7F6LyBh5FTZy8/4dEYVcAG9VjBPKryZ82LIoUuupcBkCSXzaeS/4oYcp/rBC8yVLcDstJ8CIv9n2IFw=="
},
"System.IO.Hashing": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA=="
},
"mosaic.colors": {
"type": "Project",
"dependencies": {
"SixLabors.ImageSharp": "[4.0.0, )"
}
},
"SixLabors.ImageSharp": {
"type": "CentralTransitive",
"requested": "[4.0.0, )",
"resolved": "4.0.0",
"contentHash": "6Ft5sclM1t5bY+P0SsLLGwXIAgmDASFFD9hD0tlEcfFCU8Yj9nR7ZzaqCL/p+SIfqoe8jLKFHD7LFPRJPGXZ5w==",
"dependencies": {
"System.IO.Hashing": "8.0.0"
}
}
}
}
}
\ No newline at end of file
Colors/src/ColorAnalyzer.cs
+18
-0
diff --git a/Colors/src/ColorAnalyzer.cs b/Colors/src/ColorAnalyzer.cs
new file mode 100644
index 0000000..5064119
@@ -0,0 +1,18 @@
using System.Linq;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors;
public static class ColorAnalyzer
{
extension(Image<Rgba32> image)
{
public (Rgba32 color, float proportion)[] GetPalette()
{
var quantized = image.Quantize(6);
var total = quantized.Values.Aggregate(0.0f, (t, v) => t + v);
return [.. quantized.Select(kvp => (kvp.Key, kvp.Value / total)).OrderByDescending(p => p.Item2)];
}
}
}
Colors/src/HistogramBuilder.cs
+28
-0
diff --git a/Colors/src/HistogramBuilder.cs b/Colors/src/HistogramBuilder.cs
new file mode 100644
index 0000000..200568e
@@ -0,0 +1,28 @@
using System;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors;
internal class HistogramBuilder()
{
public readonly Memory<int> Histogram = new int[MmcqQuantizer.HistogramSize];
public void Populate(PixelAccessor<Rgba32> accessor)
{
var histogram = Histogram.Span;
for (var y = 0; y < accessor.Height; y++)
{
var row = accessor.GetRowSpan(y);
for (var x = 0; x < row.Length; x++)
{
var pixel = row[x];
var r = pixel.R >> MmcqQuantizer.Shift;
var g = pixel.G >> MmcqQuantizer.Shift;
var b = pixel.B >> MmcqQuantizer.Shift;
histogram[MmcqQuantizer.GetColorIndex(r, g, b)]++;
}
}
}
}
Colors/src/MmcqQuantizer.cs
+236
-0
diff --git a/Colors/src/MmcqQuantizer.cs b/Colors/src/MmcqQuantizer.cs
new file mode 100644
index 0000000..7e6f6aa
@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors;
internal static class MmcqQuantizer
{
internal const int SignificantBits = 5;
internal const int Shift = 8 - SignificantBits;
internal const int HistogramSize = 1 << (3 * SignificantBits);
internal const int VboxLength = 1 << SignificantBits;
internal const int MaxValue = 256 >> Shift;
internal const int Multiplier = 1 << Shift;
internal const int MaxIterations = 1000;
extension(Image<Rgba32> image)
{
internal Dictionary<Rgba32, int> Quantize(int maxColorCount)
{
if (maxColorCount < 2 || 256 < maxColorCount)
{
throw new ArgumentOutOfRangeException(
paramName: nameof(maxColorCount),
message: "Should be between 2 and 256"
);
}
var histogram = image.GetHistogram();
var vBox = image.GetVbox(histogram);
var queue = new PriorityQueue<VBox, VBox>(VBox.CountComparer);
queue.Enqueue(vBox, vBox);
Iterate(queue, (int)float.Ceiling(maxColorCount * 0.75f), histogram);
var queue2 = new PriorityQueue<VBox, VBox>(queue.UnorderedItems, VBox.CountVolumeComparer);
Iterate(queue2, maxColorCount, histogram);
return queue2.UnorderedItems.ToDictionary(p => p.Element.Average, p => p.Element.Count);
}
internal Memory<int> GetHistogram()
{
var builder = new HistogramBuilder();
image.ProcessPixelRows(builder.Populate);
return builder.Histogram;
}
internal VBox GetVbox(ReadOnlyMemory<int> histogram)
{
var builder = new VboxBuilder();
image.ProcessPixelRows(builder.Populate);
return builder.Build(histogram);
}
}
internal static void Iterate(PriorityQueue<VBox, VBox> queue, int target, ReadOnlyMemory<int> histogram)
{
for (var i = 0; i < MaxIterations; i++)
{
if (queue.Count >= target)
{
return;
}
var vbox = queue.Dequeue();
if (vbox.Count is 0)
{
queue.Enqueue(vbox, vbox);
continue;
}
var result = MedianCutApply(histogram, vbox);
if (result is not [var next, ..])
{
queue.Enqueue(vbox, vbox);
return;
}
queue.Enqueue(next, next);
if (result is [_, var nextNext, ..])
{
queue.Enqueue(nextNext, nextNext);
}
}
}
internal static VBox[] MedianCutApply(ReadOnlyMemory<int> histogram, VBox vBox)
{
if (vBox.Count is 0)
{
return [];
}
if (vBox.Count is 1)
{
return [vBox];
}
var rSize = vBox.RMax - vBox.RMin + 1;
var gSize = vBox.GMax - vBox.GMin + 1;
var bSize = vBox.BMax - vBox.BMin + 1;
var maxSize = int.Max(rSize, int.Max(gSize, bSize));
var total = 0;
var partialSum = new int[VboxLength];
var lookAheadSum = new int[VboxLength];
for (var i = 0; i < VboxLength; i++)
{
partialSum[i] = -1;
lookAheadSum[i] = -1;
}
if (maxSize == rSize)
{
var span = histogram.Span;
for (var r = vBox.RMin; r <= vBox.RMax; r++)
{
var sum = 0;
for (var g = vBox.GMin; g <= vBox.GMax; g++)
for (var b = vBox.BMin; b <= vBox.BMax; b++)
{
sum += span[GetColorIndex(r, g, b)];
}
total += sum;
partialSum[r] = total;
}
}
else if (maxSize == gSize)
{
var span = histogram.Span;
for (var g = vBox.GMin; g <= vBox.GMax; g++)
{
var sum = 0;
for (var r = vBox.RMin; r <= vBox.RMax; r++)
for (var b = vBox.BMin; b <= vBox.BMax; b++)
{
sum += span[GetColorIndex(r, g, b)];
}
total += sum;
partialSum[g] = total;
}
}
else
{
var span = histogram.Span;
for (var b = vBox.BMin; b <= vBox.BMax; b++)
{
var sum = 0;
for (var r = vBox.RMin; r <= vBox.RMax; r++)
for (var g = vBox.GMin; g <= vBox.GMax; g++)
{
sum += span[GetColorIndex(r, g, b)];
}
total += sum;
partialSum[b] = total;
}
}
for (var i = 0; i < VboxLength; i++)
{
if (partialSum[i] >= 0)
{
lookAheadSum[i] = total - partialSum[i];
}
}
if (maxSize == rSize)
{
return Cut(vBox.RMin, vBox.RMax) is (var left, var right)
?
[
new(vBox.RMin, left, vBox.GMin, vBox.GMax, vBox.BMin, vBox.BMax, vBox.Histogram),
new(right, vBox.RMax, vBox.GMin, vBox.GMax, vBox.BMin, vBox.BMax, vBox.Histogram),
]
: [];
}
else if (maxSize == gSize)
{
return Cut(vBox.GMin, vBox.GMax) is (var left, var right)
?
[
new(vBox.RMin, vBox.RMax, vBox.GMin, left, vBox.BMin, vBox.BMax, vBox.Histogram),
new(vBox.RMin, vBox.RMax, right, vBox.GMax, vBox.BMin, vBox.BMax, vBox.Histogram),
]
: [];
}
else
{
return Cut(vBox.BMin, vBox.BMax) is (var left, var right)
?
[
new(vBox.RMin, vBox.RMax, vBox.GMin, vBox.GMax, vBox.BMin, left, vBox.Histogram),
new(vBox.RMin, vBox.RMax, vBox.GMin, vBox.GMax, right, vBox.BMax, vBox.Histogram),
]
: [];
}
(int, int)? Cut(int min, int max)
{
for (var i = min; i <= max; i++)
{
if (partialSum[i] < total / 2)
{
continue;
}
var left = i - min;
var right = max - i;
var d2 = left <= right ? int.Min(max - 1, i + right / 2) : int.Max(min, i - 1 - left / 2);
while (d2 < 0 || partialSum[d2] <= 0)
{
d2++;
}
for (
var count2 = lookAheadSum[d2];
count2 <= 0 && d2 > min && partialSum[d2 - 1] > 0;
count2 = lookAheadSum[d2]
)
{
d2--;
}
return (d2, d2 + 1);
}
return null;
}
}
internal static int GetColorIndex(int r, int g, int b) => (r << (2 * SignificantBits)) + (g << SignificantBits) + b;
}
Colors/src/Mosaic.Colors.csproj
+12
-0
diff --git a/Colors/src/Mosaic.Colors.csproj b/Colors/src/Mosaic.Colors.csproj
new file mode 100644
index 0000000..830a236
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Mosaic.Colors</RootNamespace>
<AssemblyName>Mosaic.Colors</AssemblyName>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Mosaic.Colors.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
</Project>
Colors/src/OklchConverter.cs
+107
-0
diff --git a/Colors/src/OklchConverter.cs b/Colors/src/OklchConverter.cs
new file mode 100644
index 0000000..5bd7f7f
@@ -0,0 +1,107 @@
using System.Numerics;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors;
public static class OklchConverter
{
extension(Image<Rgba32> image)
{
public void ConvertToScaledOklch()
{
image.ProcessPixelRows(accessor =>
{
for (var y = 0; y < accessor.Height; y++)
{
var row = accessor.GetRowSpan(y);
for (var x = 0; x < row.Length; x++)
{
row[x] = row[x].ToScaledOklch();
}
}
});
}
}
extension(Rgba32 pixel)
{
public Rgba32 ToScaledOklch()
{
var oklch = pixel.ToOklch();
return new(oklch.X, oklch.Y / 0.4f, oklch.Z / 360.0f);
}
public Vector3 ToOklch()
{
var lr = SrgbToLinear(pixel.R);
var lg = SrgbToLinear(pixel.G);
var lb = SrgbToLinear(pixel.B);
// Linear sRGB → LMS (using Oklab M1 matrix)
var l_ = 0.4122214708f * lr + 0.5363325363f * lg + 0.0514459929f * lb;
var m_ = 0.2119034982f * lr + 0.6806995451f * lg + 0.1073969566f * lb;
var s_ = 0.0883024619f * lr + 0.2817188376f * lg + 0.6299787005f * lb;
// Cube root (LMS → Lab cone response)
var l3 = float.Cbrt(l_);
var m3 = float.Cbrt(m_);
var s3 = float.Cbrt(s_);
// LMS cone response → OKLab
var L = 0.2104542553f * l3 + 0.7936177850f * m3 - 0.0040720468f * s3;
var a = 1.9779984951f * l3 - 2.4285922050f * m3 + 0.4505937099f * s3;
var bLab = 0.0259040371f * l3 + 0.7827717662f * m3 - 0.8086757660f * s3;
// OKLab → OKLCH
var C = float.Sqrt(a * a + bLab * bLab);
var H = float.Atan2(bLab, a) * (180.0f / float.Pi);
while (H < 0.0f)
{
H += 360.0f;
}
return new(L, C, H);
}
public Rgba32 FromScaledOklch() =>
Rgba32.FromOklch(new(pixel.R / 255.0f, pixel.G / 255.0f * 0.4f, pixel.B / 255.0f * 360.0f));
public static Rgba32 FromOklch(Vector3 oklch)
{
// OKLCH → OKLab
var hRad = oklch.Z * (float.Pi / 180.0f);
var a = oklch.Y * float.Cos(hRad);
var bLab = oklch.Y * float.Sin(hRad);
// OKLab → LMS cone response
var l3 = oklch.X + 0.3963377774f * a + 0.2158037573f * bLab;
var m3 = oklch.X - 0.1055613458f * a - 0.0638541728f * bLab;
var s3 = oklch.X - 0.0894841775f * a - 1.2914855480f * bLab;
// Cube (cone response → LMS)
var l_ = l3 * l3 * l3;
var m_ = m3 * m3 * m3;
var s_ = s3 * s3 * s3;
// LMS → linear sRGB (inverse of M1)
var lr = +4.0767416621f * l_ - 3.3077115913f * m_ + 0.2309699292f * s_;
var lg = -1.2684380046f * l_ + 2.6097574011f * m_ - 0.3413193965f * s_;
var lb = -0.0041960863f * l_ - 0.7034186147f * m_ + 1.7076147010f * s_;
return new(LinearToSrgb(lr), LinearToSrgb(lg), LinearToSrgb(lb));
}
}
private static float SrgbToLinear(byte v)
{
var s = v / 255.0f;
return s <= 0.04045f ? s / 12.92f : float.Pow((s + 0.055f) / 1.055f, 2.4f);
}
private static byte LinearToSrgb(float v)
{
var s = v <= 0.0031308f ? 12.92f * v : 1.055f * float.Pow(v, 1.0f / 2.4f) - 0.055f;
return (byte)float.Round(float.Clamp(s * 255.0f, 0.0f, 255.0f));
}
}
Colors/src/VBox.cs
+145
-0
diff --git a/Colors/src/VBox.cs b/Colors/src/VBox.cs
new file mode 100644
index 0000000..5faf1d3
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors;
internal class VboxBuilder
{
private int rMin = (int)MmcqQuantizer.MaxValue;
private int rMax = 0;
private int gMin = (int)MmcqQuantizer.MaxValue;
private int gMax = 0;
private int bMin = (int)MmcqQuantizer.MaxValue;
private int bMax = 0;
public void Populate(PixelAccessor<Rgba32> accessor)
{
for (var y = 0; y < accessor.Height; y++)
{
var row = accessor.GetRowSpan(y);
for (var x = 0; x < row.Length; x++)
{
var pixel = row[x];
var r = pixel.R >> MmcqQuantizer.Shift;
rMin = int.Min(rMin, r);
rMax = int.Max(rMax, r);
var g = pixel.G >> MmcqQuantizer.Shift;
gMin = int.Min(gMin, g);
gMax = int.Max(gMax, g);
var b = pixel.B >> MmcqQuantizer.Shift;
bMin = int.Min(bMin, b);
bMax = int.Max(bMax, b);
}
}
}
public VBox Build(ReadOnlyMemory<int> histogram) => new(rMin, rMax, gMin, gMax, bMin, bMax, histogram);
}
internal class VBox
{
public static IComparer<VBox> CountComparer { get; } = new CountComparerImplementation();
private sealed class CountComparerImplementation : IComparer<VBox>
{
public int Compare(VBox? x, VBox? y) => x is null || y is null ? -1 : y.Count.CompareTo(x.Count);
}
public static IComparer<VBox> CountVolumeComparer { get; } = new CountVolumeComparerImplementation();
private sealed class CountVolumeComparerImplementation : IComparer<VBox>
{
public int Compare(VBox? x, VBox? y) =>
x is null || y is null ? -1 : (y.Count * y.Volume).CompareTo(x.Count * x.Volume);
}
public readonly int RMin;
public readonly int RMax;
public readonly int GMin;
public readonly int GMax;
public readonly int BMin;
public readonly int BMax;
public readonly ReadOnlyMemory<int> Histogram;
public readonly int Volume;
private readonly Lazy<int> count;
public int Count => count.Value;
private readonly Lazy<Rgba32> average;
public Rgba32 Average => average.Value;
public VBox(int rMin, int rMax, int gMin, int gMax, int bMin, int bMax, ReadOnlyMemory<int> histogram)
{
RMin = rMin;
RMax = rMax;
GMin = gMin;
GMax = gMax;
BMin = bMin;
BMax = bMax;
Histogram = histogram;
Volume = (RMax - RMin + 1) * (GMax - GMin + 1) * (BMax - BMin + 1);
count = new(CalculateCount);
average = new(CalculateAverage);
}
private int CalculateCount()
{
var histogram = Histogram.Span;
var count = 0;
for (var r = RMin; r <= RMax; r++)
for (var g = GMin; g <= GMax; g++)
for (var b = BMin; b <= BMax; b++)
{
count += histogram[MmcqQuantizer.GetColorIndex(r, g, b)];
}
return count;
}
private Rgba32 CalculateAverage()
{
if (RMin == RMax && GMin == GMax && BMin == BMax)
{
return new(RMin << MmcqQuantizer.Shift, GMin << MmcqQuantizer.Shift, BMin << MmcqQuantizer.Shift);
}
var histogram = Histogram.Span;
var total = 0;
var rSum = 0.0d;
var gSum = 0.0d;
var bSum = 0.0d;
for (var r = RMin; r <= RMax; r++)
for (var g = GMin; g <= GMax; g++)
for (var b = BMin; b <= BMax; b++)
{
var value = histogram[MmcqQuantizer.GetColorIndex(r, g, b)];
total += value;
rSum += value * (r + 0.5d) * MmcqQuantizer.Multiplier;
gSum += value * (g + 0.5d) * MmcqQuantizer.Multiplier;
bSum += value * (b + 0.5d) * MmcqQuantizer.Multiplier;
}
if (total == 0.0d)
{
return new(
(byte)double.Floor(MmcqQuantizer.Multiplier * (RMin + RMax + 1) / 2.0d),
(byte)double.Floor(MmcqQuantizer.Multiplier * (RMin + RMax + 1) / 2.0d),
(byte)double.Floor(MmcqQuantizer.Multiplier * (RMin + RMax + 1) / 2.0d)
);
}
return new(
(byte)double.Floor(rSum / total),
(byte)double.Floor(gSum / total),
(byte)double.Floor(bSum / total)
);
}
}
Colors/src/packages.lock.json
+27
-0
diff --git a/Colors/src/packages.lock.json b/Colors/src/packages.lock.json
new file mode 100644
index 0000000..6a08c83
@@ -0,0 +1,27 @@
{
"version": 2,
"dependencies": {
"net11.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.3.0, )",
"resolved": "1.3.0",
"contentHash": "jzzLpmz7F6LyBh5FTZy8/4dEYVcAG9VjBPKryZ82LIoUuupcBkCSXzaeS/4oYcp/rBC8yVLcDstJ8CIv9n2IFw=="
},
"SixLabors.ImageSharp": {
"type": "Direct",
"requested": "[4.0.0, )",
"resolved": "4.0.0",
"contentHash": "6Ft5sclM1t5bY+P0SsLLGwXIAgmDASFFD9hD0tlEcfFCU8Yj9nR7ZzaqCL/p+SIfqoe8jLKFHD7LFPRJPGXZ5w==",
"dependencies": {
"System.IO.Hashing": "8.0.0"
}
},
"System.IO.Hashing": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA=="
}
}
}
}
\ No newline at end of file
Colors/tests/MSTestSettings.cs
+3
-0
diff --git a/Colors/tests/MSTestSettings.cs b/Colors/tests/MSTestSettings.cs
new file mode 100644
index 0000000..e466aa1
@@ -0,0 +1,3 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
Colors/tests/Mosaic.Colors.Tests.csproj
+17
-0
diff --git a/Colors/tests/Mosaic.Colors.Tests.csproj b/Colors/tests/Mosaic.Colors.Tests.csproj
new file mode 100644
index 0000000..26d7135
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Mosaic.Colors.Tests</RootNamespace>
<AssemblyName>Mosaic.Colors.Tests</AssemblyName>
<EnableMSTestRunner>true</EnableMSTestRunner>
<OutputType>Exe</OutputType>
<TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport>
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\src\Mosaic.Colors.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<PackageReference Include="Shouldly" />
</ItemGroup>
</Project>
Colors/tests/OklchConverterTests/FromOklchShould.cs
+25
-0
diff --git a/Colors/tests/OklchConverterTests/FromOklchShould.cs b/Colors/tests/OklchConverterTests/FromOklchShould.cs
new file mode 100644
index 0000000..a52dde3
@@ -0,0 +1,25 @@
using System.Numerics;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors.Tests.OklchConverterTests;
[TestClass]
public sealed class FromOklchShould
{
[TestMethod]
public void ProduceABadassColor()
{
// Arrange
var oklch = new Vector3(0.8391f, 0.1622f, 121.45f);
// Act
var rgb = Rgba32.FromOklch(oklch);
// Assert
rgb.R.ShouldBe<byte>(0xBA);
rgb.G.ShouldBe<byte>(0xDA);
rgb.B.ShouldBe<byte>(0x55);
}
}
Colors/tests/OklchConverterTests/FromScaledOklchShould.cs
+24
-0
diff --git a/Colors/tests/OklchConverterTests/FromScaledOklchShould.cs b/Colors/tests/OklchConverterTests/FromScaledOklchShould.cs
new file mode 100644
index 0000000..74177fc
@@ -0,0 +1,24 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors.Tests.OklchConverterTests;
[TestClass]
public sealed class FromScaledOklchShould
{
[TestMethod]
public void ProduceABadassColor()
{
// Arrange
var oklch = new Rgba32(214, 103, 86);
// Act
var rgb = oklch.FromScaledOklch();
// Assert
((float)rgb.R).ShouldBe(0xBA, 1.0f);
((float)rgb.G).ShouldBe(0xDA, 1.0f);
((float)rgb.B).ShouldBe(0x55, 1.0f);
}
}
Colors/tests/OklchConverterTests/RoundTrip.cs
+25
-0
diff --git a/Colors/tests/OklchConverterTests/RoundTrip.cs b/Colors/tests/OklchConverterTests/RoundTrip.cs
new file mode 100644
index 0000000..b1b1564
@@ -0,0 +1,25 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors.Tests.OklchConverterTests;
[TestClass]
public sealed class RoundTrip
{
[TestMethod]
public void RoundTripable()
{
// Arrange
var rgb = new Rgba32(0xBA, 0xDA, 0x55);
// Act
var oklch = rgb.ToScaledOklch();
var rgb2 = oklch.FromScaledOklch();
// Assert
((float)rgb2.R).ShouldBe(rgb.R, 1.0f);
((float)rgb2.G).ShouldBe(rgb.G, 1.0f);
((float)rgb2.B).ShouldBe(rgb.B, 1.0f);
}
}
Colors/tests/OklchConverterTests/ToOklchShould.cs
+24
-0
diff --git a/Colors/tests/OklchConverterTests/ToOklchShould.cs b/Colors/tests/OklchConverterTests/ToOklchShould.cs
new file mode 100644
index 0000000..3f3339f
@@ -0,0 +1,24 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors.Tests.OklchConverterTests;
[TestClass]
public sealed class ToOklchShould
{
[TestMethod]
public void ProduceABadassColor()
{
// Arrange
var rgb = new Rgba32(0xBA, 0xDA, 0x55);
// Act
var oklch = rgb.ToOklch();
// Assert
oklch.X.ShouldBe(0.8391f, 0.0001d);
oklch.Y.ShouldBe(0.1622f, 0.0001d);
oklch.Z.ShouldBe(121.45f, 0.01d);
}
}
Colors/tests/OklchConverterTests/ToScaledOklchShould.cs
+24
-0
diff --git a/Colors/tests/OklchConverterTests/ToScaledOklchShould.cs b/Colors/tests/OklchConverterTests/ToScaledOklchShould.cs
new file mode 100644
index 0000000..1c98831
@@ -0,0 +1,24 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Shouldly;
using SixLabors.ImageSharp.PixelFormats;
namespace Mosaic.Colors.Tests.OklchConverterTests;
[TestClass]
public sealed class ToScaledOklchShould
{
[TestMethod]
public void ProduceABadassColor()
{
// Arrange
var rgb = new Rgba32(0xBA, 0xDA, 0x55);
// Act
var oklch = rgb.ToScaledOklch();
// Assert
oklch.R.ShouldBe<byte>(214);
oklch.G.ShouldBe<byte>(103);
oklch.B.ShouldBe<byte>(86);
}
}
Colors/tests/packages.lock.json
+184
-0
diff --git a/Colors/tests/packages.lock.json b/Colors/tests/packages.lock.json
new file mode 100644
index 0000000..9681557
@@ -0,0 +1,184 @@
{
"version": 2,
"dependencies": {
"net11.0": {
"CSharpier.MsBuild": {
"type": "Direct",
"requested": "[1.3.0, )",
"resolved": "1.3.0",
"contentHash": "jzzLpmz7F6LyBh5FTZy8/4dEYVcAG9VjBPKryZ82LIoUuupcBkCSXzaeS/4oYcp/rBC8yVLcDstJ8CIv9n2IFw=="
},
"MSTest": {
"type": "Direct",
"requested": "[4.3.3, )",
"resolved": "4.3.3",
"contentHash": "jrnmaQgS4Z81ONQzTgB2GUDZ52r7Qvy3QYGyKZbJ7mX3YR/gwNaIcEAnbkJgefpp3V2ctpBMcn5Hg/72TV/R2g==",
"dependencies": {
"MSTest.TestAdapter": "4.3.3",
"MSTest.TestFramework": "4.3.3",
"Microsoft.NET.Test.Sdk": "18.4.0",
"Microsoft.Testing.Extensions.CodeCoverage": "18.9.0",
"Microsoft.Testing.Extensions.TrxReport": "2.3.3"
}
},
"Shouldly": {
"type": "Direct",
"requested": "[5.0.0-preview.2, )",
"resolved": "5.0.0-preview.2",
"contentHash": "vw++7WsnykjWU4cbdz2J1WJUwuJOOvTVQLfs9lsFc7ktiCOE6GzconTrOcIfCd38WnCOXZsOtsAZyfgpQitGtw=="
},
"Microsoft.ApplicationInsights": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "18.4.0",
"contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow=="
},
"Microsoft.DiaSymReader": {
"type": "Transitive",
"resolved": "2.2.9",
"contentHash": "WLbDc/W9XkHpGVtd1L6Vml8W6+PaaYZZTJASHjHF7W2WX/hld6bfNK102Ui+I3a6zIdx4+oKRJtMgHz44JmBfw=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "10.0.8",
"contentHash": "vLyZVpxmduO2jx+76ggqnsA3m81kwMY3NkWciNTj5E+Nvqb0VihqCvQP89QsGONWp0AJwMZG+u9GzaCjDdFGNw=="
},
"Microsoft.NET.Test.Sdk": {
"type": "Transitive",
"resolved": "18.4.0",
"contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==",
"dependencies": {
"Microsoft.CodeCoverage": "18.4.0",
"Microsoft.TestPlatform.TestHost": "18.4.0"
}
},
"Microsoft.Testing.Extensions.CodeCoverage": {
"type": "Transitive",
"resolved": "18.9.0",
"contentHash": "Odp8djWEUFTJoRV02y0zJaAwAHF31CtNaDgnKKM+8dBHbbaH7Ee0oo9gweLpbzjI7UrtMUzH6xLJSrICZw+Nwg==",
"dependencies": {
"Microsoft.DiaSymReader": "2.2.9",
"Microsoft.Extensions.DependencyModel": "10.0.8",
"Microsoft.Testing.Platform": "2.3.0"
}
},
"Microsoft.Testing.Extensions.Telemetry": {
"type": "Transitive",
"resolved": "2.3.3",
"contentHash": "nY8ceQyPWB9TRE1WE5Oe/sks2e10SxgPv61vBHsFYpgCeDtNwhpMZwmL29GklO3/etTdOsLdrCmNr4zJWaR2fg==",
"dependencies": {
"Microsoft.ApplicationInsights": "2.23.0",
"Microsoft.Testing.Platform": "2.3.3"
}
},
"Microsoft.Testing.Extensions.TrxReport": {
"type": "Transitive",
"resolved": "2.3.3",
"contentHash": "vYBqGSlT94B8d6d4PRdPQ83ogn3kK4kXcnI3SlHBHZu1XsU01IDTMnWoto5v5wXN/Mz4XsVM3Do/sJdGRgH29g==",
"dependencies": {
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.3",
"Microsoft.Testing.Platform": "2.3.3"
}
},
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
"type": "Transitive",
"resolved": "2.3.3",
"contentHash": "dceVNxnfTEjKnrIreLcMfkgrzzBY8kgCCJX5NAv7Lq/6vPlTP4VB5zxKeuYy0yfCoBtWuPkLjUG6qtOBMfpypA==",
"dependencies": {
"Microsoft.Testing.Platform": "2.3.3"
}
},
"Microsoft.Testing.Extensions.VSTestBridge": {
"type": "Transitive",
"resolved": "2.3.3",
"contentHash": "JLovZjH6K10YiHO/BcidsaSHo/M2DfOe9FDjxjFdV6aoHO6VYTTYlLBNcyLJSo20q5gDTmNvmxYVH39A942aJA==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.4.0",
"Microsoft.Testing.Extensions.Telemetry": "2.3.3",
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.3",
"Microsoft.Testing.Platform": "2.3.3"
}
},
"Microsoft.Testing.Platform.MSBuild": {
"type": "Transitive",
"resolved": "2.3.3",
"contentHash": "iVAvNbZ5JPDZSrTcIrCDoCsAkzjDxwDEt2mDuMd8ng1P6e2P2NCd0g0BsoBVG+nJLSmK//V+yeEvjCa3E2fxHw==",
"dependencies": {
"Microsoft.Testing.Platform": "2.3.3"
}
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "18.4.0",
"contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
"resolved": "18.4.0",
"contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "18.4.0",
"Newtonsoft.Json": "13.0.3"
}
},
"MSTest.Analyzers": {
"type": "Transitive",
"resolved": "4.3.3",
"contentHash": "cTcdSPIZ8WvchbC5DFmfHxrvVNZCtops+2W94le+Q+mx110Y9L9mqNVVc9flF35lFeL5GsY+rxGBylsjDk4+zg=="
},
"MSTest.TestAdapter": {
"type": "Transitive",
"resolved": "4.3.3",
"contentHash": "YxXA1YGqwcZ1UjZXHgeu65AWDR25PQ2lN5N5CYOhxftqOQh/FcgQk0Vz0nfHqLhqOGFI6g9uIfOWqAdobSNKSg==",
"dependencies": {
"MSTest.TestFramework": "4.3.3",
"Microsoft.Testing.Extensions.VSTestBridge": "2.3.3",
"Microsoft.Testing.Platform.MSBuild": "2.3.3"
}
},
"MSTest.TestFramework": {
"type": "Transitive",
"resolved": "4.3.3",
"contentHash": "Y4OSRh7VKV3nW+/vIqvfJ3sdLWfVJ9G11KpzynFAq3Bh/fpMaXt73v8GfL4mTz6pJ/ix3hcep4iy6qQNXt408w==",
"dependencies": {
"MSTest.Analyzers": "4.3.3"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"System.IO.Hashing": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA=="
},
"mosaic.colors": {
"type": "Project",
"dependencies": {
"SixLabors.ImageSharp": "[4.0.0, )"
}
},
"Microsoft.Testing.Platform": {
"type": "CentralTransitive",
"requested": "[2.3.3, )",
"resolved": "2.3.3",
"contentHash": "ENbH4BQh9riXtOKc25KKITfiGGWhWMBJA7pZuNaF9zxzzSaVkp5AMeZxGQ6sXYaR6xb724NLz985Is6XIYqLSg=="
},
"SixLabors.ImageSharp": {
"type": "CentralTransitive",
"requested": "[4.0.0, )",
"resolved": "4.0.0",
"contentHash": "6Ft5sclM1t5bY+P0SsLLGwXIAgmDASFFD9hD0tlEcfFCU8Yj9nR7ZzaqCL/p+SIfqoe8jLKFHD7LFPRJPGXZ5w==",
"dependencies": {
"System.IO.Hashing": "8.0.0"
}
}
}
}
}
\ No newline at end of file
Directory.Build.props
+9
-0
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..06ec977
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
</Project>
Directory.Packages.props
+18
-0
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..ad96178
@@ -0,0 +1,18 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<GlobalPackageReference Include="CSharpier.MsBuild" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</GlobalPackageReference>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Testing.Platform" Version="2.3.3" />
<PackageVersion Include="MSTest" Version="4.3.3" />
<PackageVersion Include="Shouldly" Version="5.0.0-preview.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="4.0.0" />
</ItemGroup>
</Project>
LICENSE
+21
-0
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dd6c85
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2026 Mårten Åsberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Mosaic.slnx
+7
-0
diff --git a/Mosaic.slnx b/Mosaic.slnx
new file mode 100644
index 0000000..4d52ec7
@@ -0,0 +1,7 @@
<Solution>
<Folder Name="/Colors/">
<Project Path="Colors/src/Mosaic.Colors.csproj" />
<Project Path="Colors/tests/Mosaic.Colors.Tests.csproj" />
<Project Path="Colors/example/Mosaic.Colors.Example.csproj" />
</Folder>
</Solution>
README.md
+1
-0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..637fab3
@@ -0,0 +1 @@
# Mosaic
dotnet-tools.json
+13
-0
diff --git a/dotnet-tools.json b/dotnet-tools.json
new file mode 100644
index 0000000..9e028ef
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.3.0",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}
global.json
+9
-0
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..ad870e9
@@ -0,0 +1,9 @@
{
"sdk": {
"version": "11.0.100-preview.6.26359.118",
"allowPrerelease": true
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}