Browse Source

Merge remote-tracking branch 'origin/master' into drop-pointers

af/merge-core
Anton Firszov 9 years ago
parent
commit
05369202c6
  1. 2
      src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
  2. 7
      src/ImageSharp.Drawing/Text/DrawText.cs
  3. 6
      src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs
  4. 15
      src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs
  5. 18
      src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs
  6. 49
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  7. 2
      src/ImageSharp/Formats/Png/PngEncoderOptions.cs
  8. 1
      src/ImageSharp/Quantizers/Box.cs
  9. 72
      src/ImageSharp/Quantizers/OctreeQuantizer.cs
  10. 11
      src/ImageSharp/Quantizers/PaletteQuantizer.cs
  11. 5
      src/ImageSharp/Quantizers/Quantization.cs
  12. 17
      src/ImageSharp/Quantizers/Quantizer.cs
  13. 36
      src/ImageSharp/Quantizers/WuArrayPool.cs
  14. 441
      src/ImageSharp/Quantizers/WuQuantizer.cs
  15. 3
      tests/ImageSharp.Tests/FileTestBase.cs
  16. 44
      tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs
  17. 1
      tests/ImageSharp.Tests/TestImages.cs
  18. 3
      tests/ImageSharp.Tests/TestImages/Formats/Gif/trans.gif

2
src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

@ -39,7 +39,7 @@
<PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta001">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="SixLabors.Fonts" Version="0.1.0-alpha0002" />
<PackageReference Include="SixLabors.Fonts" Version="0.1.0-alpha0005" />
<PackageReference Include="SixLabors.Shapes" Version="0.1.0-alpha0010" />
</ItemGroup>
<PropertyGroup>

7
src/ImageSharp.Drawing/Text/DrawText.cs

@ -177,13 +177,14 @@ namespace ImageSharp
dpi = new Vector2((float)source.MetaData.HorizontalResolution, (float)source.MetaData.VerticalResolution);
}
FontSpan style = new FontSpan(font)
FontSpan style = new FontSpan(font, dpi)
{
ApplyKerning = options.ApplyKerning,
TabWidth = options.TabWidth
TabWidth = options.TabWidth,
WrappingWidth = options.WrapTextWidth
};
renderer.RenderText(text, style, dpi);
renderer.RenderText(text, style);
System.Collections.Generic.IEnumerable<SixLabors.Shapes.IPath> shapesToDraw = glyphBuilder.Paths;

6
src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs

@ -41,6 +41,11 @@ namespace ImageSharp.Drawing
/// </summary>
public bool UseImageResolution;
/// <summary>
/// If greater than zero determine the width at which text should wrap.
/// </summary>
public float WrapTextWidth;
/// <summary>
/// Initializes a new instance of the <see cref="TextGraphicsOptions" /> struct.
/// </summary>
@ -52,6 +57,7 @@ namespace ImageSharp.Drawing
this.TabWidth = 4;
this.AntialiasSubpixelDepth = 16;
this.UseImageResolution = false;
this.WrapTextWidth = 0;
}
/// <summary>

15
src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs

@ -71,8 +71,19 @@ namespace ImageSharp.Dithering
public void Dither<TColor>(PixelAccessor<TColor> pixels, TColor source, TColor transformed, int x, int y, int width, int height)
where TColor : struct, IPixel<TColor>
{
// Assign the transformed pixel to the array.
pixels[x, y] = transformed;
this.Dither(pixels, source, transformed, x, y, width, height, true);
}
/// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dither<TColor>(PixelAccessor<TColor> pixels, TColor source, TColor transformed, int x, int y, int width, int height, bool replacePixel)
where TColor : struct, IPixel<TColor>
{
if (replacePixel)
{
// Assign the transformed pixel to the array.
pixels[x, y] = transformed;
}
// Calculate the error
Vector4 error = source.ToVector4() - transformed.ToVector4();

18
src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs

@ -25,5 +25,23 @@ namespace ImageSharp.Dithering
/// <typeparam name="TColor">The pixel format.</typeparam>
void Dither<TColor>(PixelAccessor<TColor> pixels, TColor source, TColor transformed, int x, int y, int width, int height)
where TColor : struct, IPixel<TColor>;
/// <summary>
/// Transforms the image applying the dither matrix. This method alters the input pixels array
/// </summary>
/// <param name="pixels">The pixel accessor </param>
/// <param name="source">The source pixel</param>
/// <param name="transformed">The transformed pixel</param>
/// <param name="x">The column index.</param>
/// <param name="y">The row index.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="replacePixel">
/// Whether to replace the pixel at the given coordinates with the transformed value.
/// Generally this would be true for standard two-color dithering but when used in conjunction with color quantization this should be false.
/// </param>
/// <typeparam name="TColor">The pixel format.</typeparam>
void Dither<TColor>(PixelAccessor<TColor> pixels, TColor source, TColor transformed, int x, int y, int width, int height, bool replacePixel)
where TColor : struct, IPixel<TColor>;
}
}

