Browse Source

Merge branch 'master' into colorspace-transforms

af/merge-core
James Jackson-South 8 years ago
parent
commit
707a753391
  1. 11
      src/ImageSharp/Common/Extensions/ComparableExtensions.cs
  2. 8
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  3. 6
      src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs
  4. 8
      src/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs
  5. 34
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  6. 2
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  7. 37
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  8. 14
      src/ImageSharp/Formats/Bmp/BmpFormat.cs
  9. 2
      src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs
  10. 18
      src/ImageSharp/Formats/Bmp/BmpMetaData.cs
  11. 2
      src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs
  12. 2
      src/ImageSharp/Formats/Bmp/ImageExtensions.cs
  13. 2
      src/ImageSharp/Formats/Gif/GifColorTableMode.cs
  14. 9
      src/ImageSharp/Formats/Gif/GifConfigurationModule.cs
  15. 13
      src/ImageSharp/Formats/Gif/GifConstants.cs
  16. 1
      src/ImageSharp/Formats/Gif/GifDecoder.cs
  17. 119
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  18. 2
      src/ImageSharp/Formats/Gif/GifDisposalMethod.cs
  19. 7
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  20. 116
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  21. 17
      src/ImageSharp/Formats/Gif/GifFormat.cs
  22. 33
      src/ImageSharp/Formats/Gif/GifFrameMetaData.cs
  23. 2
      src/ImageSharp/Formats/Gif/GifImageFormatDetector.cs
  24. 29
      src/ImageSharp/Formats/Gif/GifMetaData.cs
  25. 1
      src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
  26. 7
      src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs
  27. 2
      src/ImageSharp/Formats/Gif/ImageExtensions.cs
  28. 4
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  29. 4
      src/ImageSharp/Formats/Gif/Sections/GifImageDescriptor.cs
  30. 44
      src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
  31. 32
      src/ImageSharp/Formats/IImageFormat.cs
  32. 140
      src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs
  33. 7
      src/ImageSharp/Formats/Jpeg/IJpegEncoderOptions.cs
  34. 2
      src/ImageSharp/Formats/Jpeg/ImageExtensions.cs
  35. 9
      src/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs
  36. 15
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  37. 7
      src/ImageSharp/Formats/Jpeg/JpegEncoder.cs
  38. 52
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  39. 14
      src/ImageSharp/Formats/Jpeg/JpegFormat.cs
  40. 2
      src/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs
  41. 16
      src/ImageSharp/Formats/Jpeg/JpegMetaData.cs
  42. 18
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  43. 2
      src/ImageSharp/Formats/Png/ImageExtensions.cs
  44. 15
      src/ImageSharp/Formats/Png/PngBitDepth.cs
  45. 52
      src/ImageSharp/Formats/Png/PngChunkType.cs
  46. 4
      src/ImageSharp/Formats/Png/PngConfigurationModule.cs
  47. 60
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  48. 19
      src/ImageSharp/Formats/Png/PngEncoder.cs
  49. 139
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  50. 14
      src/ImageSharp/Formats/Png/PngFormat.cs
  51. 2
      src/ImageSharp/Formats/Png/PngImageFormatDetector.cs
  52. 27
      src/ImageSharp/Formats/Png/PngMetaData.cs
  53. 37
      src/ImageSharp/ImageFormats.cs
  54. 23
      src/ImageSharp/ImageFrame{TPixel}.cs
  55. 2
      src/ImageSharp/MetaData/FrameDecodingMode.cs
  56. 49
      src/ImageSharp/MetaData/ImageFrameMetaData.cs
  57. 50
      src/ImageSharp/MetaData/ImageMetaData.cs
  58. 9
      src/ImageSharp/Processing/Processors/Quantization/IQuantizer.cs
  59. 32
      src/ImageSharp/Processing/Processors/Quantization/OctreeFrameQuantizer{TPixel}.cs
  60. 23
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer.cs
  61. 2
      src/ImageSharp/Processing/Processors/Quantization/PaletteFrameQuantizer{TPixel}.cs
  62. 20
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  63. 165
      src/ImageSharp/Processing/Processors/Quantization/WuFrameQuantizer{TPixel}.cs
  64. 21
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer.cs
  65. 13
      tests/ImageSharp.Tests/ConfigurationTests.cs
  66. 44
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  67. 2
      tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
  68. 60
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  69. 8
      tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs
  70. 14
      tests/ImageSharp.Tests/Formats/Gif/Sections/GifImageDescriptorTests.cs
  71. 4
      tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs
  72. 37
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs
  73. 72
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  74. 33
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  75. 21
      tests/ImageSharp.Tests/MetaData/ImageFrameMetaDataTests.cs
  76. 2
      tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs
  77. 4
      tests/ImageSharp.Tests/TestImages.cs
  78. 62
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs
  79. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  80. 3
      tests/Images/Input/Bmp/rgb32.bmp
  81. 3
      tests/Images/Input/Gif/leo.gif

11
src/ImageSharp/Common/Extensions/ComparableExtensions.cs

@ -137,17 +137,6 @@ namespace SixLabors.ImageSharp
return value; return value;
} }
/// <summary>
/// Converts an <see cref="int"/> to a <see cref="byte"/> first restricting the value between the
/// minimum and maximum allowable ranges.
/// </summary>
/// <param name="value">The <see cref="int"/> this method extends.</param>
/// <returns>The <see cref="byte"/></returns>
public static byte ToByte(this int value)
{
return (byte)value.Clamp(0, 255);
}
/// <summary> /// <summary>
/// Converts an <see cref="float"/> to a <see cref="byte"/> first restricting the value between the /// Converts an <see cref="float"/> to a <see cref="byte"/> first restricting the value between the
/// minimum and maximum allowable ranges. /// minimum and maximum allowable ranges.

8
src/ImageSharp/Common/Helpers/ImageMaths.cs

@ -54,6 +54,14 @@ namespace SixLabors.ImageSharp
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBitsNeededForColorDepth(int colors) => Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2))); public static int GetBitsNeededForColorDepth(int colors) => Math.Max(1, (int)Math.Ceiling(Math.Log(colors, 2)));
/// <summary>
/// Returns how many colors will be created by the specified number of bits.
/// </summary>
/// <param name="bitDepth">The bit depth.</param>
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetColorCountForBitDepth(int bitDepth) => 1 << bitDepth;
/// <summary> /// <summary>
/// Implementation of 1D Gaussian G(x) function /// Implementation of 1D Gaussian G(x) function
/// </summary> /// </summary>

6
src/ImageSharp/Formats/Bmp/BmpBitsPerPixel.cs

@ -6,16 +6,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Enumerates the available bits per pixel for bitmap. /// Enumerates the available bits per pixel for bitmap.
/// </summary> /// </summary>
public enum BmpBitsPerPixel public enum BmpBitsPerPixel : short
{ {
/// <summary> /// <summary>
/// 24 bits per pixel. Each pixel consists of 3 bytes. /// 24 bits per pixel. Each pixel consists of 3 bytes.
/// </summary> /// </summary>
Pixel24 = 3, Pixel24 = 24,
/// <summary> /// <summary>
/// 32 bits per pixel. Each pixel consists of 4 bytes. /// 32 bits per pixel. Each pixel consists of 4 bytes.
/// </summary> /// </summary>
Pixel32 = 4 Pixel32 = 32
} }
} }

8
src/ImageSharp/Formats/Bmp/BmpConfigurationModule.cs

@ -9,11 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
public sealed class BmpConfigurationModule : IConfigurationModule public sealed class BmpConfigurationModule : IConfigurationModule
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(Configuration config) public void Configure(Configuration configuration)
{ {
config.ImageFormatsManager.SetEncoder(ImageFormats.Bmp, new BmpEncoder()); configuration.ImageFormatsManager.SetEncoder(BmpFormat.Instance, new BmpEncoder());
config.ImageFormatsManager.SetDecoder(ImageFormats.Bmp, new BmpDecoder()); configuration.ImageFormatsManager.SetDecoder(BmpFormat.Instance, new BmpDecoder());
config.ImageFormatsManager.AddImageFormatDetector(new BmpImageFormatDetector()); configuration.ImageFormatsManager.AddImageFormatDetector(new BmpImageFormatDetector());
} }
} }
} }

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

@ -175,10 +175,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
/// <returns>The <see cref="int"/> representing the inverted value.</returns> /// <returns>The <see cref="int"/> representing the inverted value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Invert(int y, int height, bool inverted) private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y;
{
return (!inverted) ? height - y - 1 : y;
}
/// <summary> /// <summary>
/// Calculates the amount of bytes to pad a row. /// Calculates the amount of bytes to pad a row.
@ -206,10 +203,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="value">The masked and shifted value</param> /// <param name="value">The masked and shifted value</param>
/// <returns>The <see cref="byte"/></returns> /// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte GetBytesFrom5BitValue(int value) private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
{
return (byte)((value << 3) | (value >> 2));
}
/// <summary> /// <summary>
/// Looks up color values and builds the image from de-compressed RLE8 data. /// Looks up color values and builds the image from de-compressed RLE8 data.
@ -524,8 +518,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
} }
// Resolution is stored in PPM. // Resolution is stored in PPM.
var meta = new ImageMetaData(); var meta = new ImageMetaData
meta.ResolutionUnits = PixelResolutionUnit.PixelsPerMeter; {
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
};
if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0) if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0)
{ {
meta.HorizontalResolution = this.infoHeader.XPelsPerMeter; meta.HorizontalResolution = this.infoHeader.XPelsPerMeter;
@ -540,6 +536,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
this.metaData = meta; this.metaData = meta;
short bitsPerPixel = this.infoHeader.BitsPerPixel;
var bmpMetaData = this.metaData.GetFormatMetaData(BmpFormat.Instance);
// We can only encode at these bit rates so far.
if (bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel24)
|| bitsPerPixel.Equals((short)BmpBitsPerPixel.Pixel32))
{
bmpMetaData.BitsPerPixel = (BmpBitsPerPixel)bitsPerPixel;
}
// skip the remaining header because we can't read those parts // skip the remaining header because we can't read those parts
this.stream.Skip(skipAmount); this.stream.Skip(skipAmount);
} }
@ -585,11 +591,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
if (this.infoHeader.ClrUsed == 0) if (this.infoHeader.ClrUsed == 0)
{ {
if (this.infoHeader.BitsPerPixel == 1 || if (this.infoHeader.BitsPerPixel == 1
this.infoHeader.BitsPerPixel == 4 || || this.infoHeader.BitsPerPixel == 4
this.infoHeader.BitsPerPixel == 8) || this.infoHeader.BitsPerPixel == 8)
{ {
colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4; colorMapSize = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * 4;
} }
} }
else else

2
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Gets or sets the number of bits per pixel. /// Gets or sets the number of bits per pixel.
/// </summary> /// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24; public BmpBitsPerPixel? BitsPerPixel { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public void Encode<TPixel>(Image<TPixel> image, Stream stream)

37
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -21,10 +21,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary> /// </summary>
private int padding; private int padding;
private readonly BmpBitsPerPixel bitsPerPixel;
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
private BmpBitsPerPixel? bitsPerPixel;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BmpEncoderCore"/> class. /// Initializes a new instance of the <see cref="BmpEncoderCore"/> class.
/// </summary> /// </summary>
@ -48,37 +48,39 @@ namespace SixLabors.ImageSharp.Formats.Bmp
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
// Cast to int will get the bytes per pixel ImageMetaData metaData = image.MetaData;
short bpp = (short)(8 * (int)this.bitsPerPixel); BmpMetaData bmpMetaData = metaData.GetFormatMetaData(BmpFormat.Instance);
this.bitsPerPixel = this.bitsPerPixel ?? bmpMetaData.BitsPerPixel;
short bpp = (short)this.bitsPerPixel;
int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32);
this.padding = bytesPerLine - (image.Width * (int)this.bitsPerPixel); this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F));
// Set Resolution. // Set Resolution.
ImageMetaData meta = image.MetaData;
int hResolution = 0; int hResolution = 0;
int vResolution = 0; int vResolution = 0;
if (meta.ResolutionUnits != PixelResolutionUnit.AspectRatio) if (metaData.ResolutionUnits != PixelResolutionUnit.AspectRatio)
{ {
if (meta.HorizontalResolution > 0 && meta.VerticalResolution > 0) if (metaData.HorizontalResolution > 0 && metaData.VerticalResolution > 0)
{ {
switch (meta.ResolutionUnits) switch (metaData.ResolutionUnits)
{ {
case PixelResolutionUnit.PixelsPerInch: case PixelResolutionUnit.PixelsPerInch:
hResolution = (int)Math.Round(UnitConverter.InchToMeter(meta.HorizontalResolution)); hResolution = (int)Math.Round(UnitConverter.InchToMeter(metaData.HorizontalResolution));
vResolution = (int)Math.Round(UnitConverter.InchToMeter(meta.VerticalResolution)); vResolution = (int)Math.Round(UnitConverter.InchToMeter(metaData.VerticalResolution));
break; break;
case PixelResolutionUnit.PixelsPerCentimeter: case PixelResolutionUnit.PixelsPerCentimeter:
hResolution = (int)Math.Round(UnitConverter.CmToMeter(meta.HorizontalResolution)); hResolution = (int)Math.Round(UnitConverter.CmToMeter(metaData.HorizontalResolution));
vResolution = (int)Math.Round(UnitConverter.CmToMeter(meta.VerticalResolution)); vResolution = (int)Math.Round(UnitConverter.CmToMeter(metaData.VerticalResolution));
break; break;
case PixelResolutionUnit.PixelsPerMeter: case PixelResolutionUnit.PixelsPerMeter:
hResolution = (int)Math.Round(meta.HorizontalResolution); hResolution = (int)Math.Round(metaData.HorizontalResolution);
vResolution = (int)Math.Round(meta.VerticalResolution); vResolution = (int)Math.Round(metaData.VerticalResolution);
break; break;
} }
@ -145,10 +147,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
} }
} }
private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding);
{
return this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding);
}
/// <summary> /// <summary>
/// Writes the 32bit color palette to the stream. /// Writes the 32bit color palette to the stream.

14
src/ImageSharp/Formats/Bmp/BmpFormat.cs

@ -8,8 +8,17 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the bmp format. /// Registers the image encoders, decoders and mime type detectors for the bmp format.
/// </summary> /// </summary>
internal sealed class BmpFormat : IImageFormat public sealed class BmpFormat : IImageFormat<BmpMetaData>
{ {
private BmpFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static BmpFormat Instance { get; } = new BmpFormat();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "BMP"; public string Name => "BMP";
@ -21,5 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<string> FileExtensions => BmpConstants.FileExtensions; public IEnumerable<string> FileExtensions => BmpConstants.FileExtensions;
/// <inheritdoc/>
public BmpMetaData CreateDefaultFormatMetaData() => new BmpMetaData();
} }
} }

2
src/ImageSharp/Formats/Bmp/BmpImageFormatDetector.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <inheritdoc/> /// <inheritdoc/>
public IImageFormat DetectFormat(ReadOnlySpan<byte> header) public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
{ {
return this.IsSupportedFileFormat(header) ? ImageFormats.Bmp : null; return this.IsSupportedFileFormat(header) ? BmpFormat.Instance : null;
} }
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)

18
src/ImageSharp/Formats/Bmp/BmpMetaData.cs

@ -0,0 +1,18 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Bmp
{
/// <summary>
/// Provides Bmp specific metadata information for the image.
/// </summary>
public class BmpMetaData
{
/// <summary>
/// Gets or sets the number of bits per pixel.
/// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24;
// TODO: Colors used once we support encoding palette bmps.
}
}

2
src/ImageSharp/Formats/Bmp/IBmpEncoderOptions.cs

@ -12,6 +12,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary> /// <summary>
/// Gets the number of bits per pixel. /// Gets the number of bits per pixel.
/// </summary> /// </summary>
BmpBitsPerPixel BitsPerPixel { get; } BmpBitsPerPixel? BitsPerPixel { get; }
} }
} }

2
src/ImageSharp/Formats/Bmp/ImageExtensions.cs

