Browse Source

Add derived format info types and allow persistance of palette lengths

af/merge-core
James Jackson-South 8 years ago
parent
commit
eec21fd89e
  1. 21
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  2. 25
      src/ImageSharp/Formats/Bmp/BmpInfo.cs
  3. 57
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  4. 30
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  5. 33
      src/ImageSharp/Formats/Gif/GifInfo.cs
  6. 2
      src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs
  7. 7
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  8. 25
      src/ImageSharp/Formats/Jpeg/JpegInfo.cs
  9. 8
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  10. 25
      src/ImageSharp/Formats/Png/PngInfo.cs
  11. 12
      src/ImageSharp/ImageInfo.cs
  12. 13
      src/ImageSharp/MetaData/ImageFrameMetaData.cs
  13. 9
      src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs
  14. 32
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  15. 23
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  16. 2
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  17. 20
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  18. 16
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  19. 21
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  20. 39
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  21. 70
      tests/ImageSharp.Tests/ImageInfoTests.cs
  22. 21
      tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs
  23. 3
      tests/ImageSharp.Tests/TestImages.cs
  24. 62
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  25. 11
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs
  26. 3
      tests/Images/Input/Gif/leo.gif

21
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Bmp
{
@ -164,7 +165,9 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public IImageInfo Identify(Stream stream)
{
this.ReadImageHeaders(stream, out _, out _);
return new ImageInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), this.infoHeader.Width, this.infoHeader.Height, this.metaData);
var size = new Size(this.infoHeader.Width, this.infoHeader.Height);
return new BmpInfo(new PixelTypeInfo(this.infoHeader.BitsPerPixel), size, this.metaData);
}
/// <summary>
@ -175,10 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="inverted">Whether the bitmap is inverted.</param>
/// <returns>The <see cref="int"/> representing the inverted value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Invert(int y, int height, bool inverted)
{
return (!inverted) ? height - y - 1 : y;
}
private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y;
/// <summary>
/// Calculates the amount of bytes to pad a row.
@ -206,10 +206,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="value">The masked and shifted value</param>
/// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte GetBytesFrom5BitValue(int value)
{
return (byte)((value << 3) | (value >> 2));
}
private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
/// <summary>
/// Looks up color values and builds the image from de-compressed RLE8 data.
@ -524,8 +521,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
// Resolution is stored in PPM.
var meta = new ImageMetaData();
meta.ResolutionUnits = PixelResolutionUnit.PixelsPerMeter;
var meta = new ImageMetaData
{
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
};
if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0)
{
meta.HorizontalResolution = this.infoHeader.XPelsPerMeter;

25
src/ImageSharp/Formats/Bmp/BmpInfo.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.MetaData;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Bmp
{
/// <summary>
/// Contains information about the bmp including dimensions, pixel type information and additional metadata.
/// </summary>
public class BmpInfo : ImageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="BmpInfo" /> class.
/// </summary>
/// <param name="pixelType">The image pixel type information.</param>
/// <param name="size">The size of the image in pixels.</param>
/// <param name="metaData">The images metadata.</param>
internal BmpInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData)
: base(pixelType, size, metaData)
{
}
}
}

