mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
215 changed files with 3331 additions and 1449 deletions
@ -0,0 +1,38 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
// <auto-generated />
|
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <summary>
|
|||
/// Represents a safe, fixed sized buffer of 4 elements.
|
|||
/// </summary>
|
|||
[InlineArray(4)] |
|||
internal struct InlineArray4<T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a safe, fixed sized buffer of 8 elements.
|
|||
/// </summary>
|
|||
[InlineArray(8)] |
|||
internal struct InlineArray8<T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a safe, fixed sized buffer of 16 elements.
|
|||
/// </summary>
|
|||
[InlineArray(16)] |
|||
internal struct InlineArray16<T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
|
|||
@ -0,0 +1,38 @@ |
|||
<#@ template debug="false" hostspecific="false" language="C#" #> |
|||
<#@ assembly name="System.Core" #> |
|||
<#@ import namespace="System.Linq" #> |
|||
<#@ import namespace="System.Text" #> |
|||
<#@ import namespace="System.Collections.Generic" #> |
|||
// Copyright (c) Six Labors. |
|||
// Licensed under the Six Labors Split License. |
|||
|
|||
// <auto-generated /> |
|||
|
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
<#GenerateInlineArrays();#> |
|||
|
|||
<#+ |
|||
private static int[] Lengths = new int[] {4, 8, 16 }; |
|||
|
|||
void GenerateInlineArrays() |
|||
{ |
|||
foreach (int length in Lengths) |
|||
{ |
|||
#> |
|||
/// <summary> |
|||
/// Represents a safe, fixed sized buffer of <#=length#> elements. |
|||
/// </summary> |
|||
[InlineArray(<#=length#>)] |
|||
internal struct InlineArray<#=length#><T> |
|||
{ |
|||
private T t; |
|||
} |
|||
|
|||
<#+ |
|||
} |
|||
} |
|||
#> |
|||
@ -0,0 +1,28 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
/// <summary>
|
|||
/// Defines the precision level used when matching colors during quantization.
|
|||
/// </summary>
|
|||
public enum ColorMatchingMode |
|||
{ |
|||
/// <summary>
|
|||
/// Uses a coarse caching strategy optimized for performance at the expense of exact matches.
|
|||
/// This provides the fastest matching but may yield approximate results.
|
|||
/// </summary>
|
|||
Coarse, |
|||
|
|||
/// <summary>
|
|||
/// Enables an exact color match cache for the first 512 unique colors encountered,
|
|||
/// falling back to coarse matching thereafter.
|
|||
/// </summary>
|
|||
Hybrid, |
|||
|
|||
/// <summary>
|
|||
/// Performs exact color matching without any caching optimizations.
|
|||
/// This is the slowest but most accurate matching strategy.
|
|||
/// </summary>
|
|||
Exact |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
/// <summary>
|
|||
/// Gets the closest color to the supplied color based upon the Euclidean distance.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <typeparam name="TCache">The cache type.</typeparam>
|
|||
/// <para>
|
|||
/// This class is not thread safe and should not be accessed in parallel.
|
|||
/// Doing so will result in non-idempotent results.
|
|||
/// </para>
|
|||
internal sealed class EuclideanPixelMap<TPixel, TCache> : PixelMap<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
where TCache : struct, IColorIndexCache<TCache> |
|||
{ |
|||
private Rgba32[] rgbaPalette; |
|||
|
|||
// Do not make readonly. It's a mutable struct.
|
|||
#pragma warning disable IDE0044 // Add readonly modifier
|
|||
private TCache cache; |
|||
#pragma warning restore IDE0044 // Add readonly modifier
|
|||
|
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel, TCache}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">Specifies the settings and resources for the pixel map's operations.</param>
|
|||
/// <param name="palette">Defines the color palette used for pixel mapping.</param>
|
|||
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.Palette = palette; |
|||
this.rgbaPalette = new Rgba32[palette.Length]; |
|||
this.cache = TCache.Create(configuration.MemoryAllocator); |
|||
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public override int GetClosestColor(TPixel color, out TPixel match) |
|||
{ |
|||
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); |
|||
Rgba32 rgba = color.ToRgba32(); |
|||
|
|||
if (this.cache.TryGetValue(rgba, out short index)) |
|||
{ |
|||
match = Unsafe.Add(ref paletteRef, (ushort)index); |
|||
return index; |
|||
} |
|||
|
|||
return this.GetClosestColorSlow(rgba, ref paletteRef, out match); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Clear(ReadOnlyMemory<TPixel> palette) |
|||
{ |
|||
this.Palette = palette; |
|||
this.rgbaPalette = new Rgba32[palette.Length]; |
|||
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); |
|||
this.cache.Clear(); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ColdPath)] |
|||
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) |
|||
{ |
|||
// Loop through the palette and find the nearest match.
|
|||
int index = 0; |
|||
float leastDistance = float.MaxValue; |
|||
for (int i = 0; i < this.rgbaPalette.Length; i++) |
|||
{ |
|||
Rgba32 candidate = this.rgbaPalette[i]; |
|||
if (candidate.PackedValue == rgba.PackedValue) |
|||
{ |
|||
index = i; |
|||
break; |
|||
} |
|||
|
|||
float distance = DistanceSquared(rgba, candidate); |
|||
if (distance == 0) |
|||
{ |
|||
index = i; |
|||
break; |
|||
} |
|||
|
|||
if (distance < leastDistance) |
|||
{ |
|||
index = i; |
|||
leastDistance = distance; |
|||
} |
|||
} |
|||
|
|||
// Now I have the index, pop it into the cache for next time
|
|||
_ = this.cache.TryAdd(rgba, (short)index); |
|||
match = Unsafe.Add(ref paletteRef, (uint)index); |
|||
|
|||
return index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the Euclidean distance squared between two specified points.
|
|||
/// </summary>
|
|||
/// <param name="a">The first point.</param>
|
|||
/// <param name="b">The second point.</param>
|
|||
/// <returns>The distance squared.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static float DistanceSquared(Rgba32 a, Rgba32 b) |
|||
{ |
|||
float deltaR = a.R - b.R; |
|||
float deltaG = a.G - b.G; |
|||
float deltaB = a.B - b.B; |
|||
float deltaA = a.A - b.A; |
|||
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Dispose() => this.cache.Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a map of colors to indices.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal abstract class PixelMap<TPixel> : IDisposable |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the color palette of this <see cref="PixelMap{TPixel}"/>.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<TPixel> Palette { get; private protected set; } |
|||
|
|||
/// <summary>
|
|||
/// Returns the closest color in the palette and the index of that pixel.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to match.</param>
|
|||
/// <param name="match">The matched color.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="int"/> index.
|
|||
/// </returns>
|
|||
public abstract int GetClosestColor(TPixel color, out TPixel match); |
|||
|
|||
/// <summary>
|
|||
/// Clears the map, resetting it to use the given palette.
|
|||
/// </summary>
|
|||
/// <param name="palette">The color palette to map from.</param>
|
|||
public abstract void Clear(ReadOnlyMemory<TPixel> palette); |
|||
|
|||
/// <inheritdoc/>
|
|||
public abstract void Dispose(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A factory for creating <see cref="PixelMap{TPixel}"/> instances.
|
|||
/// </summary>
|
|||
internal static class PixelMapFactory |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new <see cref="PixelMap{TPixel}"/> instance.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="palette">The color palette to map from.</param>
|
|||
/// <param name="colorMatchingMode">The color matching mode.</param>
|
|||
/// <returns>
|
|||
/// The <see cref="PixelMap{TPixel}"/>.
|
|||
/// </returns>
|
|||
public static PixelMap<TPixel> Create<TPixel>( |
|||
Configuration configuration, |
|||
ReadOnlyMemory<TPixel> palette, |
|||
ColorMatchingMode colorMatchingMode) |
|||
where TPixel : unmanaged, IPixel<TPixel> => colorMatchingMode switch |
|||
{ |
|||
ColorMatchingMode.Hybrid => new EuclideanPixelMap<TPixel, HybridCache>(configuration, palette), |
|||
ColorMatchingMode.Exact => new EuclideanPixelMap<TPixel, NullCache>(configuration, palette), |
|||
_ => new EuclideanPixelMap<TPixel, CoarseCache>(configuration, palette), |
|||
}; |
|||
} |
|||
@ -1,258 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
/// <summary>
|
|||
/// Gets the closest color to the supplied color based upon the Euclidean distance.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <para>
|
|||
/// This class is not thread safe and should not be accessed in parallel.
|
|||
/// Doing so will result in non-idempotent results.
|
|||
/// </para>
|
|||
internal sealed class EuclideanPixelMap<TPixel> : IDisposable |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
private Rgba32[] rgbaPalette; |
|||
private int transparentIndex; |
|||
private readonly TPixel transparentMatch; |
|||
|
|||
/// <summary>
|
|||
/// Do not make this readonly! Struct value would be always copied on non-readonly method calls.
|
|||
/// </summary>
|
|||
private ColorDistanceCache cache; |
|||
private readonly Configuration configuration; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="palette">The color palette to map from.</param>
|
|||
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette) |
|||
: this(configuration, palette, -1) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="EuclideanPixelMap{TPixel}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="palette">The color palette to map from.</param>
|
|||
/// <param name="transparentIndex">An explicit index at which to match transparent pixels.</param>
|
|||
public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory<TPixel> palette, int transparentIndex = -1) |
|||
{ |
|||
this.configuration = configuration; |
|||
this.Palette = palette; |
|||
this.rgbaPalette = new Rgba32[palette.Length]; |
|||
this.cache = new ColorDistanceCache(configuration.MemoryAllocator); |
|||
PixelOperations<TPixel>.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); |
|||
|
|||
this.transparentIndex = transparentIndex; |
|||
this.transparentMatch = TPixel.FromRgba32(default); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the color palette of this <see cref="EuclideanPixelMap{TPixel}"/>.
|
|||
/// The palette memory is owned by the palette source that created it.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<TPixel> Palette { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Returns the closest color in the palette and the index of that pixel.
|
|||
/// The palette contents must match the one used in the constructor.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to match.</param>
|
|||
/// <param name="match">The matched color.</param>
|
|||
/// <returns>The <see cref="int"/> index.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public int GetClosestColor(TPixel color, out TPixel match) |
|||
{ |
|||
ref TPixel paletteRef = ref MemoryMarshal.GetReference(this.Palette.Span); |
|||
Rgba32 rgba = color.ToRgba32(); |
|||
|
|||
// Check if the color is in the lookup table
|
|||
if (!this.cache.TryGetValue(rgba, out short index)) |
|||
{ |
|||
return this.GetClosestColorSlow(rgba, ref paletteRef, out match); |
|||
} |
|||
|
|||
match = Unsafe.Add(ref paletteRef, (ushort)index); |
|||
return index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears the map, resetting it to use the given palette.
|
|||
/// </summary>
|
|||
/// <param name="palette">The color palette to map from.</param>
|
|||
public void Clear(ReadOnlyMemory<TPixel> palette) |
|||
{ |
|||
this.Palette = palette; |
|||
this.rgbaPalette = new Rgba32[palette.Length]; |
|||
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, this.Palette.Span, this.rgbaPalette); |
|||
this.transparentIndex = -1; |
|||
this.cache.Clear(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Allows setting the transparent index after construction.
|
|||
/// </summary>
|
|||
/// <param name="index">An explicit index at which to match transparent pixels.</param>
|
|||
public void SetTransparentIndex(int index) |
|||
{ |
|||
if (index != this.transparentIndex) |
|||
{ |
|||
this.cache.Clear(); |
|||
} |
|||
|
|||
this.transparentIndex = index; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) |
|||
{ |
|||
// Loop through the palette and find the nearest match.
|
|||
int index = 0; |
|||
|
|||
if (this.transparentIndex >= 0 && rgba == default) |
|||
{ |
|||
// We have explicit instructions. No need to search.
|
|||
index = this.transparentIndex; |
|||
this.cache.Add(rgba, (byte)index); |
|||
match = this.transparentMatch; |
|||
return index; |
|||
} |
|||
|
|||
float leastDistance = float.MaxValue; |
|||
for (int i = 0; i < this.rgbaPalette.Length; i++) |
|||
{ |
|||
Rgba32 candidate = this.rgbaPalette[i]; |
|||
float distance = DistanceSquared(rgba, candidate); |
|||
|
|||
// If it's an exact match, exit the loop
|
|||
if (distance == 0) |
|||
{ |
|||
index = i; |
|||
break; |
|||
} |
|||
|
|||
if (distance < leastDistance) |
|||
{ |
|||
// Less than... assign.
|
|||
index = i; |
|||
leastDistance = distance; |
|||
} |
|||
} |
|||
|
|||
// Now I have the index, pop it into the cache for next time
|
|||
this.cache.Add(rgba, (byte)index); |
|||
match = Unsafe.Add(ref paletteRef, (uint)index); |
|||
return index; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the Euclidean distance squared between two specified points.
|
|||
/// </summary>
|
|||
/// <param name="a">The first point.</param>
|
|||
/// <param name="b">The second point.</param>
|
|||
/// <returns>The distance squared.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static float DistanceSquared(Rgba32 a, Rgba32 b) |
|||
{ |
|||
float deltaR = a.R - b.R; |
|||
float deltaG = a.G - b.G; |
|||
float deltaB = a.B - b.B; |
|||
float deltaA = a.A - b.A; |
|||
return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); |
|||
} |
|||
|
|||
public void Dispose() => this.cache.Dispose(); |
|||
|
|||
/// <summary>
|
|||
/// A cache for storing color distance matching results.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// The granularity of the cache has been determined based upon the current
|
|||
/// suite of test images and provides the lowest possible memory usage while
|
|||
/// providing enough match accuracy.
|
|||
/// Entry count is currently limited to 2335905 entries (4MB).
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
private unsafe struct ColorDistanceCache : IDisposable |
|||
{ |
|||
private const int IndexRBits = 5; |
|||
private const int IndexGBits = 5; |
|||
private const int IndexBBits = 5; |
|||
private const int IndexABits = 6; |
|||
private const int IndexRCount = (1 << IndexRBits) + 1; |
|||
private const int IndexGCount = (1 << IndexGBits) + 1; |
|||
private const int IndexBCount = (1 << IndexBBits) + 1; |
|||
private const int IndexACount = (1 << IndexABits) + 1; |
|||
private const int RShift = 8 - IndexRBits; |
|||
private const int GShift = 8 - IndexGBits; |
|||
private const int BShift = 8 - IndexBBits; |
|||
private const int AShift = 8 - IndexABits; |
|||
private const int Entries = IndexRCount * IndexGCount * IndexBCount * IndexACount; |
|||
private MemoryHandle tableHandle; |
|||
private readonly IMemoryOwner<short> table; |
|||
private readonly short* tablePointer; |
|||
|
|||
public ColorDistanceCache(MemoryAllocator allocator) |
|||
{ |
|||
this.table = allocator.Allocate<short>(Entries); |
|||
this.table.GetSpan().Fill(-1); |
|||
this.tableHandle = this.table.Memory.Pin(); |
|||
this.tablePointer = (short*)this.tableHandle.Pointer; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public readonly void Add(Rgba32 rgba, byte index) |
|||
{ |
|||
int idx = GetPaletteIndex(rgba); |
|||
this.tablePointer[idx] = index; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public readonly bool TryGetValue(Rgba32 rgba, out short match) |
|||
{ |
|||
int idx = GetPaletteIndex(rgba); |
|||
match = this.tablePointer[idx]; |
|||
return match > -1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears the cache resetting each entry to empty.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public readonly void Clear() => this.table.GetSpan().Fill(-1); |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int GetPaletteIndex(Rgba32 rgba) |
|||
{ |
|||
int rIndex = rgba.R >> RShift; |
|||
int gIndex = rgba.G >> GShift; |
|||
int bIndex = rgba.B >> BShift; |
|||
int aIndex = rgba.A >> AShift; |
|||
|
|||
return (aIndex * (IndexRCount * IndexGCount * IndexBCount)) + |
|||
(rIndex * (IndexGCount * IndexBCount)) + |
|||
(gIndex * IndexBCount) + bIndex; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (this.table != null) |
|||
{ |
|||
this.tableHandle.Dispose(); |
|||
this.table.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,569 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
/// <summary>
|
|||
/// Represents a cache used for efficiently retrieving palette indices for colors.
|
|||
/// </summary>
|
|||
internal interface IColorIndexCache : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Adds a color to the cache.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to add.</param>
|
|||
/// <param name="value">The index of the color in the palette.</param>
|
|||
/// <returns>
|
|||
/// <see langword="true"/> if the color was added; otherwise, <see langword="false"/>.
|
|||
/// </returns>
|
|||
public bool TryAdd(Rgba32 color, short value); |
|||
|
|||
/// <summary>
|
|||
/// Gets the index of the color in the palette.
|
|||
/// </summary>
|
|||
/// <param name="color">The color to get the index for.</param>
|
|||
/// <param name="value">The index of the color in the palette.</param>
|
|||
/// <returns>
|
|||
/// <see langword="true"/> if the color is in the palette; otherwise, <see langword="false"/>.
|
|||
/// </returns>
|
|||
public bool TryGetValue(Rgba32 color, out short value); |
|||
|
|||
/// <summary>
|
|||
/// Clears the cache.
|
|||
/// </summary>
|
|||
public void Clear(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a cache used for efficiently retrieving palette indices for colors.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The type of the cache.</typeparam>
|
|||
internal interface IColorIndexCache<T> : IColorIndexCache |
|||
where T : struct, IColorIndexCache |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the cache.
|
|||
/// </summary>
|
|||
/// <param name="allocator">The memory allocator to use.</param>
|
|||
/// <returns>
|
|||
/// The new instance of the cache.
|
|||
/// </returns>
|
|||
public static abstract T Create(MemoryAllocator allocator); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A hybrid color distance cache that combines a small, fixed-capacity exact-match dictionary
|
|||
/// (ExactCache, ~4–5 KB for up to 512 entries) with a coarse lookup table (CoarseCache) for 5,5,5,6 precision.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// ExactCache provides O(1) lookup for common cases using a simple 256-entry hash-based dictionary, while CoarseCache
|
|||
/// quantizes RGB channels to 5 bits (yielding 32^3 buckets) and alpha to 6 bits, storing up to 4 alpha entries per bucket
|
|||
/// (a design chosen based on probability theory to capture most real-world variations) for a total memory footprint of
|
|||
/// roughly 576 KB. Lookups and insertions are performed in constant time, making the overall design both fast and memory-predictable.
|
|||
/// </remarks>
|
|||
internal unsafe struct HybridCache : IColorIndexCache<HybridCache> |
|||
{ |
|||
private CoarseCache coarseCache; |
|||
private AccurateCache accurateCache; |
|||
|
|||
public HybridCache(MemoryAllocator allocator) |
|||
{ |
|||
this.accurateCache = AccurateCache.Create(allocator); |
|||
this.coarseCache = CoarseCache.Create(allocator); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static HybridCache Create(MemoryAllocator allocator) => new(allocator); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TryAdd(Rgba32 color, short index) |
|||
{ |
|||
if (this.accurateCache.TryAdd(color, index)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return this.coarseCache.TryAdd(color, index); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public readonly bool TryGetValue(Rgba32 color, out short value) |
|||
{ |
|||
if (this.accurateCache.TryGetValue(color, out value)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
return this.coarseCache.TryGetValue(color, out value); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public readonly void Clear() |
|||
{ |
|||
this.accurateCache.Clear(); |
|||
this.coarseCache.Clear(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.accurateCache.Dispose(); |
|||
this.coarseCache.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A coarse cache for color distance lookups that uses a fixed-size lookup table.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This cache uses a fixed lookup table with 2,097,152 bins, each storing a 2-byte value,
|
|||
/// resulting in a memory usage of approximately 4 MB. Lookups and insertions are
|
|||
/// performed in constant time (O(1)) via direct table indexing. This design is optimized for
|
|||
/// speed while maintaining a predictable, fixed memory footprint.
|
|||
/// </remarks>
|
|||
internal unsafe struct CoarseCache : IColorIndexCache<CoarseCache> |
|||
{ |
|||
private const int IndexRBits = 5; |
|||
private const int IndexGBits = 5; |
|||
private const int IndexBBits = 5; |
|||
private const int IndexABits = 6; |
|||
private const int IndexRCount = 1 << IndexRBits; // 32 bins for red
|
|||
private const int IndexGCount = 1 << IndexGBits; // 32 bins for green
|
|||
private const int IndexBCount = 1 << IndexBBits; // 32 bins for blue
|
|||
private const int IndexACount = 1 << IndexABits; // 64 bins for alpha
|
|||
private const int TotalBins = IndexRCount * IndexGCount * IndexBCount * IndexACount; // 2,097,152 bins
|
|||
|
|||
private readonly IMemoryOwner<short> binsOwner; |
|||
private readonly short* binsPointer; |
|||
private MemoryHandle binsHandle; |
|||
|
|||
private CoarseCache(MemoryAllocator allocator) |
|||
{ |
|||
this.binsOwner = allocator.Allocate<short>(TotalBins); |
|||
this.binsOwner.GetSpan().Fill(-1); |
|||
this.binsHandle = this.binsOwner.Memory.Pin(); |
|||
this.binsPointer = (short*)this.binsHandle.Pointer; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static CoarseCache Create(MemoryAllocator allocator) => new(allocator); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public readonly bool TryAdd(Rgba32 color, short value) |
|||
{ |
|||
this.binsPointer[GetCoarseIndex(color)] = value; |
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public readonly bool TryGetValue(Rgba32 color, out short value) |
|||
{ |
|||
value = this.binsPointer[GetCoarseIndex(color)]; |
|||
return value > -1; // Coarse match found
|
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int GetCoarseIndex(Rgba32 color) |
|||
{ |
|||
int rIndex = color.R >> (8 - IndexRBits); |
|||
int gIndex = color.G >> (8 - IndexGBits); |
|||
int bIndex = color.B >> (8 - IndexBBits); |
|||
int aIndex = color.A >> (8 - IndexABits); |
|||
|
|||
return (aIndex * IndexRCount * IndexGCount * IndexBCount) + |
|||
(rIndex * IndexGCount * IndexBCount) + |
|||
(gIndex * IndexBCount) + |
|||
bIndex; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public readonly void Clear() |
|||
=> this.binsOwner.GetSpan().Fill(-1); |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.binsHandle.Dispose(); |
|||
this.binsOwner.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// CoarseCache is a fast, low-memory lookup structure for caching palette indices associated with RGBA values,
|
|||
/// using a quantized representation of 5,5,5,6 (RGB: 5 bits each, Alpha: 6 bits).
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// The cache quantizes the RGB channels to 5 bits each, resulting in 32 levels per channel and a total of 32³ = 32,768 buckets.
|
|||
/// Each bucket is represented by an <see cref="AlphaBucket"/>, which holds a small, inline array of alpha entries.
|
|||
/// Each alpha entry stores the alpha value quantized to 6 bits (0–63) along with a palette index (a 16-bit value).
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Performance Characteristics:
|
|||
/// - Lookup: O(1) for computing the bucket index from the RGB channels, plus a small constant time (up to 8 iterations)
|
|||
/// to search through the alpha entries in the bucket.
|
|||
/// - Insertion: O(1) for bucket index computation and a quick linear search over a very small (fixed) number of entries.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Memory Characteristics:
|
|||
/// - The cache consists of 32,768 buckets.
|
|||
/// - Each <see cref="AlphaBucket"/> is implemented using an inline array with a capacity of 8 entries.
|
|||
/// - Each bucket occupies approximately 1 byte (Count) + (8 entries × 3 bytes each) ≈ 25 bytes.
|
|||
/// - Overall, the buckets occupy roughly 32,768 × 25 bytes = 819,200 bytes (≈ 800 KB).
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// This design provides nearly constant-time lookup and insertion with minimal memory usage,
|
|||
/// making it ideal for applications such as color distance caching in images with a limited palette (up to 256 entries).
|
|||
/// </para>
|
|||
/// </summary>
|
|||
internal unsafe struct CoarseCacheLite : IColorIndexCache<CoarseCacheLite> |
|||
{ |
|||
// Use 5 bits per channel for R, G, and B: 32 levels each.
|
|||
// Total buckets = 32^3 = 32768.
|
|||
private const int RgbBits = 5; |
|||
private const int RgbShift = 8 - RgbBits; // 3
|
|||
private const int BucketCount = 1 << (RgbBits * 3); // 32768
|
|||
private readonly IMemoryOwner<AlphaBucket> bucketsOwner; |
|||
private readonly AlphaBucket* buckets; |
|||
private MemoryHandle bucketHandle; |
|||
|
|||
private CoarseCacheLite(MemoryAllocator allocator) |
|||
{ |
|||
this.bucketsOwner = allocator.Allocate<AlphaBucket>(BucketCount, AllocationOptions.Clean); |
|||
this.bucketHandle = this.bucketsOwner.Memory.Pin(); |
|||
this.buckets = (AlphaBucket*)this.bucketHandle.Pointer; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public static CoarseCacheLite Create(MemoryAllocator allocator) => new(allocator); |
|||
|
|||
/// <inheritdoc/>
|
|||
public readonly bool TryAdd(Rgba32 color, short paletteIndex) |
|||
{ |
|||
int bucketIndex = GetBucketIndex(color.R, color.G, color.B); |
|||
byte quantAlpha = QuantizeAlpha(color.A); |
|||
this.buckets[bucketIndex].Add(quantAlpha, paletteIndex); |
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public readonly bool TryGetValue(Rgba32 color, out short paletteIndex) |
|||
{ |
|||
int bucketIndex = GetBucketIndex(color.R, color.G, color.B); |
|||
byte quantAlpha = QuantizeAlpha(color.A); |
|||
return this.buckets[bucketIndex].TryGetValue(quantAlpha, out paletteIndex); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public readonly void Clear() |
|||
{ |
|||
Span<AlphaBucket> bucketsSpan = this.bucketsOwner.GetSpan(); |
|||
bucketsSpan.Clear(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.bucketHandle.Dispose(); |
|||
this.bucketsOwner.Dispose(); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int GetBucketIndex(byte r, byte g, byte b) |
|||
{ |
|||
int qr = r >> RgbShift; |
|||
int qg = g >> RgbShift; |
|||
int qb = b >> RgbShift; |
|||
|
|||
// Combine the quantized channels into a single index.
|
|||
return (qr << (RgbBits << 1)) | (qg << RgbBits) | qb; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static byte QuantizeAlpha(byte a) |
|||
|
|||
// Quantize to 6 bits: shift right by (8 - 6) = 2 bits.
|
|||
=> (byte)(a >> 2); |
|||
|
|||
public struct AlphaEntry |
|||
{ |
|||
// Store the alpha value quantized to 6 bits (0..63)
|
|||
public byte QuantizedAlpha; |
|||
public short PaletteIndex; |
|||
} |
|||
|
|||
public struct AlphaBucket |
|||
{ |
|||
// Fixed capacity for alpha entries in this bucket.
|
|||
// We choose a capacity of 8 for several reasons:
|
|||
//
|
|||
// 1. The alpha channel is quantized to 6 bits, so there are 64 possible distinct values.
|
|||
// In the worst-case, a given RGB bucket might encounter up to 64 different alpha values.
|
|||
//
|
|||
// 2. However, in practice (based on probability theory and typical image data),
|
|||
// the number of unique alpha values that actually occur for a given quantized RGB
|
|||
// bucket is usually very small. If you randomly sample 8 values out of 64,
|
|||
// the probability that these 4 samples are all unique is high if the distribution
|
|||
// of alpha values is skewed or if only a few alpha values are used.
|
|||
//
|
|||
// 3. Statistically, for many real-world images, most RGB buckets will have only a couple
|
|||
// of unique alpha values. Allocating 8 slots per bucket provides a good trade-off:
|
|||
// it captures the common-case scenario while keeping overall memory usage low.
|
|||
//
|
|||
// 4. Even if more than 8 unique alpha values occur in a bucket,
|
|||
// our design overwrites the first entry. This behavior gives us some "wriggle room"
|
|||
// while preserving the most frequently encountered or most recent values.
|
|||
public const int Capacity = 8; |
|||
public byte Count; |
|||
private InlineArray8<AlphaEntry> entries; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TryGetValue(byte quantizedAlpha, out short paletteIndex) |
|||
{ |
|||
for (int i = 0; i < this.Count; i++) |
|||
{ |
|||
ref AlphaEntry entry = ref this.entries[i]; |
|||
if (entry.QuantizedAlpha == quantizedAlpha) |
|||
{ |
|||
paletteIndex = entry.PaletteIndex; |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
paletteIndex = -1; |
|||
return false; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Add(byte quantizedAlpha, short paletteIndex) |
|||
{ |
|||
// Check for an existing entry with the same quantized alpha.
|
|||
for (int i = 0; i < this.Count; i++) |
|||
{ |
|||
ref AlphaEntry entry = ref this.entries[i]; |
|||
if (entry.QuantizedAlpha == quantizedAlpha) |
|||
{ |
|||
// Update palette index if found.
|
|||
entry.PaletteIndex = paletteIndex; |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// If there's room, add a new entry.
|
|||
if (this.Count < Capacity) |
|||
{ |
|||
ref AlphaEntry newEntry = ref this.entries[this.Count]; |
|||
newEntry.QuantizedAlpha = quantizedAlpha; |
|||
newEntry.PaletteIndex = paletteIndex; |
|||
this.Count++; |
|||
} |
|||
else |
|||
{ |
|||
// Bucket is full. Overwrite the first entry to give us some wriggle room.
|
|||
this.entries[0].QuantizedAlpha = quantizedAlpha; |
|||
this.entries[0].PaletteIndex = paletteIndex; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// A fixed-capacity dictionary with exactly 512 entries mapping a <see cref="uint"/> key
|
|||
/// to a <see cref="short"/> value.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// The dictionary is implemented using a fixed array of 512 buckets and an entries array
|
|||
/// of the same size. The bucket for a key is computed as (key & 0x1FF), and collisions are
|
|||
/// resolved through a linked chain stored in the <see cref="Entry.Next"/> field.
|
|||
/// The overall memory usage is approximately 4–5 KB. Both lookup and insertion operations are,
|
|||
/// on average, O(1) since the bucket is determined via a simple bitmask and collision chains are
|
|||
/// typically very short; in the worst-case, the number of iterations is bounded by 256.
|
|||
/// This guarantees highly efficient and predictable performance for small, fixed-size color palettes.
|
|||
/// </remarks>
|
|||
internal unsafe struct AccurateCache : IColorIndexCache<AccurateCache> |
|||
{ |
|||
// Buckets array: each bucket holds the index (0-based) into the entries array
|
|||
// of the first entry in the chain, or -1 if empty.
|
|||
private readonly IMemoryOwner<short> bucketsOwner; |
|||
private MemoryHandle bucketsHandle; |
|||
private short* buckets; |
|||
|
|||
// Entries array: stores up to 256 entries.
|
|||
private readonly IMemoryOwner<Entry> entriesOwner; |
|||
private MemoryHandle entriesHandle; |
|||
private Entry* entries; |
|||
|
|||
public const int Capacity = 512; |
|||
|
|||
private AccurateCache(MemoryAllocator allocator) |
|||
{ |
|||
this.Count = 0; |
|||
|
|||
// Allocate exactly 512 indexes for buckets.
|
|||
this.bucketsOwner = allocator.Allocate<short>(Capacity, AllocationOptions.Clean); |
|||
Span<short> bucketSpan = this.bucketsOwner.GetSpan(); |
|||
bucketSpan.Fill(-1); |
|||
this.bucketsHandle = this.bucketsOwner.Memory.Pin(); |
|||
this.buckets = (short*)this.bucketsHandle.Pointer; |
|||
|
|||
// Allocate exactly 512 entries.
|
|||
this.entriesOwner = allocator.Allocate<Entry>(Capacity, AllocationOptions.Clean); |
|||
this.entriesHandle = this.entriesOwner.Memory.Pin(); |
|||
this.entries = (Entry*)this.entriesHandle.Pointer; |
|||
} |
|||
|
|||
public int Count { get; private set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public static AccurateCache Create(MemoryAllocator allocator) => new(allocator); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TryAdd(Rgba32 color, short value) |
|||
{ |
|||
if (this.Count == Capacity) |
|||
{ |
|||
return false; // Dictionary is full.
|
|||
} |
|||
|
|||
uint key = color.PackedValue; |
|||
|
|||
// The key is a 32-bit unsigned integer representing an RGBA color, where the bytes are laid out as R|G|B|A
|
|||
// (with R in the most significant byte and A in the least significant).
|
|||
// To compute the bucket index:
|
|||
// 1. (key >> 16) extracts the top 16 bits, effectively giving us the R and G channels.
|
|||
// 2. (key >> 8) shifts the key right by 8 bits, bringing R, G, and B into the lower 24 bits (dropping A).
|
|||
// 3. XORing these two values with the original key mixes bits from all four channels (R, G, B, and A),
|
|||
// which helps to counteract situations where one or more channels have a limited range.
|
|||
// 4. Finally, we apply a bitmask of 0x1FF to keep only the lowest 9 bits, ensuring the result is between 0 and 511,
|
|||
// which corresponds to our fixed bucket count of 512.
|
|||
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); |
|||
int i = this.buckets[bucket]; |
|||
|
|||
// Traverse the collision chain.
|
|||
Entry* entries = this.entries; |
|||
while (i != -1) |
|||
{ |
|||
Entry e = entries[i]; |
|||
if (e.Key == key) |
|||
{ |
|||
// Key already exists; do not overwrite.
|
|||
return false; |
|||
} |
|||
|
|||
i = e.Next; |
|||
} |
|||
|
|||
short index = (short)this.Count; |
|||
this.Count++; |
|||
|
|||
// Insert the new entry:
|
|||
entries[index].Key = key; |
|||
entries[index].Value = value; |
|||
|
|||
// Link this new entry into the bucket chain.
|
|||
entries[index].Next = this.buckets[bucket]; |
|||
this.buckets[bucket] = index; |
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TryGetValue(Rgba32 color, out short value) |
|||
{ |
|||
uint key = color.PackedValue; |
|||
int bucket = (int)(((key >> 16) ^ (key >> 8) ^ key) & 0x1FF); |
|||
int i = this.buckets[bucket]; |
|||
|
|||
// If the bucket is empty, return immediately.
|
|||
if (i == -1) |
|||
{ |
|||
value = -1; |
|||
return false; |
|||
} |
|||
|
|||
// Traverse the chain.
|
|||
Entry* entries = this.entries; |
|||
do |
|||
{ |
|||
Entry e = entries[i]; |
|||
if (e.Key == key) |
|||
{ |
|||
value = e.Value; |
|||
return true; |
|||
} |
|||
|
|||
i = e.Next; |
|||
} |
|||
while (i != -1); |
|||
|
|||
value = -1; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Clears the dictionary.
|
|||
/// </summary>
|
|||
public void Clear() |
|||
{ |
|||
Span<short> bucketSpan = this.bucketsOwner.GetSpan(); |
|||
bucketSpan.Fill(-1); |
|||
this.Count = 0; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.bucketsHandle.Dispose(); |
|||
this.bucketsOwner.Dispose(); |
|||
this.entriesHandle.Dispose(); |
|||
this.entriesOwner.Dispose(); |
|||
this.buckets = null; |
|||
this.entries = null; |
|||
} |
|||
|
|||
private struct Entry |
|||
{ |
|||
public uint Key; // The key (packed RGBA)
|
|||
public short Value; // The value; -1 means unused.
|
|||
public short Next; // Index of the next entry in the chain, or -1 if none.
|
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents a cache that does not store any values.
|
|||
/// It allows adding colors, but always returns false when trying to retrieve them.
|
|||
/// </summary>
|
|||
internal readonly struct NullCache : IColorIndexCache<NullCache> |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public static NullCache Create(MemoryAllocator allocator) => default; |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool TryAdd(Rgba32 color, short value) => true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool TryGetValue(Rgba32 color, out short value) |
|||
{ |
|||
value = -1; |
|||
return false; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Clear() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
|
|||
/// <summary>
|
|||
/// Defines a delegate for processing a row of pixels in an image for quantization.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">Represents a pixel type that can be processed in a quantizing operation.</typeparam>
|
|||
internal interface IQuantizingPixelRowDelegate<TPixel> |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
/// <summary>
|
|||
/// Processes a row of pixels for quantization.
|
|||
/// </summary>
|
|||
/// <param name="row">The row of pixels to process.</param>
|
|||
/// <param name="rowIndex">The index of the row being processed.</param>
|
|||
public void Invoke(ReadOnlySpan<TPixel> row, int rowIndex); |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Drawing.Imaging; |
|||
using BenchmarkDotNet.Attributes; |
|||
using SixLabors.ImageSharp.Formats.Gif; |
|||
using SixLabors.ImageSharp.Processing; |
|||
using SixLabors.ImageSharp.Processing.Processors.Quantization; |
|||
using SixLabors.ImageSharp.Tests; |
|||
using SDImage = System.Drawing.Image; |
|||
|
|||
namespace SixLabors.ImageSharp.Benchmarks.Codecs; |
|||
|
|||
public abstract class DecodeEncodeGif |
|||
{ |
|||
private MemoryStream outputStream; |
|||
|
|||
protected abstract GifEncoder Encoder { get; } |
|||
|
|||
[Params(TestImages.Gif.Leo, TestImages.Gif.Cheers)] |
|||
public string TestImage { get; set; } |
|||
|
|||
private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); |
|||
|
|||
[GlobalSetup] |
|||
public void Setup() => this.outputStream = new MemoryStream(); |
|||
|
|||
[GlobalCleanup] |
|||
public void Cleanup() => this.outputStream.Close(); |
|||
|
|||
[Benchmark(Baseline = true)] |
|||
public void SystemDrawing() |
|||
{ |
|||
this.outputStream.Position = 0; |
|||
using SDImage image = SDImage.FromFile(this.TestImageFullPath); |
|||
image.Save(this.outputStream, ImageFormat.Gif); |
|||
} |
|||
|
|||
[Benchmark] |
|||
public void ImageSharp() |
|||
{ |
|||
this.outputStream.Position = 0; |
|||
using Image image = Image.Load(this.TestImageFullPath); |
|||
image.SaveAsGif(this.outputStream, this.Encoder); |
|||
} |
|||
} |
|||
|
|||
public class DecodeEncodeGif_DefaultEncoder : DecodeEncodeGif |
|||
{ |
|||
protected override GifEncoder Encoder => new(); |
|||
} |
|||
|
|||
public class DecodeEncodeGif_CoarsePaletteEncoder : DecodeEncodeGif |
|||
{ |
|||
protected override GifEncoder Encoder => new() |
|||
{ |
|||
Quantizer = new WebSafePaletteQuantizer(new QuantizerOptions { Dither = KnownDitherings.Bayer4x4, ColorMatchingMode = ColorMatchingMode.Coarse }) |
|||
}; |
|||
} |
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:11375b15df083d98335f4a4baf0717e7fdd6b21ab2132a6815cadc787ac17e7d |
|||
oid sha256:a98b1ec707af066f77fad7d1a64b858d460986beb6d27682717dd5e221310fd4 |
|||
size 9270 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a899a84c6af24bfad89f9fde75957c7a979d65bcf096ab667cb976efd71cb560 |
|||
size 271171 |
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:7e22401dddf6552cd91517c1cdd142d3b9a66a7ad5c80d2e52ae07a7f583708e |
|||
size 57657 |
|||
oid sha256:e44c49a8f2ab1280c38e6ba71da29a93803b2aa4cf117e1e919909521b0373e6 |
|||
size 57636 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:819a0ce38e27e2adfa454d8c5ad5b24e818bf8954c9f2406f608dcecf506c2c4 |
|||
size 59838 |
|||
oid sha256:359a44bb957481c85d5acd65559b43ffc0acf806d4f4e57d6a791ca65b28295b |
|||
size 59839 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:007ac609ec61b39c7bdd04bc87a698f5cdc76eadd834c1457f41eb9c135c3f7b |
|||
oid sha256:7fb3743098a8147fd24294d933d93a61ec0155d754f52544650f6589719905be |
|||
size 60688 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:46892c07e9a93f1df71f0e38b331a437fb9b7c52d8f40cf62780cb6bd35d3b13 |
|||
size 58963 |
|||
oid sha256:41fa7d92a10db450f3b3729ab9e36074224baaefeda21cffd0466e37a111e138 |
|||
size 59113 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1b83345ca3de8d1fc0fbb5d8e68329b94ad79fc29b9f10a1392a97ffe9a0733e |
|||
size 58985 |
|||
oid sha256:bebf3b3762b339874891e3d434511e5f2557be90d66d6d7fe827b50334ede6c2 |
|||
size 58976 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:c775a5b19ba09e1b335389e0dc12cb0c3feaff6072e904da750a676fcd6b07dc |
|||
size 59202 |
|||
oid sha256:fd4358826739db2c22064e8aa90597f8b6403b9d7e2866ec280e743c51d2f41f |
|||
size 59203 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:8cc216ed952216d203836dc559234216614f1ed059651677cc0ea714010bd932 |
|||
size 58855 |
|||
oid sha256:174ee39c08eb9a174b48b19dc618d043bf6b71eee68ab7127407eb713e164e61 |
|||
size 58934 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 |
|||
size 871 |
|||
oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 |
|||
size 870 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:bb3e3b9b3001e76505fb0e2db7ad200cad2a016c06f1993c60c3cab42c134863 |
|||
size 867 |
|||
oid sha256:e51abcab66201997deda99637de604330ef977fd2d1dbebaa0416c621d03b8f9 |
|||
size 869 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 |
|||
size 871 |
|||
oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 |
|||
size 870 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:a3253003b088c9975725cf321c2fc827547a5feb199f2d1aa515c69bde59deb7 |
|||
size 871 |
|||
oid sha256:1110b46ec3296a1631420e0bb915f6fdc3d1cead4b0fc5a63a7a280fbf841ea2 |
|||
size 870 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:9316cbbcb137ae6ff31646f6a5ba1d0aec100db4512509f7684187e74d16a111 |
|||
size 51074 |
|||
oid sha256:eb86f2037a0aff48a84c0161f22eb2e2495daadbfa9c33185ddfd7b8429a4ea9 |
|||
size 51266 |
|||
|
|||
@ -1,3 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:6d2289ed4fa0c679f0f120d260fec8ab40b1599043cc0a1fbebc6b67e238ff87 |
|||
size 51428 |
|||
oid sha256:ef033a419e2e1b06b57a66175bad9068f71ae4c862a66c5734f65cdaae8a27f0 |
|||
size 51461 |
|||
|
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:0b980fb1927a70f88bd26b039c54e4ea20a6a1ad68aacd6f1a68a46eb1997a29 |
|||
size 1180 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:4006374b88ff4c4ed665333608a19e693fc083ae72beb71850d0e39ad45c9943 |
|||
size 1144 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1f5054e1e464c9e9fc999eec00b9949a6dc256ee062e9910b5718b6d4658661a |
|||
size 1303 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:49e8bcbcc5dc63fbd555f90a52b4e111cfc058f3adba2ca9c52dec966dbbae8f |
|||
size 1371 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:0b980fb1927a70f88bd26b039c54e4ea20a6a1ad68aacd6f1a68a46eb1997a29 |
|||
size 1180 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:4006374b88ff4c4ed665333608a19e693fc083ae72beb71850d0e39ad45c9943 |
|||
size 1144 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:1f5054e1e464c9e9fc999eec00b9949a6dc256ee062e9910b5718b6d4658661a |
|||
size 1303 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:49e8bcbcc5dc63fbd555f90a52b4e111cfc058f3adba2ca9c52dec966dbbae8f |
|||
size 1371 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:18b60d2066cb53d41988da37b8c521ddcb5355b995320a8413b95522a0492140 |
|||
size 687 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:30ff7708250c5f02dc02d74238d398b319d8fc6c071178f32f82a17e3b637afd |
|||
size 542 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:d21f4576486692122b6ee719d75883849f65ddb07f632ea1c62b42651c289688 |
|||
size 591 |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue