Browse Source

Merge pull request #693 from SixLabors/js/format-info

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

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

@ -36,10 +36,15 @@ namespace SixLabors.ImageSharp
/// The <see cref="int"/>
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBitsNeededForColorDepth(int colors)
{
return 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>
/// Implementation of 1D Gaussian G(x) function
@ -132,10 +137,7 @@ namespace SixLabors.ImageSharp
/// The bounding <see cref="Rectangle"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight)
{
return new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
}
public static Rectangle GetBoundingRectangle(Point topLeft, Point bottomRight) => new Rectangle(topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y);
/// <summary>
/// Finds the bounding rectangle based on the first instance of any color component other

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

@ -6,16 +6,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <summary>
/// Enumerates the available bits per pixel for bitmap.
/// </summary>
public enum BmpBitsPerPixel
public enum BmpBitsPerPixel : short
{
/// <summary>
/// 24 bits per pixel. Each pixel consists of 3 bytes.
/// </summary>
Pixel24 = 3,
Pixel24 = 24,
/// <summary>
/// 32 bits per pixel. Each pixel consists of 4 bytes.
/// </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
{
/// <inheritdoc/>
public void Configure(Configuration config)
public void Configure(Configuration configuration)
{
config.ImageFormatsManager.SetEncoder(ImageFormats.Bmp, new BmpEncoder());
config.ImageFormatsManager.SetDecoder(ImageFormats.Bmp, new BmpDecoder());
config.ImageFormatsManager.AddImageFormatDetector(new BmpImageFormatDetector());
configuration.ImageFormatsManager.SetEncoder(BmpFormat.Instance, new BmpEncoder());
configuration.ImageFormatsManager.SetDecoder(BmpFormat.Instance, new BmpDecoder());
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>
/// <returns>The <see cref="int"/> representing the inverted value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int Invert(int y, int height, bool inverted)
{
return (!inverted) ? height - y - 1 : y;
}
private static int Invert(int y, int height, bool inverted) => (!inverted) ? height - y - 1 : y;
/// <summary>
/// Calculates the amount of bytes to pad a row.
@ -206,10 +203,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <param name="value">The masked and shifted value</param>
/// <returns>The <see cref="byte"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte GetBytesFrom5BitValue(int value)
{
return (byte)((value << 3) | (value >> 2));
}
private static byte GetBytesFrom5BitValue(int value) => (byte)((value << 3) | (value >> 2));
/// <summary>
/// Looks up color values and builds the image from de-compressed RLE8 data.
@ -524,8 +518,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
// Resolution is stored in PPM.
var meta = new ImageMetaData();
meta.ResolutionUnits = PixelResolutionUnit.PixelsPerMeter;
var meta = new ImageMetaData
{
ResolutionUnits = PixelResolutionUnit.PixelsPerMeter
};
if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0)
{
meta.HorizontalResolution = this.infoHeader.XPelsPerMeter;
@ -540,6 +536,16 @@ namespace SixLabors.ImageSharp.Formats.Bmp
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
this.stream.Skip(skipAmount);
}
@ -585,11 +591,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp
if (this.infoHeader.ClrUsed == 0)
{
if (this.infoHeader.BitsPerPixel == 1 ||
this.infoHeader.BitsPerPixel == 4 ||
this.infoHeader.BitsPerPixel == 8)
if (this.infoHeader.BitsPerPixel == 1
|| this.infoHeader.BitsPerPixel == 4
|| this.infoHeader.BitsPerPixel == 8)
{
colorMapSize = (int)Math.Pow(2, this.infoHeader.BitsPerPixel) * 4;
colorMapSize = ImageMaths.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel) * 4;
}
}
else

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

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

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

@ -21,10 +21,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// </summary>
private int padding;
private readonly BmpBitsPerPixel bitsPerPixel;
private readonly MemoryAllocator memoryAllocator;
private BmpBitsPerPixel? bitsPerPixel;
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoderCore"/> class.
/// </summary>
@ -48,10 +48,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
// Cast to int will get the bytes per pixel
short bpp = (short)(8 * (int)this.bitsPerPixel);
BmpMetaData bmpMetaData = image.MetaData.GetFormatMetaData(BmpFormat.Instance);
this.bitsPerPixel = this.bitsPerPixel ?? bmpMetaData.BitsPerPixel;
short bpp = (short)this.bitsPerPixel;
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.
ImageMetaData meta = image.MetaData;
@ -145,10 +147,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
}
}
private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel)
{
return this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding);
}
private IManagedByteBuffer AllocateRow(int width, int bytesPerPixel) => this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, bytesPerPixel, this.padding);
/// <summary>
/// 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>
/// Registers the image encoders, decoders and mime type detectors for the bmp format.
/// </summary>
internal sealed class BmpFormat : IImageFormat
internal sealed class BmpFormat : IImageFormat<BmpMetaData>
{
private BmpFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static BmpFormat Instance { get; } = new BmpFormat();
/// <inheritdoc/>
public string Name => "BMP";
@ -21,5 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// <inheritdoc/>
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/>
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)

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>
/// Gets the number of bits per pixel.
/// </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>
public static void SaveAsBmp<TPixel>(this Image<TPixel> source, Stream stream, BmpEncoder encoder)
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
{
/// <summary>
/// Provides enumeration for the available Gif color table modes.
/// Provides enumeration for the available color table modes.
/// </summary>
public enum GifColorTableMode
{

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

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

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

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

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

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

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

@ -56,10 +56,20 @@ namespace SixLabors.ImageSharp.Formats.Gif
private GifGraphicControlExtension graphicsControlExtension;
/// <summary>
/// The metadata
/// The image desciptor.
/// </summary>
private GifImageDescriptor imageDescriptor;
/// <summary>
/// The abstract metadata.
/// </summary>
private ImageMetaData metaData;
/// <summary>
/// The gif specific metadata.
/// </summary>
private GifMetaData gifMetaData;
/// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
/// </summary>
@ -120,8 +130,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
int label = stream.ReadByte();
switch (label)
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
this.ReadGraphicalControlExtension();
@ -130,11 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.ReadComments();
break;
case GifConstants.ApplicationExtensionLabel:
// 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.
this.ReadApplicationExtension();
break;
case GifConstants.PlainTextLabel:
int plainLength = stream.ReadByte();
@ -178,13 +183,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
if (nextFlag == GifConstants.ImageLabel)
{
// Skip image block
this.Skip(0);
this.ReadImageDescriptor();
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
int label = stream.ReadByte();
switch (label)
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
@ -195,11 +198,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.ReadComments();
break;
case GifConstants.ApplicationExtensionLabel:
// 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.
this.ReadApplicationExtension();
break;
case GifConstants.PlainTextLabel:
int plainLength = stream.ReadByte();
@ -224,7 +223,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
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>
@ -238,14 +241,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
/// <summary>
/// Reads the image descriptor
/// Reads the image descriptor.
/// </summary>
/// <returns><see cref="GifImageDescriptor"/></returns>
private GifImageDescriptor ReadImageDescriptor()
private void ReadImageDescriptor()
{
this.stream.Read(this.buffer, 0, 9);
return GifImageDescriptor.Parse(this.buffer);
this.imageDescriptor = GifImageDescriptor.Parse(this.buffer);
}
/// <summary>
@ -258,6 +260,41 @@ namespace SixLabors.ImageSharp.Formats.Gif
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>
/// Skips the designated number of bytes in the stream.
/// </summary>
@ -312,25 +349,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame)
where TPixel : struct, IPixel<TPixel>
{
GifImageDescriptor imageDescriptor = this.ReadImageDescriptor();
this.ReadImageDescriptor();
IManagedByteBuffer localColorTable = null;
IManagedByteBuffer indices = null;
try
{
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
if (imageDescriptor.LocalColorTableFlag)
if (this.imageDescriptor.LocalColorTableFlag)
{
int length = imageDescriptor.LocalColorTableSize * 3;
int length = this.imageDescriptor.LocalColorTableSize * 3;
localColorTable = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(length, AllocationOptions.Clean);
this.stream.Read(localColorTable.Array, 0, length);
}
indices = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(imageDescriptor.Width * imageDescriptor.Height, AllocationOptions.Clean);
indices = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(this.imageDescriptor.Width * this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(imageDescriptor, indices.GetSpan());
this.ReadFrameIndices(this.imageDescriptor, indices.GetSpan());
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>((localColorTable ?? this.globalColorTable).GetSpan());
this.ReadFrameColors(ref image, ref previousFrame, indices.GetSpan(), colorTable, imageDescriptor);
this.ReadFrameColors(ref image, ref previousFrame, indices.GetSpan(), colorTable, this.imageDescriptor);
// Skip any remaining blocks
this.Skip(0);
@ -388,7 +425,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
if (this.graphicsControlExtension.DisposalMethod == GifDisposalMethod.RestoreToPrevious)
{
prevFrame = previousFrame;
}
@ -471,7 +508,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
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);
}
@ -503,12 +540,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetFrameMetaData(ImageFrameMetaData meta)
{
GifFrameMetaData gifMeta = meta.GetFormatMetaData(GifFormat.Instance);
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>
@ -552,10 +602,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
this.metaData = meta;
this.gifMetaData = meta.GetFormatMetaData(GifFormat.Instance);
this.gifMetaData.ColorTableMode = this.logicalScreenDescriptor.GlobalColorTableFlag
? GifColorTableMode.Global
: GifColorTableMode.Local;
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetaData.GlobalColorTableLength = globalColorTableLength;
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.
/// <see href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt"/> section 23
/// </summary>
public enum DisposalMethod
public enum GifDisposalMethod
{
/// <summary>
/// No disposal specified.

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

@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Gets or sets the color table mode: Global or local.
/// </summary>
public GifColorTableMode ColorTableMode { get; set; }
public GifColorTableMode? ColorTableMode { get; set; }
/// <inheritdoc/>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)

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

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -44,7 +43,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// The color table mode: Global or local.
/// </summary>
private readonly GifColorTableMode colorTableMode;
private GifColorTableMode? colorTableMode;
/// <summary>
/// A flag indicating whether to ingore the metadata when writing the image.
@ -56,6 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
private int bitDepth;
/// <summary>
/// Gif specific meta data.
/// </summary>
private GifMetaData gifMetaData;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
@ -66,8 +70,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.memoryAllocator = memoryAllocator;
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode;
this.ignoreMetadata = options.IgnoreMetadata;
this.colorTableMode = options.ColorTableMode;
}
/// <summary>
@ -82,6 +86,10 @@ namespace SixLabors.ImageSharp.Formats.Gif
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.gifMetaData = image.MetaData.GetFormatMetaData(GifFormat.Instance);
this.colorTableMode = this.colorTableMode ?? this.gifMetaData.ColorTableMode;
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
// Quantize the image returning a palette.
QuantizedFrame<TPixel> quantized =
this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
@ -94,7 +102,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Write the LSD.
int index = this.GetTransparentIndex(quantized);
bool useGlobalTable = this.colorTableMode.Equals(GifColorTableMode.Global);
this.WriteLogicalScreenDescriptor(image, index, useGlobalTable, stream);
if (useGlobalTable)
@ -108,7 +115,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
{
this.WriteApplicationExtension(stream, image.MetaData.RepeatCount);
this.WriteApplicationExtension(stream, this.gifMetaData.RepeatCount);
}
if (useGlobalTable)
@ -136,8 +143,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
this.WriteGraphicalControlExtension(frame.MetaData, transparencyIndex, stream);
GifFrameMetaData frameMetaData = frame.MetaData.GetFormatMetaData(GifFormat.Instance);
this.WriteGraphicalControlExtension(frameMetaData, transparencyIndex, stream);
this.WriteImageDescriptor(frame, false, stream);
if (i == 0)
@ -146,7 +153,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
else
{
using (QuantizedFrame<TPixel> paletteQuantized = palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame))
using (QuantizedFrame<TPixel> paletteQuantized
= palleteQuantizer.CreateFrameQuantizer(() => quantized.Palette).QuantizeFrame(frame))
{
this.WriteImageData(paletteQuantized, stream);
}
@ -157,20 +165,36 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void EncodeLocal<TPixel>(Image<TPixel> image, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
ImageFrame<TPixel> previousFrame = null;
GifFrameMetaData previousMeta = null;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
GifFrameMetaData meta = frame.MetaData.GetFormatMetaData(GifFormat.Instance);
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 != meta.ColorTableLength
&& meta.ColorTableLength > 0)
{
quantized = this.quantizer.CreateFrameQuantizer<TPixel>(meta.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(meta, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
quantized?.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = meta;
}
}
@ -210,10 +234,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="stream">The stream to write to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteHeader(Stream stream)
{
stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
}
private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
/// <summary>
/// Writes the logical screen descriptor to the stream.
@ -279,25 +300,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Application Extension Header
if (repeatCount != 1)
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.ApplicationExtensionLabel;
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);
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.WriteExtension(loopingExtension, stream);
}
}
@ -337,7 +341,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <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="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(
disposalMethod: metaData.DisposalMethod,
@ -382,7 +386,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
localColorTableSize: (byte)this.bitDepth);
localColorTableSize: this.bitDepth - 1);
var descriptor = new GifImageDescriptor(
left: 0,
@ -407,7 +411,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
int pixelCount = image.Palette.Length;
int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; // The maximium number of colors for the bit depth
// The maximium number of colors for the bit depth
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
Rgb24 rgb = default;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))

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

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

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.
using System.Text;
using SixLabors.ImageSharp.MetaData;
namespace SixLabors.ImageSharp.Formats.Gif
{

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

@ -29,6 +29,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <summary>
/// Gets the color table mode: Global or local.
/// </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>
public static void SaveAsGif<TPixel>(this Image<TPixel> source, Stream stream, GifEncoder encoder)
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
/// graphic is to be treated after being displayed.
/// </summary>
public DisposalMethod DisposalMethod => (DisposalMethod)((this.Packed & 0x1C) >> 2);
public GifDisposalMethod DisposalMethod => (GifDisposalMethod)((this.Packed & 0x1C) >> 2);
/// <summary>
/// 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];
}
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

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