57
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -55,6 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
private GifGraphicControlExtension graphicsControlExtension;
/// <summary>
/// The image desciptor.
/// </summary>
private GifImageDescriptor imageDescriptor;
/// <summary>
/// The metadata
/// </summary>
@ -120,8 +125,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
int label = stream.ReadByte();
switch (label)
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
this.ReadGraphicalControlExtension();
@ -178,13 +182,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
if (nextFlag == GifConstants.ImageLabel)
{
// Skip image block
this.Skip(0);
this.ReadImageDescriptor();
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
int label = stream.ReadByte();
switch (label)
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
@ -224,7 +226,17 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.globalColorTable?.Dispose();
}
return new ImageInfo(new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height, this.metaData);
GifColorTableMode colorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag
? GifColorTableMode.Global
: GifColorTableMode.Local;
var size = new Size(this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height);
return new GifInfo(
colorTableMode,
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
size,
this.metaData);
}
/// <summary>
@ -238,14 +250,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
/// <summary>
/// Reads the image descriptor
/// Reads the image descriptor.
/// </summary>
/// <returns><see cref="GifImageDescriptor"/></returns>
private GifImageDescriptor ReadImageDescriptor()
private void ReadImageDescriptor()
{
this.stream.Read(this.buffer, 0, 9);
return GifImageDescriptor.Parse(this.buffer);
this.imageDescriptor = GifImageDescriptor.Parse(this.buffer);
}
/// <summary>
@ -312,25 +323,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame)
where TPixel : struct, IPixel<TPixel>
{
GifImageDescriptor imageDescriptor = this.ReadImageDescriptor();
this.ReadImageDescriptor();
IManagedByteBuffer localColorTable = null;
IManagedByteBuffer indices = null;
try
{
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
if (imageDescriptor.LocalColorTableFlag)
if (this.imageDescriptor.LocalColorTableFlag)
{
int length = imageDescriptor.LocalColorTableSize * 3;
int length = this.imageDescriptor.LocalColorTableSize * 3;
localColorTable = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(length, AllocationOptions.Clean);
this.stream.Read(localColorTable.Array, 0, length);
}
indices = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(imageDescriptor.Width * imageDescriptor.Height, AllocationOptions.Clean);
indices = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(this.imageDescriptor.Width * this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(imageDescriptor, indices.GetSpan());
this.ReadFrameIndices(this.imageDescriptor, indices.GetSpan());
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>((localColorTable ?? this.globalColorTable).GetSpan());
this.ReadFrameColors(ref image, ref previousFrame, indices.GetSpan(), colorTable, imageDescriptor);
this.ReadFrameColors(ref image, ref previousFrame, indices.GetSpan(), colorTable, this.imageDescriptor);
// Skip any remaining blocks
this.Skip(0);
@ -508,6 +519,18 @@ namespace SixLabors.ImageSharp.Formats.Gif
meta.FrameDelay = this.graphicsControlExtension.DelayTime;
}
// Frames can either use the global table or their own local table.
if (this.logicalScreenDescriptor.GlobalColorTableFlag
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{
meta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}
else if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0)
{
meta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
}
meta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
}

30
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -146,7 +146,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (QuantizedFrame<TPixel> paletteQuantized = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame))
using (QuantizedFrame<TPixel> paletteQuantized
= palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame))
{
this.WriteImageData(paletteQuantized, stream);
}
@ -157,13 +158,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
ImageFrame<TPixel> previousFrame = null;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
if (quantized is null)
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
// Allow each frame to be encoded at whatever color depth the frame designates if set.
if (previousFrame != null
&& previousFrame.MetaData.ColorTableLength != frame.MetaData.ColorTableLength
&& frame.MetaData.ColorTableLength > 0)
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>(frame.MetaData.ColorTableLength).QuantizeFrame(frame);
}
else
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
}
}
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
@ -171,6 +184,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
quantized?.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
}
}
@ -210,10 +224,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="stream">The stream to write to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteHeader(Stream stream)
{
stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
}
private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
/// <summary>
/// Writes the logical screen descriptor to the stream.
@ -226,7 +237,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, int transparencyIndex, bool useGlobalTable, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth, false, this.bitDepth - 1);
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@ -382,7 +393,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
localColorTableSize: (byte)this.bitDepth);
localColorTableSize: this.bitDepth - 1);
var descriptor = new GifImageDescriptor(
left: 0,
@ -407,7 +418,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
int pixelCount = image.Palette.Length;
int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; // The maximium number of colors for the bit depth
// The maximium number of colors for the bit depth
int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3;
Rgb24 rgb = default;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))

33
src/ImageSharp/Formats/Gif/GifInfo.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.MetaData;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
/// Contains information about the bmp including dimensions, pixel type information and additional metadata.
/// </summary>
public class GifInfo : ImageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="GifInfo" /> class.
/// </summary>
/// <param name="colorTableMode">The color table mode.</param>
/// <param name="pixelType">The image pixel type information.</param>
/// <param name="size">The size of the image in pixels.</param>
/// <param name="metaData">The images metadata.</param>
internal GifInfo(
GifColorTableMode colorTableMode,
PixelTypeInfo pixelType,
Size size,
ImageMetaData metaData)
: base(pixelType, size, metaData) => this.ColorTableMode = colorTableMode;
/// <summary>
/// Gets the color table mode.
/// </summary>
public GifColorTableMode ColorTableMode { get; }
}
}

