diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 259ae4b0f..8ba0cbdd0 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -39,7 +39,7 @@ All - + diff --git a/src/ImageSharp.Drawing/Text/DrawText.cs b/src/ImageSharp.Drawing/Text/DrawText.cs index 1f5f4cdb1..eba11b5c5 100644 --- a/src/ImageSharp.Drawing/Text/DrawText.cs +++ b/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 shapesToDraw = glyphBuilder.Paths; diff --git a/src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs b/src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs index b58a40b34..6b09f2395 100644 --- a/src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs +++ b/src/ImageSharp.Drawing/Text/TextGraphicsOptions.cs @@ -41,6 +41,11 @@ namespace ImageSharp.Drawing /// public bool UseImageResolution; + /// + /// If greater than zero determine the width at which text should wrap. + /// + public float WrapTextWidth; + /// /// Initializes a new instance of the struct. /// @@ -52,6 +57,7 @@ namespace ImageSharp.Drawing this.TabWidth = 4; this.AntialiasSubpixelDepth = 16; this.UseImageResolution = false; + this.WrapTextWidth = 0; } /// diff --git a/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs index cde146f1e..d6ab8eb64 100644 --- a/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs +++ b/src/ImageSharp/Dithering/ErrorDiffusion/ErrorDiffuser.cs @@ -71,8 +71,19 @@ namespace ImageSharp.Dithering public void Dither(PixelAccessor pixels, TColor source, TColor transformed, int x, int y, int width, int height) where TColor : struct, IPixel { - // Assign the transformed pixel to the array. - pixels[x, y] = transformed; + this.Dither(pixels, source, transformed, x, y, width, height, true); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dither(PixelAccessor pixels, TColor source, TColor transformed, int x, int y, int width, int height, bool replacePixel) + where TColor : struct, IPixel + { + if (replacePixel) + { + // Assign the transformed pixel to the array. + pixels[x, y] = transformed; + } // Calculate the error Vector4 error = source.ToVector4() - transformed.ToVector4(); diff --git a/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs b/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs index 18079b1fb..66ec3d515 100644 --- a/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs +++ b/src/ImageSharp/Dithering/ErrorDiffusion/IErrorDiffuser.cs @@ -25,5 +25,23 @@ namespace ImageSharp.Dithering /// The pixel format. void Dither(PixelAccessor pixels, TColor source, TColor transformed, int x, int y, int width, int height) where TColor : struct, IPixel; + + /// + /// Transforms the image applying the dither matrix. This method alters the input pixels array + /// + /// The pixel accessor + /// The source pixel + /// The transformed pixel + /// The column index. + /// The row index. + /// The image width. + /// The image height. + /// + /// 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. + /// + /// The pixel format. + void Dither(PixelAccessor pixels, TColor source, TColor transformed, int x, int y, int width, int height, bool replacePixel) + where TColor : struct, IPixel; } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 552673179..c11fc94df 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/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(); + this.quantizer = new WuQuantizer(); } // 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 transparentPixels = new List(); + byte pixelCount = palette.Length.ToByte(); // Get max colors for bit depth. int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3; byte[] colorTable = ArrayPool.Shared.Rent(colorTableLength); + byte[] alphaTable = ArrayPool.Shared.Rent(pixelCount); byte[] bytes = ArrayPool.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.Shared.Return(colorTable); + ArrayPool.Shared.Return(alphaTable); ArrayPool.Shared.Return(bytes); } - // Write the transparency data - if (transparentPixels.Any()) - { - this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, transparentPixels.ToArray()); - } - return quantized; } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs index 2891f1974..90175c6d6 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptions.cs @@ -60,7 +60,7 @@ namespace ImageSharp.Formats /// /// Gets or sets the transparency threshold. /// - public byte Threshold { get; set; } = 0; + public byte Threshold { get; set; } = 255; /// /// Gets or sets a value indicating whether this instance should write diff --git a/src/ImageSharp/Quantizers/Wu/Box.cs b/src/ImageSharp/Quantizers/Box.cs similarity index 96% rename from src/ImageSharp/Quantizers/Wu/Box.cs rename to src/ImageSharp/Quantizers/Box.cs index e715c7839..7f2a32087 100644 --- a/src/ImageSharp/Quantizers/Wu/Box.cs +++ b/src/ImageSharp/Quantizers/Box.cs @@ -7,6 +7,7 @@ namespace ImageSharp.Quantizers { /// /// Represents a box color cube. + /// TODO: This should be a struct for performance /// internal sealed class Box { diff --git a/src/ImageSharp/Quantizers/Octree/OctreeQuantizer.cs b/src/ImageSharp/Quantizers/OctreeQuantizer.cs similarity index 89% rename from src/ImageSharp/Quantizers/Octree/OctreeQuantizer.cs rename to src/ImageSharp/Quantizers/OctreeQuantizer.cs index 2590f297e..df52ee7f9 100644 --- a/src/ImageSharp/Quantizers/Octree/OctreeQuantizer.cs +++ b/src/ImageSharp/Quantizers/OctreeQuantizer.cs @@ -58,18 +58,12 @@ namespace ImageSharp.Quantizers public override QuantizedImage Quantize(ImageBase 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); } - /// - /// Execute a second pass through the bitmap - /// - /// The source image. - /// The output pixel array - /// The width in pixels of the image - /// The height in pixels of the image + /// protected override void SecondPass(PixelAccessor 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 } } - /// - /// Process the pixel in the first pass of the algorithm - /// - /// - /// The pixel to quantize - /// - /// - /// This function need only be overridden if your quantize algorithm needs two passes, - /// such as an Octree quantizer. - /// + /// protected override void InitialQuantizePixel(TColor pixel) { // Add the color to the Octree this.octree.AddColor(pixel, this.pixelBuffer); } - /// - /// Retrieve the palette for the quantized image. - /// - /// - /// The new color palette - /// + /// 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))); } /// @@ -175,6 +150,7 @@ namespace ImageSharp.Quantizers /// /// The /// + [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 /// // 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 }; /// /// The root of the Octree @@ -379,11 +355,6 @@ namespace ImageSharp.Quantizers /// private int blue; - /// - /// Alpha component - /// - private int alpha; - /// /// The index of this node in the palette /// @@ -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 /// The number of leaves removed 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]; } } } } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs b/src/ImageSharp/Quantizers/PaletteQuantizer.cs similarity index 91% rename from src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs rename to src/ImageSharp/Quantizers/PaletteQuantizer.cs index dedb5ca23..f039fe0c5 100644 --- a/src/ImageSharp/Quantizers/Palette/PaletteQuantizer.cs +++ b/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; /// @@ -71,13 +70,7 @@ namespace ImageSharp.Quantizers return base.Quantize(image, maxColors); } - /// - /// Execute a second pass through the bitmap - /// - /// The source image. - /// The output pixel array - /// The width in pixels of the image - /// The height in pixels of the image + /// protected override void SecondPass(PixelAccessor 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; diff --git a/src/ImageSharp/Quantizers/Options/Quantization.cs b/src/ImageSharp/Quantizers/Quantization.cs similarity index 81% rename from src/ImageSharp/Quantizers/Options/Quantization.cs rename to src/ImageSharp/Quantizers/Quantization.cs index 428c8e801..039404384 100644 --- a/src/ImageSharp/Quantizers/Options/Quantization.cs +++ b/src/ImageSharp/Quantizers/Quantization.cs @@ -12,17 +12,20 @@ namespace ImageSharp { /// /// An adaptive Octree quantizer. Fast with good quality. + /// The quantizer only supports a single alpha value. /// Octree, /// /// Xiaolin Wu's Color Quantizer which generates high quality output. + /// The quantizer supports multiple alpha values. /// Wu, /// /// Palette based, Uses the collection of web-safe colors by default. + /// The quantizer supports multiple alpha values. /// Palette } -} +} \ No newline at end of file diff --git a/src/ImageSharp/Quantizers/Octree/Quantizer.cs b/src/ImageSharp/Quantizers/Quantizer.cs similarity index 90% rename from src/ImageSharp/Quantizers/Octree/Quantizer.cs rename to src/ImageSharp/Quantizers/Quantizer.cs index ccf8da3df..bb856ccc5 100644 --- a/src/ImageSharp/Quantizers/Octree/Quantizer.cs +++ b/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 clone = new Image(image)) - using (PixelAccessor 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(width, height, colorPalette, quantizedPixels); diff --git a/src/ImageSharp/Quantizers/WuArrayPool.cs b/src/ImageSharp/Quantizers/WuArrayPool.cs new file mode 100644 index 000000000..5e4956f01 --- /dev/null +++ b/src/ImageSharp/Quantizers/WuArrayPool.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageSharp.Quantizers +{ + using System.Buffers; + + /// + /// Provides array pooling for the . + /// This is a separate class so that the pools can be shared accross multiple generic quantizer instaces. + /// + internal static class WuArrayPool + { + /// + /// The long array pool. + /// + public static readonly ArrayPool LongPool = ArrayPool.Create(TableLength, 25); + + /// + /// The float array pool. + /// + public static readonly ArrayPool FloatPool = ArrayPool.Create(TableLength, 5); + + /// + /// The byte array pool. + /// + public static readonly ArrayPool BytePool = ArrayPool.Create(TableLength, 5); + + /// + /// The table length. Matches the calculated value in + /// + private const int TableLength = 2471625; + } +} \ No newline at end of file diff --git a/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs b/src/ImageSharp/Quantizers/WuQuantizer.cs similarity index 70% rename from src/ImageSharp/Quantizers/Wu/WuQuantizer.cs rename to src/ImageSharp/Quantizers/WuQuantizer.cs index d632cdd73..c1c81d0ac 100644 --- a/src/ImageSharp/Quantizers/Wu/WuQuantizer.cs +++ b/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; /// /// An implementation of Wu's color quantizer with alpha channel. @@ -30,7 +31,7 @@ namespace ImageSharp.Quantizers /// /// /// The pixel format. - public sealed class WuQuantizer : IQuantizer + public class WuQuantizer : Quantizer where TColor : struct, IPixel { /// @@ -59,103 +60,238 @@ namespace ImageSharp.Quantizers private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; /// - /// The long array pool. - /// - private static readonly ArrayPool LongPool = ArrayPool.Create(TableLength, 25); - - /// - /// The double array pool. + /// A buffer for storing pixels /// - private static readonly ArrayPool DoublePool = ArrayPool.Create(TableLength, 5); + private readonly byte[] rgbaBuffer = new byte[4]; /// - /// The byte array pool. + /// A lookup table for colors /// - private static readonly ArrayPool BytePool = ArrayPool.Create(TableLength, 5); + private readonly Dictionary colorMap = new Dictionary(); /// /// Moment of P(c). /// - private readonly long[] vwt; + private long[] vwt; /// /// Moment of r*P(c). /// - private readonly long[] vmr; + private long[] vmr; /// /// Moment of g*P(c). /// - private readonly long[] vmg; + private long[] vmg; /// /// Moment of b*P(c). /// - private readonly long[] vmb; + private long[] vmb; /// /// Moment of a*P(c). /// - private readonly long[] vma; + private long[] vma; /// /// Moment of c^2*P(c). /// - private readonly double[] m2; + private float[] m2; /// /// Color space tag. /// - private readonly byte[] tag; + private byte[] tag; /// - /// A buffer for storing pixels + /// Maximum allowed color depth /// - private readonly byte[] rgbaBuffer = new byte[4]; + private int colors; + + /// + /// The reduced image palette + /// + private TColor[] palette; + + /// + /// The color cube representing the image palette + /// + private Box[] colorCube; /// /// Initializes a new instance of the class. /// + /// + /// 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. + /// 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); } /// - public QuantizedImage Quantize(ImageBase image, int maxColors) + public override QuantizedImage Quantize(ImageBase 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); + } + } + + /// + 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; + } + + /// + 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); + } + + /// + protected override void FirstPass(PixelAccessor 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 imagePixels = image.Lock()) + /// + protected override void SecondPass(PixelAccessor 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; + } } } /// - /// Gets an index. + /// Gets the index index of the given color in the palette. /// /// The red value. /// The green value. /// The blue value. /// The alpha value. /// The index. + [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 /// The cube. /// The moment. /// The result. - 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 } } - /// - /// Clears the tables. - /// - 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); - } - - /// - /// Builds a 3-D color histogram of counts, r/g/b, c^2. - /// - /// The pixel accessor. - private void Build3DHistogram(PixelAccessor 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); - } - } - } - /// /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// @@ -369,14 +456,14 @@ namespace ImageSharp.Quantizers long[] volumeG = ArrayPool.Shared.Rent(IndexCount * IndexAlphaCount); long[] volumeB = ArrayPool.Shared.Rent(IndexCount * IndexAlphaCount); long[] volumeA = ArrayPool.Shared.Rent(IndexCount * IndexAlphaCount); - double[] volume2 = ArrayPool.Shared.Rent(IndexCount * IndexAlphaCount); + float[] volume2 = ArrayPool.Shared.Rent(IndexCount * IndexAlphaCount); long[] area = ArrayPool.Shared.Rent(IndexAlphaCount); long[] areaR = ArrayPool.Shared.Rent(IndexAlphaCount); long[] areaG = ArrayPool.Shared.Rent(IndexAlphaCount); long[] areaB = ArrayPool.Shared.Rent(IndexAlphaCount); long[] areaA = ArrayPool.Shared.Rent(IndexAlphaCount); - double[] area2 = ArrayPool.Shared.Rent(IndexAlphaCount); + float[] area2 = ArrayPool.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.Shared.Return(volumeG); ArrayPool.Shared.Return(volumeB); ArrayPool.Shared.Return(volumeA); - ArrayPool.Shared.Return(volume2); + ArrayPool.Shared.Return(volume2); ArrayPool.Shared.Return(area); ArrayPool.Shared.Return(areaR); ArrayPool.Shared.Return(areaG); ArrayPool.Shared.Return(areaB); ArrayPool.Shared.Return(areaA); - ArrayPool.Shared.Return(area2); + ArrayPool.Shared.Return(area2); } } @@ -469,15 +556,15 @@ namespace ImageSharp.Quantizers /// Computes the weighted variance of a box cube. /// /// The cube. - /// The . - private double Variance(Box cube) + /// The . + 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 /// The whole blue. /// The whole alpha. /// The whole weight. - /// The . - private double Maximize(Box cube, int direction, int first, int last, out int cut, double wholeR, double wholeG, double wholeB, double wholeA, double wholeW) + /// The . + 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 a value indicating whether the box has been split. 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 /// /// Builds the cube. /// - /// The cube. - /// The color count. - 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; } } } /// - /// Generates the quantized result. + /// Process the pixel in the second pass of the algorithm /// - /// The image pixels. - /// The color count. - /// The cube. - /// The result. - private QuantizedImage GenerateResult(PixelAccessor imagePixels, int colorCount, Box[] cube) + /// The pixel to quantize + /// + /// The quantized value + /// + [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.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.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(width, height, pallette, pixels); + return this.tag[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/FileTestBase.cs b/tests/ImageSharp.Tests/FileTestBase.cs index 765ff3a42..1fa26063d 100644 --- a/tests/ImageSharp.Tests/FileTestBase.cs +++ b/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 }; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs index 882f903d6..a7453f77c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngSmokeTests.cs +++ b/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(TestImageProvider provider) + // where TColor : struct, IPixel + //{ + // // does saving a file then repoening mean both files are identical??? + // using (Image 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 pixels1 = img1.Lock()) + // using (PixelAccessor 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(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index e9d658887..5be1240ef 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/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"; } } } diff --git a/tests/ImageSharp.Tests/TestImages/Formats/Gif/trans.gif b/tests/ImageSharp.Tests/TestImages/Formats/Gif/trans.gif new file mode 100644 index 000000000..6a7577fa1 --- /dev/null +++ b/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