49
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -7,7 +7,6 @@ namespace ImageSharp.Formats
{
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -487,7 +486,7 @@ namespace ImageSharp.Formats
if (this.quantizer == null)
{
this.quantizer = new OctreeQuantizer<TColor>();
this.quantizer = new WuQuantizer<TColor>();
}
// Quantize the image returning a palette. This boxing is icky.
@ -495,52 +494,54 @@ namespace ImageSharp.Formats
// Grab the palette and write it to the stream.
TColor[] palette = quantized.Palette;
int pixelCount = palette.Length;
List<byte> transparentPixels = new List<byte>();
byte pixelCount = palette.Length.ToByte();
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3;
byte[] colorTable = ArrayPool<byte>.Shared.Rent(colorTableLength);
byte[] alphaTable = ArrayPool<byte>.Shared.Rent(pixelCount);
byte[] bytes = ArrayPool<byte>.Shared.Rent(4);
bool anyAlpha = false;
try
{
for (int i = 0; i < pixelCount; i++)
for (byte i = 0; i < pixelCount; i++)
{
int offset = i * 3;
palette[i].ToXyzwBytes(bytes, 0);
if (quantized.Pixels.Contains(i))
{
int offset = i * 3;
palette[i].ToXyzwBytes(bytes, 0);
int alpha = bytes[3];
byte alpha = bytes[3];
colorTable[offset] = bytes[0];
colorTable[offset + 1] = bytes[1];
colorTable[offset + 2] = bytes[2];
colorTable[offset] = bytes[0];
colorTable[offset + 1] = bytes[1];
colorTable[offset + 2] = bytes[2];
if (alpha < 255 && alpha <= this.options.Threshold)
{
// Ensure the index is actually being used in our array.
// I'd like to find a faster way of doing this.
if (quantized.Pixels.Contains((byte)i))
if (alpha > this.options.Threshold)
{
transparentPixels.Add((byte)i);
alpha = 255;
}
anyAlpha = anyAlpha || alpha < 255;
alphaTable[i] = alpha;
}
}
this.WriteChunk(stream, PngChunkTypes.Palette, colorTable, 0, colorTableLength);
// Write the transparency data
if (anyAlpha)
{
this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, alphaTable, 0, pixelCount);
}
}
finally
{
ArrayPool<byte>.Shared.Return(colorTable);
ArrayPool<byte>.Shared.Return(alphaTable);
ArrayPool<byte>.Shared.Return(bytes);
}
// Write the transparency data
if (transparentPixels.Any())
{
this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, transparentPixels.ToArray());
}
return quantized;
}

2
src/ImageSharp/Formats/Png/PngEncoderOptions.cs

@ -60,7 +60,7 @@ namespace ImageSharp.Formats
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 0;
public byte Threshold { get; set; } = 255;
/// <summary>
/// Gets or sets a value indicating whether this instance should write

1
src/ImageSharp/Quantizers/Wu/Box.cs → src/ImageSharp/Quantizers/Box.cs

@ -7,6 +7,7 @@ namespace ImageSharp.Quantizers
{
/// <summary>
/// Represents a box color cube.
/// TODO: This should be a struct for performance
/// </summary>
internal sealed class Box
{

72
src/ImageSharp/Quantizers/Octree/OctreeQuantizer.cs → src/ImageSharp/Quantizers/OctreeQuantizer.cs

@ -58,18 +58,12 @@ namespace ImageSharp.Quantizers
public override QuantizedImage<TColor> Quantize(ImageBase<TColor> image, int maxColors)
{
this.colors = maxColors.Clamp(1, 255);
this.octree = new Octree(this.GetBitsNeededForColorDepth(maxColors));
this.octree = new Octree(this.GetBitsNeededForColorDepth(this.colors));
return base.Quantize(image, maxColors);
return base.Quantize(image, this.colors);
}
/// <summary>
/// Execute a second pass through the bitmap
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="output">The output pixel array</param>
/// <param name="width">The width in pixels of the image</param>
/// <param name="height">The height in pixels of the image</param>
/// <inheritdoc/>
protected override void SecondPass(PixelAccessor<TColor> source, byte[] output, int width, int height)
{
// Load up the values for the first pixel. We can use these to speed up the second
@ -107,7 +101,7 @@ namespace ImageSharp.Quantizers
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height);
this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height, false);
}
output[(y * source.Width) + x] = pixelValue;
@ -115,36 +109,17 @@ namespace ImageSharp.Quantizers
}
}
/// <summary>
/// Process the pixel in the first pass of the algorithm
/// </summary>
/// <param name="pixel">
/// The pixel to quantize
/// </param>
/// <remarks>
/// This function need only be overridden if your quantize algorithm needs two passes,
/// such as an Octree quantizer.
/// </remarks>
/// <inheritdoc/>
protected override void InitialQuantizePixel(TColor pixel)
{
// Add the color to the Octree
this.octree.AddColor(pixel, this.pixelBuffer);
}
/// <summary>
/// Retrieve the palette for the quantized image.
/// </summary>
/// <returns>
/// The new color palette
/// </returns>
/// <inheritdoc/>
protected override TColor[] GetPalette()
{
if (this.palette == null)
{
this.palette = this.octree.Palletize(Math.Max(this.colors, 1));
}
return this.palette;
return this.palette ?? (this.palette = this.octree.Palletize(Math.Max(this.colors, 1)));
}
/// <summary>
@ -175,6 +150,7 @@ namespace ImageSharp.Quantizers
/// <returns>
/// The <see cref="int"/>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetBitsNeededForColorDepth(int colorCount)
{
return (int)Math.Ceiling(Math.Log(colorCount, 2));
@ -189,7 +165,7 @@ namespace ImageSharp.Quantizers
/// Mask used when getting the appropriate pixels for a given node
/// </summary>
// ReSharper disable once StaticMemberInGenericType
private static readonly int[] Mask = { 0x100, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
private static readonly int[] Mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
/// <summary>
/// The root of the Octree
@ -379,11 +355,6 @@ namespace ImageSharp.Quantizers
/// </summary>
private int blue;
/// <summary>
/// Alpha component
/// </summary>
private int alpha;
/// <summary>
/// The index of this node in the palette
/// </summary>
@ -406,7 +377,7 @@ namespace ImageSharp.Quantizers
// Construct the new node
this.leaf = level == colorBits;
this.red = this.green = this.blue = this.alpha = 0;
this.red = this.green = this.blue = 0;
this.pixelCount = 0;
// If a leaf, increment the leaf count
@ -454,10 +425,9 @@ namespace ImageSharp.Quantizers
int shift = 7 - level;
pixel.ToXyzwBytes(buffer, 0);
int index = ((buffer[3] & Mask[0]) >> (shift - 3)) |
((buffer[2] & Mask[level + 1]) >> (shift - 2)) |
((buffer[1] & Mask[level + 1]) >> (shift - 1)) |
((buffer[0] & Mask[level + 1]) >> shift);
int index = ((buffer[2] & Mask[level]) >> (shift - 2)) |
((buffer[1] & Mask[level]) >> (shift - 1)) |
((buffer[0] & Mask[level]) >> shift);
OctreeNode child = this.children[index];
@ -479,7 +449,7 @@ namespace ImageSharp.Quantizers
/// <returns>The number of leaves removed</returns>
public int Reduce()
{
this.red = this.green = this.blue = this.alpha = 0;
this.red = this.green = this.blue = 0;
int childNodes = 0;
// Loop through all children and add their information to this node
@ -490,7 +460,6 @@ namespace ImageSharp.Quantizers
this.red += this.children[index].red;
this.green += this.children[index].green;
this.blue += this.children[index].blue;
this.alpha += this.children[index].alpha;
this.pixelCount += this.children[index].pixelCount;
++childNodes;
this.children[index] = null;
@ -517,11 +486,10 @@ namespace ImageSharp.Quantizers
byte r = (this.red / this.pixelCount).ToByte();
byte g = (this.green / this.pixelCount).ToByte();
byte b = (this.blue / this.pixelCount).ToByte();
byte a = (this.alpha / this.pixelCount).ToByte();
// And set the color of the palette entry
TColor pixel = default(TColor);
pixel.PackFromBytes(r, g, b, a);
pixel.PackFromBytes(r, g, b, 255);
palette[index] = pixel;
// Consume the next palette index
@ -558,10 +526,9 @@ namespace ImageSharp.Quantizers
int shift = 7 - level;
pixel.ToXyzwBytes(buffer, 0);
int pixelIndex = ((buffer[3] & Mask[0]) >> (shift - 3)) |
((buffer[2] & Mask[level + 1]) >> (shift - 2)) |
((buffer[1] & Mask[level + 1]) >> (shift - 1)) |
((buffer[0] & Mask[level + 1]) >> shift);
int pixelIndex = ((buffer[2] & Mask[level]) >> (shift - 2)) |
((buffer[1] & Mask[level]) >> (shift - 1)) |
((buffer[0] & Mask[level]) >> shift);
if (this.children[pixelIndex] != null)
{
@ -588,9 +555,8 @@ namespace ImageSharp.Quantizers
this.red += buffer[0];
this.green += buffer[1];
this.blue += buffer[2];
this.alpha += buffer[3];
}
}
}
}
}
}

11
src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs → src/ImageSharp/Quantizers/PaletteQuantizer.cs

@ -7,7 +7,6 @@ namespace ImageSharp.Quantizers
{
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
/// <summary>
@ -71,13 +70,7 @@ namespace ImageSharp.Quantizers
return base.Quantize(image, maxColors);
}
/// <summary>
/// Execute a second pass through the bitmap
/// </summary>
/// <param name="source">The source image.</param>
/// <param name="output">The output pixel array</param>
/// <param name="width">The width in pixels of the image</param>
/// <param name="height">The height in pixels of the image</param>
/// <inheritdoc/>
protected override void SecondPass(PixelAccessor<TColor> source, byte[] output, int width, int height)
{
// Load up the values for the first pixel. We can use these to speed up the second
@ -115,7 +108,7 @@ namespace ImageSharp.Quantizers
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height);
this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height, false);
}
output[(y * source.Width) + x] = pixelValue;

5
src/ImageSharp/Quantizers/Options/Quantization.cs → src/ImageSharp/Quantizers/Quantization.cs

@ -12,17 +12,20 @@ namespace ImageSharp
{
/// <summary>
/// An adaptive Octree quantizer. Fast with good quality.
/// The quantizer only supports a single alpha value.
/// </summary>
Octree,
/// <summary>
/// Xiaolin Wu's Color Quantizer which generates high quality output.
/// The quantizer supports multiple alpha values.
/// </summary>
Wu,
/// <summary>
/// Palette based, Uses the collection of web-safe colors by default.
/// The quantizer supports multiple alpha values.
/// </summary>
Palette
}
}
}

17
src/ImageSharp/Quantizers/Octree/Quantizer.cs → src/ImageSharp/Quantizers/Quantizer.cs

@ -66,22 +66,9 @@ namespace ImageSharp.Quantizers
this.FirstPass(pixels, width, height);
}
// Collect the palette. Octree requires this to be done before the second pass runs.
// Collect the palette. Required before the second pass runs.
colorPalette = this.GetPalette();
if (this.Dither)
{
// We clone the image as we don't want to alter the original.
using (Image<TColor> clone = new Image<TColor>(image))
using (PixelAccessor<TColor> clonedPixels = clone.Lock())
{
this.SecondPass(clonedPixels, quantizedPixels, width, height);
}
}
else
{
this.SecondPass(pixels, quantizedPixels, width, height);
}
this.SecondPass(pixels, quantizedPixels, width, height);
}
return new QuantizedImage<TColor>(width, height, colorPalette, quantizedPixels);

36
src/ImageSharp/Quantizers/WuArrayPool.cs

