diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index e02afc83e..995aee91d 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -124,7 +124,8 @@ namespace SixLabors.ImageSharp.Advanced { using (var test = new WuFrameQuantizer(Configuration.Default, new WuQuantizer(false))) { - test.QuantizeFrame(new ImageFrame(Configuration.Default, 1, 1)); + var frame = new ImageFrame(Configuration.Default, 1, 1); + test.QuantizeFrame(frame, frame.Bounds()); test.AotGetPalette(); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 1c7c606ca..a1c415f76 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -335,36 +335,36 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void Write8BitColor(Stream stream, ImageFrame image, Span colorPalette) where TPixel : struct, IPixel { - using (IQuantizedFrame quantized = this.quantizer.CreateFrameQuantizer(this.configuration, 256).QuantizeFrame(image)) + using IFrameQuantizer quantizer = this.quantizer.CreateFrameQuantizer(this.configuration, 256); + using IQuantizedFrame quantized = quantizer.QuantizeFrame(image, image.Bounds()); + + ReadOnlySpan quantizedColors = quantized.Palette.Span; + var color = default(Rgba32); + + // TODO: Use bulk conversion here for better perf + int idx = 0; + foreach (TPixel quantizedColor in quantizedColors) { - ReadOnlySpan quantizedColors = quantized.Palette.Span; - var color = default(Rgba32); + quantizedColor.ToRgba32(ref color); + colorPalette[idx] = color.B; + colorPalette[idx + 1] = color.G; + colorPalette[idx + 2] = color.R; - // TODO: Use bulk conversion here for better perf - int idx = 0; - foreach (TPixel quantizedColor in quantizedColors) - { - quantizedColor.ToRgba32(ref color); - colorPalette[idx] = color.B; - colorPalette[idx + 1] = color.G; - colorPalette[idx + 2] = color.R; - - // Padding byte, always 0. - colorPalette[idx + 3] = 0; - idx += 4; - } + // Padding byte, always 0. + colorPalette[idx + 3] = 0; + idx += 4; + } + + stream.Write(colorPalette); - stream.Write(colorPalette); + for (int y = image.Height - 1; y >= 0; y--) + { + ReadOnlySpan pixelSpan = quantized.GetRowSpan(y); + stream.Write(pixelSpan); - for (int y = image.Height - 1; y >= 0; y--) + for (int i = 0; i < this.padding; i++) { - ReadOnlySpan pixelSpan = quantized.GetRowSpan(y); - stream.Write(pixelSpan); - - for (int i = 0; i < this.padding; i++) - { - stream.WriteByte(0); - } + stream.WriteByte(0); } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index df7953230..8577ab476 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Configuration bound to the encoding operation. /// - private Configuration configuration; + private readonly Configuration configuration; /// /// A reusable buffer used to reduce allocations. @@ -84,7 +84,7 @@ namespace SixLabors.ImageSharp.Formats.Gif IQuantizedFrame quantized; using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) { - quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame); + quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds()); } // Get the number of bits. @@ -147,7 +147,7 @@ namespace SixLabors.ImageSharp.Formats.Gif using (IFrameQuantizer paletteFrameQuantizer = new PaletteFrameQuantizer(this.configuration, this.quantizer.Dither, quantized.Palette)) { - using (IQuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame)) + using (IQuantizedFrame paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds())) { this.WriteImageData(paletteQuantized, stream); } @@ -173,14 +173,14 @@ namespace SixLabors.ImageSharp.Formats.Gif { using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration, frameMetadata.ColorTableLength)) { - quantized = frameQuantizer.QuantizeFrame(frame); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } else { using (IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(this.configuration)) { - quantized = frameQuantizer.QuantizeFrame(frame); + quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs index b494c164f..dc3d9d3ce 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderOptionsHelpers.cs @@ -78,7 +78,8 @@ namespace SixLabors.ImageSharp.Formats.Png // Create quantized frame returning the palette and set the bit depth. using (IFrameQuantizer frameQuantizer = options.Quantizer.CreateFrameQuantizer(image.GetConfiguration())) { - return frameQuantizer.QuantizeFrame(image.Frames.RootFrame); + ImageFrame frame = image.Frames.RootFrame; + return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs b/src/ImageSharp/Processing/Processors/Dithering/DitherType.cs similarity index 55% rename from src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs rename to src/ImageSharp/Processing/Processors/Dithering/DitherType.cs index 682363064..0dac15787 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/DitherTransformColorBehavior.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/DitherType.cs @@ -6,16 +6,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering /// /// Enumerates the possible dithering algorithm transform behaviors. /// - public enum DitherTransformColorBehavior + public enum DitherType { /// - /// The transformed color should be precalulated and passed to the dithering algorithm. + /// Error diffusion. Spreads the difference between source and quanized color values as distributed error. /// - PreOperation, + ErrorDiffusion, /// - /// The transformed color should be calculated as a result of the dithering algorithm. + /// Ordered dithering. Applies thresholding matrices agains the source to determine the quantized color. /// - PostOperation + OrderedDither } } diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 2ab570610..91ca4e95e 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } /// - public DitherTransformColorBehavior TransformColorBehavior { get; } = DitherTransformColorBehavior.PreOperation; + public DitherType DitherType { get; } = DitherType.ErrorDiffusion; /// public TPixel Dither( diff --git a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs index 45c9d4b58..0d7841884 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/IDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/IDither.cs @@ -11,14 +11,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering public interface IDither { /// - /// Gets the which determines whether the + /// Gets the which determines whether the /// transformed color should be calculated and supplied to the algorithm. /// - public DitherTransformColorBehavior TransformColorBehavior { get; } + public DitherType DitherType { get; } /// /// Transforms the image applying a dither matrix. - /// When is this + /// When is this /// this method is destructive and will alter the input pixels. /// /// The image. diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 0e15c700f..c3277e326 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering float m2 = length * length; for (int y = 0; y < length; y++) { - for (int x = 0; x < length; y++) + for (int x = 0; x < length; x++) { thresholdMatrix[y, x] = ((ditherMatrix[y, x] + 1) / m2) - .5F; } @@ -43,7 +43,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering } /// - public DitherTransformColorBehavior TransformColorBehavior { get; } = DitherTransformColorBehavior.PostOperation; + public DitherType DitherType { get; } = DitherType.OrderedDither; /// [MethodImpl(InliningOptions.ShortMethod)] diff --git a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs index ed7e3a353..041404f39 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering // Error diffusion. The difference between the source and transformed color // is spread to neighboring pixels. - if (this.dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation) + if (this.dither.DitherType == DitherType.ErrorDiffusion) { for (int y = interest.Top; y < interest.Bottom; y++) { diff --git a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs index c5c729300..63d6875d8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/FrameQuantizer{TPixel}.cs @@ -29,14 +29,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The quantizer + /// The quantizer. /// - /// If true, the quantization process only needs to loop through the source pixels once + /// If true, the quantization process only needs to loop through the source pixels once. /// /// /// If you construct this class with a true for , then the code will - /// only call the method. - /// If two passes are required, the code will also call . + /// only call the method. + /// If two passes are required, the code will also call . /// protected FrameQuantizer(Configuration configuration, IQuantizer quantizer, bool singlePass) { @@ -58,8 +58,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// /// If you construct this class with a true for , then the code will - /// only call the method. - /// If two passes are required, the code will also call . + /// only call the method. + /// If two passes are required, the code will also call . /// protected FrameQuantizer(Configuration configuration, IDither diffuser, bool singlePass) { @@ -88,41 +88,38 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - public IQuantizedFrame QuantizeFrame(ImageFrame image) + public IQuantizedFrame QuantizeFrame(ImageFrame image, Rectangle bounds) { Guard.NotNull(image, nameof(image)); - - // Get the size of the source image - int height = image.Height; - int width = image.Width; + var interest = Rectangle.Intersect(image.Bounds(), bounds); // Call the FirstPass function if not a single pass algorithm. // For something like an Octree quantizer, this will run through // all image pixels, build a data structure, and create a palette. if (!this.singlePass) { - this.FirstPass(image, width, height); + this.FirstPass(image, interest); } // Collect the palette. Required before the second pass runs. - ReadOnlyMemory palette = this.GetPalette(); + ReadOnlyMemory palette = this.GenerateQuantizedPalette(); MemoryAllocator memoryAllocator = this.Configuration.MemoryAllocator; this.pixelMap = new EuclideanPixelMap(palette); - var quantizedFrame = new QuantizedFrame(memoryAllocator, width, height, palette); + var quantizedFrame = new QuantizedFrame(memoryAllocator, interest.Width, interest.Height, palette); - Span pixelSpan = quantizedFrame.GetWritablePixelSpan(); + Memory output = quantizedFrame.GetWritablePixelMemory(); if (this.DoDither) { - // We clone the image as we don't want to alter the original via dithering. + // We clone the image as we don't want to alter the original via error diffusion based dithering. using (ImageFrame clone = image.Clone()) { - this.SecondPass(clone, pixelSpan, palette.Span, width, height); + this.SecondPass(clone, interest, output, palette); } } else { - this.SecondPass(image, pixelSpan, palette.Span, width, height); + this.SecondPass(image, interest, output, palette); } return quantizedFrame; @@ -146,9 +143,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Execute the first pass through the pixels in the image to create the palette. /// /// The source data. - /// The width in pixels of the image. - /// The height in pixels of the image. - protected virtual void FirstPass(ImageFrame source, int width, int height) + /// The bounds within the source image to quantize. + protected virtual void FirstPass(ImageFrame source, Rectangle bounds) { } @@ -156,86 +152,169 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Returns the index and color from the quantized palette corresponding to the give to the given color. /// /// The color to match. + /// The output color palette. /// The matched color. - /// The + /// The index. [MethodImpl(InliningOptions.ShortMethod)] - protected virtual byte GetQuantizedColor(TPixel color, out TPixel match) + protected virtual byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) => this.pixelMap.GetClosestColor(color, out match); /// - /// Retrieve the palette for the quantized image. + /// Generates the palette for the quantized image. /// /// /// /// - protected abstract ReadOnlyMemory GetPalette(); + protected abstract ReadOnlyMemory GenerateQuantizedPalette(); /// /// Execute a second pass through the image to assign the pixels to a palette entry. /// /// The source image. + /// The bounds within the source image to quantize. /// The output pixel array. /// The output color palette. - /// The width in pixels of the image. - /// The height in pixels of the image. protected virtual void SecondPass( ImageFrame source, - Span output, - ReadOnlySpan palette, - int width, - int height) + Rectangle bounds, + Memory output, + ReadOnlyMemory palette) { - Rectangle interest = source.Bounds(); - int bitDepth = ImageMaths.GetBitsNeededForColorDepth(palette.Length); - + ReadOnlySpan paletteSpan = palette.Span; if (!this.DoDither) { - // TODO: This can be parallel. - for (int y = interest.Top; y < interest.Bottom; y++) + var operation = new RowIntervalOperation(source, output, bounds, this, palette); + ParallelRowIterator.IterateRows( + this.Configuration, + bounds, + in operation); + + return; + } + + // Error diffusion. + // The difference between the source and transformed color is spread to neighboring pixels. + // TODO: Investigate parallel strategy. + Span outputSpan = output.Span; + + int bitDepth = ImageMaths.GetBitsNeededForColorDepth(paletteSpan.Length); + if (this.Dither.DitherType == DitherType.ErrorDiffusion) + { + int width = bounds.Width; + int offsetX = bounds.Left; + for (int y = bounds.Top; y < bounds.Bottom; y++) { Span row = source.GetPixelRowSpan(y); int offset = y * width; - for (int x = interest.Left; x < interest.Right; x++) + for (int x = bounds.Left; x < bounds.Right; x++) { - output[offset + x] = this.GetQuantizedColor(row[x], out TPixel _); + TPixel sourcePixel = row[x]; + outputSpan[offset + x - offsetX] = this.GetQuantizedColor(sourcePixel, paletteSpan, out TPixel transformed); + this.Dither.Dither(source, bounds, sourcePixel, transformed, x, y, bitDepth); } } return; } - // Error diffusion. The difference between the source and transformed color - // is spread to neighboring pixels. - if (this.Dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation) + // Ordered dithering. We are only operating on a single pixel so we can work in parallel. + var ditherOperation = new DitherRowIntervalOperation(source, output, bounds, this, palette, bitDepth); + ParallelRowIterator.IterateRows( + this.Configuration, + bounds, + in ditherOperation); + } + + private readonly struct RowIntervalOperation : IRowIntervalOperation + { + private readonly ImageFrame source; + private readonly Memory output; + private readonly Rectangle bounds; + private readonly FrameQuantizer quantizer; + private readonly ReadOnlyMemory palette; + + [MethodImpl(InliningOptions.ShortMethod)] + public RowIntervalOperation( + ImageFrame source, + Memory output, + Rectangle bounds, + FrameQuantizer quantizer, + ReadOnlyMemory palette) + { + this.source = source; + this.output = output; + this.bounds = bounds; + this.quantizer = quantizer; + this.palette = palette; + } + + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) { - for (int y = interest.Top; y < interest.Bottom; y++) + ReadOnlySpan paletteSpan = this.palette.Span; + Span outputSpan = this.output.Span; + int width = this.bounds.Width; + int offsetX = this.bounds.Left; + + for (int y = rows.Min; y < rows.Max; y++) { - Span row = source.GetPixelRowSpan(y); + Span row = this.source.GetPixelRowSpan(y); int offset = y * width; - for (int x = interest.Left; x < interest.Right; x++) + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - TPixel sourcePixel = row[x]; - output[offset + x] = this.GetQuantizedColor(sourcePixel, out TPixel transformed); - this.Dither.Dither(source, interest, sourcePixel, transformed, x, y, bitDepth); + outputSpan[offset + x - offsetX] = this.quantizer.GetQuantizedColor(row[x], paletteSpan, out TPixel _); } } + } + } - return; + private readonly struct DitherRowIntervalOperation : IRowIntervalOperation + { + private readonly ImageFrame source; + private readonly Memory output; + private readonly Rectangle bounds; + private readonly FrameQuantizer quantizer; + private readonly ReadOnlyMemory palette; + private readonly int bitDepth; + + [MethodImpl(InliningOptions.ShortMethod)] + public DitherRowIntervalOperation( + ImageFrame source, + Memory output, + Rectangle bounds, + FrameQuantizer quantizer, + ReadOnlyMemory palette, + int bitDepth) + { + this.source = source; + this.output = output; + this.bounds = bounds; + this.quantizer = quantizer; + this.palette = palette; + this.bitDepth = bitDepth; } - // TODO: This can be parallel. - // Ordered dithering. We are only operating on a single pixel. - for (int y = interest.Top; y < interest.Bottom; y++) + [MethodImpl(InliningOptions.ShortMethod)] + public void Invoke(in RowInterval rows) { - Span row = source.GetPixelRowSpan(y); - int offset = y * width; + ReadOnlySpan paletteSpan = this.palette.Span; + Span outputSpan = this.output.Span; + int width = this.bounds.Width; + IDither dither = this.quantizer.Dither; + TPixel transformed = default; + int offsetX = this.bounds.Left; - for (int x = interest.Left; x < interest.Right; x++) + for (int y = rows.Min; y < rows.Max; y++) { - TPixel dithered = this.Dither.Dither(source, interest, row[x], default, x, y, bitDepth); - output[offset + x] = this.GetQuantizedColor(dithered, out TPixel _); + Span row = this.source.GetPixelRowSpan(y); + int offset = y * width; + for (int x = this.bounds.Left; x < this.bounds.Right; x++) + { + TPixel dithered = dither.Dither(this.source, this.bounds, row[x], transformed, x, y, this.bitDepth); + outputSpan[offset + x - offsetX] = this.quantizer.GetQuantizedColor(dithered, paletteSpan, out TPixel _); + } } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs index 4561727fb..30d58ab0b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/IFrameQuantizer{TPixel}.cs @@ -27,10 +27,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Quantize an image frame and return the resulting output pixels. /// - /// The image to quantize. + /// The image to quantize. + /// The bounds within the source image to quantize. /// - /// A representing a quantized version of the image pixels. + /// A representing a quantized version of the source image pixels. /// - IQuantizedFrame QuantizeFrame(ImageFrame image); + IQuantizedFrame QuantizeFrame(ImageFrame source, Rectangle bounds); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs index 20b276c74..56a523f9b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs @@ -29,6 +29,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// private readonly Octree octree; + /// + /// The reduced image palette + /// private TPixel[] palette; /// @@ -63,18 +66,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - protected override void FirstPass(ImageFrame source, int width, int height) + protected override void FirstPass(ImageFrame source, Rectangle bounds) { // Loop through each row - for (int y = 0; y < height; y++) + int offset = bounds.Left; + for (int y = bounds.Top; y < bounds.Bottom; y++) { Span row = source.GetPixelRowSpan(y); ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row); // And loop through each column - for (int x = 0; x < width; x++) + for (int x = bounds.Left; x < bounds.Right; x++) { - ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x); + ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x - offset); // Add the color to the Octree this.octree.AddColor(ref pixel); @@ -84,23 +88,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// [MethodImpl(InliningOptions.ShortMethod)] - protected override byte GetQuantizedColor(TPixel color, out TPixel match) + protected override byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { if (!this.DoDither) { var index = (byte)this.octree.GetPaletteIndex(ref color); - match = this.GetPalette().Span[index]; + match = palette[index]; return index; } - return base.GetQuantizedColor(color, out match); + return base.GetQuantizedColor(color, palette, out match); } - internal ReadOnlyMemory AotGetPalette() => this.GetPalette(); + internal ReadOnlyMemory AotGetPalette() => this.GenerateQuantizedPalette(); /// [MethodImpl(InliningOptions.ShortMethod)] - protected override ReadOnlyMemory GetPalette() + protected override ReadOnlyMemory GenerateQuantizedPalette() => this.palette ?? (this.palette = this.octree.Palletize(this.colors)); /// @@ -430,7 +434,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The palette /// The current palette index - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(InliningOptions.ColdPath)] public void ConstructPalette(TPixel[] palette, ref int index) { if (this.leaf) @@ -462,10 +466,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// The representing the index of the pixel in the palette. /// - [MethodImpl(MethodImplOptions.NoInlining)] + [MethodImpl(InliningOptions.ColdPath)] public int GetPaletteIndex(ref TPixel pixel, int level) { - // TODO: pass index around so we can do this in parallel. int index = this.paletteIndex; if (!this.leaf) diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs index 0a932b13f..2aad3c43d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs @@ -44,8 +44,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// /// Initializes a new instance of the class. /// - /// The maximum number of colors to hold in the color palette. /// Whether to apply dithering to the output image. + /// The maximum number of colors to hold in the color palette. public OctreeQuantizer(bool dither, int maxColors) : this(GetDiffuser(dither), maxColors) { diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs index 1c9c22481..f60e6d79a 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs @@ -3,7 +3,6 @@ using System; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Dithering; @@ -32,70 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization : base(configuration, diffuser, true) => this.palette = colors; /// - protected override void SecondPass( - ImageFrame source, - Span output, - ReadOnlySpan palette, - int width, - int height) - { - Rectangle interest = source.Bounds(); - int bitDepth = ImageMaths.GetBitsNeededForColorDepth(palette.Length); - - if (!this.DoDither) - { - // TODO: This can be parallel. - for (int y = interest.Top; y < interest.Bottom; y++) - { - Span row = source.GetPixelRowSpan(y); - int offset = y * width; - - for (int x = interest.Left; x < interest.Right; x++) - { - output[offset + x] = this.GetQuantizedColor(row[x], out TPixel _); - } - } - - return; - } - - // Error diffusion. The difference between the source and transformed color - // is spread to neighboring pixels. - if (this.Dither.TransformColorBehavior == DitherTransformColorBehavior.PreOperation) - { - for (int y = interest.Top; y < interest.Bottom; y++) - { - Span row = source.GetPixelRowSpan(y); - int offset = y * width; - - for (int x = interest.Left; x < interest.Right; x++) - { - TPixel sourcePixel = row[x]; - output[offset + x] = this.GetQuantizedColor(sourcePixel, out TPixel transformed); - this.Dither.Dither(source, interest, sourcePixel, transformed, x, y, bitDepth); - } - } - - return; - } - - // TODO: This can be parallel. - // Ordered dithering. We are only operating on a single pixel. - for (int y = interest.Top; y < interest.Bottom; y++) - { - Span row = source.GetPixelRowSpan(y); - int offset = y * width; - - for (int x = interest.Left; x < interest.Right; x++) - { - TPixel dithered = this.Dither.Dither(source, interest, row[x], default, x, y, bitDepth); - output[offset + x] = this.GetQuantizedColor(dithered, out TPixel _); - } - } - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override ReadOnlyMemory GetPalette() => this.palette; + [MethodImpl(InliningOptions.ShortMethod)] + protected override ReadOnlyMemory GenerateQuantizedPalette() => this.palette; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 276919d60..b842c6362 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -35,14 +35,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// protected override void OnFrameApply(ImageFrame source) { + var interest = Rectangle.Intersect(source.Bounds(), this.SourceRectangle); + Configuration configuration = this.Configuration; using IFrameQuantizer frameQuantizer = this.quantizer.CreateFrameQuantizer(configuration); - using IQuantizedFrame quantized = frameQuantizer.QuantizeFrame(source); + using IQuantizedFrame quantized = frameQuantizer.QuantizeFrame(source, interest); var operation = new RowIntervalOperation(this.SourceRectangle, source, quantized); ParallelRowIterator.IterateRows( configuration, - this.SourceRectangle, + interest, in operation); } @@ -71,14 +73,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlySpan quantizedPixelSpan = this.quantized.GetPixelSpan(); ReadOnlySpan paletteSpan = this.quantized.Palette.Span; + int offset = this.bounds.Left; for (int y = rows.Min; y < rows.Max; y++) { Span row = this.source.GetPixelRowSpan(y); int yy = y * this.bounds.Width; - for (int x = this.bounds.X; x < this.bounds.Right; x++) + for (int x = this.bounds.Left; x < this.bounds.Right; x++) { - int i = x + yy; + int i = yy + x - offset; row[x] = paletteSpan[Math.Min(this.maxPaletteIndex, quantizedPixelSpan[i])]; } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index 4938f0e12..90183473b 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Represents a quantized image frame where the pixels indexed by a color palette. /// /// The pixel format. - public class QuantizedFrame : IQuantizedFrame + public sealed class QuantizedFrame : IQuantizedFrame where TPixel : struct, IPixel { private IMemoryOwner pixels; @@ -67,8 +67,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - /// Get the non-readonly span of pixel data so can fill it. + /// Get the non-readonly memory of pixel data so can fill it. /// - internal Span GetWritablePixelSpan() => this.pixels.GetSpan(); + internal Memory GetWritablePixelMemory() => this.pixels.Memory; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs index bf37a7755..3cf67f308 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs @@ -147,10 +147,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization base.Dispose(true); } - internal ReadOnlyMemory AotGetPalette() => this.GetPalette(); + internal ReadOnlyMemory AotGetPalette() => this.GenerateQuantizedPalette(); /// - protected override ReadOnlyMemory GetPalette() + protected override ReadOnlyMemory GenerateQuantizedPalette() { if (this.palette is null) { @@ -175,16 +175,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization } /// - protected override void FirstPass(ImageFrame source, int width, int height) + protected override void FirstPass(ImageFrame source, Rectangle bounds) { - this.Build3DHistogram(source, width, height); + this.Build3DHistogram(source, bounds); this.Get3DMoments(this.memoryAllocator); this.BuildCube(); } /// [MethodImpl(InliningOptions.ShortMethod)] - protected override byte GetQuantizedColor(TPixel color, out TPixel match) + protected override byte GetQuantizedColor(TPixel color, ReadOnlySpan palette, out TPixel match) { if (!this.DoDither) { @@ -199,11 +199,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization ReadOnlySpan tagSpan = this.tag.GetSpan(); var index = tagSpan[GetPaletteIndex(r + 1, g + 1, b + 1, a + 1)]; - match = this.GetPalette().Span[index]; + match = palette[index]; return index; } - return base.GetQuantizedColor(color, out match); + return base.GetQuantizedColor(color, palette, out match); } /// @@ -378,9 +378,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization /// Builds a 3-D color histogram of counts, r/g/b, c^2. /// /// The source data. - /// The width in pixels of the image. - /// The height in pixels of the image. - private void Build3DHistogram(ImageFrame source, int width, int height) + /// The bounds within the source image to quantize. + private void Build3DHistogram(ImageFrame source, Rectangle bounds) { Span momentSpan = this.moments.GetSpan(); @@ -390,15 +389,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Span rgbaSpan = rgbaBuffer.GetSpan(); ref Rgba32 scanBaseRef = ref MemoryMarshal.GetReference(rgbaSpan); - for (int y = 0; y < height; y++) + int offset = bounds.Left; + for (int y = bounds.Top; y < bounds.Bottom; y++) { Span row = source.GetPixelRowSpan(y); PixelOperations.Instance.ToRgba32(source.GetConfiguration(), row, rgbaSpan); // And loop through each column - for (int x = 0; x < width; x++) + for (int x = bounds.Left; x < bounds.Right; x++) { - ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, x); + ref Rgba32 rgba = ref Unsafe.Add(ref scanBaseRef, x - offset); int r = (rgba.R >> (8 - IndexBits)) + 1; int g = (rgba.G >> (8 - IndexBits)) + 1; diff --git a/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs b/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs index 1676197d4..35a05b801 100644 --- a/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs +++ b/tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers { using (var image = new Image(Configuration.Default, 800, 800, Color.BlanchedAlmond)) { - image.Mutate(x => x.Diffuse()); + image.Mutate(x => x.Dither()); return image.Size(); } diff --git a/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs b/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs deleted file mode 100644 index d20407be9..000000000 --- a/tests/ImageSharp.Tests/Processing/Binarization/BinaryDitherTest.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Binarization; -using SixLabors.ImageSharp.Processing.Processors.Dithering; - -using Xunit; - -namespace SixLabors.ImageSharp.Tests.Processing.Binarization -{ - public class BinaryDitherTest : BaseImageOperationsExtensionTest - { - private readonly IDither orderedDither; - private readonly IDither errorDiffuser; - - public BinaryDitherTest() - { - this.orderedDither = KnownDitherers.BayerDither4x4; - this.errorDiffuser = KnownDiffusers.FloydSteinberg; - } - - [Fact] - public void BinaryDither_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither); - BinaryOrderedDitherProcessor p = this.Verify(); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_rect_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither, this.rect); - BinaryOrderedDitherProcessor p = this.Verify(this.rect); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_index_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink); - BinaryOrderedDitherProcessor p = this.Verify(); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.Yellow, p.UpperColor); - Assert.Equal(Color.HotPink, p.LowerColor); - } - - [Fact] - public void BinaryDither_index_rect_CorrectProcessor() - { - this.operations.BinaryDither(this.orderedDither, Color.Yellow, Color.HotPink, this.rect); - BinaryOrderedDitherProcessor p = this.Verify(this.rect); - Assert.Equal(this.orderedDither, p.Dither); - Assert.Equal(Color.HotPink, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_CorrectProcessor() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .4F); - BinaryErrorDiffusionProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.4F, p.Threshold); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_rect_CorrectProcessor() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .3F, this.rect); - BinaryErrorDiffusionProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.3F, p.Threshold); - Assert.Equal(Color.White, p.UpperColor); - Assert.Equal(Color.Black, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_CorrectProcessorWithColors() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow); - BinaryErrorDiffusionProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); - Assert.Equal(Color.HotPink, p.UpperColor); - Assert.Equal(Color.Yellow, p.LowerColor); - } - - [Fact] - public void BinaryDither_ErrorDiffuser_rect_CorrectProcessorWithColors() - { - this.operations.BinaryDiffuse(this.errorDiffuser, .5F, Color.HotPink, Color.Yellow, this.rect); - BinaryErrorDiffusionProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); - Assert.Equal(Color.HotPink, p.UpperColor); - Assert.Equal(Color.Yellow, p.LowerColor); - } - } -} diff --git a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs index 3bdbd8e52..3b04f216c 100644 --- a/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs +++ b/tests/ImageSharp.Tests/Processing/Dithering/DitherTest.cs @@ -2,10 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; - using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Dithering; - using Xunit; namespace SixLabors.ImageSharp.Tests.Processing.Binarization @@ -32,14 +30,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public DitherTest() { this.orderedDither = KnownDitherers.BayerDither4x4; - this.errorDiffuser = KnownDiffusers.FloydSteinberg; + this.errorDiffuser = KnownDitherers.FloydSteinberg; } [Fact] public void Dither_CorrectProcessor() { this.operations.Dither(this.orderedDither); - OrderedDitherPaletteProcessor p = this.Verify(); + PaletteDitherProcessor p = this.Verify(); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } @@ -48,7 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_rect_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.rect); - OrderedDitherPaletteProcessor p = this.Verify(this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } @@ -57,7 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_index_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.testPalette); - OrderedDitherPaletteProcessor p = this.Verify(); + PaletteDitherProcessor p = this.Verify(); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(this.testPalette, p.Palette); } @@ -66,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization public void Dither_index_rect_CorrectProcessor() { this.operations.Dither(this.orderedDither, this.testPalette, this.rect); - OrderedDitherPaletteProcessor p = this.Verify(this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); Assert.Equal(this.orderedDither, p.Dither); Assert.Equal(this.testPalette, p.Palette); } @@ -74,40 +72,36 @@ namespace SixLabors.ImageSharp.Tests.Processing.Binarization [Fact] public void Dither_ErrorDiffuser_CorrectProcessor() { - this.operations.Diffuse(this.errorDiffuser, .4F); - ErrorDiffusionPaletteProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.4F, p.Threshold); + this.operations.Dither(this.errorDiffuser); + PaletteDitherProcessor p = this.Verify(); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_rect_CorrectProcessor() { - this.operations.Diffuse(this.errorDiffuser, .3F, this.rect); - ErrorDiffusionPaletteProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.3F, p.Threshold); + this.operations.Dither(this.errorDiffuser, this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(Color.WebSafePalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_CorrectProcessorWithColors() { - this.operations.Diffuse(this.errorDiffuser, .5F, this.testPalette); - ErrorDiffusionPaletteProcessor p = this.Verify(); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); + this.operations.Dither(this.errorDiffuser, this.testPalette); + PaletteDitherProcessor p = this.Verify(); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(this.testPalette, p.Palette); } [Fact] public void Dither_ErrorDiffuser_rect_CorrectProcessorWithColors() { - this.operations.Diffuse(this.errorDiffuser, .5F, this.testPalette, this.rect); - ErrorDiffusionPaletteProcessor p = this.Verify(this.rect); - Assert.Equal(this.errorDiffuser, p.Diffuser); - Assert.Equal(.5F, p.Threshold); + this.operations.Dither(this.errorDiffuser, this.testPalette, this.rect); + PaletteDitherProcessor p = this.Verify(this.rect); + Assert.Equal(this.errorDiffuser, p.Dither); Assert.Equal(this.testPalette, p.Palette); } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs index 00eacdaf5..3b6f51a89 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Binarization/BinaryDitherTests.cs @@ -28,22 +28,22 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization public static readonly TheoryData ErrorDiffusers = new TheoryData { - { "Atkinson", KnownDiffusers.Atkinson }, - { "Burks", KnownDiffusers.Burks }, - { "FloydSteinberg", KnownDiffusers.FloydSteinberg }, - { "JarvisJudiceNinke", KnownDiffusers.JarvisJudiceNinke }, - { "Sierra2", KnownDiffusers.Sierra2 }, - { "Sierra3", KnownDiffusers.Sierra3 }, - { "SierraLite", KnownDiffusers.SierraLite }, - { "StevensonArce", KnownDiffusers.StevensonArce }, - { "Stucki", KnownDiffusers.Stucki }, + { "Atkinson", KnownDitherers.Atkinson }, + { "Burks", KnownDitherers.Burks }, + { "FloydSteinberg", KnownDitherers.FloydSteinberg }, + { "JarvisJudiceNinke", KnownDitherers.JarvisJudiceNinke }, + { "Sierra2", KnownDitherers.Sierra2 }, + { "Sierra3", KnownDitherers.Sierra3 }, + { "SierraLite", KnownDitherers.SierraLite }, + { "StevensonArce", KnownDitherers.StevensonArce }, + { "Stucki", KnownDitherers.Stucki }, }; public const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24; private static IDither DefaultDitherer => KnownDitherers.BayerDither4x4; - private static IDither DefaultErrorDiffuser => KnownDiffusers.Atkinson; + private static IDither DefaultErrorDiffuser => KnownDitherers.Atkinson; [Theory] [WithFileCollection(nameof(CommonTestImages), nameof(OrderedDitherers), PixelTypes.Rgba32)] @@ -66,7 +66,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.BinaryDiffuse(diffuser, .5F)); + image.Mutate(x => x.BinaryDither(diffuser)); image.DebugSave(provider, name); } } @@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { using (Image image = provider.GetImage()) { - image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, 0.5f)); + image.Mutate(x => x.BinaryDither(DefaultErrorDiffuser)); image.DebugSave(provider); } } @@ -122,7 +122,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization { var bounds = new Rectangle(10, 10, image.Width / 2, image.Height / 2); - image.Mutate(x => x.BinaryDiffuse(DefaultErrorDiffuser, .5F, bounds)); + image.Mutate(x => x.BinaryDither(DefaultErrorDiffuser, bounds)); image.DebugSave(provider); ImageComparer.Tolerant().VerifySimilarityIgnoreRegion(source, image, bounds); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 94a2ec824..0900d6956 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -20,15 +20,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization public static readonly TheoryData ErrorDiffusers = new TheoryData { - KnownDiffusers.Atkinson, - KnownDiffusers.Burks, - KnownDiffusers.FloydSteinberg, - KnownDiffusers.JarvisJudiceNinke, - KnownDiffusers.Sierra2, - KnownDiffusers.Sierra3, - KnownDiffusers.SierraLite, - KnownDiffusers.StevensonArce, - KnownDiffusers.Stucki, + KnownDitherers.Atkinson, + KnownDitherers.Burks, + KnownDitherers.FloydSteinberg, + KnownDitherers.JarvisJudiceNinke, + KnownDitherers.Sierra2, + KnownDitherers.Sierra3, + KnownDitherers.SierraLite, + KnownDitherers.StevensonArce, + KnownDitherers.Stucki, }; public static readonly TheoryData OrderedDitherers @@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization private static IDither DefaultDitherer => KnownDitherers.BayerDither4x4; - private static IDither DefaultErrorDiffuser => KnownDiffusers.Atkinson; + private static IDither DefaultErrorDiffuser => KnownDitherers.Atkinson; /// /// The output is visually correct old 32bit runtime, @@ -64,7 +64,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization } provider.RunRectangleConstrainedValidatingProcessorTest( - (x, rect) => x.Diffuse(DefaultErrorDiffuser, .5F, rect), + (x, rect) => x.Dither(DefaultErrorDiffuser, rect), comparer: ValidatorComparer); } @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization // Increased tolerance because of compatibility issues on .NET 4.6.2: var comparer = ImageComparer.TolerantPercentage(1f); - provider.RunValidatingProcessorTest(x => x.Diffuse(DefaultErrorDiffuser, 0.5f), comparer: comparer); + provider.RunValidatingProcessorTest(x => x.Dither(DefaultErrorDiffuser), comparer: comparer); } [Theory] @@ -111,7 +111,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization } provider.RunValidatingProcessorTest( - x => x.Diffuse(diffuser, 0.5f), + x => x.Dither(diffuser), testOutputDetails: diffuser.GetType().Name, comparer: ValidatorComparer, appendPixelTypeToFileName: false); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs index bd1efaa64..5ea3d7863 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/OctreeQuantizerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -16,19 +16,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization var quantizer = new OctreeQuantizer(128); Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(KnownDitherers.FloydSteinberg, quantizer.Dither); quantizer = new OctreeQuantizer(false); Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Null(quantizer.Diffuser); + Assert.Null(quantizer.Dither); - quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson); + quantizer = new OctreeQuantizer(KnownDitherers.Atkinson); Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + Assert.Equal(KnownDitherers.Atkinson, quantizer.Dither); - quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson, 128); + quantizer = new OctreeQuantizer(KnownDitherers.Atkinson, 128); Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + Assert.Equal(KnownDitherers.Atkinson, quantizer.Dither); } [Fact] @@ -38,21 +38,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither); + Assert.True(frameQuantizer.DoDither); + Assert.Equal(KnownDitherers.FloydSteinberg, frameQuantizer.Dither); quantizer = new OctreeQuantizer(false); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.Dither); + Assert.False(frameQuantizer.DoDither); Assert.Null(frameQuantizer.Dither); - quantizer = new OctreeQuantizer(KnownDiffusers.Atkinson); + quantizer = new OctreeQuantizer(KnownDitherers.Atkinson); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither); + Assert.True(frameQuantizer.DoDither); + Assert.Equal(KnownDitherers.Atkinson, frameQuantizer.Dither); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs index c21e6dc12..1d5c3163c 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/PaletteQuantizerTests.cs @@ -18,15 +18,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization var quantizer = new PaletteQuantizer(Rgb); Assert.Equal(Rgb, quantizer.Palette); - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(KnownDitherers.FloydSteinberg, quantizer.Dither); quantizer = new PaletteQuantizer(Rgb, false); Assert.Equal(Rgb, quantizer.Palette); - Assert.Null(quantizer.Diffuser); + Assert.Null(quantizer.Dither); - quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson); + quantizer = new PaletteQuantizer(Rgb, KnownDitherers.Atkinson); Assert.Equal(Rgb, quantizer.Palette); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + Assert.Equal(KnownDitherers.Atkinson, quantizer.Dither); } [Fact] @@ -36,35 +36,35 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither); + Assert.True(frameQuantizer.DoDither); + Assert.Equal(KnownDitherers.FloydSteinberg, frameQuantizer.Dither); quantizer = new PaletteQuantizer(Rgb, false); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.Dither); + Assert.False(frameQuantizer.DoDither); Assert.Null(frameQuantizer.Dither); - quantizer = new PaletteQuantizer(Rgb, KnownDiffusers.Atkinson); + quantizer = new PaletteQuantizer(Rgb, KnownDitherers.Atkinson); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither); + Assert.True(frameQuantizer.DoDither); + Assert.Equal(KnownDitherers.Atkinson, frameQuantizer.Dither); } [Fact] public void KnownQuantizersWebSafeTests() { IQuantizer quantizer = KnownQuantizers.WebSafe; - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(KnownDitherers.FloydSteinberg, quantizer.Dither); } [Fact] public void KnownQuantizersWernerTests() { IQuantizer quantizer = KnownQuantizers.Werner; - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(KnownDitherers.FloydSteinberg, quantizer.Dither); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs index 8287e6e44..08f51940d 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Quantization/WuQuantizerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.PixelFormats; @@ -16,19 +16,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization var quantizer = new WuQuantizer(128); Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.FloydSteinberg, quantizer.Diffuser); + Assert.Equal(KnownDitherers.FloydSteinberg, quantizer.Dither); quantizer = new WuQuantizer(false); Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Null(quantizer.Diffuser); + Assert.Null(quantizer.Dither); - quantizer = new WuQuantizer(KnownDiffusers.Atkinson); + quantizer = new WuQuantizer(KnownDitherers.Atkinson); Assert.Equal(QuantizerConstants.MaxColors, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + Assert.Equal(KnownDitherers.Atkinson, quantizer.Dither); - quantizer = new WuQuantizer(KnownDiffusers.Atkinson, 128); + quantizer = new WuQuantizer(KnownDitherers.Atkinson, 128); Assert.Equal(128, quantizer.MaxColors); - Assert.Equal(KnownDiffusers.Atkinson, quantizer.Diffuser); + Assert.Equal(KnownDitherers.Atkinson, quantizer.Dither); } [Fact] @@ -38,21 +38,21 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Quantization IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.FloydSteinberg, frameQuantizer.Dither); + Assert.True(frameQuantizer.DoDither); + Assert.Equal(KnownDitherers.FloydSteinberg, frameQuantizer.Dither); quantizer = new WuQuantizer(false); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.False(frameQuantizer.Dither); + Assert.False(frameQuantizer.DoDither); Assert.Null(frameQuantizer.Dither); - quantizer = new WuQuantizer(KnownDiffusers.Atkinson); + quantizer = new WuQuantizer(KnownDitherers.Atkinson); frameQuantizer = quantizer.CreateFrameQuantizer(Configuration.Default); Assert.NotNull(frameQuantizer); - Assert.True(frameQuantizer.Dither); - Assert.Equal(KnownDiffusers.Atkinson, frameQuantizer.Dither); + Assert.True(frameQuantizer.DoDither); + Assert.Equal(KnownDitherers.Atkinson, frameQuantizer.Dither); } } } diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index 775001709..0b11395a8 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -22,15 +22,30 @@ namespace SixLabors.ImageSharp.Tests var octree = new OctreeQuantizer(); var wu = new WuQuantizer(); - Assert.NotNull(werner.Diffuser); - Assert.NotNull(webSafe.Diffuser); - Assert.NotNull(octree.Diffuser); - Assert.NotNull(wu.Diffuser); - - Assert.True(werner.CreateFrameQuantizer(this.Configuration).Dither); - Assert.True(webSafe.CreateFrameQuantizer(this.Configuration).Dither); - Assert.True(octree.CreateFrameQuantizer(this.Configuration).Dither); - Assert.True(wu.CreateFrameQuantizer(this.Configuration).Dither); + Assert.NotNull(werner.Dither); + Assert.NotNull(webSafe.Dither); + Assert.NotNull(octree.Dither); + Assert.NotNull(wu.Dither); + + using (IFrameQuantizer quantizer = werner.CreateFrameQuantizer(this.Configuration)) + { + Assert.True(quantizer.DoDither); + } + + using (IFrameQuantizer quantizer = webSafe.CreateFrameQuantizer(this.Configuration)) + { + Assert.True(quantizer.DoDither); + } + + using (IFrameQuantizer quantizer = octree.CreateFrameQuantizer(this.Configuration)) + { + Assert.True(quantizer.DoDither); + } + + using (IFrameQuantizer quantizer = wu.CreateFrameQuantizer(this.Configuration)) + { + Assert.True(quantizer.DoDither); + } } [Theory] @@ -49,11 +64,12 @@ namespace SixLabors.ImageSharp.Tests foreach (ImageFrame frame in image.Frames) { - IQuantizedFrame quantized = - quantizer.CreateFrameQuantizer(this.Configuration).QuantizeFrame(frame); - - int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) + using (IQuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + { + int index = this.GetTransparentIndex(quantized); + Assert.Equal(index, quantized.GetPixelSpan()[0]); + } } } } @@ -72,11 +88,12 @@ namespace SixLabors.ImageSharp.Tests foreach (ImageFrame frame in image.Frames) { - IQuantizedFrame quantized = - quantizer.CreateFrameQuantizer(this.Configuration).QuantizeFrame(frame); - - int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelSpan()[0]); + using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(this.Configuration)) + using (IQuantizedFrame quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) + { + int index = this.GetTransparentIndex(quantized); + Assert.Equal(index, quantized.GetPixelSpan()[0]); + } } } } diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index c83adea91..f0ee57623 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -17,15 +17,17 @@ namespace SixLabors.ImageSharp.Tests.Quantization Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); - using (var image = new Image(config, 1, 1, Color.Black)) - using (IQuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) - { - Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + using var image = new Image(config, 1, 1, Color.Black); + ImageFrame frame = image.Frames.RootFrame; - Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); - } + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using IQuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + + Assert.Equal(1, result.Palette.Length); + Assert.Equal(1, result.GetPixelSpan().Length); + + Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); + Assert.Equal(0, result.GetPixelSpan()[0]); } [Fact] @@ -34,15 +36,17 @@ namespace SixLabors.ImageSharp.Tests.Quantization Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); - using (var image = new Image(config, 1, 1, default(Rgba32))) - using (IQuantizedFrame result = quantizer.CreateFrameQuantizer(config).QuantizeFrame(image.Frames[0])) - { - Assert.Equal(1, result.Palette.Length); - Assert.Equal(1, result.GetPixelSpan().Length); + using var image = new Image(config, 1, 1, default(Rgba32)); + ImageFrame frame = image.Frames.RootFrame; - Assert.Equal(default, result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelSpan()[0]); - } + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using IQuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + + Assert.Equal(1, result.Palette.Length); + Assert.Equal(1, result.GetPixelSpan().Length); + + Assert.Equal(default, result.Palette.Span[0]); + Assert.Equal(0, result.GetPixelSpan()[0]); } [Fact] @@ -63,46 +67,47 @@ namespace SixLabors.ImageSharp.Tests.Quantization [Fact] public void Palette256() { - using (var image = new Image(1, 256)) + using var image = new Image(1, 256); + + for (int i = 0; i < 256; i++) { - for (int i = 0; i < 256; i++) - { - byte r = (byte)((i % 4) * 85); - byte g = (byte)(((i / 4) % 4) * 85); - byte b = (byte)(((i / 16) % 4) * 85); - byte a = (byte)((i / 64) * 85); + byte r = (byte)((i % 4) * 85); + byte g = (byte)(((i / 4) % 4) * 85); + byte b = (byte)(((i / 16) % 4) * 85); + byte a = (byte)((i / 64) * 85); - image[0, i] = new Rgba32(r, g, b, a); - } + image[0, i] = new Rgba32(r, g, b, a); + } - Configuration config = Configuration.Default; - var quantizer = new WuQuantizer(false); - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) - { - Assert.Equal(256, result.Palette.Length); - Assert.Equal(256, result.GetPixelSpan().Length); + Configuration config = Configuration.Default; + var quantizer = new WuQuantizer(false); - var actualImage = new Image(1, 256); + ImageFrame frame = image.Frames.RootFrame; - ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = result.Palette.Length - 1; - for (int y = 0; y < actualImage.Height; y++) - { - Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); - int yy = y * actualImage.Width; + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using IQuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); - for (int x = 0; x < actualImage.Width; x++) - { - int i = x + yy; - row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])]; - } - } + Assert.Equal(256, result.Palette.Length); + Assert.Equal(256, result.GetPixelSpan().Length); + + var actualImage = new Image(1, 256); - Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); + ReadOnlySpan paletteSpan = result.Palette.Span; + int paletteCount = result.Palette.Length - 1; + for (int y = 0; y < actualImage.Height; y++) + { + Span row = actualImage.GetPixelRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.GetPixelSpan(); + int yy = y * actualImage.Width; + + for (int x = 0; x < actualImage.Width; x++) + { + int i = x + yy; + row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[i])]; } } + + Assert.True(image.GetPixelSpan().SequenceEqual(actualImage.GetPixelSpan())); } [Theory] @@ -115,11 +120,12 @@ namespace SixLabors.ImageSharp.Tests.Quantization { Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); - using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) - { - Assert.Equal(48, result.Palette.Length); - } + ImageFrame frame = image.Frames.RootFrame; + + using IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config); + using IQuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds()); + + Assert.Equal(48, result.Palette.Length); } } @@ -144,8 +150,9 @@ namespace SixLabors.ImageSharp.Tests.Quantization Configuration config = Configuration.Default; var quantizer = new WuQuantizer(false); + ImageFrame frame = image.Frames.RootFrame; using (IFrameQuantizer frameQuantizer = quantizer.CreateFrameQuantizer(config)) - using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(image.Frames[0])) + using (IQuantizedFrame result = frameQuantizer.QuantizeFrame(frame, frame.Bounds())) { Assert.Equal(4 * 8, result.Palette.Length); Assert.Equal(256, result.GetPixelSpan().Length); diff --git a/tests/Images/Input/Png/CalliphoraPartial2.png b/tests/Images/Input/Png/CalliphoraPartial2.png new file mode 100644 index 000000000..46eee03cf --- /dev/null +++ b/tests/Images/Input/Png/CalliphoraPartial2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fb48e3c495d7834df09a17d6a6cadbce047a0e791b0cb78ca3a6d334d309b13 +size 75628