@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
return MemoryMarshal.Cast<byte, GifImageDescriptor>(buffer)[0];
}
public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, byte localColorTableSize)
public static byte GetPackedValue(bool localColorTableFlag, bool interfaceFlag, bool sortFlag, int localColorTableSize)
{
/*
Local Color Table Flag | 1 Bit
@ -108,7 +108,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
value |= 1 << 5;
}
value |= (byte)(localColorTableSize - 1);
value |= (byte)localColorTableSize;
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
{
/// <summary>
/// Describes an image format.
/// Defines the contract for an image format.
/// </summary>
public interface IImageFormat
{
@ -30,4 +30,34 @@ namespace SixLabors.ImageSharp.Formats
/// </summary>
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>
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>
/// Gets the quality, that will be used to encode the image. Quality
/// index must be between 0 and 100 (compression from max to min).
/// </summary>
/// <value>The quality of the jpg image from 0 to 100.</value>
int Quality { get; }
int? Quality { get; }
/// <summary>
/// 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>
public static void SaveAsJpeg<TPixel>(this Image<TPixel> source, Stream stream, JpegEncoder encoder)
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
{
/// <inheritdoc/>
public void Configure(Configuration config)
public void Configure(Configuration configuration)
{
config.ImageFormatsManager.SetEncoder(ImageFormats.Jpeg, new JpegEncoder());
config.ImageFormatsManager.SetDecoder(ImageFormats.Jpeg, new JpegDecoder());
config.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector());
configuration.ImageFormatsManager.SetEncoder(JpegFormat.Instance, new JpegEncoder());
configuration.ImageFormatsManager.SetDecoder(JpegFormat.Instance, new JpegDecoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new JpegImageFormatDetector());
}
}
}

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

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

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

@ -11,17 +11,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
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>
/// 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).
/// Defaults to <value>75</value>.
/// </summary>
public int Quality { get; set; } = 75;
public int? Quality { get; set; }
/// <summary>
/// Gets or sets the subsample ration, that will be used to encode the image.

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

@ -123,19 +123,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private readonly byte[] huffmanBuffer = new byte[179];
/// <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>
private readonly bool ignoreMetadata;
private JpegSubsample? subsample;
/// <summary>
/// The quality, that will be used to encode the image.
/// </summary>
private readonly int quality;
/// <summary>
/// Gets or sets the subsampling method to use.
/// </summary>
private readonly JpegSubsample? subsample;
private readonly int? quality;
/// <summary>
/// The accumulated bits to write to the stream.
@ -168,11 +163,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="options">The options</param>
public JpegEncoderCore(IJpegEncoderOptions options)
{
// System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1.
this.quality = options.Quality.Clamp(1, 100);
this.subsample = options.Subsample ?? (this.quality >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420);
this.ignoreMetadata = options.IgnoreMetadata;
this.quality = options.Quality;
this.subsample = options.Subsample;
}
/// <summary>
@ -195,15 +187,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream = stream;
// System.Drawing produces identical output for jpegs with a quality parameter of 0 and 1.
int qlty = (this.quality ?? image.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.
int scale;
if (this.quality < 50)
if (qlty < 50)
{
scale = 5000 / this.quality;
scale = 5000 / qlty;
}
else
{
scale = 200 - (this.quality * 2);
scale = 200 - (qlty * 2);
}
// Initialize the quantization tables.
@ -767,11 +763,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void WriteProfiles<TPixel>(Image<TPixel> image)
where TPixel : struct, IPixel<TPixel>
{
if (this.ignoreMetadata)
{
return;
}
image.MetaData.SyncProfiles();
this.WriteExifProfile(image.MetaData.ExifProfile);
this.WriteIccProfile(image.MetaData.IccProfile);

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

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

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).
/// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary>
PngBitDepth BitDepth { get; }
PngBitDepth? BitDepth { get; }
/// <summary>
/// Gets the color type
/// </summary>
PngColorType ColorType { get; }
PngColorType? ColorType { get; }
/// <summary>
/// Gets the filter method.
@ -33,15 +33,13 @@ namespace SixLabors.ImageSharp.Formats.Png
int CompressionLevel { get; }
/// <summary>
/// Gets the gamma value, that will be written
/// the the stream, when the <see cref="WriteGamma"/> property
/// is set to true. The default value is 2.2F.
/// Gets the gamma value, that will be written the the image.
/// </summary>
/// <value>The gamma value of the image.</value>
float Gamma { get; }
float? Gamma { get; }
/// <summary>
/// Gets quantizer for reducing the color count.
/// Gets the quantizer for reducing the color count.
/// </summary>
IQuantizer Quantizer { get; }
@ -49,11 +47,5 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets the transparency threshold.
/// </summary>
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>
public static void SaveAsPng<TPixel>(this Image<TPixel> source, Stream stream, PngEncoder encoder)
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>
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>
/// 8 bits per sample or per palette index (not per pixel).
/// </summary>

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

@ -8,58 +8,58 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
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>
/// 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.
/// </summary>
Data = 0x49444154U, // IDAT
Data = 0x49444154U,
/// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
/// </summary>
End = 0x49454E44U, // IEND
End = 0x49454E44U,
/// <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).
/// 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>
PaletteAlpha = 0x74524E53U, // tRNS
Header = 0x49484452U,
/// <summary>
/// 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.
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
/// </summary>
Palette = 0x504C5445U,
/// <summary>
/// The eXIf data chunk which contains the Exif profile.
/// </summary>
Text = 0x74455874U, // tEXt
Exif = 0x65584966U,
/// <summary>
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
/// </summary>
Gamma = 0x67414D41U, // gAMA
Gamma = 0x67414D41U,
/// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary>
Physical = 0x70485973U, // pHYs
Physical = 0x70485973U,
/// <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>
Exif = 0x65584966U // eXIf
PaletteAlpha = 0x74524E53U
}
}

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

@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(ImageFormats.Png, new PngEncoder());
configuration.ImageFormatsManager.SetDecoder(ImageFormats.Png, new PngDecoder());
configuration.ImageFormatsManager.SetEncoder(PngFormat.Instance, new PngEncoder());
configuration.ImageFormatsManager.SetDecoder(PngFormat.Instance, new PngDecoder());
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.Text;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Filters;
using SixLabors.ImageSharp.Formats.Png.Zlib;
using SixLabors.ImageSharp.Memory;
@ -217,7 +216,8 @@ namespace SixLabors.ImageSharp.Formats.Png
public Image<TPixel> Decode<TPixel>(Stream stream)
where TPixel : struct, IPixel<TPixel>
{
var metadata = new ImageMetaData();
var metaData = new ImageMetaData();
var pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance);
this.currentStream = stream;
this.currentStream.Skip(8);
Image<TPixel> image = null;
@ -232,16 +232,19 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (chunk.Type)
{
case PngChunkType.Header:
this.ReadHeaderChunk(chunk.Data.Array);
this.ReadHeaderChunk(pngMetaData, chunk.Data.Array);
this.ValidateHeader();
break;
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;
case PngChunkType.Data:
if (image is null)
{
this.InitializeImage(metadata, out image);
this.InitializeImage(metaData, out image);
}
deframeStream.AllocateNewBytes(chunk.Length);
@ -260,14 +263,14 @@ namespace SixLabors.ImageSharp.Formats.Png
this.AssignTransparentMarkers(alpha);
break;
case PngChunkType.Text:
this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length);
this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length);
break;
case PngChunkType.Exif:
if (!this.ignoreMetadata)
{
byte[] exifData = new byte[chunk.Length];
Buffer.BlockCopy(chunk.Data.Array, 0, exifData, 0, chunk.Length);
metadata.ExifProfile = new ExifProfile(exifData);
metaData.ExifProfile = new ExifProfile(exifData);
}
break;
@ -303,7 +306,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
public IImageInfo Identify(Stream stream)
{
var metadata = new ImageMetaData();
var metaData = new ImageMetaData();
var pngMetaData = metaData.GetFormatMetaData(PngFormat.Instance);
this.currentStream = stream;
this.currentStream.Skip(8);
try
@ -315,17 +319,20 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (chunk.Type)
{
case PngChunkType.Header:
this.ReadHeaderChunk(chunk.Data.Array);
this.ReadHeaderChunk(pngMetaData, chunk.Data.Array);
this.ValidateHeader();
break;
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;
case PngChunkType.Data:
this.SkipChunkDataAndCrc(chunk);
break;
case PngChunkType.Text:
this.ReadTextChunk(metadata, chunk.Data.Array, chunk.Length);
this.ReadTextChunk(metaData, chunk.Data.Array, chunk.Length);
break;
case PngChunkType.End:
this.isEndChunkReached = true;
@ -349,7 +356,7 @@ namespace SixLabors.ImageSharp.Formats.Png
throw new ImageFormatException("PNG Image does not contain a header chunk");
}
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metadata);
return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), this.header.Width, this.header.Height, metaData);
}
/// <summary>
@ -360,9 +367,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <returns>The <see cref="int"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte ReadByteLittleEndian(ReadOnlySpan<byte> buffer, int offset)
{
return (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF));
}
=> (byte)(((buffer[offset] & 0xFF) << 16) | (buffer[offset + 1] & 0xFF));
/// <summary>
/// Attempts to convert a byte array to a new array where each value in the original array is represented by the
@ -430,6 +435,18 @@ namespace SixLabors.ImageSharp.Formats.Png
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>
/// Initializes the image and various buffers needed for processing
/// </summary>
@ -711,7 +728,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
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)
{
@ -933,7 +950,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
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)
{
@ -1270,17 +1287,22 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Reads a header chunk from the data.
/// </summary>
/// <param name="pngMetaData">The png metadata.</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(
width: BinaryPrimitives.ReadInt32BigEndian(data.Slice(0, 4)),
height: BinaryPrimitives.ReadInt32BigEndian(data.Slice(4, 4)),
bitDepth: data[8],
bitDepth: bitDepth,
colorType: (PngColorType)data[9],
compressionMethod: data[10],
filterMethod: data[11],
interlaceMethod: (PngInterlaceMode)data[12]);
pngMetaData.BitDepth = (PngBitDepth)bitDepth;
pngMetaData.ColorType = this.header.ColorType;
}
/// <summary>

13
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).
/// Not all values are allowed for all <see cref="ColorType"/> values.
/// </summary>
public PngBitDepth BitDepth { get; set; } = PngBitDepth.Bit8;
public PngBitDepth? BitDepth { get; set; }
/// <summary>
/// Gets or sets the color type.
/// </summary>
public PngColorType ColorType { get; set; } = PngColorType.RgbWithAlpha;
public PngColorType? ColorType { get; set; }
/// <summary>
/// Gets or sets the filter method.
@ -36,18 +36,15 @@ namespace SixLabors.ImageSharp.Formats.Png
public int CompressionLevel { get; set; } = 6;
/// <summary>
/// Gets or sets the gamma value, that will be written
/// the the stream, when the <see cref="WriteGamma"/> property
/// is set to true. The default value is 2.2F.
/// Gets or sets the gamma value, that will be written the the image.
/// </summary>
/// <value>The gamma value of the image.</value>
public float Gamma { get; set; } = 2.2F;
public float? Gamma { get; set; }
/// <summary>
/// Gets or sets quantizer for reducing the color count.
/// Defaults to the <see cref="WuQuantizer"/>
/// </summary>
public IQuantizer Quantizer { get; set; } = new WuQuantizer();
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.

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

@ -4,7 +4,6 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Filters;
@ -45,49 +44,49 @@ namespace SixLabors.ImageSharp.Formats.Png
private readonly Crc32 crc = new Crc32();
/// <summary>
/// The png bit depth
/// The png filter method.
/// </summary>
private readonly PngBitDepth pngBitDepth;
private readonly PngFilterMethod pngFilterMethod;
/// <summary>
/// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
/// Gets or sets the CompressionLevel value
/// </summary>
private readonly bool use16Bit;
private readonly int compressionLevel;
/// <summary>
/// The png color type.
/// Gets or sets the alpha threshold value
/// </summary>
private readonly PngColorType pngColorType;
private readonly byte threshold;
/// <summary>
/// The png filter method.
/// The quantizer for reducing the color count.
/// </summary>
private readonly PngFilterMethod pngFilterMethod;
private IQuantizer quantizer;
/// <summary>
/// The quantizer for reducing the color count.
/// Gets or sets a value indicating whether to write the gamma chunk
/// </summary>
private readonly IQuantizer quantizer;
private bool writeGamma;
/// <summary>
/// Gets or sets the CompressionLevel value
/// The png bit depth
/// </summary>
private readonly int compressionLevel;
private PngBitDepth? pngBitDepth;
/// <summary>
/// Gets or sets the Gamma value
/// Gets or sets a value indicating whether to use 16 bit encoding for supported color types.
/// </summary>
private readonly float gamma;
private bool use16Bit;
/// <summary>
/// Gets or sets the Threshold value
/// The png color type.
/// </summary>
private readonly byte threshold;
private PngColorType? pngColorType;
/// <summary>
/// Gets or sets a value indicating whether to Write Gamma
/// Gets or sets the Gamma value
/// </summary>
private readonly bool writeGamma;
private float? gamma;
/// <summary>
/// The image width.
@ -158,14 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Png
{
this.memoryAllocator = memoryAllocator;
this.pngBitDepth = options.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
this.pngColorType = options.ColorType;
this.pngFilterMethod = options.FilterMethod;
this.compressionLevel = options.CompressionLevel;
this.gamma = options.Gamma;
this.quantizer = options.Quantizer;
this.threshold = options.Threshold;
this.writeGamma = options.WriteGamma;
}
/// <summary>
@ -183,23 +180,41 @@ namespace SixLabors.ImageSharp.Formats.Png
this.width = image.Width;
this.height = image.Height;
// Always take the encoder options over the metadata values.
PngMetaData pngMetaData = image.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);
QuantizedFrame<TPixel> quantized = null;
ReadOnlySpan<byte> quantizedPixelsSpan = default;
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.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
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
if (bits == 3)
{
bits = 4;
}
else if (bits >= 5 || bits <= 7)
else if (bits >= 5 && bits <= 7)
{
bits = 8;
}
@ -217,7 +232,7 @@ namespace SixLabors.ImageSharp.Formats.Png
width: image.Width,
height: image.Height,
bitDepth: this.bitDepth,
colorType: this.pngColorType,
colorType: this.pngColorType.Value,
compressionMethod: 0, // None
filterMethod: 0,
interlaceMethod: 0); // TODO: Can't write interlaced yet.
@ -549,7 +564,7 @@ namespace SixLabors.ImageSharp.Formats.Png
byte pixelCount = palette.Length.ToByte();
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3;
int colorTableLength = ImageMaths.GetColorCountForBitDepth(header.BitDepth) * 3;
Rgba32 rgba = default;
bool anyAlpha = false;
@ -693,7 +708,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
this.bytesPerScanline = this.width * this.bytesPerPixel;
this.bytesPerScanline = this.CalculateScanlineLength(this.width);
int resultLength = this.bytesPerScanline + 1;
this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
@ -781,10 +796,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Writes the chunk end to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
private void WriteEndChunk(Stream stream)
{
this.WriteChunk(stream, PngChunkType.End, null);
}
private void WriteEndChunk(Stream stream) => this.WriteChunk(stream, PngChunkType.End, null);
/// <summary>
/// Writes a chunk to the stream.
@ -792,10 +804,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
private void WriteChunk(Stream stream, PngChunkType type, byte[] data)
{
this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
}
private void WriteChunk(Stream stream, PngChunkType type, byte[] data) => this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
/// <summary>
/// Writes a chunk of a specified length to the stream at the given offset.
@ -827,5 +836,26 @@ namespace SixLabors.ImageSharp.Formats.Png
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>
/// Registers the image encoders, decoders and mime type detectors for the png format.
/// </summary>
internal sealed class PngFormat : IImageFormat
internal sealed class PngFormat : IImageFormat<PngMetaData>
{
private PngFormat()
{
}
/// <summary>
/// Gets the current instance.
/// </summary>
public static PngFormat Instance { get; } = new PngFormat();
/// <inheritdoc/>
public string Name => "PNG";
@ -21,5 +30,8 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc/>
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/>
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)

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();
}
}

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

@ -1,7 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Gif
namespace SixLabors.ImageSharp.MetaData
{
/// <summary>
/// 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.
// 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
{
@ -10,6 +12,8 @@ namespace SixLabors.ImageSharp.MetaData
/// </summary>
public sealed class ImageFrameMetaData
{
private readonly Dictionary<IImageFormat, object> formatMetaData = new Dictionary<IImageFormat, object>();
/// <summary>
/// Initializes a new instance of the <see cref="ImageFrameMetaData"/> class.
/// </summary>
@ -28,32 +32,39 @@ namespace SixLabors.ImageSharp.MetaData
{
DebugGuard.NotNull(other, nameof(other));
this.FrameDelay = other.FrameDelay;
this.DisposalMethod = other.DisposalMethod;
foreach (KeyValuePair<IImageFormat, object> meta in other.formatMetaData)
{
this.formatMetaData.Add(meta.Key, meta.Value);
}
}
/// <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.
/// Clones this ImageFrameMetaData.
/// </summary>
public DisposalMethod DisposalMethod { get; set; }
/// <returns>The cloned instance.</returns>
public ImageFrameMetaData Clone() => new ImageFrameMetaData(this);
/// <summary>
/// Clones this ImageFrameMetaData.
/// Gets the metadata value associated with the specified key.
/// </summary>
/// <returns>The cloned instance.</returns>
public ImageFrameMetaData Clone()
/// <typeparam name="TFormatMetaData">The type of format metadata.</typeparam>
/// <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.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.MetaData.Profiles.Icc;
@ -24,6 +26,7 @@ namespace SixLabors.ImageSharp.MetaData
/// </summary>
public const double DefaultVerticalResolution = 96;
private readonly Dictionary<IImageFormat, object> formatMetaData = new Dictionary<IImageFormat, object>();
private double horizontalResolution;
private double verticalResolution;
@ -48,7 +51,11 @@ namespace SixLabors.ImageSharp.MetaData
this.HorizontalResolution = other.HorizontalResolution;
this.VerticalResolution = other.VerticalResolution;
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)
{
@ -125,10 +132,31 @@ namespace SixLabors.ImageSharp.MetaData
public IList<ImageProperty> Properties { get; } = new List<ImageProperty>();
/// <summary>
/// Gets or sets the number of times any animation is repeated.
/// <remarks>0 means to repeat indefinitely.</remarks>
/// Gets the metadata value associated with the specified key.
/// </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>
public ushort RepeatCount { get; set; }
/// <returns>The cloned metadata instance</returns>
public ImageMetaData Clone() => new ImageMetaData(this);
/// <summary>
/// Looks up a property with the provided name.
@ -153,21 +181,9 @@ namespace SixLabors.ImageSharp.MetaData
return false;
}
/// <summary>
/// Clones this into a new instance
/// </summary>
/// <returns>The cloned metadata instance</returns>
public ImageMetaData Clone()
{
return new ImageMetaData(this);
}
/// <summary>
/// Synchronizes the profiles with the current meta data.
/// </summary>
internal void SyncProfiles()
{
this.ExifProfile?.Sync(this);
}
internal void SyncProfiles() => 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>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>()
where TPixel : struct, IPixel<TPixel>;
/// <summary>
/// Creates the generic frame quantizer
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="maxColors">The maximum number of colors to hold in the color palette.</param>
/// <returns>The <see cref="IFrameQuantizer{TPixel}"/></returns>
IFrameQuantizer<TPixel> CreateFrameQuantizer<TPixel>(int maxColors)
where TPixel : struct, IPixel<TPixel>;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

13
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -4,6 +4,7 @@
using System;
using System.Linq;
using Moq;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.IO;
using Xunit;
// ReSharper disable InconsistentNaming
@ -37,19 +38,13 @@ namespace SixLabors.ImageSharp.Tests
/// Test that the default configuration is not null.
/// </summary>
[Fact]
public void TestDefaultConfigurationIsNotNull()
{
Assert.True(Configuration.Default != null);
}
public void TestDefaultConfigurationIsNotNull() => Assert.True(Configuration.Default != null);
/// <summary>
/// Test that the default configuration read origin options is set to begin.
/// </summary>
[Fact]
public void TestDefaultConfigurationReadOriginIsCurrent()
{
Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current);
}
public void TestDefaultConfigurationReadOriginIsCurrent() => Assert.True(Configuration.Default.ReadOrigin == ReadOrigin.Current);
/// <summary>
/// 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());
config.ImageFormatsManager.AddImageFormat(ImageFormats.Bmp);
config.ImageFormatsManager.AddImageFormat(BmpFormat.Instance);
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 }
};
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; }
@ -61,13 +65,34 @@ namespace SixLabors.ImageSharp.Tests
}
[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>
[MemberData(nameof(BmpBitsPerPixelFiles))]
public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel)
{
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]
[WithTestPatternImages(nameof(BitsPerPixel), 48, 24, 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)]
[WithTestPatternImages(nameof(BitsPerPixel), 7, 5, PixelTypes.Rgba32)]
public void Encode_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : struct, IPixel<TPixel>
{
TestBmpEncoderCore(provider, bitsPerPixel);
}
where TPixel : struct, IPixel<TPixel> => TestBmpEncoderCore(provider, bitsPerPixel);
private static void TestBmpEncoderCore<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
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())
{
string filename = path + "/" + file.FileNameWithoutExtension + ".txt";
File.WriteAllText(filename, image.ToBase64String(ImageFormats.Png));
File.WriteAllText(filename, image.ToBase64String(PngFormat.Instance));
}
}
}

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

@ -179,5 +179,49 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
Assert.True(fileInfoGlobal.Length < fileInfoLocal.Length);
}
}
[Fact]
public void NonMutatingEncodePreservesPaletteCount()
{
using (var inStream = new MemoryStream(TestFile.Create(TestImages.Gif.Leo).Bytes))
using (var outStream = new MemoryStream())
{
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]
public void TestPackedValue()
{
Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(DisposalMethod.Unspecified, false, false));
Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(DisposalMethod.RestoreToBackground, true, true));
Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(DisposalMethod.NotDispose, false, false));
Assert.Equal(14, GifGraphicControlExtension.GetPackedValue(DisposalMethod.RestoreToPrevious, true, false));
Assert.Equal(0, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.Unspecified, false, false));
Assert.Equal(11, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.RestoreToBackground, true, true));
Assert.Equal(4, GifGraphicControlExtension.GetPackedValue(GifDisposalMethod.NotDispose, false, 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]
public void TestPackedValue()
{
Assert.Equal(128, GifImageDescriptor.GetPackedValue(true, false, false, 1)); // localColorTable
Assert.Equal(64, GifImageDescriptor.GetPackedValue(false, true, false, 1)); // interfaceFlag
Assert.Equal(32, GifImageDescriptor.GetPackedValue(false, false, true, 1)); // sortFlag
Assert.Equal(224, GifImageDescriptor.GetPackedValue(true, true, true, 1)); // all
Assert.Equal(7, GifImageDescriptor.GetPackedValue(false, false, false, 8));
Assert.Equal(227, GifImageDescriptor.GetPackedValue(true, true, true, 4));
Assert.Equal(231, GifImageDescriptor.GetPackedValue(true, true, true, 8));
Assert.Equal(129, GifImageDescriptor.GetPackedValue(true, false, false, 1)); // localColorTable
Assert.Equal(65, GifImageDescriptor.GetPackedValue(false, true, false, 1)); // interfaceFlag
Assert.Equal(33, GifImageDescriptor.GetPackedValue(false, false, true, 1)); // sortFlag
Assert.Equal(225, GifImageDescriptor.GetPackedValue(true, true, true, 1)); // all
Assert.Equal(8, GifImageDescriptor.GetPackedValue(false, false, false, 8));
Assert.Equal(228, GifImageDescriptor.GetPackedValue(true, true, true, 4));
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>(() =>
{
this.DefaultFormatsManager.SetEncoder(ImageFormats.Bmp, null);
this.DefaultFormatsManager.SetEncoder(BmpFormat.Instance, null);
});
Assert.Throws<ArgumentNullException>(() =>
{
@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Tests
});
Assert.Throws<ArgumentNullException>(() =>
{
this.DefaultFormatsManager.SetDecoder(ImageFormats.Bmp, null);
this.DefaultFormatsManager.SetDecoder(BmpFormat.Instance, null);
});
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 }
};
public static readonly TheoryData<string, int> QualityFiles =
new TheoryData<string, int>
{
{ TestImages.Jpeg.Baseline.Calliphora, 80},
{ TestImages.Jpeg.Progressive.Fb, 75 }
};
[Theory]
[MemberData(nameof(MetaDataTestData))]
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)
{
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 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 =
new TheoryData<JpegSubsample, int>
{
@ -34,6 +41,29 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
{ 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]
[WithFile(TestImages.Png.CalliphoraPartial, nameof(BitsPerPixel_Quality), 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)]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)]
public void EncodeBaseline_WorksWithDifferentSizes<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : struct, IPixel<TPixel>
{
TestJpegEncoderCore(provider, subsample, quality);
}
where TPixel : struct, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
[Theory]
[WithTestPatternImages(nameof(BitsPerPixel_Quality), 48, 48, PixelTypes.Rgba32 | PixelTypes.Bgra32)]
public void EncodeBaseline_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider, JpegSubsample subsample, int quality)
where TPixel : struct, IPixel<TPixel>
{
TestJpegEncoderCore(provider, subsample, quality);
}
where TPixel : struct, IPixel<TPixel> => TestJpegEncoderCore(provider, subsample, quality);
/// <summary>
/// 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]
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?
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>
/// All types except Palette
/// </summary>
@ -221,7 +228,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
: image;
float paletteToleranceHack = 80f / paletteSize;
paletteToleranceHack = paletteToleranceHack * paletteToleranceHack;
paletteToleranceHack *= paletteToleranceHack;
ImageComparer comparer = pngColorType == PngColorType.Palette
? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack)
: 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.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.MetaData;
using Xunit;
@ -16,14 +15,22 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void ConstructorImageFrameMetaData()
{
ImageFrameMetaData metaData = new ImageFrameMetaData();
metaData.FrameDelay = 42;
metaData.DisposalMethod = DisposalMethod.RestoreToBackground;
const int frameDelay = 42;
const int colorTableLength = 128;
const 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);
Assert.Equal(DisposalMethod.RestoreToBackground, clone.DisposalMethod);
var clone = new ImageFrameMetaData(metaData);
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.VerticalResolution = 2;
metaData.Properties.Add(imageProperty);
metaData.RepeatCount = 1;
ImageMetaData clone = metaData.Clone();
@ -36,7 +35,6 @@ namespace SixLabors.ImageSharp.Tests
Assert.Equal(4, clone.HorizontalResolution);
Assert.Equal(2, clone.VerticalResolution);
Assert.Equal(imageProperty, clone.Properties[0]);
Assert.Equal(1, clone.RepeatCount);
}
[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 Bit16 = "Bmp/test16.bmp";
public const string Bit16Inverted = "Bmp/test16-inverted.bmp";
public const string Bit32Rgb = "Bmp/rgb32.bmp";
public static readonly string[] All
= {
@ -204,6 +205,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Cheers = "Gif/cheers.gif";
public const string Trans = "Gif/trans.gif";
public const string Kumin = "Gif/kumin.gif";
public const string Leo = "Gif/leo.gif";
public const string Ratio4x1 = "Gif/base_4x1.gif";
public const string Ratio1x4 = "Gif/base_1x4.gif";
@ -214,7 +216,7 @@ namespace SixLabors.ImageSharp.Tests
public const string BadDescriptorWidth = "Gif/issues/issue403_baddescriptorwidth.gif";
}
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Ratio4x1, Ratio1x4 };
public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 };
}
}
}

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Loading…
Cancel
Save