@ -0,0 +1,36 @@
// <copyright file="WuArrayPool.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageSharp.Quantizers
{
using System.Buffers;
/// <summary>
/// Provides array pooling for the <see cref="WuQuantizer{TColor}"/>.
/// This is a separate class so that the pools can be shared accross multiple generic quantizer instaces.
/// </summary>
internal static class WuArrayPool
{
/// <summary>
/// The long array pool.
/// </summary>
public static readonly ArrayPool<long> LongPool = ArrayPool<long>.Create(TableLength, 25);
/// <summary>
/// The float array pool.
/// </summary>
public static readonly ArrayPool<float> FloatPool = ArrayPool<float>.Create(TableLength, 5);
/// <summary>
/// The byte array pool.
/// </summary>
public static readonly ArrayPool<byte> BytePool = ArrayPool<byte>.Create(TableLength, 5);
/// <summary>
/// The table length. Matches the calculated value in <see cref="WuQuantizer{TColor}"/>
/// </summary>
private const int TableLength = 2471625;
}
}

441
src/ImageSharp/Quantizers/Wu/WuQuantizer.cs → src/ImageSharp/Quantizers/WuQuantizer.cs

@ -7,8 +7,9 @@ namespace ImageSharp.Quantizers
{
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
/// <summary>
/// An implementation of Wu's color quantizer with alpha channel.
@ -30,7 +31,7 @@ namespace ImageSharp.Quantizers
/// </para>
/// </remarks>
/// <typeparam name="TColor">The pixel format.</typeparam>
public sealed class WuQuantizer<TColor> : IQuantizer<TColor>
public class WuQuantizer<TColor> : Quantizer<TColor>
where TColor : struct, IPixel<TColor>
{
/// <summary>
@ -59,103 +60,238 @@ namespace ImageSharp.Quantizers
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary>
/// The long array pool.
/// </summary>
private static readonly ArrayPool<long> LongPool = ArrayPool<long>.Create(TableLength, 25);
/// <summary>
/// The double array pool.
/// A buffer for storing pixels
/// </summary>
private static readonly ArrayPool<double> DoublePool = ArrayPool<double>.Create(TableLength, 5);
private readonly byte[] rgbaBuffer = new byte[4];
/// <summary>
/// The byte array pool.
/// A lookup table for colors
/// </summary>
private static readonly ArrayPool<byte> BytePool = ArrayPool<byte>.Create(TableLength, 5);
private readonly Dictionary<TColor, byte> colorMap = new Dictionary<TColor, byte>();
/// <summary>
/// Moment of <c>P(c)</c>.
/// </summary>
private readonly long[] vwt;
private long[] vwt;
/// <summary>
/// Moment of <c>r*P(c)</c>.
/// </summary>
private readonly long[] vmr;
private long[] vmr;
/// <summary>
/// Moment of <c>g*P(c)</c>.
/// </summary>
private readonly long[] vmg;
private long[] vmg;
/// <summary>
/// Moment of <c>b*P(c)</c>.
/// </summary>
private readonly long[] vmb;
private long[] vmb;
/// <summary>
/// Moment of <c>a*P(c)</c>.
/// </summary>
private readonly long[] vma;
private long[] vma;
/// <summary>
/// Moment of <c>c^2*P(c)</c>.
/// </summary>
private readonly double[] m2;
private float[] m2;
/// <summary>
/// Color space tag.
/// </summary>
private readonly byte[] tag;
private byte[] tag;
/// <summary>
/// A buffer for storing pixels
/// Maximum allowed color depth
/// </summary>
private readonly byte[] rgbaBuffer = new byte[4];
private int colors;
/// <summary>
/// The reduced image palette
/// </summary>
private TColor[] palette;
/// <summary>
/// The color cube representing the image palette
/// </summary>
private Box[] colorCube;
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer{TColor}"/> class.
/// </summary>
/// <remarks>
/// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram,
/// the second pass quantizes a color based on the position in the histogram.
/// </remarks>
public WuQuantizer()
: base(false)
{
this.vwt = LongPool.Rent(TableLength);
this.vmr = LongPool.Rent(TableLength);
this.vmg = LongPool.Rent(TableLength);
this.vmb = LongPool.Rent(TableLength);
this.vma = LongPool.Rent(TableLength);
this.m2 = DoublePool.Rent(TableLength);
this.tag = BytePool.Rent(TableLength);
}
/// <inheritdoc/>
public QuantizedImage<TColor> Quantize(ImageBase<TColor> image, int maxColors)
public override QuantizedImage<TColor> Quantize(ImageBase<TColor> image, int maxColors)
{
Guard.NotNull(image, nameof(image));
int colorCount = maxColors.Clamp(1, 256);
this.colors = maxColors.Clamp(1, 256);
try
{
this.vwt = WuArrayPool.LongPool.Rent(TableLength);
this.vmr = WuArrayPool.LongPool.Rent(TableLength);
this.vmg = WuArrayPool.LongPool.Rent(TableLength);
this.vmb = WuArrayPool.LongPool.Rent(TableLength);
this.vma = WuArrayPool.LongPool.Rent(TableLength);
this.m2 = WuArrayPool.FloatPool.Rent(TableLength);
this.tag = WuArrayPool.BytePool.Rent(TableLength);
return base.Quantize(image, this.colors);
}
finally
{
WuArrayPool.LongPool.Return(this.vwt, true);
WuArrayPool.LongPool.Return(this.vmr, true);
WuArrayPool.LongPool.Return(this.vmg, true);
WuArrayPool.LongPool.Return(this.vmb, true);
WuArrayPool.LongPool.Return(this.vma, true);
WuArrayPool.FloatPool.Return(this.m2, true);
WuArrayPool.BytePool.Return(this.tag, true);
}
}
/// <inheritdoc/>
protected override TColor[] GetPalette()
{
if (this.palette == null)
{
this.palette = new TColor[this.colors];
for (int k = 0; k < this.colors; k++)
{
this.Mark(this.colorCube[k], (byte)k);
float weight = Volume(this.colorCube[k], this.vwt);
if (MathF.Abs(weight) > Constants.Epsilon)
{
float r = Volume(this.colorCube[k], this.vmr) / weight;
float g = Volume(this.colorCube[k], this.vmg) / weight;
float b = Volume(this.colorCube[k], this.vmb) / weight;
float a = Volume(this.colorCube[k], this.vma) / weight;
TColor color = default(TColor);
color.PackFromVector4(new Vector4(r, g, b, a) / 255F);
this.palette[k] = color;
}
}
}
return this.palette;
}
/// <inheritdoc/>
protected override void InitialQuantizePixel(TColor pixel)
{
// Add the color to a 3-D color histogram.
// Colors are expected in r->g->b->a format
pixel.ToXyzwBytes(this.rgbaBuffer, 0);
byte r = this.rgbaBuffer[0];
byte g = this.rgbaBuffer[1];
byte b = this.rgbaBuffer[2];
byte a = this.rgbaBuffer[3];
int inr = r >> (8 - IndexBits);
int ing = g >> (8 - IndexBits);
int inb = b >> (8 - IndexBits);
int ina = a >> (8 - IndexAlphaBits);
int ind = GetPaletteIndex(inr + 1, ing + 1, inb + 1, ina + 1);
this.vwt[ind]++;
this.vmr[ind] += r;
this.vmg[ind] += g;
this.vmb[ind] += b;
this.vma[ind] += a;
this.m2[ind] += (r * r) + (g * g) + (b * b) + (a * a);
}
/// <inheritdoc/>
protected override void FirstPass(PixelAccessor<TColor> source, int width, int height)
{
// Build up the 3-D color histogram
// Loop through each row
for (int y = 0; y < height; y++)
{
// And loop through each column
for (int x = 0; x < width; x++)
{
// Now I have the pixel, call the FirstPassQuantize function...
this.InitialQuantizePixel(source[x, y]);
}
}
this.Clear();
this.Get3DMoments();
this.BuildCube();
}
using (PixelAccessor<TColor> imagePixels = image.Lock())
/// <inheritdoc/>
protected override void SecondPass(PixelAccessor<TColor> source, byte[] output, int width, int height)
{
// Load up the values for the first pixel. We can use these to speed up the second
// pass of the algorithm by avoiding transforming rows of identical color.
TColor sourcePixel = source[0, 0];
TColor previousPixel = sourcePixel;
byte pixelValue = this.QuantizePixel(sourcePixel);
TColor[] colorPalette = this.GetPalette();
TColor transformedPixel = colorPalette[pixelValue];
for (int y = 0; y < height; y++)
{
this.Build3DHistogram(imagePixels);
this.Get3DMoments();
// And loop through each column
for (int x = 0; x < width; x++)
{
// Get the pixel.
sourcePixel = source[x, y];
// Check if this is the same as the last pixel. If so use that value
// rather than calculating it again. This is an inexpensive optimization.
if (!previousPixel.Equals(sourcePixel))
{
// Quantize the pixel
pixelValue = this.QuantizePixel(sourcePixel);
// And setup the previous pointer
previousPixel = sourcePixel;
if (this.Dither)
{
transformedPixel = colorPalette[pixelValue];
}
}
Box[] cube;
this.BuildCube(out cube, ref colorCount);
if (this.Dither)
{
// Apply the dithering matrix. We have to reapply the value now as the original has changed.
this.DitherType.Dither(source, sourcePixel, transformedPixel, x, y, width, height, false);
}
return this.GenerateResult(imagePixels, colorCount, cube);
output[(y * source.Width) + x] = pixelValue;
}
}
}
/// <summary>
/// Gets an index.
/// Gets the index index of the given color in the palette.
/// </summary>
/// <param name="r">The red value.</param>
/// <param name="g">The green value.</param>
/// <param name="b">The blue value.</param>
/// <param name="a">The alpha value.</param>
/// <returns>The index.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetPaletteIndex(int r, int g, int b, int a)
{
return (r << ((IndexBits * 2) + IndexAlphaBits)) + (r << (IndexBits + IndexAlphaBits + 1))
@ -169,7 +305,7 @@ namespace ImageSharp.Quantizers
/// <param name="cube">The cube.</param>
/// <param name="moment">The moment.</param>
/// <returns>The result.</returns>
private static double Volume(Box cube, long[] moment)
private static float Volume(Box cube, long[] moment)
{
return moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A1)]
- moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)]
@ -310,55 +446,6 @@ namespace ImageSharp.Quantizers
}
}
/// <summary>
/// Clears the tables.
/// </summary>
private void Clear()
{
Array.Clear(this.vwt, 0, TableLength);
Array.Clear(this.vmr, 0, TableLength);
Array.Clear(this.vmg, 0, TableLength);
Array.Clear(this.vmb, 0, TableLength);
Array.Clear(this.vma, 0, TableLength);
Array.Clear(this.m2, 0, TableLength);
Array.Clear(this.tag, 0, TableLength);
}
/// <summary>
/// Builds a 3-D color histogram of <c>counts, r/g/b, c^2</c>.
/// </summary>
/// <param name="pixels">The pixel accessor.</param>
private void Build3DHistogram(PixelAccessor<TColor> pixels)
{
for (int y = 0; y < pixels.Height; y++)
{
for (int x = 0; x < pixels.Width; x++)
{
// Colors are expected in r->g->b->a format
pixels[x, y].ToXyzwBytes(this.rgbaBuffer, 0);
byte r = this.rgbaBuffer[0];
byte g = this.rgbaBuffer[1];
byte b = this.rgbaBuffer[2];
byte a = this.rgbaBuffer[3];
int inr = r >> (8 - IndexBits);
int ing = g >> (8 - IndexBits);
int inb = b >> (8 - IndexBits);
int ina = a >> (8 - IndexAlphaBits);
int ind = GetPaletteIndex(inr + 1, ing + 1, inb + 1, ina + 1);
this.vwt[ind]++;
this.vmr[ind] += r;
this.vmg[ind] += g;
this.vmb[ind] += b;
this.vma[ind] += a;
this.m2[ind] += (r * r) + (g * g) + (b * b) + (a * a);
}
}
}
/// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary>
@ -369,14 +456,14 @@ namespace ImageSharp.Quantizers
long[] volumeG = ArrayPool<long>.Shared.Rent(IndexCount * IndexAlphaCount);
long[] volumeB = ArrayPool<long>.Shared.Rent(IndexCount * IndexAlphaCount);
long[] volumeA = ArrayPool<long>.Shared.Rent(IndexCount * IndexAlphaCount);
double[] volume2 = ArrayPool<double>.Shared.Rent(IndexCount * IndexAlphaCount);
float[] volume2 = ArrayPool<float>.Shared.Rent(IndexCount * IndexAlphaCount);
long[] area = ArrayPool<long>.Shared.Rent(IndexAlphaCount);
long[] areaR = ArrayPool<long>.Shared.Rent(IndexAlphaCount);
long[] areaG = ArrayPool<long>.Shared.Rent(IndexAlphaCount);
long[] areaB = ArrayPool<long>.Shared.Rent(IndexAlphaCount);
long[] areaA = ArrayPool<long>.Shared.Rent(IndexAlphaCount);
double[] area2 = ArrayPool<double>.Shared.Rent(IndexAlphaCount);
float[] area2 = ArrayPool<float>.Shared.Rent(IndexAlphaCount);
try
{
@ -405,7 +492,7 @@ namespace ImageSharp.Quantizers
long lineG = 0;
long lineB = 0;
long lineA = 0;
double line2 = 0;
float line2 = 0;
for (int a = 1; a < IndexAlphaCount; a++)
{
@ -454,14 +541,14 @@ namespace ImageSharp.Quantizers
ArrayPool<long>.Shared.Return(volumeG);
ArrayPool<long>.Shared.Return(volumeB);
ArrayPool<long>.Shared.Return(volumeA);
ArrayPool<double>.Shared.Return(volume2);
ArrayPool<float>.Shared.Return(volume2);
ArrayPool<long>.Shared.Return(area);
ArrayPool<long>.Shared.Return(areaR);
ArrayPool<long>.Shared.Return(areaG);
ArrayPool<long>.Shared.Return(areaB);
ArrayPool<long>.Shared.Return(areaA);
ArrayPool<double>.Shared.Return(area2);
ArrayPool<float>.Shared.Return(area2);
}
}
@ -469,15 +556,15 @@ namespace ImageSharp.Quantizers
/// Computes the weighted variance of a box cube.
/// </summary>
/// <param name="cube">The cube.</param>
/// <returns>The <see cref="double"/>.</returns>
private double Variance(Box cube)
/// <returns>The <see cref="float"/>.</returns>
private float Variance(Box cube)
{
double dr = Volume(cube, this.vmr);
double dg = Volume(cube, this.vmg);
double db = Volume(cube, this.vmb);
double da = Volume(cube, this.vma);
float dr = Volume(cube, this.vmr);
float dg = Volume(cube, this.vmg);
float db = Volume(cube, this.vmb);
float da = Volume(cube, this.vma);
double xx =
float xx =
this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A1)]
- this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)]
- this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)]
@ -515,8 +602,8 @@ namespace ImageSharp.Quantizers
/// <param name="wholeB">The whole blue.</param>
/// <param name="wholeA">The whole alpha.</param>
/// <param name="wholeW">The whole weight.</param>
/// <returns>The <see cref="double"/>.</returns>
private double Maximize(Box cube, int direction, int first, int last, out int cut, double wholeR, double wholeG, double wholeB, double wholeA, double wholeW)
/// <returns>The <see cref="float"/>.</returns>
private float Maximize(Box cube, int direction, int first, int last, out int cut, float wholeR, float wholeG, float wholeB, float wholeA, float wholeW)
{
long baseR = Bottom(cube, direction, this.vmr);
long baseG = Bottom(cube, direction, this.vmg);
@ -524,20 +611,20 @@ namespace ImageSharp.Quantizers
long baseA = Bottom(cube, direction, this.vma);
long baseW = Bottom(cube, direction, this.vwt);
double max = 0.0;
float max = 0F;
cut = -1;
for (int i = first; i < last; i++)
{
double halfR = baseR + Top(cube, direction, i, this.vmr);
double halfG = baseG + Top(cube, direction, i, this.vmg);
double halfB = baseB + Top(cube, direction, i, this.vmb);
double halfA = baseA + Top(cube, direction, i, this.vma);
double halfW = baseW + Top(cube, direction, i, this.vwt);
float halfR = baseR + Top(cube, direction, i, this.vmr);
float halfG = baseG + Top(cube, direction, i, this.vmg);
float halfB = baseB + Top(cube, direction, i, this.vmb);
float halfA = baseA + Top(cube, direction, i, this.vma);
float halfW = baseW + Top(cube, direction, i, this.vwt);
double temp;
float temp;
if (Math.Abs(halfW) < Constants.Epsilon)
if (MathF.Abs(halfW) < Constants.Epsilon)
{
continue;
}
@ -550,7 +637,7 @@ namespace ImageSharp.Quantizers
halfA = wholeA - halfA;
halfW = wholeW - halfW;
if (Math.Abs(halfW) < Constants.Epsilon)
if (MathF.Abs(halfW) < Constants.Epsilon)
{
continue;
}
@ -575,21 +662,16 @@ namespace ImageSharp.Quantizers
/// <returns>Returns a value indicating whether the box has been split.</returns>
private bool Cut(Box set1, Box set2)
{
double wholeR = Volume(set1, this.vmr);
double wholeG = Volume(set1, this.vmg);
double wholeB = Volume(set1, this.vmb);
double wholeA = Volume(set1, this.vma);
double wholeW = Volume(set1, this.vwt);
int cutr;
int cutg;
int cutb;
int cuta;
double maxr = this.Maximize(set1, 0, set1.R0 + 1, set1.R1, out cutr, wholeR, wholeG, wholeB, wholeA, wholeW);
double maxg = this.Maximize(set1, 1, set1.G0 + 1, set1.G1, out cutg, wholeR, wholeG, wholeB, wholeA, wholeW);
double maxb = this.Maximize(set1, 2, set1.B0 + 1, set1.B1, out cutb, wholeR, wholeG, wholeB, wholeA, wholeW);
double maxa = this.Maximize(set1, 3, set1.A0 + 1, set1.A1, out cuta, wholeR, wholeG, wholeB, wholeA, wholeW);
float wholeR = Volume(set1, this.vmr);
float wholeG = Volume(set1, this.vmg);
float wholeB = Volume(set1, this.vmb);
float wholeA = Volume(set1, this.vma);
float wholeW = Volume(set1, this.vwt);
float maxr = this.Maximize(set1, 0, set1.R0 + 1, set1.R1, out int cutr, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxg = this.Maximize(set1, 1, set1.G0 + 1, set1.G1, out int cutg, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxb = this.Maximize(set1, 2, set1.B0 + 1, set1.B1, out int cutb, wholeR, wholeG, wholeB, wholeA, wholeW);
float maxa = this.Maximize(set1, 3, set1.A0 + 1, set1.A1, out int cuta, wholeR, wholeG, wholeB, wholeA, wholeW);
int dir;
@ -686,40 +768,38 @@ namespace ImageSharp.Quantizers
/// <summary>
/// Builds the cube.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="colorCount">The color count.</param>
private void BuildCube(out Box[] cube, ref int colorCount)
private void BuildCube()
{
cube = new Box[colorCount];
double[] vv = new double[colorCount];
this.colorCube = new Box[this.colors];
float[] vv = new float[this.colors];
for (int i = 0; i < colorCount; i++)
for (int i = 0; i < this.colors; i++)
{
cube[i] = new Box();
this.colorCube[i] = new Box();
}
cube[0].R0 = cube[0].G0 = cube[0].B0 = cube[0].A0 = 0;
cube[0].R1 = cube[0].G1 = cube[0].B1 = IndexCount - 1;
cube[0].A1 = IndexAlphaCount - 1;
this.colorCube[0].R0 = this.colorCube[0].G0 = this.colorCube[0].B0 = this.colorCube[0].A0 = 0;
this.colorCube[0].R1 = this.colorCube[0].G1 = this.colorCube[0].B1 = IndexCount - 1;
this.colorCube[0].A1 = IndexAlphaCount - 1;
int next = 0;
for (int i = 1; i < colorCount; i++)
for (int i = 1; i < this.colors; i++)
{
if (this.Cut(cube[next], cube[i]))
if (this.Cut(this.colorCube[next], this.colorCube[i]))
{
vv[next] = cube[next].Volume > 1 ? this.Variance(cube[next]) : 0.0;
vv[i] = cube[i].Volume > 1 ? this.Variance(cube[i]) : 0.0;
vv[next] = this.colorCube[next].Volume > 1 ? this.Variance(this.colorCube[next]) : 0F;
vv[i] = this.colorCube[i].Volume > 1 ? this.Variance(this.colorCube[i]) : 0F;
}
else
{
vv[next] = 0.0;
vv[next] = 0F;
i--;
}
next = 0;
double temp = vv[0];
float temp = vv[0];
for (int k = 1; k <= i; k++)
{
if (vv[k] > temp)
@ -731,79 +811,38 @@ namespace ImageSharp.Quantizers
if (temp <= 0.0)
{
colorCount = i + 1;
this.colors = i + 1;
break;
}
}
}
/// <summary>
/// Generates the quantized result.
/// Process the pixel in the second pass of the algorithm
/// </summary>
/// <param name="imagePixels">The image pixels.</param>
/// <param name="colorCount">The color count.</param>
/// <param name="cube">The cube.</param>
/// <returns>The result.</returns>
private QuantizedImage<TColor> GenerateResult(PixelAccessor<TColor> imagePixels, int colorCount, Box[] cube)
/// <param name="pixel">The pixel to quantize</param>
/// <returns>
/// The quantized value
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte QuantizePixel(TColor pixel)
{
TColor[] pallette = new TColor[colorCount];
byte[] pixels = new byte[imagePixels.Width * imagePixels.Height];
int width = imagePixels.Width;
int height = imagePixels.Height;
for (int k = 0; k < colorCount; k++)
if (this.Dither)
{
this.Mark(cube[k], (byte)k);
double weight = Volume(cube[k], this.vwt);
if (Math.Abs(weight) > Constants.Epsilon)
{
float r = (float)(Volume(cube[k], this.vmr) / weight);
float g = (float)(Volume(cube[k], this.vmg) / weight);
float b = (float)(Volume(cube[k], this.vmb) / weight);
float a = (float)(Volume(cube[k], this.vma) / weight);
TColor color = default(TColor);
color.PackFromVector4(new Vector4(r, g, b, a) / 255F);
pallette[k] = color;
}
// The colors have changed so we need to use Euclidean distance caclulation to find the closest value.
// This palette can never be null here.
return this.GetClosestColor(pixel, this.palette, this.colorMap);
}
Parallel.For(
0,
height,
imagePixels.ParallelOptions,
y =>
{
byte[] rgba = ArrayPool<byte>.Shared.Rent(4);
for (int x = 0; x < width; x++)
{
// Expected order r->g->b->a
imagePixels[x, y].ToXyzwBytes(rgba, 0);
int r = rgba[0] >> (8 - IndexBits);
int g = rgba[1] >> (8 - IndexBits);
int b = rgba[2] >> (8 - IndexBits);
int a = rgba[3] >> (8 - IndexAlphaBits);
int ind = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
pixels[(y * width) + x] = this.tag[ind];
}
ArrayPool<byte>.Shared.Return(rgba);
});
// Expected order r->g->b->a
pixel.ToXyzwBytes(this.rgbaBuffer, 0);
// Cleanup
LongPool.Return(this.vwt);
LongPool.Return(this.vmr);
LongPool.Return(this.vmg);
LongPool.Return(this.vmb);
LongPool.Return(this.vma);
DoublePool.Return(this.m2);
BytePool.Return(this.tag);
int r = this.rgbaBuffer[0] >> (8 - IndexBits);
int g = this.rgbaBuffer[1] >> (8 - IndexBits);
int b = this.rgbaBuffer[2] >> (8 - IndexBits);
int a = this.rgbaBuffer[3] >> (8 - IndexAlphaBits);
return new QuantizedImage<TColor>(width, height, pallette, pixels);
return this.tag[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)];
}
}
}

3
tests/ImageSharp.Tests/FileTestBase.cs

@ -28,7 +28,7 @@ namespace ImageSharp.Tests
// TestFile.Create(TestImages.Jpeg.Baseline.GammaDalaiLamaGray), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Jpeg.Progressive.Bad.BadEOF), // Perf: Enable for local testing only
TestFile.Create(TestImages.Bmp.Car),
// TestFile.Create(TestImages.Bmp.Neg_height), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Bmp.NegHeight), // Perf: Enable for local testing only
TestFile.Create(TestImages.Png.Splash),
// TestFile.Create(TestImages.Png.ChunkLength1), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Png.ChunkLength2), // Perf: Enable for local testing only
@ -46,6 +46,7 @@ namespace ImageSharp.Tests
// TestFile.Create(TestImages.Png.P1), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Png.Pd), // Perf: Enable for local testing only
TestFile.Create(TestImages.Gif.Rings),
// TestFile.Create(TestImages.Gif.Trans), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Gif.Cheers), // Perf: Enable for local testing only
// TestFile.Create(TestImages.Gif.Giphy) // Perf: Enable for local testing only
};

44
tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs

@ -13,6 +13,7 @@ namespace ImageSharp.Tests.Formats.Png
using ImageSharp.Formats;
using System.Linq;
using ImageSharp.IO;
using System.Numerics;
public class PngSmokeTests
{
@ -58,6 +59,49 @@ namespace ImageSharp.Tests.Formats.Png
}
}
// JJS: Commented out for now since the test does not take into lossy nature of indexing.
//[Theory]
//[WithTestPatternImages(100, 100, PixelTypes.Color)]
//public void CanSaveIndexedPngTwice<TColor>(TestImageProvider<TColor> provider)
// where TColor : struct, IPixel<TColor>
//{
// // does saving a file then repoening mean both files are identical???
// using (Image<TColor> source = provider.GetImage())
// using (MemoryStream ms = new MemoryStream())
// {
// source.MetaData.Quality = 256;
// source.Save(ms, new PngEncoder(), new PngEncoderOptions {
// Threshold = 200
// });
// ms.Position = 0;
// using (Image img1 = Image.Load(ms, new PngDecoder()))
// {
// using (MemoryStream ms2 = new MemoryStream())
// {
// img1.Save(ms2, new PngEncoder(), new PngEncoderOptions
// {
// Threshold = 200
// });
// ms2.Position = 0;
// using (Image img2 = Image.Load(ms2, new PngDecoder()))
// {
// using (PixelAccessor<Color> pixels1 = img1.Lock())
// using (PixelAccessor<Color> pixels2 = img2.Lock())
// {
// for (int y = 0; y < img1.Height; y++)
// {
// for (int x = 0; x < img1.Width; x++)
// {
// Assert.Equal(pixels1[x, y], pixels2[x, y]);
// }
// }
// }
// }
// }
// }
// }
//}
[Theory]
[WithTestPatternImages(300, 300, PixelTypes.All)]
public void Resize<TColor>(TestImageProvider<TColor> provider)

1
tests/ImageSharp.Tests/TestImages.cs

@ -103,6 +103,7 @@ namespace ImageSharp.Tests
public const string Rings = "Gif/rings.gif";
public const string Giphy = "Gif/giphy.gif";
public const string Cheers = "Gif/cheers.gif";
public const string Trans = "Gif/trans.gif";
}
}
}

3
tests/ImageSharp.Tests/TestImages/Formats/Gif/trans.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:99b20b417a63c30f62fa69a00fa0a76c2cb9848988e5def1b886460a129197f7
size 13707
Loading…
Cancel
Save