2
src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs

@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
return MemoryMarshal.Cast<byte, GifImageDescriptor>(buffer)[0];
}
public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, byte localColorTableSize)
public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, int localColorTableSize)
{
/*
Local Color Table Flag | 1 Bit

7
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -234,7 +234,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile();
this.InitIccProfile();
this.InitDerivedMetaDataProperties();
return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData);
return new JpegInfo(new PixelTypeInfo(this.BitsPerPixel), new Size(this.ImageWidth, this.ImageHeight), this.MetaData);
}
/// <summary>
@ -899,9 +900,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="values">The values</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BuildHuffmanTable(HuffmanTables tables, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values)
{
tables[index] = new HuffmanTable(this.configuration.MemoryAllocator, codeLengths, values);
}
=> tables[index] = new HuffmanTable(this.configuration.MemoryAllocator, codeLengths, values);
/// <summary>
/// Reads a <see cref="ushort"/> from the stream advancing it by two bytes

25
src/ImageSharp/Formats/Jpeg/JpegInfo.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.MetaData;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// Contains information about the bmp including dimensions, pixel type information and additional metadata.
/// </summary>
public class JpegInfo : ImageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="JpegInfo" /> class.
/// </summary>
/// <param name="pixelType">The image pixel type information.</param>
/// <param name="size">The size of the image in pixels.</param>
/// <param name="metaData">The images metadata.</param>
internal JpegInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData)
: base(pixelType, size, metaData)
{
}
}
}

8
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -10,7 +10,6 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory;
@ -18,6 +17,7 @@ using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Png
{
@ -349,7 +349,7 @@ namespace SixLabors.ImageSharp.Formats.Png
throw new ImageFormatException("PNG Image does not contain a header chunk");
}
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata);
return new PngInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new Size(this.header.Width, this.header.Height), metadata);
}
/// <summary>
@ -360,9 +360,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ReadByteLittleEndian(ReadOnlySpan<byte> buffer, int offset)
{
return (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF));
}
=> (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF));
/// <summary>
/// Attempts to convert a byte array to a new array where each value in the original array is represented by the

25
src/ImageSharp/Formats/Png/PngInfo.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.MetaData;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Contains information about the bmp including dimensions, pixel type information and additional metadata.
/// </summary>
public class PngInfo : ImageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="PngInfo" /> class.
/// </summary>
/// <param name="pixelType">The image pixel type information.</param>
/// <param name="size">The size of the image in pixels.</param>
/// <param name="metaData">The images metadata.</param>
internal PngInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData)
: base(pixelType, size, metaData)
{
}
}
}

12
src/ImageSharp/ImageInfo.cs

@ -3,26 +3,26 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.MetaData;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp
{
/// <summary>
/// Contains information about the image including dimensions, pixel type information and additional metadata
/// </summary>
internal sealed class ImageInfo : IImageInfo
public abstract class ImageInfo : IImageInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageInfo"/> class.
/// </summary>
/// <param name="pixelType">The image pixel type information.</param>
/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="size">The size of the image in pixels.</param>
/// <param name="metaData">The images metadata.</param>
public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetaData metaData)
protected ImageInfo(PixelTypeInfo pixelType, Size size, ImageMetaData metaData)
{
this.PixelType = pixelType;
this.Width = width;
this.Height = height;
this.Width = size.Width;
this.Height = size.Height;
this.MetaData = metaData;
}

13
src/ImageSharp/MetaData/ImageFrameMetaData.cs

@ -28,10 +28,18 @@ namespace SixLabors.ImageSharp.MetaData
{
DebugGuard.NotNull(other, nameof(other));
this.ColorTableLength = other.ColorTableLength;
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
}
/// <summary>
/// Gets or sets the length of the color table for paletted images.
/// If not 0, then this field indicates the maximum number of colors to use when quantizing the
/// image frame.
/// </summary>
public int ColorTableLength { get; set; }
/// <summary>
/// Gets or sets the frame delay for animated images.
/// If not 0, when utilized in Gif animation, this field specifies the number of hundredths (1/100) of a second to
@ -51,9 +59,6 @@ namespace SixLabors.ImageSharp.MetaData
/// Clones this ImageFrameMetaData.
/// </summary>
/// <returns>The cloned instance.</returns>
public ImageFrameMetaData Clone()
{
return new ImageFrameMetaData(this);
}
public ImageFrameMetaData Clone() => new ImageFrameMetaData(this);
}
}

9
src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs

@ -23,5 +23,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>;
/// <summary>
/// Creates the generic frame quantizer
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(int maxColors)
where TPixel : struct, IPixel<TPixel>;
}
}

32
src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs

@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Maximum allowed color depth
/// </summary>
private readonly byte colors;
private readonly int colors;
/// <summary>
/// Stores the tree
@ -43,9 +43,23 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// the second pass quantizes a color based on the nodes in the tree
/// </remarks>
public OctreeFrameQuantizer(OctreeQuantizer quantizer)
: this(quantizer, quantizer.MaxColors)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="OctreeFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="quantizer">The octree quantizer.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <remarks>
/// The Octree quantizer is a two pass algorithm. The initial pass sets up the Octree,
/// the second pass quantizes a color based on the nodes in the tree
/// </remarks>
public OctreeFrameQuantizer(OctreeQuantizer quantizer, int maxColors)
: base(quantizer, false)
{
this.colors = (byte)quantizer.MaxColors;
this.colors = maxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8));
}
@ -261,13 +275,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TPixel[] Palletize(int colorCount)
{
while (this.Leaves > colorCount)
while (this.Leaves > colorCount - 1)
{
this.Reduce();
}
// Now palletize the nodes
var palette = new TPixel[colorCount + 1];
var palette = new TPixel[colorCount];
int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex);
@ -285,10 +299,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// The <see cref="int"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPaletteIndex(ref TPixel pixel, ref Rgba32 rgba)
{
return this.root.GetPaletteIndex(ref pixel, 0, ref rgba);
}
public int GetPaletteIndex(ref TPixel pixel, ref Rgba32 rgba) => this.root.GetPaletteIndex(ref pixel, 0, ref rgba);
/// <summary>
/// Keep track of the previous node that was quantized
@ -297,10 +308,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// The node last quantized
/// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void TrackPrevious(OctreeNode node)
{
this.previousNode = node;
}
protected void TrackPrevious(OctreeNode node) => this.previousNode = node;
/// <summary>
/// Reduce the depth of the tree

23
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs

@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class OctreeQuantizer : IQuantizer
{
/// <summary>
/// The default maximum number of colors to use when quantizing the image.
/// </summary>
public const int DefaultMaxColors = 256;
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
@ -26,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary>
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
public OctreeQuantizer(int maxColors)
: this(GetDiffuser(true), maxColors)
{
@ -37,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public OctreeQuantizer(bool dither)
: this(GetDiffuser(dither), 255)
: this(GetDiffuser(dither), DefaultMaxColors)
{
}
@ -46,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public OctreeQuantizer(IErrorDiffuser diffuser)
: this(diffuser, 255)
: this(diffuser, DefaultMaxColors)
{
}
@ -57,10 +62,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors)
{
Guard.MustBeBetweenOrEqualTo(maxColors, 1, 255, nameof(maxColors));
this.Diffuser = diffuser;
this.MaxColors = maxColors;
this.MaxColors = maxColors.Clamp(1, DefaultMaxColors);
}
/// <inheritdoc />
@ -76,6 +79,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel>
=> new OctreeFrameQuantizer<TPixel>(this);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(int maxColors)
where TPixel : struct, IPixel<TPixel>
{
maxColors = maxColors.Clamp(1, DefaultMaxColors);
return new OctreeFrameQuantizer<TPixel>(this, maxColors);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
}
}

2
src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs

@ -36,6 +36,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
public PaletteFrameQuantizer(PaletteQuantizer quantizer, TPixel[] colors)
: base(quantizer, true)
{
// TODO: Why is this value constrained? Gif has limitations but theoretically
// we might want to reduce the palette of an image to greater than that limitation.
Guard.MustBeBetweenOrEqualTo(colors.Length, 1, 256, nameof(colors));
this.palette = colors;
this.paletteVector = new Vector4[this.palette.Length];

20
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -37,10 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public PaletteQuantizer(IErrorDiffuser diffuser)
{
this.Diffuser = diffuser;
}
public PaletteQuantizer(IErrorDiffuser diffuser) => this.Diffuser = diffuser;
/// <inheritdoc />
public IErrorDiffuser Diffuser { get; }
@ -50,6 +47,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel>
=> this.CreateFrameQuantizer(() => NamedColors<TPixel>.WebSafePalette);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(int maxColors)
where TPixel : struct, IPixel<TPixel>
{
TPixel[] websafe = NamedColors<TPixel>.WebSafePalette;
int max = Math.Min(maxColors, websafe.Length);
if (max != websafe.Length)
{
return this.CreateFrameQuantizer(() => NamedColors<TPixel>.WebSafePalette.AsSpan(0, max).ToArray());
}
return this.CreateFrameQuantizer(() => websafe);
}
/// <summary>
/// Gets the palette to use to quantize the image.
/// </summary>

16
src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs

@ -3,7 +3,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -128,11 +127,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// the second pass quantizes a color based on the position in the histogram.
/// </remarks>
public WuFrameQuantizer(WuQuantizer quantizer)
: base(quantizer, false)
: this(quantizer, quantizer.MaxColors)
{
this.colors = quantizer.MaxColors;
}
/// <summary>
/// Initializes a new instance of the <see cref="WuFrameQuantizer{TPixel}"/> class.
/// </summary>
/// <param name="quantizer">The wu quantizer.</param>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <remarks>
/// The Wu quantizer is a two pass algorithm. The initial pass sets up the 3-D color histogram,
/// the second pass quantizes a color based on the position in the histogram.
/// </remarks>
public WuFrameQuantizer(WuQuantizer quantizer, int maxColors)
: base(quantizer, false) => this.colors = maxColors;
/// <inheritdoc/>
public override QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image)
{

21
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs

@ -14,6 +14,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
public class WuQuantizer : IQuantizer
{
/// <summary>
/// The default maximum number of colors to use when quantizing the image.
/// </summary>
public const int DefaultMaxColors = 256;
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
@ -36,7 +41,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param>
public WuQuantizer(bool dither)
: this(GetDiffuser(dither), 255)
: this(GetDiffuser(dither), DefaultMaxColors)
{
}
@ -45,7 +50,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public WuQuantizer(IErrorDiffuser diffuser)
: this(diffuser, 255)
: this(diffuser, DefaultMaxColors)
{
}
@ -56,10 +61,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public WuQuantizer(IErrorDiffuser diffuser, int maxColors)
{
Guard.MustBeBetweenOrEqualTo(maxColors, 1, 255, nameof(maxColors));
this.Diffuser = diffuser;
this.MaxColors = maxColors;
this.MaxColors = maxColors.Clamp(1, DefaultMaxColors);
}
/// <inheritdoc />
@ -75,6 +78,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel>
=> new WuFrameQuantizer<TPixel>(this);
/// <inheritdoc/>
public IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(int maxColors)
where TPixel : struct, IPixel<TPixel>
{
maxColors = maxColors.Clamp(1, DefaultMaxColors);
return new WuFrameQuantizer<TPixel>(this, maxColors);
}
private static IErrorDiffuser GetDiffuser(bool dither) => dither ? KnownDiffusers.FloydSteinberg : null;
}
}

39
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -179,5 +179,44 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length);
}
}
[Fact]
public void NonMutatingEncodePreservesPaletteCount()
{
using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes))
using (var outStream = new MemoryStream())
{
var info = (GifInfo)Image.Identify(inStream);
GifColorTableMode colorMode = info.ColorTableMode;
inStream.Position = 0;
var image = Image.Load(inStream);
var encoder = new GifEncoder()
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(image.Frames.RootFrame.MetaData.ColorTableLength)
};
image.Save(outStream, encoder);
outStream.Position = 0;
var cloneInfo = (GifInfo)Image.Identify(outStream);
outStream.Position = 0;
var clone = Image.Load(outStream);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, image.Frames.RootFrame.MetaData.ColorTableLength);
Assert.Equal(info.ColorTableMode, cloneInfo.ColorTableMode);
for (int i = 0; i < image.Frames.Count; i++)
{
Assert.Equal(image.Frames[i].MetaData.ColorTableLength, clone.Frames[i].MetaData.ColorTableLength);
Assert.Equal(image.Frames[i].MetaData.FrameDelay, clone.Frames[i].MetaData.FrameDelay);
}
image.Dispose();
clone.Dispose();
}
}
}
}

70
tests/ImageSharp.Tests/ImageInfoTests.cs

@ -2,6 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.MetaData;
using SixLabors.Primitives;
@ -12,7 +16,7 @@ namespace SixLabors.ImageSharp.Tests
public class ImageInfoTests
{
[Fact]
public void ImageInfoInitializesCorrectly()
public void JpegInfoInitializesCorrectly()
{
const int Width = 50;
const int Height = 60;
@ -21,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests
var pixelType = new PixelTypeInfo(8);
var meta = new ImageMetaData();
var info = new ImageInfo(pixelType, Width, Height, meta);
var info = new JpegInfo(pixelType, size, meta);
Assert.Equal(pixelType, info.PixelType);
Assert.Equal(Width, info.Width);
@ -30,5 +34,67 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal(rectangle, info.Bounds());
Assert.Equal(meta, info.MetaData);
}
[Fact]
public void BmpInfoInitializesCorrectly()
{
const int Width = 50;
const int Height = 60;
var size = new Size(Width, Height);
var rectangle = new Rectangle(0, 0, Width, Height);
var pixelType = new PixelTypeInfo(8);
var meta = new ImageMetaData();
var info = new BmpInfo(pixelType, size, meta);
Assert.Equal(pixelType, info.PixelType);
Assert.Equal(Width, info.Width);
Assert.Equal(Height, info.Height);
Assert.Equal(size, info.Size());
Assert.Equal(rectangle, info.Bounds());
Assert.Equal(meta, info.MetaData);
}
[Fact]
public void PngInfoInitializesCorrectly()
{
const int Width = 50;
const int Height = 60;
var size = new Size(Width, Height);
var rectangle = new Rectangle(0, 0, Width, Height);
var pixelType = new PixelTypeInfo(8);
var meta = new ImageMetaData();
var info = new PngInfo(pixelType, size, meta);
Assert.Equal(pixelType, info.PixelType);
Assert.Equal(Width, info.Width);
Assert.Equal(Height, info.Height);
Assert.Equal(size, info.Size());
Assert.Equal(rectangle, info.Bounds());
Assert.Equal(meta, info.MetaData);
}
[Fact]
public void GifInfoInitializesCorrectly()
{
const GifColorTableMode mode = GifColorTableMode.Local;
const int Width = 50;
const int Height = 60;
var size = new Size(Width, Height);
var rectangle = new Rectangle(0, 0, Width, Height);
var pixelType = new PixelTypeInfo(8);
var meta = new ImageMetaData();
var info = new GifInfo(mode, pixelType, size, meta);
Assert.Equal(mode, info.ColorTableMode);
Assert.Equal(pixelType, info.PixelType);
Assert.Equal(Width, info.Width);
Assert.Equal(Height, info.Height);
Assert.Equal(size, info.Size());
Assert.Equal(rectangle, info.Bounds());
Assert.Equal(meta, info.MetaData);
}
}
}

21
tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.MetaData;
using Xunit;
@ -16,14 +15,22 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void ConstructorImageFrameMetaData()
{
ImageFrameMetaData metaData = new ImageFrameMetaData();
metaData.FrameDelay = 42;
metaData.DisposalMethod = DisposalMethod.RestoreToBackground;
const int frameDelay = 42;
const int colorTableLength = 128;
const DisposalMethod disposalMethod = DisposalMethod.RestoreToBackground;
ImageFrameMetaData clone = new ImageFrameMetaData(metaData);
var metaData = new ImageFrameMetaData
{
FrameDelay = frameDelay,
ColorTableLength = colorTableLength,
DisposalMethod = disposalMethod
};
Assert.Equal(42, clone.FrameDelay);
Assert.Equal(DisposalMethod.RestoreToBackground, clone.DisposalMethod);
var clone = new ImageFrameMetaData(metaData);
Assert.Equal(frameDelay, clone.FrameDelay);
Assert.Equal(colorTableLength, clone.ColorTableLength);
Assert.Equal(disposalMethod, clone.DisposalMethod);
}
}
}

3
tests/ImageSharp.Tests/TestImages.cs

@ -201,6 +201,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Cheers = "Gif/cheers.gif";
public const string Trans = "Gif/trans.gif";
public const string Kumin = "Gif/kumin.gif";
public const string Leo = "Gif/leo.gif";
public const string Ratio4x1 = "Gif/base_4x1.gif";
public const string Ratio1x4 = "Gif/base_1x4.gif";
@ -211,7 +212,7 @@ namespace SixLabors.ImageSharp.Tests
public const string BadDescriptorWidth = "Gif/issues/issue403_baddescriptorwidth.gif";
}
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Ratio4x1, Ratio1x4 };
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };
}
}
}

62
tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs

@ -55,9 +55,20 @@ namespace SixLabors.ImageSharp.Tests
public bool Equals(Key other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
if (!this.commonValues.Equals(other.commonValues)) return false;
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (!this.commonValues.Equals(other.commonValues))
{
return false;
}
if (this.decoderParameters.Count != other.decoderParameters.Count)
{
@ -66,8 +77,7 @@ namespace SixLabors.ImageSharp.Tests
foreach (KeyValuePair<string, object> kv in this.decoderParameters)
{
object otherVal;
if (!other.decoderParameters.TryGetValue(kv.Key, out otherVal))
if (!other.decoderParameters.TryGetValue(kv.Key, out object otherVal))
{
return false;
}
@ -81,26 +91,29 @@ namespace SixLabors.ImageSharp.Tests
public override bool Equals(object obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
if (obj is null)
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != this.GetType())
{
return false;
}
return this.Equals((Key)obj);
}
public override int GetHashCode()
{
return this.commonValues.GetHashCode();
}
public override int GetHashCode() => this.commonValues.GetHashCode();
public static bool operator ==(Key left, Key right)
{
return Equals(left, right);
}
public static bool operator ==(Key left, Key right) => Equals(left, right);
public static bool operator !=(Key left, Key right)
{
return !Equals(left, right);
}
public static bool operator !=(Key left, Key right) => !Equals(left, right);
}
private static readonly ConcurrentDictionary<Key, Image<TPixel>> cache = new ConcurrentDictionary<Key, Image<TPixel>>();
@ -111,10 +124,7 @@ namespace SixLabors.ImageSharp.Tests
{
}
public FileProvider(string filePath)
{
this.FilePath = filePath;
}
public FileProvider(string filePath) => this.FilePath = filePath;
/// <summary>
/// Gets the file path relative to the "~/tests/images" folder
@ -135,12 +145,12 @@ namespace SixLabors.ImageSharp.Tests
if (!TestEnvironment.Is64BitProcess)
{
return LoadImage(decoder);
return this.LoadImage(decoder);
}
var key = new Key(this.PixelType, this.FilePath, decoder);
Image<TPixel> cachedImage = cache.GetOrAdd(key, fn => { return LoadImage(decoder); });
Image<TPixel> cachedImage = cache.GetOrAdd(key, _ => this.LoadImage(decoder));
return cachedImage.Clone();
}

11
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs

@ -48,7 +48,16 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
using (var sourceBitmap = new System.Drawing.Bitmap(stream))
{
var pixelType = new PixelTypeInfo(System.Drawing.Image.GetPixelFormatSize(sourceBitmap.PixelFormat));
return new ImageInfo(pixelType, sourceBitmap.Width, sourceBitmap.Height, new ImageMetaData());
var size = new SixLabors.Primitives.Size(sourceBitmap.Width, sourceBitmap.Height);
return new SystemDrawingInfo(pixelType, size, new ImageMetaData());
}
}
private class SystemDrawingInfo : ImageInfo
{
public SystemDrawingInfo(PixelTypeInfo pixelType, SixLabors.Primitives.Size size, ImageMetaData metaData)
: base(pixelType, size, metaData)
{
}
}
}

3
tests/Images/Input/Gif/leo.gif

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