@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsBmp<TPixel>(this Image<TPixel> source, Stream stream, BmpEncoder encoder) public static void SaveAsBmp<TPixel>(this Image<TPixel> source, Stream stream, BmpEncoder encoder)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Bmp)); => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(BmpFormat.Instance));
} }
} }

2
src/ImageSharp/Formats/Gif/GifColorTableMode.cs

@ -4,7 +4,7 @@
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
{ {
/// <summary> /// <summary>
/// Provides enumeration for the available Gif color table modes. /// Provides enumeration for the available color table modes.
/// </summary> /// </summary>
public enum GifColorTableMode public enum GifColorTableMode
{ {

9
src/ImageSharp/Formats/Gif/GifConfigurationModule.cs

@ -9,12 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
public sealed class GifConfigurationModule : IConfigurationModule public sealed class GifConfigurationModule : IConfigurationModule
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(Configuration config) public void Configure(Configuration configuration)
{ {
config.ImageFormatsManager.SetEncoder(ImageFormats.Gif, new GifEncoder()); configuration.ImageFormatsManager.SetEncoder(GifFormat.Instance, new GifEncoder());
config.ImageFormatsManager.SetDecoder(ImageFormats.Gif, new GifDecoder()); configuration.ImageFormatsManager.SetDecoder(GifFormat.Instance, new GifDecoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new GifImageFormatDetector());
config.ImageFormatsManager.AddImageFormatDetector(new GifImageFormatDetector());
} }
} }
} }

13
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -41,20 +41,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
public const byte ApplicationExtensionLabel = 0xFF; public const byte ApplicationExtensionLabel = 0xFF;
/// <summary>
/// The application block size.
/// </summary>
public const byte ApplicationBlockSize = 11;
/// <summary> /// <summary>
/// The application identification. /// The application identification.
/// </summary> /// </summary>
public const string ApplicationIdentification = "NETSCAPE2.0"; public const string NetscapeApplicationIdentification = "NETSCAPE2.0";
/// <summary> /// <summary>
/// The ASCII encoded application identification bytes. /// The ASCII encoded application identification bytes.
/// </summary> /// </summary>
internal static readonly byte[] ApplicationIdentificationBytes = Encoding.UTF8.GetBytes(ApplicationIdentification); internal static readonly byte[] NetscapeApplicationIdentificationBytes = Encoding.UTF8.GetBytes(NetscapeApplicationIdentification);
/// <summary> /// <summary>
/// The application block size. /// The Netscape looping application sub block size.
/// </summary> /// </summary>
public const byte ApplicationBlockSize = 11; public const byte NetscapeLoopingSubBlockSize = 3;
/// <summary> /// <summary>
/// The comment label. /// The comment label.

1
src/ImageSharp/Formats/Gif/GifDecoder.cs

@ -3,6 +3,7 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif

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

@ -56,10 +56,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
private GifGraphicControlExtension graphicsControlExtension; private GifGraphicControlExtension graphicsControlExtension;
/// <summary> /// <summary>
/// The metadata /// The image desciptor.
/// </summary>
private GifImageDescriptor imageDescriptor;
/// <summary>
/// The abstract metadata.
/// </summary> /// </summary>
private ImageMetaData metaData; private ImageMetaData metaData;
/// <summary>
/// The gif specific metadata.
/// </summary>
private GifMetaData gifMetaData;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class. /// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
/// </summary> /// </summary>
@ -120,8 +130,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
else if (nextFlag == GifConstants.ExtensionIntroducer) else if (nextFlag == GifConstants.ExtensionIntroducer)
{ {
int label = stream.ReadByte(); switch (stream.ReadByte())
switch (label)
{ {
case GifConstants.GraphicControlLabel: case GifConstants.GraphicControlLabel:
this.ReadGraphicalControlExtension(); this.ReadGraphicalControlExtension();
@ -130,11 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.ReadComments(); this.ReadComments();
break; break;
case GifConstants.ApplicationExtensionLabel: case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension();
// The application extension length should be 11 but we've got test images that incorrectly
// set this to 252.
int appLength = stream.ReadByte();
this.Skip(appLength); // No need to read.
break; break;
case GifConstants.PlainTextLabel: case GifConstants.PlainTextLabel:
int plainLength = stream.ReadByte(); int plainLength = stream.ReadByte();
@ -178,13 +183,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
{ {
if (nextFlag == GifConstants.ImageLabel) if (nextFlag == GifConstants.ImageLabel)
{ {
// Skip image block this.ReadImageDescriptor();
this.Skip(0);
} }
else if (nextFlag == GifConstants.ExtensionIntroducer) else if (nextFlag == GifConstants.ExtensionIntroducer)
{ {
int label = stream.ReadByte(); switch (stream.ReadByte())
switch (label)
{ {
case GifConstants.GraphicControlLabel: case GifConstants.GraphicControlLabel:
@ -195,11 +198,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.ReadComments(); this.ReadComments();
break; break;
case GifConstants.ApplicationExtensionLabel: case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension();
// The application extension length should be 11 but we've got test images that incorrectly
// set this to 252.
int appLength = stream.ReadByte();
this.Skip(appLength); // No need to read.
break; break;
case GifConstants.PlainTextLabel: case GifConstants.PlainTextLabel:
int plainLength = stream.ReadByte(); int plainLength = stream.ReadByte();
@ -224,7 +223,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.globalColorTable?.Dispose(); this.globalColorTable?.Dispose();
} }
return new ImageInfo(new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel), this.logicalScreenDescriptor.Width, this.logicalScreenDescriptor.Height, this.metaData); return new ImageInfo(
new PixelTypeInfo(this.logicalScreenDescriptor.BitsPerPixel),
this.logicalScreenDescriptor.Width,
this.logicalScreenDescriptor.Height,
this.metaData);
} }
/// <summary> /// <summary>
@ -238,14 +241,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
/// <summary> /// <summary>
/// Reads the image descriptor /// Reads the image descriptor.
/// </summary> /// </summary>
/// <returns><see cref="GifImageDescriptor"/></returns> private void ReadImageDescriptor()
private GifImageDescriptor ReadImageDescriptor()
{ {
this.stream.Read(this.buffer, 0, 9); this.stream.Read(this.buffer, 0, 9);
return GifImageDescriptor.Parse(this.buffer); this.imageDescriptor = GifImageDescriptor.Parse(this.buffer);
} }
/// <summary> /// <summary>
@ -258,6 +260,41 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer);
} }
/// <summary>
/// Reads the application extension block parsing any animation information
/// if present.
/// </summary>
private void ReadApplicationExtension()
{
int appLength = this.stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE or ANIMEXTS extension. We want the loop count from this.
if (appLength == GifConstants.ApplicationBlockSize)
{
this.stream.Skip(appLength);
int subBlockSize = this.stream.ReadByte();
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetaData.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
this.stream.Skip(1); // Skip the terminator.
return;
}
// Could be XMP or something else not supported yet.
// Back up and skip.
this.stream.Position -= appLength + 1;
this.Skip(appLength);
return;
}
this.Skip(appLength); // Not supported by any known decoder.
}
/// <summary> /// <summary>
/// Skips the designated number of bytes in the stream. /// Skips the designated number of bytes in the stream.
/// </summary> /// </summary>
@ -312,25 +349,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame) private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
GifImageDescriptor imageDescriptor = this.ReadImageDescriptor(); this.ReadImageDescriptor();
IManagedByteBuffer localColorTable = null; IManagedByteBuffer localColorTable = null;
IManagedByteBuffer indices = null; IManagedByteBuffer indices = null;
try try
{ {
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. // 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); localColorTable = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(length, AllocationOptions.Clean);
this.stream.Read(localColorTable.Array, 0, length); 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()); 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 // Skip any remaining blocks
this.Skip(0); this.Skip(0);
@ -388,7 +425,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
else else
{ {
if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
{ {
prevFrame = previousFrame; prevFrame = previousFrame;
} }
@ -471,7 +508,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
previousFrame = currentFrame ?? image.Frames.RootFrame; previousFrame = currentFrame ?? image.Frames.RootFrame;
if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToBackground)
{ {
this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height); this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height);
} }
@ -503,12 +540,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetFrameMetaData(ImageFrameMetaData meta) private void SetFrameMetaData(ImageFrameMetaData meta)
{ {
GifFrameMetaData gifMeta = meta.GetFormatMetaData(GifFormat.Instance);
if (this.graphicsControlExtension.DelayTime > 0) if (this.graphicsControlExtension.DelayTime > 0)
{ {
meta.FrameDelay = this.graphicsControlExtension.DelayTime; gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime;
}
// Frames can either use the global table or their own local table.
if (this.logicalScreenDescriptor.GlobalColorTableFlag
&& this.logicalScreenDescriptor.GlobalColorTableSize > 0)
{
gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize;
}
else if (this.imageDescriptor.LocalColorTableFlag
&& this.imageDescriptor.LocalColorTableSize > 0)
{
gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize;
} }
meta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod;
} }
/// <summary> /// <summary>
@ -552,10 +602,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
this.metaData = meta; this.metaData = meta;
this.gifMetaData = meta.GetFormatMetaData(GifFormat.Instance);
this.gifMetaData.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag
? GifColorTableMode.Global
: GifColorTableMode.Local;
if (this.logicalScreenDescriptor.GlobalColorTableFlag) if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{ {
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetaData.GlobalColorTableLength = globalColorTableLength;
this.globalColorTable = this.MemoryAllocator.AllocateManagedByteBuffer(globalColorTableLength, AllocationOptions.Clean); this.globalColorTable = this.MemoryAllocator.AllocateManagedByteBuffer(globalColorTableLength, AllocationOptions.Clean);

2
src/ImageSharp/Formats/Gif/DisposalMethod.cs → src/ImageSharp/Formats/Gif/GifDisposalMethod.cs

@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// in an animation sequence. /// in an animation sequence.
/// <see href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt"/> section 23 /// <see href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt"/> section 23
/// </summary> /// </summary>
public enum DisposalMethod public enum GifDisposalMethod
{ {
/// <summary> /// <summary>
/// No disposal specified. /// No disposal specified.

7
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -14,11 +14,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions
{ {
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being encoded.
/// </summary>
public bool IgnoreMetadata { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets the encoding that should be used when writing comments. /// Gets or sets the encoding that should be used when writing comments.
/// </summary> /// </summary>
@ -33,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Gets or sets the color table mode: Global or local. /// Gets or sets the color table mode: Global or local.
/// </summary> /// </summary>
public GifColorTableMode ColorTableMode { get; set; } public GifColorTableMode? ColorTableMode { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public void Encode<TPixel>(Image<TPixel> image, Stream stream)

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

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers.Binary;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -44,17 +43,17 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// The color table mode: Global or local. /// The color table mode: Global or local.
/// </summary> /// </summary>
private readonly GifColorTableMode colorTableMode; private GifColorTableMode? colorTableMode;
/// <summary> /// <summary>
/// A flag indicating whether to ingore the metadata when writing the image. /// The number of bits requires to store the color palette.
/// </summary> /// </summary>
private readonly bool ignoreMetadata; private int bitDepth;
/// <summary> /// <summary>
/// The number of bits requires to store the color palette. /// Gif specific meta data.
/// </summary> /// </summary>
private int bitDepth; private GifMetaData gifMetaData;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class. /// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
@ -67,7 +66,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding; this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.quantizer = options.Quantizer; this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode; this.colorTableMode = options.ColorTableMode;
this.ignoreMetadata = options.IgnoreMetadata;
} }
/// <summary> /// <summary>
@ -82,6 +80,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
ImageMetaData metaData = image.MetaData;
this.gifMetaData = metaData.GetFormatMetaData(GifFormat.Instance);
this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode;
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
// Quantize the image returning a palette. // Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized = QuantizedFrame<TPixel> quantized =
this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame); this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
@ -94,8 +97,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Write the LSD. // Write the LSD.
int index = this.GetTransparentIndex(quantized); int index = this.GetTransparentIndex(quantized);
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global); this.WriteLogicalScreenDescriptor(metaData, image.Width, image.Height, index, useGlobalTable, stream);
this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream);
if (useGlobalTable) if (useGlobalTable)
{ {
@ -103,12 +105,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
// Write the comments. // Write the comments.
this.WriteComments(image.MetaData, stream); this.WriteComments(metaData, stream);
// Write application extension to allow additional frames. // Write application extension to allow additional frames.
if (image.Frames.Count > 1) if (image.Frames.Count > 1)
{ {
this.WriteApplicationExtension(stream, image.MetaData.RepeatCount); this.WriteApplicationExtension(stream, this.gifMetaData.RepeatCount);
} }
if (useGlobalTable) if (useGlobalTable)
@ -136,8 +138,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
for (int i = 0; i < image.Frames.Count; i++) for (int i = 0; i < image.Frames.Count; i++)
{ {
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetaData metaData = frame.MetaData;
this.WriteGraphicalControlExtension(frame.MetaData, transparencyIndex, stream); GifFrameMetaData frameMetaData = metaData.GetFormatMetaData(GifFormat.Instance);
this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream);
this.WriteImageDescriptor(frame, false, stream); this.WriteImageDescriptor(frame, false, stream);
if (i == 0) if (i == 0)
@ -146,7 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
else 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); this.WriteImageData(paletteQuantized, stream);
} }
@ -157,20 +161,37 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream) private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
ImageFrame<TPixel> previousFrame = null;
GifFrameMetaData previousMeta = null;
foreach (ImageFrame<TPixel> frame in image.Frames) foreach (ImageFrame<TPixel> frame in image.Frames)
{ {
ImageFrameMetaData metaData = frame.MetaData;
GifFrameMetaData frameMetaData = metaData.GetFormatMetaData(GifFormat.Instance);
if (quantized is null) 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
&& previousMeta.ColorTableLength != frameMetaData.ColorTableLength
&& frameMetaData.ColorTableLength > 0)
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>(frameMetaData.ColorTableLength).QuantizeFrame(frame);
}
else
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(frame);
}
} }
this.WriteGraphicalControlExtension(frame.MetaData, this.GetTransparentIndex(quantized), stream); this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.WriteGraphicalControlExtension(frameMetaData, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream); this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream); this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream); this.WriteImageData(quantized, stream);
quantized?.Dispose(); quantized?.Dispose();
quantized = null; // So next frame can regenerate it quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetaData;
} }
} }
@ -210,21 +231,24 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteHeader(Stream stream) private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
{
stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
}
/// <summary> /// <summary>
/// Writes the logical screen descriptor to the stream. /// Writes the logical screen descriptor to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <param name="metaData">The image metadata.</param>
/// <param name="image">The image to encode.</param> /// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param> /// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
/// <param name="useGlobalTable">Whether to use a global or local color table.</param> /// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor<TPixel>(Image<TPixel> image, int transparencyIndex, bool useGlobalTable, Stream stream) private void WriteLogicalScreenDescriptor(
where TPixel : struct, IPixel<TPixel> ImageMetaData metaData,
int width,
int height,
int transparencyIndex,
bool useGlobalTable,
Stream stream)
{ {
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1); byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
@ -237,13 +261,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
// 1..255 - Value used in the computation. // 1..255 - Value used in the computation.
// //
// Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64
ImageMetaData meta = image.MetaData;
byte ratio = 0; byte ratio = 0;
if (meta.ResolutionUnits == PixelResolutionUnit.AspectRatio) if (metaData.ResolutionUnits == PixelResolutionUnit.AspectRatio)
{ {
double hr = meta.HorizontalResolution; double hr = metaData.HorizontalResolution;
double vr = meta.VerticalResolution; double vr = metaData.VerticalResolution;
if (hr != vr) if (hr != vr)
{ {
if (hr > vr) if (hr > vr)
@ -258,8 +281,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
var descriptor = new GifLogicalScreenDescriptor( var descriptor = new GifLogicalScreenDescriptor(
width: (ushort)image.Width, width: (ushort)width,
height: (ushort)image.Height, height: (ushort)height,
packed: packedValue, packed: packedValue,
backgroundColorIndex: unchecked((byte)transparencyIndex), backgroundColorIndex: unchecked((byte)transparencyIndex),
ratio); ratio);
@ -279,25 +302,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Application Extension Header // Application Extension Header
if (repeatCount != 1) if (repeatCount != 1)
{ {
this.buffer[0] = GifConstants.ExtensionIntroducer; var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.buffer[1] = GifConstants.ApplicationExtensionLabel; this.WriteExtension(loopingExtension, stream);
this.buffer[2] = GifConstants.ApplicationBlockSize;
// Write NETSCAPE2.0
GifConstants.ApplicationIdentificationBytes.AsSpan().CopyTo(this.buffer.AsSpan(3, 11));
// Application Data ----
this.buffer[14] = 3; // Application block length
this.buffer[15] = 1; // Data sub-block index (always 1)
// 0 means loop indefinitely. Count is set as play n + 1 times.
repeatCount = (ushort)Math.Max(0, repeatCount - 1);
BinaryPrimitives.WriteUInt16LittleEndian(this.buffer.AsSpan(16, 2), repeatCount); // Repeat count for images.
this.buffer[18] = GifConstants.Terminator; // Terminator
stream.Write(this.buffer, 0, 19);
} }
} }
@ -308,11 +314,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteComments(ImageMetaData metadata, Stream stream) private void WriteComments(ImageMetaData metadata, Stream stream)
{ {
if (this.ignoreMetadata)
{
return;
}
if (!metadata.TryGetProperty(GifConstants.Comments, out ImageProperty property) || string.IsNullOrEmpty(property.Value)) if (!metadata.TryGetProperty(GifConstants.Comments, out ImageProperty property) || string.IsNullOrEmpty(property.Value))
{ {
return; return;
@ -337,7 +338,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="metaData">The metadata of the image or frame.</param> /// <param name="metaData">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param> /// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(ImageFrameMetaData metaData, int transparencyIndex, Stream stream) private void WriteGraphicalControlExtension(GifFrameMetaData metaData, int transparencyIndex, Stream stream)
{ {
byte packedValue = GifGraphicControlExtension.GetPackedValue( byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metaData.DisposalMethod, disposalMethod: metaData.DisposalMethod,
@ -382,7 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
localColorTableFlag: hasColorTable, localColorTableFlag: hasColorTable,
interfaceFlag: false, interfaceFlag: false,
sortFlag: false, sortFlag: false,
localColorTableSize: (byte)this.bitDepth); localColorTableSize: this.bitDepth - 1);
var descriptor = new GifImageDescriptor( var descriptor = new GifImageDescriptor(
left: 0, left: 0,
@ -407,7 +408,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
{ {
int pixelCount = image.Palette.Length; 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 = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
Rgb24 rgb = default; Rgb24 rgb = default;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))

17
src/ImageSharp/Formats/Gif/GifFormat.cs

@ -8,8 +8,17 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the gif format. /// Registers the image encoders, decoders and mime type detectors for the gif format.
/// </summary> /// </summary>
internal sealed class GifFormat : IImageFormat public sealed class GifFormat : IImageFormat<GifMetaData, GifFrameMetaData>
{ {
private GifFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static GifFormat Instance { get; } = new GifFormat();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "GIF"; public string Name => "GIF";
@ -21,5 +30,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<string> FileExtensions => GifConstants.FileExtensions; public IEnumerable<string> FileExtensions => GifConstants.FileExtensions;
/// <inheritdoc/>
public GifMetaData CreateDefaultFormatMetaData() => new GifMetaData();
/// <inheritdoc/>
public GifFrameMetaData CreateDefaultFormatFrameMetaData() => new GifFrameMetaData();
} }
} }

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

@ -0,0 +1,33 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
/// Provides Gif specific metadata information for the image frame.
/// </summary>
public class GifFrameMetaData
{
/// <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
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
public int FrameDelay { get; set; }
/// <summary>
/// Gets or sets the disposal method for animated images.
/// Primarily used in Gif animation, this field indicates the way in which the graphic is to
/// be treated after being displayed.
/// </summary>
public GifDisposalMethod DisposalMethod { get; set; }
}
}

2
src/ImageSharp/Formats/Gif/GifImageFormatDetector.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <inheritdoc/> /// <inheritdoc/>
public IImageFormat DetectFormat(ReadOnlySpan<byte> header) public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
{ {
return this.IsSupportedFileFormat(header) ? ImageFormats.Gif : null; return this.IsSupportedFileFormat(header) ? GifFormat.Instance : null;
} }
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)

29
src/ImageSharp/Formats/Gif/GifMetaData.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
/// Provides Gif specific metadata information for the image.
/// </summary>
public class GifMetaData
{
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>
/// 0 means to repeat indefinitely, count is set as play n + 1 times
/// </remarks>
/// </summary>
public ushort RepeatCount { get; set; }
/// <summary>
/// Gets or sets the color table mode.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }
/// <summary>
/// Gets or sets the length of the global color table if present.
/// </summary>
public int GlobalColorTableLength { get; set; }
}
}

1
src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System.Text; using System.Text;
using SixLabors.ImageSharp.MetaData;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
{ {

7
src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs

@ -11,11 +11,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
internal interface IGifEncoderOptions internal interface IGifEncoderOptions
{ {
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being encoded.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary> /// <summary>
/// Gets the text encoding used to write comments. /// Gets the text encoding used to write comments.
/// </summary> /// </summary>
@ -29,6 +24,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary> /// <summary>
/// Gets the color table mode: Global or local. /// Gets the color table mode: Global or local.
/// </summary> /// </summary>
GifColorTableMode ColorTableMode { get; } GifColorTableMode? ColorTableMode { get; }
} }
} }

2
src/ImageSharp/Formats/Gif/ImageExtensions.cs

@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsGif<TPixel>(this Image<TPixel> source, Stream stream, GifEncoder encoder) public static void SaveAsGif<TPixel>(this Image<TPixel> source, Stream stream, GifEncoder encoder)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Gif)); => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(GifFormat.Instance));
} }
} }

4
src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs

@ -53,7 +53,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Gets the disposal method which indicates the way in which the /// Gets the disposal method which indicates the way in which the
/// graphic is to be treated after being displayed. /// graphic is to be treated after being displayed.
/// </summary> /// </summary>
public DisposalMethod DisposalMethod => (DisposalMethod)((this.Packed & 0x1C) >> 2); public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2);
/// <summary> /// <summary>
/// Gets a value indicating whether transparency flag is to be set. /// Gets a value indicating whether transparency flag is to be set.
@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
return MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0]; return MemoryMarshal.Cast<byte, GifGraphicControlExtension>(buffer)[0];
} }
public static byte GetPackedValue(DisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false) public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false)
{ {
/* /*
Reserved | 3 Bits Reserved | 3 Bits

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

@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
return MemoryMarshal.Cast<byte, GifImageDescriptor>(buffer)[0]; 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 Local Color Table Flag | 1 Bit
@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
value |= 1 << 5; value |= 1 << 5;
} }
value |= (byte)(localColorTableSize - 1); value |= (byte)localColorTableSize;
return value; return value;
} }

44
src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
namespace SixLabors.ImageSharp.Formats.Gif
{
internal readonly struct GifNetscapeLoopingApplicationExtension : IGifExtension
{
public GifNetscapeLoopingApplicationExtension(ushort repeatCount) => this.RepeatCount = repeatCount;
public byte Label => GifConstants.ApplicationExtensionLabel;
/// <summary>
/// Gets the repeat count.
/// 0 means loop indefinitely. Count is set as play n + 1 times.
/// </summary>
public ushort RepeatCount { get; }
public static GifNetscapeLoopingApplicationExtension Parse(ReadOnlySpan<byte> buffer)
{
ushort repeatCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(0, 2));
return new GifNetscapeLoopingApplicationExtension(repeatCount);
}
public int WriteTo(Span<byte> buffer)
{
buffer[0] = GifConstants.ApplicationBlockSize;
// Write NETSCAPE2.0
GifConstants.NetscapeApplicationIdentificationBytes.AsSpan().CopyTo(buffer.Slice(1, 11));
// Application Data ----
buffer[12] = 3; // Application block length (always 3)
buffer[13] = 1; // Data sub-block indentity (always 1)
// 0 means loop indefinitely. Count is set as play n + 1 times.
BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.RepeatCount);
return 16; // Length - Introducer + Label + Terminator.
}
}
}

32
src/ImageSharp/Formats/IImageFormat.cs

@ -6,7 +6,7 @@ using System.Collections.Generic;
namespace SixLabors.ImageSharp.Formats namespace SixLabors.ImageSharp.Formats
{ {
/// <summary> /// <summary>
/// Describes an image format. /// Defines the contract for an image format.
/// </summary> /// </summary>
public interface IImageFormat public interface IImageFormat
{ {
@ -30,4 +30,34 @@ namespace SixLabors.ImageSharp.Formats
/// </summary> /// </summary>
IEnumerable<string> FileExtensions { get; } IEnumerable<string> FileExtensions { get; }
} }
/// <summary>
/// Defines the contract for an image format containing metadata.
/// </summary>
/// <typeparam name="TFormatMetaData">The type of format metadata.</typeparam>
public interface IImageFormat<out TFormatMetaData> : IImageFormat
where TFormatMetaData : class
{
/// <summary>
/// Creates a default instance of the format metadata.
/// </summary>
/// <returns>The <typeparamref name="TFormatMetaData"/>.</returns>
TFormatMetaData CreateDefaultFormatMetaData();
}
/// <summary>
/// Defines the contract for an image format containing metadata with multiple frames.
/// </summary>
/// <typeparam name="TFormatMetaData">The type of format metadata.</typeparam>
/// <typeparam name="TFormatFrameMetaData">The type of format frame metadata.</typeparam>
public interface IImageFormat<out TFormatMetaData, out TFormatFrameMetaData> : IImageFormat<TFormatMetaData>
where TFormatMetaData : class
where TFormatFrameMetaData : class
{
/// <summary>
/// Creates a default instance of the format frame metadata.
/// </summary>
/// <returns>The <typeparamref name="TFormatFrameMetaData"/>.</returns>
TFormatFrameMetaData CreateDefaultFormatFrameMetaData();
}
} }

140
src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs

@ -0,0 +1,140 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{
/// <summary>
/// Provides methods to evaluate the quality of an image.
/// Ported from <see href="https://github.com/ImageMagick/ImageMagick/blob/f362c02083d27211b913c6e44794f0ac6edaf2bd/coders/jpeg.c#L855"/>
/// </summary>
internal static class QualityEvaluator
{
private static readonly int[] Hash = new int[101]
{
1020, 1015, 932, 848, 780, 735, 702, 679, 660, 645,
632, 623, 613, 607, 600, 594, 589, 585, 581, 571,
555, 542, 529, 514, 494, 474, 457, 439, 424, 410,
397, 386, 373, 364, 351, 341, 334, 324, 317, 309,
299, 294, 287, 279, 274, 267, 262, 257, 251, 247,
243, 237, 232, 227, 222, 217, 213, 207, 202, 198,
192, 188, 183, 177, 173, 168, 163, 157, 153, 148,
143, 139, 132, 128, 125, 119, 115, 108, 104, 99,
94, 90, 84, 79, 74, 70, 64, 59, 55, 49,
45, 40, 34, 30, 25, 20, 15, 11, 6, 4,
0
};
private static readonly int[] Sums = new int[101]
{
32640, 32635, 32266, 31495, 30665, 29804, 29146, 28599, 28104,
27670, 27225, 26725, 26210, 25716, 25240, 24789, 24373, 23946,
23572, 22846, 21801, 20842, 19949, 19121, 18386, 17651, 16998,
16349, 15800, 15247, 14783, 14321, 13859, 13535, 13081, 12702,
12423, 12056, 11779, 11513, 11135, 10955, 10676, 10392, 10208,
9928, 9747, 9564, 9369, 9193, 9017, 8822, 8639, 8458,
8270, 8084, 7896, 7710, 7527, 7347, 7156, 6977, 6788,
6607, 6422, 6236, 6054, 5867, 5684, 5495, 5305, 5128,
4945, 4751, 4638, 4442, 4248, 4065, 3888, 3698, 3509,
3326, 3139, 2957, 2775, 2586, 2405, 2216, 2037, 1846,
1666, 1483, 1297, 1109, 927, 735, 554, 375, 201,
128, 0
};
private static readonly int[] Hash1 = new int[101]
{
510, 505, 422, 380, 355, 338, 326, 318, 311, 305,
300, 297, 293, 291, 288, 286, 284, 283, 281, 280,
279, 278, 277, 273, 262, 251, 243, 233, 225, 218,
211, 205, 198, 193, 186, 181, 177, 172, 168, 164,
158, 156, 152, 148, 145, 142, 139, 136, 133, 131,
129, 126, 123, 120, 118, 115, 113, 110, 107, 105,
102, 100, 97, 94, 92, 89, 87, 83, 81, 79,
76, 74, 70, 68, 66, 63, 61, 57, 55, 52,
50, 48, 44, 42, 39, 37, 34, 31, 29, 26,
24, 21, 18, 16, 13, 11, 8, 6, 3, 2,
0
};
private static readonly int[] Sums1 = new int[101]
{
16320, 16315, 15946, 15277, 14655, 14073, 13623, 13230, 12859,
12560, 12240, 11861, 11456, 11081, 10714, 10360, 10027, 9679,
9368, 9056, 8680, 8331, 7995, 7668, 7376, 7084, 6823,
6562, 6345, 6125, 5939, 5756, 5571, 5421, 5240, 5086,
4976, 4829, 4719, 4616, 4463, 4393, 4280, 4166, 4092,
3980, 3909, 3835, 3755, 3688, 3621, 3541, 3467, 3396,
3323, 3247, 3170, 3096, 3021, 2952, 2874, 2804, 2727,
2657, 2583, 2509, 2437, 2362, 2290, 2211, 2136, 2068,
1996, 1915, 1858, 1773, 1692, 1620, 1552, 1477, 1398,
1326, 1251, 1179, 1109, 1031, 961, 884, 814, 736,
667, 592, 518, 441, 369, 292, 221, 151, 86,
64, 0
};
/// <summary>
/// Returns an estimated quality of the image based on the quantization tables.
/// </summary>
/// <param name="quantizationTables">The quantization tables.</param>
/// <returns>The <see cref="int"/>.</returns>
public static int EstimateQuality(Block8x8F[] quantizationTables)
{
int quality = 75;
float sum = 0;
for (int i = 0; i < quantizationTables.Length; i++)
{
ref Block8x8F qTable = ref quantizationTables[i];
for (int j = 0; j < Block8x8F.Size; j++)
{
sum += qTable[j];
}
}
ref Block8x8F qTable0 = ref quantizationTables[0];
ref Block8x8F qTable1 = ref quantizationTables[1];
if (!qTable0.Equals(default))
{
if (!qTable1.Equals(default))
{
quality = (int)(qTable0[2]
+ qTable0[53]
+ qTable1[0]
+ qTable1[Block8x8F.Size - 1]);
for (int i = 0; i < 100; i++)
{
if (quality < Hash[i] && sum < Sums[i])
{
continue;
}
if (((quality <= Hash[i]) && (sum <= Sums[i])) || (i >= 50))
{
return i + 1;
}
}
}
else
{
quality = (int)(qTable0[2] + qTable0[53]);
for (int i = 0; i < 100; i++)
{
if (quality < Hash1[i] && sum < Sums1[i])
{
continue;
}
if (((quality <= Hash1[i]) && (sum <= Sums1[i])) || (i >= 50))
{
return i + 1;
}
}
}
}
return quality;
}
}
}

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

@ -8,17 +8,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
internal interface IJpegEncoderOptions internal interface IJpegEncoderOptions
{ {
/// <summary>
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary> /// <summary>
/// Gets the quality, that will be used to encode the image. Quality /// Gets the quality, that will be used to encode the image. Quality
/// index must be between 0 and 100 (compression from max to min). /// index must be between 0 and 100 (compression from max to min).
/// </summary> /// </summary>
/// <value>The quality of the jpg image from 0 to 100.</value> /// <value>The quality of the jpg image from 0 to 100.</value>
int Quality { get; } int? Quality { get; }
/// <summary> /// <summary>
/// Gets the subsample ration, that will be used to encode the image. /// Gets the subsample ration, that will be used to encode the image.

2
src/ImageSharp/Formats/Jpeg/ImageExtensions.cs

@ -34,6 +34,6 @@ namespace SixLabors.ImageSharp
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsJpeg<TPixel>(this Image<TPixel> source, Stream stream, JpegEncoder encoder) public static void SaveAsJpeg<TPixel>(this Image<TPixel> source, Stream stream, JpegEncoder encoder)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Jpeg)); => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(JpegFormat.Instance));
} }
} }

9
src/ImageSharp/Formats/Jpeg/JpegConfigurationModule.cs

@ -9,12 +9,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
public sealed class JpegConfigurationModule : IConfigurationModule public sealed class JpegConfigurationModule : IConfigurationModule
{ {
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(Configuration config) public void Configure(Configuration configuration)
{ {
config.ImageFormatsManager.SetEncoder(ImageFormats.Jpeg, new JpegEncoder()); configuration.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder());
config.ImageFormatsManager.SetDecoder(ImageFormats.Jpeg, new JpegDecoder()); configuration.ImageFormatsManager.SetDecoder(JpegFormat.Instance, new JpegDecoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector());
config.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector());
} }
} }
} }

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

@ -234,6 +234,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitDerivedMetaDataProperties(); this.InitDerivedMetaDataProperties();
return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData); return new ImageInfo(new PixelTypeInfo(this.BitsPerPixel), this.ImageWidth, this.ImageHeight, this.MetaData);
} }
@ -258,11 +259,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InputStream.Read(this.markerBuffer, 0, 2); this.InputStream.Read(this.markerBuffer, 0, 2);
byte marker = this.markerBuffer[1]; byte marker = this.markerBuffer[1];
fileMarker = new JpegFileMarker(marker, (int)this.InputStream.Position - 2); fileMarker = new JpegFileMarker(marker, (int)this.InputStream.Position - 2);
this.QuantizationTables = new Block8x8F[4];
// Only assign what we need // Only assign what we need
if (!metadataOnly) if (!metadataOnly)
{ {
this.QuantizationTables = new Block8x8F[4];
this.dcHuffmanTables = new HuffmanTables(); this.dcHuffmanTables = new HuffmanTables();
this.acHuffmanTables = new HuffmanTables(); this.acHuffmanTables = new HuffmanTables();
this.fastACTables = new FastACTables(this.configuration.MemoryAllocator); this.fastACTables = new FastACTables(this.configuration.MemoryAllocator);
@ -313,15 +314,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
break; break;
case JpegConstants.Markers.DQT: case JpegConstants.Markers.DQT:
if (metadataOnly) this.ProcessDefineQuantizationTablesMarker(remaining);
{
this.InputStream.Skip(remaining);
}
else
{
this.ProcessDefineQuantizationTablesMarker(remaining);
}
break; break;
case JpegConstants.Markers.DRI: case JpegConstants.Markers.DRI:
@ -707,6 +700,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
throw new ImageFormatException("DQT has wrong length"); throw new ImageFormatException("DQT has wrong length");
} }
this.MetaData.GetFormatMetaData(JpegFormat.Instance).Quality = QualityEvaluator.EstimateQuality(this.QuantizationTables);
} }
/// <summary> /// <summary>

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

@ -11,17 +11,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions public sealed class JpegEncoder : IImageEncoder, IJpegEncoderOptions
{ {
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
public bool IgnoreMetadata { get; set; }
/// <summary> /// <summary>
/// Gets or sets the quality, that will be used to encode the image. Quality /// Gets or sets the quality, that will be used to encode the image. Quality
/// index must be between 0 and 100 (compression from max to min). /// index must be between 0 and 100 (compression from max to min).
/// Defaults to <value>75</value>. /// Defaults to <value>75</value>.
/// </summary> /// </summary>
public int Quality { get; set; } = 75; public int? Quality { get; set; }
/// <summary> /// <summary>
/// Gets or sets the subsample ration, that will be used to encode the image. /// Gets or sets the subsample ration, that will be used to encode the image.

52
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -123,19 +123,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private readonly byte[] huffmanBuffer = new byte[179]; private readonly byte[] huffmanBuffer = new byte[179];
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. /// Gets or sets the subsampling method to use.
/// </summary> /// </summary>
private readonly bool ignoreMetadata; private JpegSubsample? subsample;
/// <summary> /// <summary>
/// The quality, that will be used to encode the image. /// The quality, that will be used to encode the image.
/// </summary> /// </summary>
private readonly int quality; private readonly int? quality;
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
private readonly JpegSubsample? subsample;
/// <summary> /// <summary>
/// The accumulated bits to write to the stream. /// The accumulated bits to write to the stream.
@ -168,11 +163,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="options">The options</param> /// <param name="options">The options</param>
public JpegEncoderCore(IJpegEncoderOptions options) public JpegEncoderCore(IJpegEncoderOptions options)
{ {
// System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1. this.quality = options.Quality;
this.quality = options.Quality.Clamp(1, 100); this.subsample = options.Subsample;
this.subsample = options.Subsample ?? (this.quality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420);
this.ignoreMetadata = options.IgnoreMetadata;
} }
/// <summary> /// <summary>
@ -194,16 +186,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
} }
this.outputStream = stream; this.outputStream = stream;
ImageMetaData metaData = image.MetaData;
// System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1.
int qlty = (this.quality ?? metaData.GetFormatMetaData(JpegFormat.Instance).Quality).Clamp(1, 100);
this.subsample = this.subsample ?? (qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420);
// Convert from a quality rating to a scaling factor. // Convert from a quality rating to a scaling factor.
int scale; int scale;
if (this.quality < 50) if (qlty < 50)
{ {
scale = 5000 / this.quality; scale = 5000 / qlty;
} }
else else
{ {
scale = 200 - (this.quality * 2); scale = 200 - (qlty * 2);
} }
// Initialize the quantization tables. // Initialize the quantization tables.
@ -214,10 +211,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int componentCount = 3; int componentCount = 3;
// Write the Start Of Image marker. // Write the Start Of Image marker.
this.WriteApplicationHeader(image.MetaData); this.WriteApplicationHeader(metaData);
// Write Exif and ICC profiles // Write Exif and ICC profiles
this.WriteProfiles(image); this.WriteProfiles(metaData);
// Write the quantization tables. // Write the quantization tables.
this.WriteDefineQuantizationTables(); this.WriteDefineQuantizationTables();
@ -624,6 +621,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </exception> /// </exception>
private void WriteExifProfile(ExifProfile exifProfile) private void WriteExifProfile(ExifProfile exifProfile)
{ {
if (exifProfile is null)
{
return;
}
const int MaxBytesApp1 = 65533; const int MaxBytesApp1 = 65533;
const int MaxBytesWithExifId = 65527; const int MaxBytesWithExifId = 65527;
@ -762,19 +764,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Writes the metadata profiles to the image. /// Writes the metadata profiles to the image.
/// </summary> /// </summary>
/// <param name="image">The image.</param> /// <param name="metaData">The image meta data.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam> private void WriteProfiles(ImageMetaData metaData)
private void WriteProfiles<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{ {
if (this.ignoreMetadata) if (metaData is null)
{ {
return; return;
} }
image.MetaData.SyncProfiles(); metaData.SyncProfiles();
this.WriteExifProfile(image.MetaData.ExifProfile); this.WriteExifProfile(metaData.ExifProfile);
this.WriteIccProfile(image.MetaData.IccProfile); this.WriteIccProfile(metaData.IccProfile);
} }
/// <summary> /// <summary>

14
src/ImageSharp/Formats/Jpeg/JpegFormat.cs

@ -8,8 +8,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the jpeg format. /// Registers the image encoders, decoders and mime type detectors for the jpeg format.
/// </summary> /// </summary>
internal sealed class JpegFormat : IImageFormat public sealed class JpegFormat : IImageFormat<JpegMetaData>
{ {
private JpegFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static JpegFormat Instance { get; } = new JpegFormat();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "JPEG"; public string Name => "JPEG";
@ -21,5 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<string> FileExtensions => JpegConstants.FileExtensions; public IEnumerable<string> FileExtensions => JpegConstants.FileExtensions;
/// <inheritdoc/>
public JpegMetaData CreateDefaultFormatMetaData() => new JpegMetaData();
} }
} }

2
src/ImageSharp/Formats/Jpeg/JpegImageFormatDetector.cs

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <inheritdoc/> /// <inheritdoc/>
public IImageFormat DetectFormat(ReadOnlySpan<byte> header) public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
{ {
return this.IsSupportedFileFormat(header) ? ImageFormats.Jpeg : null; return this.IsSupportedFileFormat(header) ? JpegFormat.Instance : null;
} }
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)

16
src/ImageSharp/Formats/Jpeg/JpegMetaData.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Jpeg
{
/// <summary>
/// Provides Jpeg specific metadata information for the image.
/// </summary>
public class JpegMetaData
{
/// <summary>
/// Gets or sets the encoded quality.
/// </summary>
public int Quality { get; set; } = 75;
}
}

18
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -14,12 +14,12 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets the number of bits per sample or per palette index (not per pixel). /// Gets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType"/> values. /// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary> /// </summary>
PngBitDepth BitDepth { get; } PngBitDepth? BitDepth { get; }
/// <summary> /// <summary>
/// Gets the color type /// Gets the color type
/// </summary> /// </summary>
PngColorType ColorType { get; } PngColorType? ColorType { get; }
/// <summary> /// <summary>
/// Gets the filter method. /// Gets the filter method.
@ -33,15 +33,13 @@ namespace SixLabors.ImageSharp.Formats.Png
int CompressionLevel { get; } int CompressionLevel { get; }
/// <summary> /// <summary>
/// Gets the gamma value, that will be written /// Gets the gamma value, that will be written the the image.
/// the the stream, when the <see cref="WriteGamma"/> property
/// is set to true. The default value is 2.2F.
/// </summary> /// </summary>
/// <value>The gamma value of the image.</value> /// <value>The gamma value of the image.</value>
float Gamma { get; } float? Gamma { get; }
/// <summary> /// <summary>
/// Gets quantizer for reducing the color count. /// Gets the quantizer for reducing the color count.
/// </summary> /// </summary>
IQuantizer Quantizer { get; } IQuantizer Quantizer { get; }
@ -49,11 +47,5 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets the transparency threshold. /// Gets the transparency threshold.
/// </summary> /// </summary>
byte Threshold { get; } byte Threshold { get; }
/// <summary>
/// Gets a value indicating whether this instance should write
/// gamma information to the stream. The default value is false.
/// </summary>
bool WriteGamma { get; }
} }
} }

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

@ -35,6 +35,6 @@ namespace SixLabors.ImageSharp
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsPng<TPixel>(this Image<TPixel> source, Stream stream, PngEncoder encoder) public static void SaveAsPng<TPixel>(this Image<TPixel> source, Stream stream, PngEncoder encoder)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ImageFormats.Png)); => source.Save(stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(PngFormat.Instance));
} }
} }

15
src/ImageSharp/Formats/Png/PngBitDepth.cs

@ -9,6 +9,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
public enum PngBitDepth public enum PngBitDepth
{ {
/// <summary>
/// 1 bit per sample or per palette index (not per pixel).
/// </summary>
Bit1 = 1,
/// <summary>
/// 2 bits per sample or per palette index (not per pixel).
/// </summary>
Bit2 = 2,
/// <summary>
/// 4 bits per sample or per palette index (not per pixel).
/// </summary>
Bit4 = 4,
/// <summary> /// <summary>
/// 8 bits per sample or per palette index (not per pixel). /// 8 bits per sample or per palette index (not per pixel).
/// </summary> /// </summary>

52
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -8,58 +8,58 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
internal enum PngChunkType : uint internal enum PngChunkType : uint
{ {
/// <summary>
/// The first chunk in a png file. Can only exists once. Contains
/// common information like the width and the height of the image or
/// the used compression method.
/// </summary>
Header = 0x49484452U, // IHDR
/// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
/// </summary>
Palette = 0x504C5445U, // PLTE
/// <summary> /// <summary>
/// The IDAT chunk contains the actual image data. The image can contains more /// The IDAT chunk contains the actual image data. The image can contains more
/// than one chunk of this type. All chunks together are the whole image. /// than one chunk of this type. All chunks together are the whole image.
/// </summary> /// </summary>
Data = 0x49444154U, // IDAT Data = 0x49444154U,
/// <summary> /// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream. /// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty. /// The chunk's data field is empty.
/// </summary> /// </summary>
End = 0x49454E44U, // IEND End = 0x49454E44U,
/// <summary> /// <summary>
/// This chunk specifies that the image uses simple transparency: /// The first chunk in a png file. Can only exists once. Contains
/// either alpha values associated with palette entries (for indexed-color images) /// common information like the width and the height of the image or
/// or a single transparent color (for grayscale and true color images). /// the used compression method.
/// </summary> /// </summary>
PaletteAlpha = 0x74524E53U, // tRNS Header = 0x49484452U,
/// <summary> /// <summary>
/// Textual information that the encoder wishes to record with the image can be stored in /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// tEXt chunks. Each tEXt chunk contains a keyword and a text string. /// series in the RGB format.
/// </summary>
Palette = 0x504C5445U,
/// <summary>
/// The eXIf data chunk which contains the Exif profile.
/// </summary> /// </summary>
Text = 0x74455874U, // tEXt Exif = 0x65584966U,
/// <summary> /// <summary>
/// This chunk specifies the relationship between the image samples and the desired /// This chunk specifies the relationship between the image samples and the desired
/// display output intensity. /// display output intensity.
/// </summary> /// </summary>
Gamma = 0x67414D41U, // gAMA Gamma = 0x67414D41U,
/// <summary> /// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary> /// </summary>
Physical = 0x70485973U, // pHYs Physical = 0x70485973U,
/// <summary> /// <summary>
/// The data chunk which contains the Exif profile. /// Textual information that the encoder wishes to record with the image can be stored in
/// tEXt chunks. Each tEXt chunk contains a keyword and a text string.
/// </summary>
Text = 0x74455874U,
/// <summary>
/// This chunk specifies that the image uses simple transparency:
/// either alpha values associated with palette entries (for indexed-color images)
/// or a single transparent color (for grayscale and true color images).
/// </summary> /// </summary>
Exif = 0x65584966U // eXIf PaletteAlpha = 0x74524E53U
} }
} }

4
src/ImageSharp/Formats/Png/PngConfigurationModule.cs

@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/> /// <inheritdoc/>
public void Configure(Configuration configuration) public void Configure(Configuration configuration)
{ {
configuration.ImageFormatsManager.SetEncoder(ImageFormats.Png, new PngEncoder()); configuration.ImageFormatsManager.SetEncoder(PngFormat.Instance, new PngEncoder());
configuration.ImageFormatsManager.SetDecoder(ImageFormats.Png, new PngDecoder()); configuration.ImageFormatsManager.SetDecoder(PngFormat.Instance, new PngDecoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new PngImageFormatDetector()); configuration.ImageFormatsManager.AddImageFormatDetector(new PngImageFormatDetector());
} }
} }

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

@ -10,7 +10,6 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib; using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -217,7 +216,8 @@ namespace SixLabors.ImageSharp.Formats.Png
public Image<TPixel> Decode<TPixel>(Stream stream) public Image<TPixel> Decode<TPixel>(Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
var metadata = new ImageMetaData(); var metaData = new ImageMetaData();
PngMetaData pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance);
this.currentStream = stream; this.currentStream = stream;
this.currentStream.Skip(8); this.currentStream.Skip(8);
Image<TPixel> image = null; Image<TPixel> image = null;
@ -232,16 +232,19 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (chunk.Type) switch (chunk.Type)
{ {
case PngChunkType.Header: case PngChunkType.Header:
this.ReadHeaderChunk(chunk.Data.Array); this.ReadHeaderChunk(pngMetaData, chunk.Data.Array);
this.ValidateHeader(); this.ValidateHeader();
break; break;
case PngChunkType.Physical: case PngChunkType.Physical:
this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan());
break;
case PngChunkType.Gamma:
this.ReadGammaChunk(pngMetaData, chunk.Data.GetSpan());
break; break;
case PngChunkType.Data: case PngChunkType.Data:
if (image is null) if (image is null)
{ {
this.InitializeImage(metadata, out image); this.InitializeImage(metaData, out image);
} }
deframeStream.AllocateNewBytes(chunk.Length); deframeStream.AllocateNewBytes(chunk.Length);
@ -260,14 +263,14 @@ namespace SixLabors.ImageSharp.Formats.Png
this.AssignTransparentMarkers(alpha); this.AssignTransparentMarkers(alpha);
break; break;
case PngChunkType.Text: case PngChunkType.Text:
this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length); this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length);
break; break;
case PngChunkType.Exif: case PngChunkType.Exif:
if (!this.ignoreMetadata) if (!this.ignoreMetadata)
{ {
byte[] exifData = new byte[chunk.Length]; byte[] exifData = new byte[chunk.Length];
Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length); Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length);
metadata.ExifProfile = new ExifProfile(exifData); metaData.ExifProfile = new ExifProfile(exifData);
} }
break; break;
@ -303,7 +306,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
public IImageInfo Identify(Stream stream) public IImageInfo Identify(Stream stream)
{ {
var metadata = new ImageMetaData(); var metaData = new ImageMetaData();
PngMetaData pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance);
this.currentStream = stream; this.currentStream = stream;
this.currentStream.Skip(8); this.currentStream.Skip(8);
try try
@ -315,17 +319,20 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (chunk.Type) switch (chunk.Type)
{ {
case PngChunkType.Header: case PngChunkType.Header:
this.ReadHeaderChunk(chunk.Data.Array); this.ReadHeaderChunk(pngMetaData, chunk.Data.Array);
this.ValidateHeader(); this.ValidateHeader();
break; break;
case PngChunkType.Physical: case PngChunkType.Physical:
this.ReadPhysicalChunk(metadata, chunk.Data.GetSpan()); this.ReadPhysicalChunk(metaData, chunk.Data.GetSpan());
break;
case PngChunkType.Gamma:
this.ReadGammaChunk(pngMetaData, chunk.Data.GetSpan());
break; break;
case PngChunkType.Data: case PngChunkType.Data:
this.SkipChunkDataAndCrc(chunk); this.SkipChunkDataAndCrc(chunk);
break; break;
case PngChunkType.Text: case PngChunkType.Text:
this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length); this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length);
break; break;
case PngChunkType.End: case PngChunkType.End:
this.isEndChunkReached = true; this.isEndChunkReached = true;
@ -349,7 +356,7 @@ namespace SixLabors.ImageSharp.Formats.Png
throw new ImageFormatException("PNG Image does not contain a header chunk"); 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 ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metaData);
} }
/// <summary> /// <summary>
@ -360,9 +367,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <returns>The <see cref="int"/></returns> /// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ReadByteLittleEndian(ReadOnlySpan<byte> buffer, int offset) private static byte ReadByteLittleEndian(ReadOnlySpan<byte> buffer, int offset)
{ => (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF));
return (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF));
}
/// <summary> /// <summary>
/// Attempts to convert a byte array to a new array where each value in the original array is represented by the /// Attempts to convert a byte array to a new array where each value in the original array is represented by the
@ -430,6 +435,18 @@ namespace SixLabors.ImageSharp.Formats.Png
metadata.VerticalResolution = vResolution; metadata.VerticalResolution = vResolution;
} }
/// <summary>
/// Reads the data chunk containing gamma data.
/// </summary>
/// <param name="pngMetadata">The metadata to read to.</param>
/// <param name="data">The data containing physical data.</param>
private void ReadGammaChunk(PngMetaData pngMetadata, ReadOnlySpan<byte> data)
{
// The value is encoded as a 4-byte unsigned integer, representing gamma times 100000.
// For example, a gamma of 1/2.2 would be stored as 45455.
pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) / 100_000F;
}
/// <summary> /// <summary>
/// Initializes the image and various buffers needed for processing /// Initializes the image and various buffers needed for processing
/// </summary> /// </summary>
@ -711,7 +728,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
case PngColorType.Grayscale: case PngColorType.Grayscale:
int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1); int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1);
if (!this.hasTrans) if (!this.hasTrans)
{ {
@ -933,7 +950,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
case PngColorType.Grayscale: case PngColorType.Grayscale:
int factor = 255 / ((int)Math.Pow(2, this.header.BitDepth) - 1); int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1);
if (!this.hasTrans) if (!this.hasTrans)
{ {
@ -1270,17 +1287,22 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary> /// <summary>
/// Reads a header chunk from the data. /// Reads a header chunk from the data.
/// </summary> /// </summary>
/// <param name="pngMetaData">The png metadata.</param>
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param> /// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private void ReadHeaderChunk(ReadOnlySpan<byte> data) private void ReadHeaderChunk(PngMetaData pngMetaData, ReadOnlySpan<byte> data)
{ {
byte bitDepth = data[8];
this.header = new PngHeader( this.header = new PngHeader(
width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)),
height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)),
bitDepth: data[8], bitDepth: bitDepth,
colorType: (PngColorType)data[9], colorType: (PngColorType)data[9],
compressionMethod: data[10], compressionMethod: data[10],
filterMethod: data[11], filterMethod: data[11],
interlaceMethod: (PngInterlaceMode)data[12]); interlaceMethod: (PngInterlaceMode)data[12]);
pngMetaData.BitDepth = (PngBitDepth)bitDepth;
pngMetaData.ColorType = this.header.ColorType;
} }
/// <summary> /// <summary>

19
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -17,12 +17,12 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets or sets the number of bits per sample or per palette index (not per pixel). /// Gets or sets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType"/> values. /// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary> /// </summary>
public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8; public PngBitDepth? BitDepth { get; set; }
/// <summary> /// <summary>
/// Gets or sets the color type. /// Gets or sets the color type.
/// </summary> /// </summary>
public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha; public PngColorType? ColorType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the filter method. /// Gets or sets the filter method.
@ -36,30 +36,21 @@ namespace SixLabors.ImageSharp.Formats.Png
public int CompressionLevel { get; set; } = 6; public int CompressionLevel { get; set; } = 6;
/// <summary> /// <summary>
/// Gets or sets the gamma value, that will be written /// Gets or sets the gamma value, that will be written the the image.
/// the the stream, when the <see cref="WriteGamma"/> property
/// is set to true. The default value is 2.2F.
/// </summary> /// </summary>
/// <value>The gamma value of the image.</value> public float? Gamma { get; set; }
public float Gamma { get; set; } = 2.2F;
/// <summary> /// <summary>
/// Gets or sets quantizer for reducing the color count. /// Gets or sets quantizer for reducing the color count.
/// Defaults to the <see cref="WuQuantizer"/> /// Defaults to the <see cref="WuQuantizer"/>
/// </summary> /// </summary>
public IQuantizer Quantizer { get; set; } = new WuQuantizer(); public IQuantizer Quantizer { get; set; }
/// <summary> /// <summary>
/// Gets or sets the transparency threshold. /// Gets or sets the transparency threshold.
/// </summary> /// </summary>
public byte Threshold { get; set; } = 255; public byte Threshold { get; set; } = 255;
/// <summary>
/// Gets or sets a value indicating whether this instance should write
/// gamma information to the stream. The default value is false.
/// </summary>
public bool WriteGamma { get; set; }
/// <summary> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary> /// </summary>

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

@ -4,7 +4,6 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO; using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Filters;
@ -45,49 +44,49 @@ namespace SixLabors.ImageSharp.Formats.Png
private readonly Crc32 crc = new Crc32(); private readonly Crc32 crc = new Crc32();
/// <summary> /// <summary>
/// The png bit depth /// The png filter method.
/// </summary> /// </summary>
private readonly PngBitDepth pngBitDepth; private readonly PngFilterMethod pngFilterMethod;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to use 16 bit encoding for supported color types. /// Gets or sets the CompressionLevel value
/// </summary> /// </summary>
private readonly bool use16Bit; private readonly int compressionLevel;
/// <summary> /// <summary>
/// The png color type. /// Gets or sets the alpha threshold value
/// </summary> /// </summary>
private readonly PngColorType pngColorType; private readonly byte threshold;
/// <summary> /// <summary>
/// The png filter method. /// The quantizer for reducing the color count.
/// </summary> /// </summary>
private readonly PngFilterMethod pngFilterMethod; private IQuantizer quantizer;
/// <summary> /// <summary>
/// The quantizer for reducing the color count. /// Gets or sets a value indicating whether to write the gamma chunk
/// </summary> /// </summary>
private readonly IQuantizer quantizer; private bool writeGamma;
/// <summary> /// <summary>
/// Gets or sets the CompressionLevel value /// The png bit depth
/// </summary> /// </summary>
private readonly int compressionLevel; private PngBitDepth? pngBitDepth;
/// <summary> /// <summary>
/// Gets or sets the Gamma value /// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
/// </summary> /// </summary>
private readonly float gamma; private bool use16Bit;
/// <summary> /// <summary>
/// Gets or sets the Threshold value /// The png color type.
/// </summary> /// </summary>
private readonly byte threshold; private PngColorType? pngColorType;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to Write Gamma /// Gets or sets the Gamma value
/// </summary> /// </summary>
private readonly bool writeGamma; private float? gamma;
/// <summary> /// <summary>
/// The image width. /// The image width.
@ -158,14 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.pngBitDepth = options.BitDepth; this.pngBitDepth = options.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
this.pngColorType = options.ColorType; this.pngColorType = options.ColorType;
this.pngFilterMethod = options.FilterMethod; this.pngFilterMethod = options.FilterMethod;
this.compressionLevel = options.CompressionLevel; this.compressionLevel = options.CompressionLevel;
this.gamma = options.Gamma; this.gamma = options.Gamma;
this.quantizer = options.Quantizer; this.quantizer = options.Quantizer;
this.threshold = options.Threshold; this.threshold = options.Threshold;
this.writeGamma = options.WriteGamma;
} }
/// <summary> /// <summary>
@ -183,23 +180,42 @@ namespace SixLabors.ImageSharp.Formats.Png
this.width = image.Width; this.width = image.Width;
this.height = image.Height; this.height = image.Height;
// Always take the encoder options over the metadata values.
ImageMetaData metaData = image.MetaData;
PngMetaData pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance);
this.gamma = this.gamma ?? pngMetaData.Gamma;
this.writeGamma = this.gamma > 0;
this.pngColorType = this.pngColorType ?? pngMetaData.ColorType;
this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length); stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
QuantizedFrame<TPixel> quantized = null; QuantizedFrame<TPixel> quantized = null;
ReadOnlySpan<byte> quantizedPixelsSpan = default; ReadOnlySpan<byte> quantizedPixelsSpan = default;
if (this.pngColorType == PngColorType.Palette) if (this.pngColorType == PngColorType.Palette)
{ {
byte bits;
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (this.quantizer == null)
{
bits = (byte)Math.Min(8u, (short)this.pngBitDepth);
int colorSize = ImageMaths.GetColorCountForBitDepth(bits);
this.quantizer = new WuQuantizer(colorSize);
}
// Create quantized frame returning the palette and set the bit depth. // Create quantized frame returning the palette and set the bit depth.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame); quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
quantizedPixelsSpan = quantized.GetPixelSpan(); quantizedPixelsSpan = quantized.GetPixelSpan();
byte bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8); bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
if (bits == 3) if (bits == 3)
{ {
bits = 4; bits = 4;
} }
else if (bits >= 5 || bits <= 7) else if (bits >= 5 && bits <= 7)
{ {
bits = 8; bits = 8;
} }
@ -217,7 +233,7 @@ namespace SixLabors.ImageSharp.Formats.Png
width: image.Width, width: image.Width,
height: image.Height, height: image.Height,
bitDepth: this.bitDepth, bitDepth: this.bitDepth,
colorType: this.pngColorType, colorType: this.pngColorType.Value,
compressionMethod: 0, // None compressionMethod: 0, // None
filterMethod: 0, filterMethod: 0,
interlaceMethod: 0); // TODO: Can't write interlaced yet. interlaceMethod: 0); // TODO: Can't write interlaced yet.
@ -227,12 +243,12 @@ namespace SixLabors.ImageSharp.Formats.Png
// Collect the indexed pixel data // Collect the indexed pixel data
if (quantized != null) if (quantized != null)
{ {
this.WritePaletteChunk(stream, header, quantized); this.WritePaletteChunk(stream, quantized);
} }
this.WritePhysicalChunk(stream, image); this.WritePhysicalChunk(stream, metaData);
this.WriteGammaChunk(stream); this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, image); this.WriteExifChunk(stream, metaData);
this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream); this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream);
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
stream.Flush(); stream.Flush();
@ -539,30 +555,27 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
/// <param name="quantized">The quantized frame.</param> /// <param name="quantized">The quantized frame.</param>
private void WritePaletteChunk<TPixel>(Stream stream, in PngHeader header, QuantizedFrame<TPixel> quantized) private void WritePaletteChunk<TPixel>(Stream stream, QuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
// Grab the palette and write it to the stream. // Grab the palette and write it to the stream.
TPixel[] palette = quantized.Palette; TPixel[] palette = quantized.Palette;
byte pixelCount = palette.Length.ToByte(); int paletteLength = Math.Min(palette.Length, 256);
int colorTableLength = paletteLength * 3;
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3;
Rgba32 rgba = default; Rgba32 rgba = default;
bool anyAlpha = false; bool anyAlpha = false;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength)) using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(pixelCount)) using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength))
{ {
Span<byte> colorTableSpan = colorTable.GetSpan(); Span<byte> colorTableSpan = colorTable.GetSpan();
Span<byte> alphaTableSpan = alphaTable.GetSpan(); Span<byte> alphaTableSpan = alphaTable.GetSpan();
Span<byte> quantizedSpan = quantized.GetPixelSpan(); Span<byte> quantizedSpan = quantized.GetPixelSpan();
for (byte i = 0; i < pixelCount; i++) for (int i = 0; i < paletteLength; i++)
{ {
if (quantizedSpan.IndexOf(i) > -1) if (quantizedSpan.IndexOf((byte)i) > -1)
{ {
int offset = i * 3; int offset = i * 3;
palette[i].ToRgba32(ref rgba); palette[i].ToRgba32(ref rgba);
@ -588,7 +601,7 @@ namespace SixLabors.ImageSharp.Formats.Png
// Write the transparency data // Write the transparency data
if (anyAlpha) if (anyAlpha)
{ {
this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaTable.Array, 0, pixelCount); this.WriteChunk(stream, PngChunkType.PaletteAlpha, alphaTable.Array, 0, paletteLength);
} }
} }
} }
@ -596,11 +609,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary> /// <summary>
/// Writes the physical dimension information to the stream. /// Writes the physical dimension information to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="image">The image.</param> /// <param name="meta">The image meta data.</param>
private void WritePhysicalChunk<TPixel>(Stream stream, Image<TPixel> image) private void WritePhysicalChunk(Stream stream, ImageMetaData meta)
where TPixel : struct, IPixel<TPixel>
{ {
// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains: // The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. It contains:
// Pixels per unit, X axis: 4 bytes (unsigned integer) // Pixels per unit, X axis: 4 bytes (unsigned integer)
@ -612,7 +623,6 @@ namespace SixLabors.ImageSharp.Formats.Png
// 1: unit is the meter // 1: unit is the meter
// //
// When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified. // When the unit specifier is 0, the pHYs chunk defines pixel aspect ratio only; the actual size of the pixels remains unspecified.
ImageMetaData meta = image.MetaData;
Span<byte> hResolution = this.chunkDataBuffer.AsSpan(0, 4); Span<byte> hResolution = this.chunkDataBuffer.AsSpan(0, 4);
Span<byte> vResolution = this.chunkDataBuffer.AsSpan(4, 4); Span<byte> vResolution = this.chunkDataBuffer.AsSpan(4, 4);
@ -653,16 +663,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary> /// <summary>
/// Writes the eXIf chunk to the stream, if any EXIF Profile values are present in the meta data. /// Writes the eXIf chunk to the stream, if any EXIF Profile values are present in the meta data.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="image">The image.</param> /// <param name="meta">The image meta data.</param>
private void WriteExifChunk<TPixel>(Stream stream, Image<TPixel> image) private void WriteExifChunk(Stream stream, ImageMetaData meta)
where TPixel : struct, IPixel<TPixel>
{ {
if (image.MetaData.ExifProfile?.Values.Count > 0) if (meta.ExifProfile?.Values.Count > 0)
{ {
image.MetaData.SyncProfiles(); meta.SyncProfiles();
this.WriteChunk(stream, PngChunkType.Exif, image.MetaData.ExifProfile.ToByteArray()); this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
} }
} }
@ -693,7 +701,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream) private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
this.bytesPerScanline = this.width * this.bytesPerPixel; this.bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = this.bytesPerScanline + 1; int resultLength = this.bytesPerScanline + 1;
this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean); this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
@ -781,10 +789,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Writes the chunk end to the stream. /// Writes the chunk end to the stream.
/// </summary> /// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param> /// <param name="stream">The <see cref="Stream"/> containing image data.</param>
private void WriteEndChunk(Stream stream) private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null);
{
this.WriteChunk(stream, PngChunkType.End, null);
}
/// <summary> /// <summary>
/// Writes a chunk to the stream. /// Writes a chunk to the stream.
@ -792,10 +797,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param> /// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param> /// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
private void WriteChunk(Stream stream, PngChunkType type, byte[] data) private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
{
this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
}
/// <summary> /// <summary>
/// Writes a chunk of a specified length to the stream at the given offset. /// Writes a chunk of a specified length to the stream at the given offset.
@ -827,5 +829,26 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(this.buffer, 0, 4); // write the crc stream.Write(this.buffer, 0, 4); // write the crc
} }
/// <summary>
/// Calculates the scanline length.
/// </summary>
/// <param name="width">The width of the row.</param>
/// <returns>
/// The <see cref="int"/> representing the length.
/// </returns>
private int CalculateScanlineLength(int width)
{
int mod = this.bitDepth == 16 ? 16 : 8;
int scanlineLength = width * this.bitDepth * this.bytesPerPixel;
int amount = scanlineLength % mod;
if (amount != 0)
{
scanlineLength += mod - amount;
}
return scanlineLength / mod;
}
} }
} }

14
src/ImageSharp/Formats/Png/PngFormat.cs

@ -8,8 +8,17 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary> /// <summary>
/// Registers the image encoders, decoders and mime type detectors for the png format. /// Registers the image encoders, decoders and mime type detectors for the png format.
/// </summary> /// </summary>
internal sealed class PngFormat : IImageFormat public sealed class PngFormat : IImageFormat<PngMetaData>
{ {
private PngFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static PngFormat Instance { get; } = new PngFormat();
/// <inheritdoc/> /// <inheritdoc/>
public string Name => "PNG"; public string Name => "PNG";
@ -21,5 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<string> FileExtensions => PngConstants.FileExtensions; public IEnumerable<string> FileExtensions => PngConstants.FileExtensions;
/// <inheritdoc/>
public PngMetaData CreateDefaultFormatMetaData() => new PngMetaData();
} }
} }

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

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/> /// <inheritdoc/>
public IImageFormat DetectFormat(ReadOnlySpan<byte> header) public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
{ {
return this.IsSupportedFileFormat(header) ? ImageFormats.Png : null; return this.IsSupportedFileFormat(header) ? PngFormat.Instance : null;
} }
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)

27
src/ImageSharp/Formats/Png/PngMetaData.cs

@ -0,0 +1,27 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Provides Png specific metadata information for the image.
/// </summary>
public class PngMetaData
{
/// <summary>
/// Gets or sets the number of bits per sample or per palette index (not per pixel).
/// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary>
public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8;
/// <summary>
/// Gets or sets the color type.
/// </summary>
public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
/// <summary>
/// Gets or sets the gamma value for the image.
/// </summary>
public float Gamma { get; set; }
}
}

37
src/ImageSharp/ImageFormats.cs

@ -1,37 +0,0 @@
// Copyright (c) Six Labors and contributors.
// 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;
namespace SixLabors.ImageSharp
{
/// <summary>
/// The static collection of all the default image formats
/// </summary>
public static class ImageFormats
{
/// <summary>
/// The format details for the jpegs.
/// </summary>
public static readonly IImageFormat Jpeg = new JpegFormat();
/// <summary>
/// The format details for the pngs.
/// </summary>
public static readonly IImageFormat Png = new PngFormat();
/// <summary>
/// The format details for the gifs.
/// </summary>
public static readonly IImageFormat Gif = new GifFormat();
/// <summary>
/// The format details for the bitmaps.
/// </summary>
public static readonly IImageFormat Bmp = new BmpFormat();
}
}

23
src/ImageSharp/ImageFrame{TPixel}.cs

@ -84,12 +84,11 @@ namespace SixLabors.ImageSharp
Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(configuration, nameof(configuration));
Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height)); Guard.MustBeGreaterThan(height, 0, nameof(height));
Guard.NotNull(metaData, nameof(metaData));
this.configuration = configuration; this.configuration = configuration;
this.MemoryAllocator = configuration.MemoryAllocator; this.MemoryAllocator = configuration.MemoryAllocator;
this.PixelBuffer = this.MemoryAllocator.Allocate2D<TPixel>(width, height); this.PixelBuffer = this.MemoryAllocator.Allocate2D<TPixel>(width, height);
this.MetaData = metaData; this.MetaData = metaData ?? new ImageFrameMetaData();
this.Clear(configuration.GetParallelOptions(), backgroundColor); this.Clear(configuration.GetParallelOptions(), backgroundColor);
} }
@ -201,10 +200,7 @@ namespace SixLabors.ImageSharp
/// <param name="y">The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image.</param> /// <param name="y">The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image.</param>
/// <returns>The <see typeparam="TPixel"/> at the specified position.</returns> /// <returns>The <see typeparam="TPixel"/> at the specified position.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref TPixel GetPixelReference(int x, int y) internal ref TPixel GetPixelReference(int x, int y) => ref this.PixelBuffer[x, y];
{
return ref this.PixelBuffer[x, y];
}
/// <summary> /// <summary>
/// Copies the pixels to a <see cref="Buffer2D{TPixel}"/> of the same size. /// Copies the pixels to a <see cref="Buffer2D{TPixel}"/> of the same size.
@ -249,10 +245,7 @@ namespace SixLabors.ImageSharp
} }
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() public override string ToString() => $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}";
{
return $"ImageFrame<{typeof(TPixel).Name}>: {this.Width}x{this.Height}";
}
/// <summary> /// <summary>
/// Returns a copy of the image frame in the given pixel format. /// Returns a copy of the image frame in the given pixel format.
@ -309,15 +302,9 @@ namespace SixLabors.ImageSharp
/// Clones the current instance. /// Clones the current instance.
/// </summary> /// </summary>
/// <returns>The <see cref="ImageFrame{TPixel}"/></returns> /// <returns>The <see cref="ImageFrame{TPixel}"/></returns>
internal ImageFrame<TPixel> Clone() internal ImageFrame<TPixel> Clone() => new ImageFrame<TPixel>(this.configuration, this);
{
return new ImageFrame<TPixel>(this.configuration, this);
}
/// <inheritdoc/> /// <inheritdoc/>
void IDisposable.Dispose() void IDisposable.Dispose() => this.Dispose();
{
this.Dispose();
}
} }
} }

2
src/ImageSharp/Formats/Gif/FrameDecodingMode.cs → src/ImageSharp/MetaData/FrameDecodingMode.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.MetaData
{ {
/// <summary> /// <summary>
/// Enumerated frame process modes to apply to multi-frame images. /// Enumerated frame process modes to apply to multi-frame images.

49
src/ImageSharp/MetaData/ImageFrameMetaData.cs

@ -1,7 +1,9 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Gif; using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
namespace SixLabors.ImageSharp.MetaData namespace SixLabors.ImageSharp.MetaData
{ {
@ -10,6 +12,8 @@ namespace SixLabors.ImageSharp.MetaData
/// </summary> /// </summary>
public sealed class ImageFrameMetaData public sealed class ImageFrameMetaData
{ {
private readonly Dictionary<IImageFormat, object> formatMetaData = new Dictionary<IImageFormat, object>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageFrameMetaData"/> class. /// Initializes a new instance of the <see cref="ImageFrameMetaData"/> class.
/// </summary> /// </summary>
@ -28,32 +32,39 @@ namespace SixLabors.ImageSharp.MetaData
{ {
DebugGuard.NotNull(other, nameof(other)); DebugGuard.NotNull(other, nameof(other));
this.FrameDelay = other.FrameDelay; foreach (KeyValuePair<IImageFormat, object> meta in other.formatMetaData)
this.DisposalMethod = other.DisposalMethod; {
this.formatMetaData.Add(meta.Key, meta.Value);
}
} }
/// <summary> /// <summary>
/// Gets or sets the frame delay for animated images. /// Clones this ImageFrameMetaData.
/// If not 0, when utilized in Gif animation, this field specifies the number of hundredths (1/100) of a second to
/// wait before continuing with the processing of the Data Stream.
/// The clock starts ticking immediately after the graphic is rendered.
/// </summary>
public int FrameDelay { get; set; }
/// <summary>
/// Gets or sets the disposal method for animated images.
/// Primarily used in Gif animation, this field indicates the way in which the graphic is to
/// be treated after being displayed.
/// </summary> /// </summary>
public DisposalMethod DisposalMethod { get; set; } /// <returns>The cloned instance.</returns>
public ImageFrameMetaData Clone() => new ImageFrameMetaData(this);
/// <summary> /// <summary>
/// Clones this ImageFrameMetaData. /// Gets the metadata value associated with the specified key.
/// </summary> /// </summary>
/// <returns>The cloned instance.</returns> /// <typeparam name="TFormatMetaData">The type of format metadata.</typeparam>
public ImageFrameMetaData Clone() /// <typeparam name="TFormatFrameMetaData">The type of format frame metadata.</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <returns>
/// The <typeparamref name="TFormatFrameMetaData"/>.
/// </returns>
public TFormatFrameMetaData GetFormatMetaData<TFormatMetaData, TFormatFrameMetaData>(IImageFormat<TFormatMetaData, TFormatFrameMetaData> key)
where TFormatMetaData : class
where TFormatFrameMetaData : class
{ {
return new ImageFrameMetaData(this); if (this.formatMetaData.TryGetValue(key, out object meta))
{
return (TFormatFrameMetaData)meta;
}
TFormatFrameMetaData newMeta = key.CreateDefaultFormatFrameMetaData();
this.formatMetaData[key] = newMeta;
return newMeta;
} }
} }
} }

50
src/ImageSharp/MetaData/ImageMetaData.cs

@ -1,7 +1,9 @@
// Copyright (c) Six Labors and contributors. // Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.MetaData.Profiles.Icc; using SixLabors.ImageSharp.MetaData.Profiles.Icc;
@ -24,6 +26,7 @@ namespace SixLabors.ImageSharp.MetaData
/// </summary> /// </summary>
public const double DefaultVerticalResolution = 96; public const double DefaultVerticalResolution = 96;
private readonly Dictionary<IImageFormat, object> formatMetaData = new Dictionary<IImageFormat, object>();
private double horizontalResolution; private double horizontalResolution;
private double verticalResolution; private double verticalResolution;
@ -48,7 +51,11 @@ namespace SixLabors.ImageSharp.MetaData
this.HorizontalResolution = other.HorizontalResolution; this.HorizontalResolution = other.HorizontalResolution;
this.VerticalResolution = other.VerticalResolution; this.VerticalResolution = other.VerticalResolution;
this.ResolutionUnits = other.ResolutionUnits; this.ResolutionUnits = other.ResolutionUnits;
this.RepeatCount = other.RepeatCount;
foreach (KeyValuePair<IImageFormat, object> meta in other.formatMetaData)
{
this.formatMetaData.Add(meta.Key, meta.Value);
}
foreach (ImageProperty property in other.Properties) foreach (ImageProperty property in other.Properties)
{ {
@ -125,10 +132,31 @@ namespace SixLabors.ImageSharp.MetaData
public IList<ImageProperty> Properties { get; } = new List<ImageProperty>(); public IList<ImageProperty> Properties { get; } = new List<ImageProperty>();
/// <summary> /// <summary>
/// Gets or sets the number of times any animation is repeated. /// Gets the metadata value associated with the specified key.
/// <remarks>0 means to repeat indefinitely.</remarks> /// </summary>
/// <typeparam name="TFormatMetaData">The type of metadata.</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <returns>
/// The <typeparamref name="TFormatMetaData"/>.
/// </returns>
public TFormatMetaData GetFormatMetaData<TFormatMetaData>(IImageFormat<TFormatMetaData> key)
where TFormatMetaData : class
{
if (this.formatMetaData.TryGetValue(key, out object meta))
{
return (TFormatMetaData)meta;
}
TFormatMetaData newMeta = key.CreateDefaultFormatMetaData();
this.formatMetaData[key] = newMeta;
return newMeta;
}
/// <summary>
/// Clones this into a new instance
/// </summary> /// </summary>
public ushort RepeatCount { get; set; } /// <returns>The cloned metadata instance</returns>
public ImageMetaData Clone() => new ImageMetaData(this);
/// <summary> /// <summary>
/// Looks up a property with the provided name. /// Looks up a property with the provided name.
@ -153,21 +181,9 @@ namespace SixLabors.ImageSharp.MetaData
return false; return false;
} }
/// <summary>
/// Clones this into a new instance
/// </summary>
/// <returns>The cloned metadata instance</returns>
public ImageMetaData Clone()
{
return new ImageMetaData(this);
}
/// <summary> /// <summary>
/// Synchronizes the profiles with the current meta data. /// Synchronizes the profiles with the current meta data.
/// </summary> /// </summary>
internal void SyncProfiles() internal void SyncProfiles() => this.ExifProfile?.Sync(this);
{
this.ExifProfile?.Sync(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> /// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>() IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>()
where TPixel : struct, IPixel<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> /// <summary>
/// Maximum allowed color depth /// Maximum allowed color depth
/// </summary> /// </summary>
private readonly byte colors; private readonly int colors;
/// <summary> /// <summary>
/// Stores the tree /// 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 /// the second pass quantizes a color based on the nodes in the tree
/// </remarks> /// </remarks>
public OctreeFrameQuantizer(OctreeQuantizer quantizer) 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) : base(quantizer, false)
{ {
this.colors = (byte)quantizer.MaxColors; this.colors = maxColors;
this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8)); this.octree = new Octree(ImageMaths.GetBitsNeededForColorDepth(this.colors).Clamp(1, 8));
} }
@ -261,13 +275,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public TPixel[] Palletize(int colorCount) public TPixel[] Palletize(int colorCount)
{ {
while (this.Leaves > colorCount) while (this.Leaves > colorCount - 1)
{ {
this.Reduce(); this.Reduce();
} }
// Now palletize the nodes // Now palletize the nodes
var palette = new TPixel[colorCount + 1]; var palette = new TPixel[colorCount];
int paletteIndex = 0; int paletteIndex = 0;
this.root.ConstructPalette(palette, ref paletteIndex); this.root.ConstructPalette(palette, ref paletteIndex);
@ -285,10 +299,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// The <see cref="int"/>. /// The <see cref="int"/>.
/// </returns> /// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetPaletteIndex(ref TPixel pixel, ref Rgba32 rgba) public int GetPaletteIndex(ref TPixel pixel, ref Rgba32 rgba) => this.root.GetPaletteIndex(ref pixel, 0, ref rgba);
{
return this.root.GetPaletteIndex(ref pixel, 0, ref rgba);
}
/// <summary> /// <summary>
/// Keep track of the previous node that was quantized /// Keep track of the previous node that was quantized
@ -297,10 +308,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// The node last quantized /// The node last quantized
/// </param> /// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void TrackPrevious(OctreeNode node) protected void TrackPrevious(OctreeNode node) => this.previousNode = node;
{
this.previousNode = node;
}
/// <summary> /// <summary>
/// Reduce the depth of the tree /// 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> /// </summary>
public class OctreeQuantizer : IQuantizer public class OctreeQuantizer : IQuantizer
{ {
/// <summary>
/// The default maximum number of colors to use when quantizing the image.
/// </summary>
public const int DefaultMaxColors = 256;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class. /// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary> /// </summary>
@ -26,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="OctreeQuantizer"/> class. /// Initializes a new instance of the <see cref="OctreeQuantizer"/> class.
/// </summary> /// </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) public OctreeQuantizer(int maxColors)
: this(GetDiffuser(true), maxColors) : this(GetDiffuser(true), maxColors)
{ {
@ -37,7 +42,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
/// <param name="dither">Whether to apply dithering to the output image</param> /// <param name="dither">Whether to apply dithering to the output image</param>
public OctreeQuantizer(bool dither) public OctreeQuantizer(bool dither)
: this(GetDiffuser(dither), 255) : this(GetDiffuser(dither), DefaultMaxColors)
{ {
} }
@ -46,7 +51,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// </summary> /// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param> /// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public OctreeQuantizer(IErrorDiffuser diffuser) 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> /// <param name="maxColors">The maximum number of colors to hold in the color palette</param>
public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors) public OctreeQuantizer(IErrorDiffuser diffuser, int maxColors)
{ {
Guard.MustBeBetweenOrEqualTo(maxColors, 1, 255, nameof(maxColors));
this.Diffuser = diffuser; this.Diffuser = diffuser;
this.MaxColors = maxColors; this.MaxColors = maxColors.Clamp(1, DefaultMaxColors);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -76,6 +79,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> new OctreeFrameQuantizer<TPixel>(this); => 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; 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) public PaletteFrameQuantizer(PaletteQuantizer quantizer, TPixel[] colors)
: base(quantizer, true) : 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)); Guard.MustBeBetweenOrEqualTo(colors.Length, 1, 256, nameof(colors));
this.palette = colors; this.palette = colors;
this.paletteVector = new Vector4[this.palette.Length]; 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. /// Initializes a new instance of the <see cref="PaletteQuantizer"/> class.
/// </summary> /// </summary>
/// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param> /// <param name="diffuser">The error diffusion algorithm, if any, to apply to the output image</param>
public PaletteQuantizer(IErrorDiffuser diffuser) public PaletteQuantizer(IErrorDiffuser diffuser) => this.Diffuser = diffuser;
{
this.Diffuser = diffuser;
}
/// <inheritdoc /> /// <inheritdoc />
public IErrorDiffuser Diffuser { get; } public IErrorDiffuser Diffuser { get; }
@ -50,6 +47,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> this.CreateFrameQuantizer(() => NamedColors<TPixel>.WebSafePalette); => 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> /// <summary>
/// Gets the palette to use to quantize the image. /// Gets the palette to use to quantize the image.
/// </summary> /// </summary>

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

@ -3,7 +3,6 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -38,7 +37,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
// TODO: The WuFrameQuantizer<TPixel> code is rising several questions: // TODO: The WuFrameQuantizer<TPixel> code is rising several questions:
// - Do we really need to ALWAYS allocate the whole table of size TableLength? (~ 2471625 * sizeof(long) * 5 bytes ) // - Do we really need to ALWAYS allocate the whole table of size TableLength? (~ 2471625 * sizeof(long) * 5 bytes ) JS. I'm afraid so.
// - Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case? // - Isn't an AOS ("array of structures") layout more efficient & more readable than SOA ("structure of arrays") for this particular use case?
// (T, R, G, B, A, M2) could be grouped together! // (T, R, G, B, A, M2) could be grouped together!
// - It's a frequently used class, we need tests! (So we can optimize safely.) There are tests in the original!!! We should just adopt them! // - It's a frequently used class, we need tests! (So we can optimize safely.) There are tests in the original!!! We should just adopt them!
@ -47,12 +46,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <summary> /// <summary>
/// The index bits. /// The index bits.
/// </summary> /// </summary>
private const int IndexBits = 6; private const int IndexBits = 5;
/// <summary> /// <summary>
/// The index alpha bits. /// The index alpha bits. Keep separate for now to allow easy adjustment.
/// </summary> /// </summary>
private const int IndexAlphaBits = 3; private const int IndexAlphaBits = 5;
/// <summary> /// <summary>
/// The index count. /// The index count.
@ -65,7 +64,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1; private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1;
/// <summary> /// <summary>
/// The table length. /// The table length. Now 1185921.
/// </summary> /// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
@ -128,11 +127,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// the second pass quantizes a color based on the position in the histogram. /// the second pass quantizes a color based on the position in the histogram.
/// </remarks> /// </remarks>
public WuFrameQuantizer(WuQuantizer quantizer) 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/> /// <inheritdoc/>
public override QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image) public override QuantizedFrame<TPixel> QuantizeFrame(ImageFrame<TPixel> image)
{ {
@ -169,18 +179,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
if (this.palette is null) if (this.palette is null)
{ {
this.palette = new TPixel[this.colors]; this.palette = new TPixel[this.colors];
Span<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> vmaSpan = this.vma.GetSpan();
for (int k = 0; k < this.colors; k++) for (int k = 0; k < this.colors; k++)
{ {
this.Mark(ref this.colorCube[k], (byte)k); this.Mark(ref this.colorCube[k], (byte)k);
float weight = Volume(ref this.colorCube[k], this.vwt.GetSpan()); float weight = Volume(ref this.colorCube[k], vwtSpan);
if (MathF.Abs(weight) > Constants.Epsilon) if (MathF.Abs(weight) > Constants.Epsilon)
{ {
float r = Volume(ref this.colorCube[k], this.vmr.GetSpan()); float r = Volume(ref this.colorCube[k], vmrSpan);
float g = Volume(ref this.colorCube[k], this.vmg.GetSpan()); float g = Volume(ref this.colorCube[k], vmgSpan);
float b = Volume(ref this.colorCube[k], this.vmb.GetSpan()); float b = Volume(ref this.colorCube[k], vmbSpan);
float a = Volume(ref this.colorCube[k], this.vma.GetSpan()); float a = Volume(ref this.colorCube[k], vmaSpan);
ref TPixel color = ref this.palette[k]; ref TPixel color = ref this.palette[k];
color.PackFromScaledVector4(new Vector4(r, g, b, a) / weight / 255F); color.PackFromScaledVector4(new Vector4(r, g, b, a) / weight / 255F);
@ -191,57 +207,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
return this.palette; return this.palette;
} }
/// <summary>
/// Quantizes the pixel
/// </summary>
/// <param name="rgba">The rgba used to quantize the pixel input</param>
private void QuantizePixel(ref Rgba32 rgba)
{
// Add the color to a 3-D color histogram.
int r = rgba.R >> (8 - IndexBits);
int g = rgba.G >> (8 - IndexBits);
int b = rgba.B >> (8 - IndexBits);
int a = rgba.A >> (8 - IndexAlphaBits);
int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
Span<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> vmaSpan = this.vma.GetSpan();
Span<float> m2Span = this.m2.GetSpan();
vwtSpan[index]++;
vmrSpan[index] += rgba.R;
vmgSpan[index] += rgba.G;
vmbSpan[index] += rgba.B;
vmaSpan[index] += rgba.A;
var vector = new Vector4(rgba.R, rgba.G, rgba.B, rgba.A);
m2Span[index] += Vector4.Dot(vector, vector);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void FirstPass(ImageFrame<TPixel> source, int width, int height) protected override void FirstPass(ImageFrame<TPixel> source, int width, int height)
{ {
// Build up the 3-D color histogram this.Build3DHistogram(source, width, height);
// Loop through each row
for (int y = 0; y < height; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row);
// And loop through each column
Rgba32 rgba = default;
for (int x = 0; x < width; x++)
{
ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x);
pixel.ToRgba32(ref rgba);
this.QuantizePixel(ref rgba);
}
}
this.Get3DMoments(source.MemoryAllocator); this.Get3DMoments(source.MemoryAllocator);
this.BuildCube(); this.BuildCube();
} }
@ -456,6 +425,54 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
} }
} }
/// <summary>
/// Builds a 3-D color histogram of <c>counts, r/g/b, c^2</c>.
/// </summary>
/// <param name="source">The source data.</param>
/// <param name="width">The width in pixels of the image.</param>
/// <param name="height">The height in pixels of the image.</param>
private void Build3DHistogram(ImageFrame<TPixel> source, int width, int height)
{
Span<long> vwtSpan = this.vwt.GetSpan();
Span<long> vmrSpan = this.vmr.GetSpan();
Span<long> vmgSpan = this.vmg.GetSpan();
Span<long> vmbSpan = this.vmb.GetSpan();
Span<long> vmaSpan = this.vma.GetSpan();
Span<float> m2Span = this.m2.GetSpan();
// Build up the 3-D color histogram
// Loop through each row
for (int y = 0; y < height; y++)
{
Span<TPixel> row = source.GetPixelRowSpan(y);
ref TPixel scanBaseRef = ref MemoryMarshal.GetReference(row);
// And loop through each column
Rgba32 rgba = default;
for (int x = 0; x < width; x++)
{
ref TPixel pixel = ref Unsafe.Add(ref scanBaseRef, x);
pixel.ToRgba32(ref rgba);
int r = rgba.R >> (8 - IndexBits);
int g = rgba.G >> (8 - IndexBits);
int b = rgba.B >> (8 - IndexBits);
int a = rgba.A >> (8 - IndexAlphaBits);
int index = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1);
vwtSpan[index]++;
vmrSpan[index] += rgba.R;
vmgSpan[index] += rgba.G;
vmbSpan[index] += rgba.B;
vmaSpan[index] += rgba.A;
var vector = new Vector4(rgba.R, rgba.G, rgba.B, rgba.A);
m2Span[index] += Vector4.Dot(vector, vector);
}
}
}
/// <summary> /// <summary>
/// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box.
/// </summary> /// </summary>
@ -621,22 +638,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// <returns>The <see cref="float"/>.</returns> /// <returns>The <see cref="float"/>.</returns>
private float Maximize(ref Box cube, int direction, int first, int last, out int cut, float wholeR, float wholeG, float wholeB, float wholeA, float wholeW) private float Maximize(ref Box cube, int direction, int first, int last, out int cut, float wholeR, float wholeG, float wholeB, float wholeA, float wholeW)
{ {
long baseR = Bottom(ref cube, direction, this.vmr.GetSpan()); Span<long> vwtSpan = this.vwt.GetSpan();
long baseG = Bottom(ref cube, direction, this.vmg.GetSpan()); Span<long> vmrSpan = this.vmr.GetSpan();
long baseB = Bottom(ref cube, direction, this.vmb.GetSpan()); Span<long> vmgSpan = this.vmg.GetSpan();
long baseA = Bottom(ref cube, direction, this.vma.GetSpan()); Span<long> vmbSpan = this.vmb.GetSpan();
long baseW = Bottom(ref cube, direction, this.vwt.GetSpan()); Span<long> vmaSpan = this.vma.GetSpan();
long baseR = Bottom(ref cube, direction, vmrSpan);
long baseG = Bottom(ref cube, direction, vmgSpan);
long baseB = Bottom(ref cube, direction, vmbSpan);
long baseA = Bottom(ref cube, direction, vmaSpan);
long baseW = Bottom(ref cube, direction, vwtSpan);
float max = 0F; float max = 0F;
cut = -1; cut = -1;
for (int i = first; i < last; i++) for (int i = first; i < last; i++)
{ {
float halfR = baseR + Top(ref cube, direction, i, this.vmr.GetSpan()); float halfR = baseR + Top(ref cube, direction, i, vmrSpan);
float halfG = baseG + Top(ref cube, direction, i, this.vmg.GetSpan()); float halfG = baseG + Top(ref cube, direction, i, vmgSpan);
float halfB = baseB + Top(ref cube, direction, i, this.vmb.GetSpan()); float halfB = baseB + Top(ref cube, direction, i, vmbSpan);
float halfA = baseA + Top(ref cube, direction, i, this.vma.GetSpan()); float halfA = baseA + Top(ref cube, direction, i, vmaSpan);
float halfW = baseW + Top(ref cube, direction, i, this.vwt.GetSpan()); float halfW = baseW + Top(ref cube, direction, i, vwtSpan);
if (MathF.Abs(halfW) < Constants.Epsilon) if (MathF.Abs(halfW) < Constants.Epsilon)
{ {

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

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

13
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Moq; using Moq;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -37,19 +38,13 @@ namespace SixLabors.ImageSharp.Tests
/// Test that the default configuration is not null. /// Test that the default configuration is not null.
/// </summary> /// </summary>
[Fact] [Fact]
public void TestDefaultConfigurationIsNotNull() public void TestDefaultConfigurationIsNotNull() => Assert.True(Configuration.Default != null);
{
Assert.True(Configuration.Default != null);
}
/// <summary> /// <summary>
/// Test that the default configuration read origin options is set to begin. /// Test that the default configuration read origin options is set to begin.
/// </summary> /// </summary>
[Fact] [Fact]
public void TestDefaultConfigurationReadOriginIsCurrent() public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current);
{
Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current);
}
/// <summary> /// <summary>
/// Test that the default configuration parallel options max degrees of parallelism matches the /// Test that the default configuration parallel options max degrees of parallelism matches the
@ -101,7 +96,7 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal(count, config.ImageFormats.Count()); Assert.Equal(count, config.ImageFormats.Count());
config.ImageFormatsManager.AddImageFormat(ImageFormats.Bmp); config.ImageFormatsManager.AddImageFormat(BmpFormat.Instance);
Assert.Equal(count, config.ImageFormats.Count()); Assert.Equal(count, config.ImageFormats.Count());
} }

44
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -28,10 +28,14 @@ namespace SixLabors.ImageSharp.Tests
{ TestImages.Bmp.RLE, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } { TestImages.Bmp.RLE, 2835, 2835, PixelResolutionUnit.PixelsPerMeter }
}; };
public BmpEncoderTests(ITestOutputHelper output) public static readonly TheoryData<string, BmpBitsPerPixel> BmpBitsPerPixelFiles =
new TheoryData<string, BmpBitsPerPixel>
{ {
this.Output = output; { TestImages.Bmp.Car, BmpBitsPerPixel.Pixel24 },
} { TestImages.Bmp.Bit32Rgb, BmpBitsPerPixel.Pixel32 }
};
public BmpEncoderTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; } private ITestOutputHelper Output { get; }
@ -61,13 +65,34 @@ namespace SixLabors.ImageSharp.Tests
} }
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)] [MemberData(nameof(BmpBitsPerPixelFiles))]
public void Encode_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel)
where TPixel : struct, IPixel<TPixel>
{ {
TestBmpEncoderCore(provider, bitsPerPixel); var options = new BmpEncoder();
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateImage())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
BmpMetaData meta = output.MetaData.GetFormatMetaData(BmpFormat.Instance);
Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel);
}
}
}
} }
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel), 24, 24, PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.Rgb24)]
public void Encode_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel);
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel), 48, 24, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 48, 24, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel), 47, 8, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 47, 8, PixelTypes.Rgba32)]
@ -75,10 +100,7 @@ namespace SixLabors.ImageSharp.Tests
[WithSolidFilledImages(nameof(BitsPerPixel), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithSolidFilledImages(nameof(BitsPerPixel), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel), 7, 5, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel), 7, 5, PixelTypes.Rgba32)]
public void Encode_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) public void Encode_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel);
{
TestBmpEncoderCore(provider, bitsPerPixel);
}
private static void TestBmpEncoderCore<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel) private static void TestBmpEncoderCore<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>

2
tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs

@ -44,7 +44,7 @@ namespace SixLabors.ImageSharp.Tests
using (Image<Rgba32> image = file.CreateImage()) using (Image<Rgba32> image = file.CreateImage())
{ {
string filename = path + "/" + file.FileNameWithoutExtension + ".txt"; string filename = path + "/" + file.FileNameWithoutExtension + ".txt";
File.WriteAllText(filename, image.ToBase64String(ImageFormats.Png)); File.WriteAllText(filename, image.ToBase64String(PngFormat.Instance));
} }
} }
} }

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

@ -55,10 +55,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
[MemberData(nameof(RatioFiles))] [MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{ {
var options = new GifEncoder() var options = new GifEncoder();
{
IgnoreMetadata = false
};
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateImage()) using (Image<Rgba32> input = testFile.CreateImage())
@ -82,10 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
[Fact] [Fact]
public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten()
{ {
var options = new GifEncoder() var options = new GifEncoder();
{
IgnoreMetadata = false
};
var testFile = TestFile.Create(TestImages.Gif.Rings); var testFile = TestFile.Create(TestImages.Gif.Rings);
@ -109,15 +103,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
[Fact] [Fact]
public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten() public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten()
{ {
var options = new GifEncoder() var options = new GifEncoder();
{
IgnoreMetadata = true
};
var testFile = TestFile.Create(TestImages.Gif.Rings); var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateImage()) using (Image<Rgba32> input = testFile.CreateImage())
{ {
input.MetaData.Properties.Clear();
using (var memStream = new MemoryStream()) using (var memStream = new MemoryStream())
{ {
input.SaveAsGif(memStream, options); input.SaveAsGif(memStream, options);
@ -179,5 +171,49 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length); 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())
{
inStream.Position = 0;
var image = Image.Load(inStream);
GifMetaData metaData = image.MetaData.GetFormatMetaData(GifFormat.Instance);
GifFrameMetaData frameMetaData = image.Frames.RootFrame.MetaData.GetFormatMetaData(GifFormat.Instance);
GifColorTableMode colorMode = metaData.ColorTableMode;
var encoder = new GifEncoder()
{
ColorTableMode = colorMode,
Quantizer = new OctreeQuantizer(frameMetaData.ColorTableLength)
};
image.Save(outStream, encoder);
outStream.Position = 0;
outStream.Position = 0;
var clone = Image.Load(outStream);
GifMetaData cloneMetaData = clone.MetaData.GetFormatMetaData<GifMetaData>(GifFormat.Instance);
Assert.Equal(metaData.ColorTableMode, cloneMetaData.ColorTableMode);
// Gifiddle and Cyotek GifInfo say this image has 64 colors.
Assert.Equal(64, frameMetaData.ColorTableLength);
for (int i = 0; i < image.Frames.Count; i++)
{
GifFrameMetaData ifm = image.Frames[i].MetaData.GetFormatMetaData(GifFormat.Instance);
GifFrameMetaData cifm = clone.Frames[i].MetaData.GetFormatMetaData(GifFormat.Instance);
Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength);
Assert.Equal(ifm.FrameDelay, cifm.FrameDelay);
}
image.Dispose();
clone.Dispose();
}
}
} }
} }

8
tests/ImageSharp.Tests/Formats/Gif/Sections/GifGraphicControlExtensionTests.cs

@ -12,10 +12,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
[Fact] [Fact]
public void TestPackedValue() public void TestPackedValue()
{ {
Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(DisposalMethod.Unspecified, false, false)); Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.Unspecified, false, false));
Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(DisposalMethod.RestoreToBackground, true, true)); Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToBackground, true, true));
Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(DisposalMethod.NotDispose, false, false)); Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.NotDispose, false, false));
Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(DisposalMethod.RestoreToPrevious, true, false)); Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToPrevious, true, false));
} }
} }
} }

14
tests/ImageSharp.Tests/Formats/Gif/Sections/GifImageDescriptorTests.cs

@ -12,13 +12,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
[Fact] [Fact]
public void TestPackedValue() public void TestPackedValue()
{ {
Assert.Equal(128, GifImageDescriptor.GetPackedValue(true, false, false, 1)); // localColorTable Assert.Equal(129, GifImageDescriptor.GetPackedValue(true, false, false, 1)); // localColorTable
Assert.Equal(64, GifImageDescriptor.GetPackedValue(false, true, false, 1)); // interfaceFlag Assert.Equal(65, GifImageDescriptor.GetPackedValue(false, true, false, 1)); // interfaceFlag
Assert.Equal(32, GifImageDescriptor.GetPackedValue(false, false, true, 1)); // sortFlag Assert.Equal(33, GifImageDescriptor.GetPackedValue(false, false, true, 1)); // sortFlag
Assert.Equal(224, GifImageDescriptor.GetPackedValue(true, true, true, 1)); // all Assert.Equal(225, GifImageDescriptor.GetPackedValue(true, true, true, 1)); // all
Assert.Equal(7, GifImageDescriptor.GetPackedValue(false, false, false, 8)); Assert.Equal(8, GifImageDescriptor.GetPackedValue(false, false, false, 8));
Assert.Equal(227, GifImageDescriptor.GetPackedValue(true, true, true, 4)); Assert.Equal(228, GifImageDescriptor.GetPackedValue(true, true, true, 4));
Assert.Equal(231, GifImageDescriptor.GetPackedValue(true, true, true, 8)); Assert.Equal(232, GifImageDescriptor.GetPackedValue(true, true, true, 8));
} }
} }
} }

4
tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

@ -61,7 +61,7 @@ namespace SixLabors.ImageSharp.Tests
}); });
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {
this.DefaultFormatsManager.SetEncoder(ImageFormats.Bmp, null); this.DefaultFormatsManager.SetEncoder(BmpFormat.Instance, null);
}); });
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {
@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Tests
}); });
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {
this.DefaultFormatsManager.SetDecoder(ImageFormats.Bmp, null); this.DefaultFormatsManager.SetDecoder(BmpFormat.Instance, null);
}); });
Assert.Throws<ArgumentNullException>(() => Assert.Throws<ArgumentNullException>(() =>
{ {

37
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.MetaData.cs

@ -49,6 +49,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch }
}; };
public static readonly TheoryData<string, int> QualityFiles =
new TheoryData<string, int>
{
{ TestImages.Jpeg.Baseline.Calliphora, 80},
{ TestImages.Jpeg.Progressive.Fb, 75 }
};
[Theory] [Theory]
[MemberData(nameof(MetaDataTestData))] [MemberData(nameof(MetaDataTestData))]
public void MetaDataIsParsedCorrectly( public void MetaDataIsParsedCorrectly(
@ -101,6 +108,36 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
} }
} }
[Theory]
[MemberData(nameof(QualityFiles))]
public void Identify_VerifyQuality(string imagePath, int quality)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new JpegDecoder();
IImageInfo image = decoder.Identify(Configuration.Default, stream);
JpegMetaData meta = image.MetaData.GetFormatMetaData(JpegFormat.Instance);
Assert.Equal(quality, meta.Quality);
}
}
[Theory]
[MemberData(nameof(QualityFiles))]
public void Decode_VerifyQuality(string imagePath, int quality)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new JpegDecoder();
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
{
JpegMetaData meta = image.MetaData.GetFormatMetaData(JpegFormat.Instance);
Assert.Equal(quality, meta.Quality);
}
}
}
private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action<IImageInfo> test) private static void TestImageInfo(string imagePath, IImageDecoder decoder, bool useIdentify, Action<IImageInfo> test)
{ {
var testFile = TestFile.Create(imagePath); var testFile = TestFile.Create(imagePath);

72
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -14,6 +14,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ {
public class JpegEncoderTests public class JpegEncoderTests
{ {
public static readonly TheoryData<string, int> QualityFiles =
new TheoryData<string, int>
{
{ TestImages.Jpeg.Baseline.Calliphora, 80},
{ TestImages.Jpeg.Progressive.Fb, 75 }
};
public static readonly TheoryData<JpegSubsample, int> BitsPerPixel_Quality = public static readonly TheoryData<JpegSubsample, int> BitsPerPixel_Quality =
new TheoryData<JpegSubsample, int> new TheoryData<JpegSubsample, int>
{ {
@ -34,6 +41,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch }
}; };
[Theory]
[MemberData(nameof(QualityFiles))]
public void Encode_PreserveQuality(string imagePath, int quality)
{
var options = new JpegEncoder();
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateImage())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
JpegMetaData meta = output.MetaData.GetFormatMetaData(JpegFormat.Instance);
Assert.Equal(quality, meta.Quality);
}
}
}
}
[Theory] [Theory]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)] [WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 73, 71, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 73, 71, PixelTypes.Rgba32)]
@ -43,18 +73,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)]
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality) public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
{
TestJpegEncoderCore(provider, subsample, quality);
}
[Theory] [Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality) public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
{
TestJpegEncoderCore(provider, subsample, quality);
}
/// <summary> /// <summary>
/// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation
@ -103,38 +127,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
} }
} }
[Theory]
[InlineData(false)]
[InlineData(true)]
public void IgnoreMetadata_ControlsIfExifProfileIsWritten(bool ignoreMetaData)
{
var encoder = new JpegEncoder()
{
IgnoreMetadata = ignoreMetaData
};
using (Image<Rgba32> input = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateImage())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, encoder);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
if (ignoreMetaData)
{
Assert.Null(output.MetaData.ExifProfile);
}
else
{
Assert.NotNull(output.MetaData.ExifProfile);
}
}
}
}
}
[Fact] [Fact]
public void Quality_0_And_1_Are_Identical() public void Quality_0_And_1_Are_Identical()
{ {

33
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -23,6 +23,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
// The images are an exact match. Maybe the submodule isn't updating? // The images are an exact match. Maybe the submodule isn't updating?
private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100; private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100;
public static readonly TheoryData<string, PngBitDepth> PngBitDepthFiles =
new TheoryData<string, PngBitDepth>
{
{ TestImages.Png.Rgb48Bpp, PngBitDepth.Bit16 },
{ TestImages.Png.Bpp1, PngBitDepth.Bit1 }
};
/// <summary> /// <summary>
/// All types except Palette /// All types except Palette
/// </summary> /// </summary>
@ -221,7 +228,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
: image; : image;
float paletteToleranceHack = 80f / paletteSize; float paletteToleranceHack = 80f / paletteSize;
paletteToleranceHack = paletteToleranceHack * paletteToleranceHack; paletteToleranceHack *= paletteToleranceHack;
ImageComparer comparer = pngColorType == PngColorType.Palette ImageComparer comparer = pngColorType == PngColorType.Palette
? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack) ? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack)
: ImageComparer.Exact; : ImageComparer.Exact;
@ -290,5 +297,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
} }
} }
} }
[Theory]
[MemberData(nameof(PngBitDepthFiles))]
public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
{
var options = new PngEncoder();
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateImage())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
PngMetaData meta = output.MetaData.GetFormatMetaData(PngFormat.Instance);
Assert.Equal(pngBitDepth, meta.BitDepth);
}
}
}
}
} }
} }

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

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

2
tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs

@ -28,7 +28,6 @@ namespace SixLabors.ImageSharp.Tests
metaData.HorizontalResolution = 4; metaData.HorizontalResolution = 4;
metaData.VerticalResolution = 2; metaData.VerticalResolution = 2;
metaData.Properties.Add(imageProperty); metaData.Properties.Add(imageProperty);
metaData.RepeatCount = 1;
ImageMetaData clone = metaData.Clone(); ImageMetaData clone = metaData.Clone();
@ -36,7 +35,6 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal(4, clone.HorizontalResolution); Assert.Equal(4, clone.HorizontalResolution);
Assert.Equal(2, clone.VerticalResolution); Assert.Equal(2, clone.VerticalResolution);
Assert.Equal(imageProperty, clone.Properties[0]); Assert.Equal(imageProperty, clone.Properties[0]);
Assert.Equal(1, clone.RepeatCount);
} }
[Fact] [Fact]

4
tests/ImageSharp.Tests/TestImages.cs

@ -178,6 +178,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Bit8Inverted = "Bmp/test8-inverted.bmp"; public const string Bit8Inverted = "Bmp/test8-inverted.bmp";
public const string Bit16 = "Bmp/test16.bmp"; public const string Bit16 = "Bmp/test16.bmp";
public const string Bit16Inverted = "Bmp/test16-inverted.bmp"; public const string Bit16Inverted = "Bmp/test16-inverted.bmp";
public const string Bit32Rgb = "Bmp/rgb32.bmp";
public static readonly string[] All public static readonly string[] All
= { = {
@ -204,6 +205,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Cheers = "Gif/cheers.gif"; public const string Cheers = "Gif/cheers.gif";
public const string Trans = "Gif/trans.gif"; public const string Trans = "Gif/trans.gif";
public const string Kumin = "Gif/kumin.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 Ratio4x1 = "Gif/base_4x1.gif";
public const string Ratio1x4 = "Gif/base_1x4.gif"; public const string Ratio1x4 = "Gif/base_1x4.gif";
@ -214,7 +216,7 @@ namespace SixLabors.ImageSharp.Tests
public const string BadDescriptorWidth = "Gif/issues/issue403_baddescriptorwidth.gif"; 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) public bool Equals(Key other)
{ {
if (other is null) return false; if (other is null)
if (ReferenceEquals(this, other)) return true; {
if (!this.commonValues.Equals(other.commonValues)) return false; return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (!this.commonValues.Equals(other.commonValues))
{
return false;
}
if (this.decoderParameters.Count != other.decoderParameters.Count) if (this.decoderParameters.Count != other.decoderParameters.Count)
{ {
@ -66,8 +77,7 @@ namespace SixLabors.ImageSharp.Tests
foreach (KeyValuePair<string, object> kv in this.decoderParameters) foreach (KeyValuePair<string, object> kv in this.decoderParameters)
{ {
object otherVal; if (!other.decoderParameters.TryGetValue(kv.Key, out object otherVal))
if (!other.decoderParameters.TryGetValue(kv.Key, out otherVal))
{ {
return false; return false;
} }
@ -81,26 +91,29 @@ namespace SixLabors.ImageSharp.Tests
public override bool Equals(object obj) public override bool Equals(object obj)
{ {
if (obj is null) return false; if (obj is null)
if (ReferenceEquals(this, obj)) return true; {
if (obj.GetType() != this.GetType()) return false; return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != this.GetType())
{
return false;
}
return this.Equals((Key)obj); return this.Equals((Key)obj);
} }
public override int GetHashCode() public override int GetHashCode() => this.commonValues.GetHashCode();
{
return this.commonValues.GetHashCode();
}
public static bool operator ==(Key left, Key right) public static bool operator ==(Key left, Key right) => Equals(left, right);
{
return Equals(left, right);
}
public static bool operator !=(Key left, Key right) public static bool operator !=(Key left, Key right) => !Equals(left, right);
{
return !Equals(left, right);
}
} }
private static readonly ConcurrentDictionary<Key, Image<TPixel>> cache = new ConcurrentDictionary<Key, Image<TPixel>>(); 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) public FileProvider(string filePath) => this.FilePath = filePath;
{
this.FilePath = filePath;
}
/// <summary> /// <summary>
/// Gets the file path relative to the "~/tests/images" folder /// Gets the file path relative to the "~/tests/images" folder
@ -135,12 +145,12 @@ namespace SixLabors.ImageSharp.Tests
if (!TestEnvironment.Is64BitProcess) if (!TestEnvironment.Is64BitProcess)
{ {
return LoadImage(decoder); return this.LoadImage(decoder);
} }
var key = new Key(this.PixelType, this.FilePath, 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(); return cachedImage.Clone();
} }

4
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -62,13 +62,13 @@ namespace SixLabors.ImageSharp.Tests
IImageEncoder bmpEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); IImageEncoder bmpEncoder = IsWindows ? (IImageEncoder)SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();
cfg.ConfigureCodecs( cfg.ConfigureCodecs(
ImageFormats.Png, PngFormat.Instance,
MagickReferenceDecoder.Instance, MagickReferenceDecoder.Instance,
pngEncoder, pngEncoder,
new PngImageFormatDetector()); new PngImageFormatDetector());
cfg.ConfigureCodecs( cfg.ConfigureCodecs(
ImageFormats.Bmp, BmpFormat.Instance,
SystemDrawingReferenceDecoder.Instance, SystemDrawingReferenceDecoder.Instance,
bmpEncoder, bmpEncoder,
new BmpImageFormatDetector()); new BmpImageFormatDetector());

3
tests/Images/Input/Bmp/rgb32.bmp

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4c79cb8ffd2f1c096af27f9d82b5feaa0aa2cb049c791e0f6251de0435066e5
size 32566

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