Browse Source

Convert gif format.

Former-commit-id: 085c4bc99388de35847b2b8dd26b20d518e272de
Former-commit-id: e8b3af949ca0f63f001483188dbab20cd263c10f
Former-commit-id: 8660e1a08e5da2bfebcc269d75ee20488542d4f6
pull/1/head
James Jackson-South 10 years ago
parent
commit
5ef0e7f43c
  1. 2
      src/ImageProcessorCore/Bootstrapper.cs
  2. 132
      src/ImageProcessorCore/Formats/Gif/BitEncoder.cs
  3. 37
      src/ImageProcessorCore/Formats/Gif/DisposalMethod.cs
  4. 83
      src/ImageProcessorCore/Formats/Gif/GifConstants.cs
  5. 65
      src/ImageProcessorCore/Formats/Gif/GifDecoder.cs
  6. 426
      src/ImageProcessorCore/Formats/Gif/GifDecoderCore.cs
  7. 64
      src/ImageProcessorCore/Formats/Gif/GifEncoder.cs
  8. 313
      src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs
  9. 19
      src/ImageProcessorCore/Formats/Gif/GifFormat.cs
  10. 231
      src/ImageProcessorCore/Formats/Gif/LzwDecoder.cs
  11. 385
      src/ImageProcessorCore/Formats/Gif/LzwEncoder.cs
  12. 194
      src/ImageProcessorCore/Formats/Gif/PackedField.cs
  13. 4
      src/ImageProcessorCore/Formats/Gif/README.md
  14. 42
      src/ImageProcessorCore/Formats/Gif/Sections/GifGraphicsControlExtension.cs
  15. 59
      src/ImageProcessorCore/Formats/Gif/Sections/GifImageDescriptor.cs
  16. 55
      src/ImageProcessorCore/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs
  17. 3
      src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs
  18. 66
      src/ImageProcessorCore/Image/ImageExtensions.cs
  19. 5
      src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs
  20. 6
      tests/ImageProcessorCore.Tests/FileTestBase.cs
  21. 2
      tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

2
src/ImageProcessorCore/Bootstrapper.cs

@ -40,7 +40,7 @@ namespace ImageProcessorCore
new BmpFormat(),
//new JpegFormat(),
new PngFormat(),
//new GifFormat()
new GifFormat()
};
this.pixelAccessors = new Dictionary<Type, Func<IImageBase, IPixelAccessor>>

132
src/ImageProcessorCore/Formats/Gif/BitEncoder.cs

@ -0,0 +1,132 @@
// <copyright file="BitEncoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System.Collections.Generic;
/// <summary>
/// Handles the encoding of bits for compression.
/// </summary>
internal class BitEncoder
{
/// <summary>
/// The inner list for collecting the bits.
/// </summary>
private readonly List<byte> list = new List<byte>();
/// <summary>
/// The current working bit.
/// </summary>
private int currentBit;
/// <summary>
/// The current value.
/// </summary>
private int currentValue;
/// <summary>
/// Initializes a new instance of the <see cref="BitEncoder"/> class.
/// </summary>
/// <param name="initial">
/// The initial bits.
/// </param>
public BitEncoder(int initial)
{
this.IntitialBit = initial;
}
/// <summary>
/// Gets or sets the intitial bit.
/// </summary>
public int IntitialBit { get; set; }
/// <summary>
/// The number of bytes in the encoder.
/// </summary>
public int Length => this.list.Count;
/// <summary>
/// Adds the current byte to the end of the encoder.
/// </summary>
/// <param name="item">
/// The byte to add.
/// </param>
public void Add(int item)
{
this.currentValue |= item << this.currentBit;
this.currentBit += this.IntitialBit;
while (this.currentBit >= 8)
{
byte value = (byte)(this.currentValue & 0XFF);
this.currentValue = this.currentValue >> 8;
this.currentBit -= 8;
this.list.Add(value);
}
}
/// <summary>
/// Adds the collection of bytes to the end of the encoder.
/// </summary>
/// <param name="collection">
/// The collection of bytes to add.
/// The collection itself cannot be null but can contain elements that are null.</param>
public void AddRange(byte[] collection)
{
this.list.AddRange(collection);
}
/// <summary>
/// Copies a range of elements from the encoder to a compatible one-dimensional array,
/// starting at the specified index of the target array.
/// </summary>
/// <param name="index">
/// The zero-based index in the source <see cref="BitEncoder"/> at which copying begins.
/// </param>
/// <param name="array">
/// The one-dimensional Array that is the destination of the elements copied
/// from <see cref="BitEncoder"/>. The Array must have zero-based indexing
/// </param>
/// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
/// <param name="count">The number of bytes to copy.</param>
public void CopyTo(int index, byte[] array, int arrayIndex, int count)
{
this.list.CopyTo(index, array, arrayIndex, count);
}
/// <summary>
/// Removes all the bytes from the encoder.
/// </summary>
public void Clear()
{
this.list.Clear();
}
/// <summary>
/// Copies the bytes into a new array.
/// </summary>
/// <returns><see cref="T:byte[]"/></returns>
public byte[] ToArray()
{
return this.list.ToArray();
}
/// <summary>
/// The end.
/// </summary>
internal void End()
{
while (this.currentBit > 0)
{
byte value = (byte)(this.currentValue & 0XFF);
this.currentValue = this.currentValue >> 8;
this.currentBit -= 8;
this.list.Add(value);
}
}
}
}

37
src/ImageProcessorCore/Formats/Gif/DisposalMethod.cs

@ -0,0 +1,37 @@
// <copyright file="DisposalMethod.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Provides enumeration for instructing the decoder what to do with the last image
/// in an animation sequence.
/// <see href="http://www.w3.org/Graphics/GIF/spec-gif89a.txt"/> section 23
/// </summary>
public enum DisposalMethod
{
/// <summary>
/// No disposal specified. The decoder is not required to take any action.
/// </summary>
Unspecified = 0,
/// <summary>
/// Do not dispose. The graphic is to be left in place.
/// </summary>
NotDispose = 1,
/// <summary>
/// Restore to background color. The area used by the graphic must be restored to
/// the background color.
/// </summary>
RestoreToBackground = 2,
/// <summary>
/// Restore to previous. The decoder is required to restore the area overwritten by the
/// graphic with what was there prior to rendering the graphic.
/// </summary>
RestoreToPrevious = 3
}
}

83
src/ImageProcessorCore/Formats/Gif/GifConstants.cs

@ -0,0 +1,83 @@
// <copyright file="GifConstants.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Constants that define specific points within a gif.
/// </summary>
internal sealed class GifConstants
{
/// <summary>
/// The file type.
/// </summary>
public const string FileType = "GIF";
/// <summary>
/// The file version.
/// </summary>
public const string FileVersion = "89a";
/// <summary>
/// The extension block introducer <value>!</value>.
/// </summary>
public const byte ExtensionIntroducer = 0x21;
/// <summary>
/// The graphic control label.
/// </summary>
public const byte GraphicControlLabel = 0xF9;
/// <summary>
/// The application extension label.
/// </summary>
public const byte ApplicationExtensionLabel = 0xFF;
/// <summary>
/// The application identification.
/// </summary>
public const string ApplicationIdentification = "NETSCAPE2.0";
/// <summary>
/// The application block size.
/// </summary>
public const byte ApplicationBlockSize = 0x0b;
/// <summary>
/// The comment label.
/// </summary>
public const byte CommentLabel = 0xFE;
/// <summary>
/// The maximum comment length.
/// </summary>
public const int MaxCommentLength = 1024 * 8;
/// <summary>
/// The image descriptor label <value>,</value>.
/// </summary>
public const byte ImageDescriptorLabel = 0x2C;
/// <summary>
/// The plain text label.
/// </summary>
public const byte PlainTextLabel = 0x01;
/// <summary>
/// The image label introducer <value>,</value>.
/// </summary>
public const byte ImageLabel = 0x2C;
/// <summary>
/// The terminator.
/// </summary>
public const byte Terminator = 0;
/// <summary>
/// The end introducer trailer <value>;</value>.
/// </summary>
public const byte EndIntroducer = 0x3B;
}
}

65
src/ImageProcessorCore/Formats/Gif/GifDecoder.cs

@ -0,0 +1,65 @@
// <copyright file="GifDecoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
/// <summary>
/// Decoder for generating an image out of a gif encoded stream.
/// </summary>
public class GifDecoder : IImageDecoder
{
/// <summary>
/// Gets the size of the header for this image type.
/// </summary>
/// <value>The size of the header.</value>
public int HeaderSize => 6;
/// <summary>
/// Returns a value indicating whether the <see cref="IImageDecoder"/> supports the specified
/// file header.
/// </summary>
/// <param name="extension">The <see cref="string"/> containing the file extension.</param>
/// <returns>
/// True if the decoder supports the file extension; otherwise, false.
/// </returns>
public bool IsSupportedFileExtension(string extension)
{
Guard.NotNullOrEmpty(extension, nameof(extension));
extension = extension.StartsWith(".") ? extension.Substring(1) : extension;
return extension.Equals("GIF", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Returns a value indicating whether the <see cref="IImageDecoder"/> supports the specified
/// file header.
/// </summary>
/// <param name="header">The <see cref="T:byte[]"/> containing the file header.</param>
/// <returns>
/// True if the decoder supports the file header; otherwise, false.
/// </returns>
public bool IsSupportedFileFormat(byte[] header)
{
return header.Length >= 6 &&
header[0] == 0x47 && // G
header[1] == 0x49 && // I
header[2] == 0x46 && // F
header[3] == 0x38 && // 8
(header[4] == 0x39 || header[4] == 0x37) && // 9 or 7
header[5] == 0x61; // a
}
/// <inheritdoc/>
public void Decode<T, TP>(Image<T, TP> image, Stream stream)
where T : IPackedVector<TP>
where TP : struct
{
new GifDecoderCore<T, TP>().Decode(image, stream);
}
}
}

426
src/ImageProcessorCore/Formats/Gif/GifDecoderCore.cs

@ -0,0 +1,426 @@
// <copyright file="GifDecoderCore.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
/// <summary>
/// Performs the gif decoding operation.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
internal class GifDecoderCore<T, TP>
where T : IPackedVector<TP>
where TP : struct
{
/// <summary>
/// The image to decode the information to.
/// </summary>
private Image<T, TP> decodedImage;
/// <summary>
/// The currently loaded stream.
/// </summary>
private Stream currentStream;
/// <summary>
/// The global color table.
/// </summary>
private byte[] globalColorTable;
/// <summary>
/// The current frame.
/// </summary>
private T[] currentFrame;
/// <summary>
/// The logical screen descriptor.
/// </summary>
private GifLogicalScreenDescriptor logicalScreenDescriptor;
/// <summary>
/// The graphics control extension.
/// </summary>
private GifGraphicsControlExtension graphicsControlExtension;
/// <summary>
/// Decodes the stream to the image.
/// </summary>
/// <param name="image">The image to decode to.</param>
/// <param name="stream">The stream containing image data. </param>
public void Decode(Image<T, TP> image, Stream stream)
{
this.decodedImage = image;
this.currentStream = stream;
// Skip the identifier
this.currentStream.Seek(6, SeekOrigin.Current);
this.ReadLogicalScreenDescriptor();
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
this.globalColorTable = new byte[this.logicalScreenDescriptor.GlobalColorTableSize * 3];
// Read the global color table from the stream
stream.Read(this.globalColorTable, 0, this.globalColorTable.Length);
}
// Loop though the respective gif parts and read the data.
int nextFlag = stream.ReadByte();
while (nextFlag != GifConstants.Terminator)
{
if (nextFlag == GifConstants.ImageLabel)
{
this.ReadFrame();
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
int label = stream.ReadByte();
switch (label)
{
case GifConstants.GraphicControlLabel:
this.ReadGraphicalControlExtension();
break;
case GifConstants.CommentLabel:
this.ReadComments();
break;
case GifConstants.ApplicationExtensionLabel:
this.Skip(12); // No need to read.
break;
case GifConstants.PlainTextLabel:
this.Skip(13); // Not supported by any known decoder.
break;
}
}
else if (nextFlag == GifConstants.EndIntroducer)
{
break;
}
nextFlag = stream.ReadByte();
}
}
/// <summary>
/// Reads the graphic control extension.
/// </summary>
private void ReadGraphicalControlExtension()
{
byte[] buffer = new byte[6];
this.currentStream.Read(buffer, 0, buffer.Length);
byte packed = buffer[1];
this.graphicsControlExtension = new GifGraphicsControlExtension
{
DelayTime = BitConverter.ToInt16(buffer, 2),
TransparencyIndex = buffer[4],
TransparencyFlag = (packed & 0x01) == 1,
DisposalMethod = (DisposalMethod)((packed & 0x1C) >> 2)
};
}
/// <summary>
/// Reads the image descriptor
/// </summary>
/// <returns><see cref="GifImageDescriptor"/></returns>
private GifImageDescriptor ReadImageDescriptor()
{
byte[] buffer = new byte[9];
this.currentStream.Read(buffer, 0, buffer.Length);
byte packed = buffer[8];
GifImageDescriptor imageDescriptor = new GifImageDescriptor
{
Left = BitConverter.ToInt16(buffer, 0),
Top = BitConverter.ToInt16(buffer, 2),
Width = BitConverter.ToInt16(buffer, 4),
Height = BitConverter.ToInt16(buffer, 6),
LocalColorTableFlag = ((packed & 0x80) >> 7) == 1,
LocalColorTableSize = 2 << (packed & 0x07),
InterlaceFlag = ((packed & 0x40) >> 6) == 1
};
return imageDescriptor;
}
/// <summary>
/// Reads the logical screen descriptor.
/// </summary>
private void ReadLogicalScreenDescriptor()
{
byte[] buffer = new byte[7];
this.currentStream.Read(buffer, 0, buffer.Length);
byte packed = buffer[4];
this.logicalScreenDescriptor = new GifLogicalScreenDescriptor
{
Width = BitConverter.ToInt16(buffer, 0),
Height = BitConverter.ToInt16(buffer, 2),
BackgroundColorIndex = buffer[5],
PixelAspectRatio = buffer[6],
GlobalColorTableFlag = ((packed & 0x80) >> 7) == 1,
GlobalColorTableSize = 2 << (packed & 0x07)
};
if (this.logicalScreenDescriptor.GlobalColorTableSize > 255 * 4)
{
throw new ImageFormatException(
$"Invalid gif colormap size '{this.logicalScreenDescriptor.GlobalColorTableSize}'");
}
if (this.logicalScreenDescriptor.Width > this.decodedImage.MaxWidth || this.logicalScreenDescriptor.Height > this.decodedImage.MaxHeight)
{
throw new ArgumentOutOfRangeException(
$"The input gif '{this.logicalScreenDescriptor.Width}x{this.logicalScreenDescriptor.Height}' is bigger then the max allowed size '{this.decodedImage.MaxWidth}x{this.decodedImage.MaxHeight}'");
}
}
/// <summary>
/// Skips the designated number of bytes in the stream.
/// </summary>
/// <param name="length">The number of bytes to skip.</param>
private void Skip(int length)
{
this.currentStream.Seek(length, SeekOrigin.Current);
int flag;
while ((flag = this.currentStream.ReadByte()) != 0)
{
this.currentStream.Seek(flag, SeekOrigin.Current);
}
}
/// <summary>
/// Reads the gif comments.
/// </summary>
private void ReadComments()
{
int flag;
while ((flag = this.currentStream.ReadByte()) != 0)
{
if (flag > GifConstants.MaxCommentLength)
{
throw new ImageFormatException($"Gif comment length '{flag}' exceeds max '{GifConstants.MaxCommentLength}'");
}
byte[] buffer = new byte[flag];
this.currentStream.Read(buffer, 0, flag);
this.decodedImage.Properties.Add(new ImageProperty("Comments", BitConverter.ToString(buffer)));
}
}
/// <summary>
/// Reads an individual gif frame.
/// </summary>
private void ReadFrame()
{
GifImageDescriptor imageDescriptor = this.ReadImageDescriptor();
byte[] localColorTable = this.ReadFrameLocalColorTable(imageDescriptor);
byte[] indices = this.ReadFrameIndices(imageDescriptor);
// Determine the color table for this frame. If there is a local one, use it
// otherwise use the global color table.
byte[] colorTable = localColorTable ?? this.globalColorTable;
this.ReadFrameColors(indices, colorTable, imageDescriptor);
// Skip any remaining blocks
this.Skip(0);
}
/// <summary>
/// Reads the frame indices marking the color to use for each pixel.
/// </summary>
/// <param name="imageDescriptor">The <see cref="GifImageDescriptor"/>.</param>
/// <returns>The <see cref="T:byte[]"/></returns>
private byte[] ReadFrameIndices(GifImageDescriptor imageDescriptor)
{
int dataSize = this.currentStream.ReadByte();
LzwDecoder lzwDecoder = new LzwDecoder(this.currentStream);
byte[] indices = lzwDecoder.DecodePixels(imageDescriptor.Width, imageDescriptor.Height, dataSize);
return indices;
}
/// <summary>
/// Reads the local color table from the current frame.
/// </summary>
/// <param name="imageDescriptor">The <see cref="GifImageDescriptor"/>.</param>
/// <returns>The <see cref="T:byte[]"/></returns>
private byte[] ReadFrameLocalColorTable(GifImageDescriptor imageDescriptor)
{
byte[] localColorTable = null;
if (imageDescriptor.LocalColorTableFlag)
{
localColorTable = new byte[imageDescriptor.LocalColorTableSize * 3];
this.currentStream.Read(localColorTable, 0, localColorTable.Length);
}
return localColorTable;
}
/// <summary>
/// Reads the frames colors, mapping indices to colors.
/// </summary>
/// <param name="indices">The indexed pixels.</param>
/// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
private void ReadFrameColors(byte[] indices, byte[] colorTable, GifImageDescriptor descriptor)
{
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
if (this.currentFrame == null)
{
this.currentFrame = new T[imageWidth * imageHeight];
}
T[] lastFrame = null;
if (this.graphicsControlExtension != null &&
this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
{
lastFrame = new T[imageWidth * imageHeight];
Array.Copy(this.currentFrame, lastFrame, lastFrame.Length);
}
int offset, i = 0;
int interlacePass = 0; // The interlace pass
int interlaceIncrement = 8; // The interlacing line increment
int interlaceY = 0; // The current interlaced line
for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++)
{
// Check if this image is interlaced.
int writeY; // the target y offset to write to
if (descriptor.InterlaceFlag)
{
// If so then we read lines at predetermined offsets.
// When an entire image height worth of offset lines has been read we consider this a pass.
// With each pass the number of offset lines changes and the starting line changes.
if (interlaceY >= descriptor.Height)
{
interlacePass++;
switch (interlacePass)
{
case 1:
interlaceY = 4;
break;
case 2:
interlaceY = 2;
interlaceIncrement = 4;
break;
case 3:
interlaceY = 1;
interlaceIncrement = 2;
break;
}
}
writeY = interlaceY + descriptor.Top;
interlaceY += interlaceIncrement;
}
else
{
writeY = y;
}
for (int x = descriptor.Left; x < descriptor.Left + descriptor.Width; x++)
{
offset = (writeY * imageWidth) + x;
int index = indices[i];
if (this.graphicsControlExtension == null ||
this.graphicsControlExtension.TransparencyFlag == false ||
this.graphicsControlExtension.TransparencyIndex != index)
{
// Stored in r-> g-> b-> a order.
int indexOffset = index * 3;
T pixel = default(T);
pixel.PackBytes(colorTable[indexOffset], colorTable[indexOffset + 1], colorTable[indexOffset + 2], 255);
this.currentFrame[offset] = pixel;
}
i++;
}
}
T[] pixels = new T[imageWidth * imageHeight];
Array.Copy(this.currentFrame, pixels, pixels.Length);
ImageBase<T, TP> currentImage;
if (this.decodedImage.Pixels == null)
{
currentImage = this.decodedImage;
currentImage.SetPixels(imageWidth, imageHeight, pixels);
currentImage.Quality = colorTable.Length / 3;
if (this.graphicsControlExtension != null && this.graphicsControlExtension.DelayTime > 0)
{
this.decodedImage.FrameDelay = this.graphicsControlExtension.DelayTime;
}
}
else
{
ImageFrame<T, TP> frame = new ImageFrame<T, TP>();
currentImage = frame;
currentImage.SetPixels(imageWidth, imageHeight, pixels);
currentImage.Quality = colorTable.Length / 3;
if (this.graphicsControlExtension != null && this.graphicsControlExtension.DelayTime > 0)
{
currentImage.FrameDelay = this.graphicsControlExtension.DelayTime;
}
this.decodedImage.Frames.Add(frame);
}
if (this.graphicsControlExtension != null)
{
if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground)
{
for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++)
{
for (int x = descriptor.Left; x < descriptor.Left + descriptor.Width; x++)
{
offset = (y * imageWidth) + x;
// Stored in r-> g-> b-> a order.
this.currentFrame[offset] = default(T);
}
}
}
else if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
{
this.currentFrame = lastFrame;
}
}
}
}
}

64
src/ImageProcessorCore/Formats/Gif/GifEncoder.cs

@ -0,0 +1,64 @@
// <copyright file="GifEncoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
using ImageProcessorCore.Quantizers;
/// <summary>
/// Image encoder for writing image data to a stream in gif format.
/// </summary>
public class GifEncoder : IImageEncoder
{
/// <summary>
/// Gets or sets the quality of output for images.
/// </summary>
/// <remarks>For gifs the value ranges from 1 to 256.</remarks>
public int Quality { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 128;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <inheritdoc/>
public string Extension => "gif";
/// <inheritdoc/>
public string MimeType => "image/gif";
/// <inheritdoc/>
public bool IsSupportedFileExtension(string extension)
{
Guard.NotNullOrEmpty(extension, nameof(extension));
extension = extension.StartsWith(".") ? extension.Substring(1) : extension;
return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc/>
public void Encode<T,TP>(ImageBase<T,TP> image, Stream stream)
where T : IPackedVector<TP>
where TP : struct
{
GifEncoderCore encoder = new GifEncoderCore
{
Quality = this.Quality,
Quantizer = this.Quantizer,
Threshold = this.Threshold
};
encoder.Encode(image, stream);
}
}
}

313
src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs

@ -0,0 +1,313 @@
// <copyright file="GifEncoderCore.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using IO;
using Quantizers;
/// <summary>
/// Performs the gif encoding operation.
/// </summary>
internal sealed class GifEncoderCore
{
/// <summary>
/// The number of bits requires to store the image palette.
/// </summary>
private int bitDepth;
/// <summary>
/// Gets or sets the quality of output for images.
/// </summary>
/// <remarks>For gifs the value ranges from 1 to 256.</remarks>
public int Quality { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 128;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageBase{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="imageBase">The <see cref="ImageBase{T,TP}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<T, TP>(ImageBase<T, TP> imageBase, Stream stream)
where T : IPackedVector<TP>
where TP : struct
{
Guard.NotNull(imageBase, nameof(imageBase));
Guard.NotNull(stream, nameof(stream));
Image<T, TP> image = (Image<T, TP>)imageBase;
if (this.Quantizer == null)
{
this.Quantizer = new OctreeQuantizer<T, TP> { Threshold = this.Threshold };
}
// Do not use IDisposable pattern here as we want to preserve the stream.
EndianBinaryWriter writer = new EndianBinaryWriter(EndianBitConverter.Little, stream);
// Ensure that quality can be set but has a fallback.
int quality = this.Quality > 0 ? this.Quality : imageBase.Quality;
this.Quality = quality > 0 ? quality.Clamp(1, 256) : 256;
// Get the number of bits.
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(this.Quality);
// Quantize the image returning a palette.
QuantizedImage<T, TP> quantized = ((IQuantizer<T, TP>)this.Quantizer).Quantize(image, this.Quality);
// Write the header.
this.WriteHeader(writer);
// Write the LSD. We'll use local color tables for now.
this.WriteLogicalScreenDescriptor(image, writer, quantized.TransparentIndex);
// Write the first frame.
this.WriteGraphicalControlExtension(imageBase, writer, quantized.TransparentIndex);
this.WriteImageDescriptor(image, writer);
this.WriteColorTable(quantized, writer);
this.WriteImageData(quantized, writer);
// Write additional frames.
if (image.Frames.Any())
{
this.WriteApplicationExtension(writer, image.RepeatCount, image.Frames.Count);
foreach (ImageFrame<T, TP> frame in image.Frames)
{
QuantizedImage<T, TP> quantizedFrame = ((IQuantizer<T, TP>)this.Quantizer).Quantize(frame, this.Quality);
this.WriteGraphicalControlExtension(frame, writer, quantizedFrame.TransparentIndex);
this.WriteImageDescriptor(frame, writer);
this.WriteColorTable(quantizedFrame, writer);
this.WriteImageData(quantizedFrame, writer);
}
}
// TODO: Write Comments extension etc
writer.Write(GifConstants.EndIntroducer);
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>
/// <param name="writer">The writer to write to the stream with.</param>
private void WriteHeader(EndianBinaryWriter writer)
{
writer.Write((GifConstants.FileType + GifConstants.FileVersion).ToCharArray());
}
/// <summary>
/// Writes the logical screen descriptor to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The image to encode.</param>
/// <param name="writer">The writer to write to the stream with.</param>
/// <param name="tranparencyIndex">The transparency index to set the default backgound index to.</param>
private void WriteLogicalScreenDescriptor<T, TP>(Image<T, TP> image, EndianBinaryWriter writer, int tranparencyIndex)
where T : IPackedVector<TP>
where TP : struct
{
GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor
{
Width = (short)image.Width,
Height = (short)image.Height,
GlobalColorTableFlag = false, // Always false for now.
GlobalColorTableSize = this.bitDepth - 1,
BackgroundColorIndex = (byte)(tranparencyIndex > -1 ? tranparencyIndex : 255)
};
writer.Write((ushort)descriptor.Width);
writer.Write((ushort)descriptor.Height);
PackedField field = new PackedField();
field.SetBit(0, descriptor.GlobalColorTableFlag); // 1 : Global color table flag = 1 || 0 (GCT used/ not used)
field.SetBits(1, 3, descriptor.GlobalColorTableSize); // 2-4 : color resolution
field.SetBit(4, false); // 5 : GCT sort flag = 0
field.SetBits(5, 3, descriptor.GlobalColorTableSize); // 6-8 : GCT size. 2^(N+1)
// Reduce the number of writes
byte[] arr = {
field.Byte,
descriptor.BackgroundColorIndex, // Background Color Index
descriptor.PixelAspectRatio // Pixel aspect ratio. Assume 1:1
};
writer.Write(arr);
}
/// <summary>
/// Writes the application exstension to the stream.
/// </summary>
/// <param name="writer">The writer to write to the stream with.</param>
/// <param name="repeatCount">The animated image repeat count.</param>
/// <param name="frames">Th number of image frames.</param>
private void WriteApplicationExtension(EndianBinaryWriter writer, ushort repeatCount, int frames)
{
// Application Extension Header
if (repeatCount != 1 && frames > 0)
{
byte[] ext =
{
GifConstants.ExtensionIntroducer,
GifConstants.ApplicationExtensionLabel,
GifConstants.ApplicationBlockSize
};
writer.Write(ext);
writer.Write(GifConstants.ApplicationIdentification.ToCharArray()); // NETSCAPE2.0
writer.Write((byte)3); // Application block length
writer.Write((byte)1); // Data sub-block index (always 1)
// 0 means loop indefinitely. Count is set as play n + 1 times.
repeatCount = (ushort)(Math.Max((ushort)0, repeatCount) - 1);
writer.Write(repeatCount); // Repeat count for images.
writer.Write(GifConstants.Terminator); // Terminator
}
}
/// <summary>
/// Writes the graphics control extension to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The <see cref="ImageBase{T,TP}"/> to encode.</param>
/// <param name="writer">The stream to write to.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
private void WriteGraphicalControlExtension<T, TP>(ImageBase<T, TP> image, EndianBinaryWriter writer, int transparencyIndex)
where T : IPackedVector<TP>
where TP : struct
{
// TODO: Check transparency logic.
bool hasTransparent = transparencyIndex > -1;
DisposalMethod disposalMethod = hasTransparent
? DisposalMethod.RestoreToBackground
: DisposalMethod.Unspecified;
GifGraphicsControlExtension extension = new GifGraphicsControlExtension()
{
DisposalMethod = disposalMethod,
TransparencyFlag = hasTransparent,
TransparencyIndex = transparencyIndex,
DelayTime = image.FrameDelay
};
// Reduce the number of writes.
byte[] intro = {
GifConstants.ExtensionIntroducer,
GifConstants.GraphicControlLabel,
4 // Size
};
writer.Write(intro);
PackedField field = new PackedField();
field.SetBits(3, 3, (int)extension.DisposalMethod); // 1-3 : Reserved, 4-6 : Disposal
// TODO: Allow this as an option.
field.SetBit(6, false); // 7 : User input - 0 = none
field.SetBit(7, extension.TransparencyFlag); // 8: Has transparent.
writer.Write(field.Byte);
writer.Write((ushort)extension.DelayTime);
writer.Write((byte)(extension.TransparencyIndex == -1 ? 255 : extension.TransparencyIndex));
writer.Write(GifConstants.Terminator);
}
/// <summary>
/// Writes the image descriptor to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The <see cref="ImageBase{T,TP}"/> to be encoded.</param>
/// <param name="writer">The stream to write to.</param>
private void WriteImageDescriptor<T, TP>(ImageBase<T, TP> image, EndianBinaryWriter writer)
where T : IPackedVector<TP>
where TP : struct
{
writer.Write(GifConstants.ImageDescriptorLabel); // 2c
// TODO: Can we capture this?
writer.Write((ushort)0); // Left position
writer.Write((ushort)0); // Top position
writer.Write((ushort)image.Width);
writer.Write((ushort)image.Height);
PackedField field = new PackedField();
field.SetBit(0, true); // 1: Local color table flag = 1 (LCT used)
field.SetBit(1, false); // 2: Interlace flag 0
field.SetBit(2, false); // 3: Sort flag 0
field.SetBits(5, 3, this.bitDepth - 1); // 4-5: Reserved, 6-8 : LCT size. 2^(N+1)
writer.Write(field.Byte);
}
/// <summary>
/// Writes the color table to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The <see cref="ImageBase{T,TP}"/> to encode.</param>
/// <param name="writer">The writer to write to the stream with.</param>
private void WriteColorTable<T, TP>(QuantizedImage<T, TP> image, EndianBinaryWriter writer)
where T : IPackedVector<TP>
where TP : struct
{
// Grab the palette and write it to the stream.
T[] palette = image.Palette;
int pixelCount = palette.Length;
// Get max colors for bit depth.
int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3;
byte[] colorTable = new byte[colorTableLength];
Parallel.For(0, pixelCount,
i =>
{
int offset = i * 3;
byte[] color = palette[i].ToBytes();
colorTable[offset] = color[0];
colorTable[offset + 1] = color[1];
colorTable[offset + 2] = color[2];
});
writer.Write(colorTable, 0, colorTableLength);
}
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The <see cref="QuantizedImage{T,TP}"/> containing indexed pixels.</param>
/// <param name="writer">The stream to write to.</param>
private void WriteImageData<T, TP>(QuantizedImage<T, TP> image, EndianBinaryWriter writer)
where T : IPackedVector<TP>
where TP : struct
{
byte[] indexedPixels = image.Pixels;
LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)this.bitDepth);
encoder.Encode(writer.BaseStream);
}
}
}

19
src/ImageProcessorCore/Formats/Gif/GifFormat.cs

@ -0,0 +1,19 @@
// <copyright file="GifFormat.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Encapsulates the means to encode and decode gif images.
/// </summary>
public class GifFormat : IImageFormat
{
/// <inheritdoc/>
public IImageDecoder Decoder => new GifDecoder();
/// <inheritdoc/>
public IImageEncoder Encoder => new GifEncoder();
}
}

231
src/ImageProcessorCore/Formats/Gif/LzwDecoder.cs

@ -0,0 +1,231 @@
// <copyright file="LzwDecoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
/// <summary>
/// Decompresses and decodes data using the dynamic LZW algorithms.
/// </summary>
internal sealed class LzwDecoder
{
/// <summary>
/// The max decoder pixel stack size.
/// </summary>
private const int MaxStackSize = 4096;
/// <summary>
/// The null code.
/// </summary>
private const int NullCode = -1;
/// <summary>
/// The stream to decode.
/// </summary>
private readonly Stream stream;
/// <summary>
/// Initializes a new instance of the <see cref="LzwDecoder"/> class
/// and sets the stream, where the compressed data should be read from.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <exception cref="ArgumentNullException"><paramref name="stream"/> is null.</exception>
public LzwDecoder(Stream stream)
{
Guard.NotNull(stream, nameof(stream));
this.stream = stream;
}
/// <summary>
/// Decodes and decompresses all pixel indices from the stream.
/// <remarks>
/// </remarks>
/// </summary>
/// <param name="width">The width of the pixel index array.</param>
/// <param name="height">The height of the pixel index array.</param>
/// <param name="dataSize">Size of the data.</param>
/// <returns>The decoded and uncompressed array.</returns>
public byte[] DecodePixels(int width, int height, int dataSize)
{
Guard.MustBeLessThan(dataSize, int.MaxValue, nameof(dataSize));
// The resulting index table.
byte[] pixels = new byte[width * height];
// Calculate the clear code. The value of the clear code is 2 ^ dataSize
int clearCode = 1 << dataSize;
int codeSize = dataSize + 1;
// Calculate the end code
int endCode = clearCode + 1;
// Calculate the available code.
int availableCode = clearCode + 2;
// Jillzhangs Code see: http://giflib.codeplex.com/
// Adapted from John Cristy's ImageMagick.
int code;
int oldCode = NullCode;
int codeMask = (1 << codeSize) - 1;
int bits = 0;
int[] prefix = new int[MaxStackSize];
int[] suffix = new int[MaxStackSize];
int[] pixelStatck = new int[MaxStackSize + 1];
int top = 0;
int count = 0;
int bi = 0;
int xyz = 0;
int data = 0;
int first = 0;
for (code = 0; code < clearCode; code++)
{
prefix[code] = 0;
suffix[code] = (byte)code;
}
byte[] buffer = null;
while (xyz < pixels.Length)
{
if (top == 0)
{
if (bits < codeSize)
{
// Load bytes until there are enough bits for a code.
if (count == 0)
{
// Read a new data block.
buffer = this.ReadBlock();
count = buffer.Length;
if (count == 0)
{
break;
}
bi = 0;
}
if (buffer != null)
{
data += buffer[bi] << bits;
}
bits += 8;
bi++;
count--;
continue;
}
// Get the next code
code = data & codeMask;
data >>= codeSize;
bits -= codeSize;
// Interpret the code
if (code > availableCode || code == endCode)
{
break;
}
if (code == clearCode)
{
// Reset the decoder
codeSize = dataSize + 1;
codeMask = (1 << codeSize) - 1;
availableCode = clearCode + 2;
oldCode = NullCode;
continue;
}
if (oldCode == NullCode)
{
pixelStatck[top++] = suffix[code];
oldCode = code;
first = code;
continue;
}
int inCode = code;
if (code == availableCode)
{
pixelStatck[top++] = (byte)first;
code = oldCode;
}
while (code > clearCode)
{
pixelStatck[top++] = suffix[code];
code = prefix[code];
}
first = suffix[code];
pixelStatck[top++] = suffix[code];
// Fix for Gifs that have "deferred clear code" as per here :
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (availableCode < MaxStackSize)
{
prefix[availableCode] = oldCode;
suffix[availableCode] = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
codeSize++;
codeMask = (1 << codeSize) - 1;
}
}
oldCode = inCode;
}
// Pop a pixel off the pixel stack.
top--;
// Clear missing pixels
pixels[xyz++] = (byte)pixelStatck[top];
}
return pixels;
}
/// <summary>
/// Reads the next data block from the stream. A data block begins with a byte,
/// which defines the size of the block, followed by the block itself.
/// </summary>
/// <returns>
/// The <see cref="T:byte[]"/>.
/// </returns>
private byte[] ReadBlock()
{
int blockSize = this.stream.ReadByte();
return this.ReadBytes(blockSize);
}
/// <summary>
/// Reads the specified number of bytes from the data stream.
/// </summary>
/// <param name="length">
/// The number of bytes to read.
/// </param>
/// <returns>
/// The <see cref="T:byte[]"/>.
/// </returns>
private byte[] ReadBytes(int length)
{
byte[] buffer = new byte[length];
this.stream.Read(buffer, 0, length);
return buffer;
}
}
}

385
src/ImageProcessorCore/Formats/Gif/LzwEncoder.cs

@ -0,0 +1,385 @@
// <copyright file="LzwEncoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
/// <summary>
/// Encodes and compresses the image data using dynamic Lempel-Ziv compression.
/// </summary>
/// <remarks>
/// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. K Weiner 12/00
/// <para>
/// GIFCOMPR.C - GIF Image compression routines
///
/// Lempel-Ziv compression based on 'compress'. GIF modifications by
/// David Rowley (mgardi@watdcsu.waterloo.edu)
/// </para>
/// <para>
/// GIF Image compression - modified 'compress'
///
/// Based on: compress.c - File compression ala IEEE Computer, June 1984.
///
/// By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
/// Jim McKie (decvax!mcvax!jim)
/// Steve Davies (decvax!vax135!petsd!peora!srd)
/// Ken Turkowski (decvax!decwrl!turtlevax!ken)
/// James A. Woods (decvax!ihnp4!ames!jaw)
/// Joe Orost (decvax!vax135!petsd!joe)
/// </para>
/// </remarks>
internal sealed class LzwEncoder
{
private const int Eof = -1;
private const int Bits = 12;
private const int HashSize = 5003; // 80% occupancy
private readonly byte[] pixelArray;
private readonly int initialCodeSize;
private int curPixel;
/// <summary>
/// Number of bits/code
/// </summary>
private int bitCount;
/// <summary>
/// User settable max # bits/code
/// </summary>
private int maxbits = Bits;
private int maxcode; // maximum code, given bitCount
private int maxmaxcode = 1 << Bits; // should NEVER generate this code
private readonly int[] hashTable = new int[HashSize];
private readonly int[] codeTable = new int[HashSize];
/// <summary>
/// For dynamic table sizing
/// </summary>
private int hsize = HashSize;
/// <summary>
/// First unused entry
/// </summary>
private int freeEntry;
/// <summary>
/// Block compression parameters -- after all codes are used up,
/// and compression rate changes, start over.
/// </summary>
private bool clearFlag;
// Algorithm: use open addressing double hashing (no chaining) on the
// prefix code / next character combination. We do a variant of Knuth's
// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
// secondary probe. Here, the modular division first probe is gives way
// to a faster exclusive-or manipulation. Also do block compression with
// an adaptive reset, whereby the code table is cleared when the compression
// ratio decreases, but after the table fills. The variable-length output
// codes are re-sized at this point, and a special CLEAR code is generated
// for the decompressor. Late addition: construct the table according to
// file size for noticeable speed improvement on small files. Please direct
// questions about this implementation to ames!jaw.
private int globalInitialBits;
private int clearCode;
private int eofCode;
// output
//
// Output the given code.
// Inputs:
// code: A bitCount-bit integer. If == -1, then EOF. This assumes
// that bitCount =< wordsize - 1.
// Outputs:
// Outputs code to the file.
// Assumptions:
// Chars are 8 bits long.
// Algorithm:
// Maintain a BITS character long buffer (so that 8 codes will
// fit in it exactly). Use the VAX insv instruction to insert each
// code in turn. When the buffer fills up empty it and start over.
private int currentAccumulator;
private int currentBits;
private readonly int[] masks =
{
0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF,
0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF
};
/// <summary>
/// Number of characters so far in this 'packet'
/// </summary>
private int accumulatorCount;
/// <summary>
/// Define the storage for the packet accumulator.
/// </summary>
private readonly byte[] accumulators = new byte[256];
/// <summary>
/// Initializes a new instance of the <see cref="LzwEncoder"/> class.
/// </summary>
/// <param name="indexedPixels">The array of indexed pixels.</param>
/// <param name="colorDepth">The color depth in bits.</param>
public LzwEncoder(byte[] indexedPixels, int colorDepth)
{
this.pixelArray = indexedPixels;
this.initialCodeSize = Math.Max(2, colorDepth);
}
/// <summary>
/// Encodes and compresses the indexed pixels to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
public void Encode(Stream stream)
{
// Write "initial code size" byte
stream.WriteByte((byte)this.initialCodeSize);
this.curPixel = 0;
// Compress and write the pixel data
this.Compress(this.initialCodeSize + 1, stream);
// Write block terminator
stream.WriteByte(GifConstants.Terminator);
}
/// <summary>
/// Gets the maximum code value
/// </summary>
/// <param name="bitCount">The number of bits</param>
/// <returns>See <see cref="int"/></returns>
private static int GetMaxcode(int bitCount)
{
return (1 << bitCount) - 1;
}
/// <summary>
/// Add a character to the end of the current packet, and if it is 254 characters,
/// flush the packet to disk.
/// </summary>
/// <param name="c">The character to add.</param>
/// <param name="stream">The stream to write to.</param>
private void AddCharacter(byte c, Stream stream)
{
this.accumulators[this.accumulatorCount++] = c;
if (this.accumulatorCount >= 254)
{
this.FlushPacket(stream);
}
}
/// <summary>
/// Table clear for block compress
/// </summary>
/// <param name="stream">The output stream.</param>
private void ClearBlock(Stream stream)
{
this.ResetCodeTable(this.hsize);
this.freeEntry = this.clearCode + 2;
this.clearFlag = true;
this.Output(this.clearCode, stream);
}
/// <summary>
/// Reset the code table.
/// </summary>
/// <param name="size">The hash size.</param>
private void ResetCodeTable(int size)
{
for (int i = 0; i < size; ++i)
{
this.hashTable[i] = -1;
}
}
/// <summary>
/// Compress the packets to the stream.
/// </summary>
/// <param name="intialBits">The inital bits.</param>
/// <param name="stream">The stream to write to.</param>
private void Compress(int intialBits, Stream stream)
{
int fcode;
int c;
int ent;
int hsizeReg;
int hshift;
// Set up the globals: globalInitialBits - initial number of bits
this.globalInitialBits = intialBits;
// Set up the necessary values
this.clearFlag = false;
this.bitCount = this.globalInitialBits;
this.maxcode = GetMaxcode(this.bitCount);
this.clearCode = 1 << (intialBits - 1);
this.eofCode = this.clearCode + 1;
this.freeEntry = this.clearCode + 2;
this.accumulatorCount = 0; // clear packet
ent = this.NextPixel();
hshift = 0;
for (fcode = this.hsize; fcode < 65536; fcode *= 2) { ++hshift; }
hshift = 8 - hshift; // set hash code range bound
hsizeReg = this.hsize;
this.ResetCodeTable(hsizeReg); // clear hash table
this.Output(this.clearCode, stream);
while ((c = this.NextPixel()) != Eof)
{
fcode = (c << this.maxbits) + ent;
int i = (c << hshift) ^ ent /* = 0 */;
if (this.hashTable[i] == fcode)
{
ent = this.codeTable[i];
continue;
}
// Non-empty slot
if (this.hashTable[i] >= 0)
{
int disp = hsizeReg - i;
if (i == 0) disp = 1;
do
{
if ((i -= disp) < 0) { i += hsizeReg; }
if (this.hashTable[i] == fcode)
{
ent = this.codeTable[i];
break;
}
}
while (this.hashTable[i] >= 0);
if (this.hashTable[i] == fcode) { continue; }
}
this.Output(ent, stream);
ent = c;
if (this.freeEntry < this.maxmaxcode)
{
this.codeTable[i] = this.freeEntry++; // code -> hashtable
this.hashTable[i] = fcode;
}
else this.ClearBlock(stream);
}
// Put out the final code.
this.Output(ent, stream);
this.Output(this.eofCode, stream);
}
// Flush the packet to disk, and reset the accumulator
private void FlushPacket(Stream outs)
{
if (this.accumulatorCount > 0)
{
outs.WriteByte((byte)this.accumulatorCount);
outs.Write(this.accumulators, 0, this.accumulatorCount);
this.accumulatorCount = 0;
}
}
/// <summary>
/// Return the next pixel from the image
/// </summary>
/// <returns>
/// The <see cref="int"/>
/// </returns>
private int NextPixel()
{
if (this.curPixel == this.pixelArray.Length)
{
return Eof;
}
if (this.curPixel == this.pixelArray.Length)
return Eof;
this.curPixel++;
return this.pixelArray[this.curPixel - 1] & 0xff;
}
/// <summary>
/// Output the current code to the stream.
/// </summary>
/// <param name="code">The code.</param>
/// <param name="outs">The stream to write to.</param>
private void Output(int code, Stream outs)
{
this.currentAccumulator &= this.masks[this.currentBits];
if (this.currentBits > 0) this.currentAccumulator |= (code << this.currentBits);
else this.currentAccumulator = code;
this.currentBits += this.bitCount;
while (this.currentBits >= 8)
{
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
// If the next entry is going to be too big for the code size,
// then increase it, if possible.
if (this.freeEntry > this.maxcode || this.clearFlag)
{
if (this.clearFlag)
{
this.maxcode = GetMaxcode(this.bitCount = this.globalInitialBits);
this.clearFlag = false;
}
else
{
++this.bitCount;
this.maxcode = this.bitCount == this.maxbits
? this.maxmaxcode
: GetMaxcode(this.bitCount);
}
}
if (code == this.eofCode)
{
// At EOF, write the rest of the buffer.
while (this.currentBits > 0)
{
this.AddCharacter((byte)(this.currentAccumulator & 0xff), outs);
this.currentAccumulator >>= 8;
this.currentBits -= 8;
}
this.FlushPacket(outs);
}
}
}
}

194
src/ImageProcessorCore/Formats/Gif/PackedField.cs

@ -0,0 +1,194 @@
// <copyright file="PackedField.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
/// <summary>
/// Represents a byte of data in a GIF data stream which contains a number
/// of data items.
/// </summary>
internal struct PackedField : IEquatable<PackedField>
{
/// <summary>
/// The individual bits representing the packed byte.
/// </summary>
private static readonly bool[] Bits = new bool[8];
/// <summary>
/// Gets the byte which represents the data items held in this instance.
/// </summary>
public byte Byte
{
get
{
int returnValue = 0;
int bitShift = 7;
foreach (bool bit in Bits)
{
int bitValue;
if (bit)
{
bitValue = 1 << bitShift;
}
else
{
bitValue = 0;
}
returnValue |= bitValue;
bitShift--;
}
return Convert.ToByte(returnValue & 0xFF);
}
}
/// <summary>
/// Returns a new <see cref="PackedField"/> with the bits in the packed fields to
/// the corresponding bits from the supplied byte.
/// </summary>
/// <param name="value">The value to pack.</param>
/// <returns>The <see cref="PackedField"/></returns>
public static PackedField FromInt(byte value)
{
PackedField packed = new PackedField();
packed.SetBits(0, 8, value);
return packed;
}
/// <summary>
/// Sets the specified bit within the packed fields to the supplied
/// value.
/// </summary>
/// <param name="index">
/// The zero-based index within the packed fields of the bit to set.
/// </param>
/// <param name="valueToSet">
/// The value to set the bit to.
/// </param>
public void SetBit(int index, bool valueToSet)
{
if (index < 0 || index > 7)
{
string message
= "Index must be between 0 and 7. Supplied index: "
+ index;
throw new ArgumentOutOfRangeException(nameof(index), message);
}
Bits[index] = valueToSet;
}
/// <summary>
/// Sets the specified bits within the packed fields to the supplied
/// value.
/// </summary>
/// <param name="startIndex">The zero-based index within the packed fields of the first bit to set.</param>
/// <param name="length">The number of bits to set.</param>
/// <param name="valueToSet">The value to set the bits to.</param>
public void SetBits(int startIndex, int length, int valueToSet)
{
if (startIndex < 0 || startIndex > 7)
{
string message = $"Start index must be between 0 and 7. Supplied index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(startIndex), message);
}
if (length < 1 || startIndex + length > 8)
{
string message = "Length must be greater than zero and the sum of length and start index must be less than 8. "
+ $"Supplied length: {length}. Supplied start index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(length), message);
}
int bitShift = length - 1;
for (int i = startIndex; i < startIndex + length; i++)
{
int bitValueIfSet = (1 << bitShift);
int bitValue = (valueToSet & bitValueIfSet);
int bitIsSet = (bitValue >> bitShift);
Bits[i] = (bitIsSet == 1);
bitShift--;
}
}
/// <summary>
/// Gets the value of the specified bit within the byte.
/// </summary>
/// <param name="index">The zero-based index of the bit to get.</param>
/// <returns>
/// The value of the specified bit within the byte.
/// </returns>
public bool GetBit(int index)
{
if (index < 0 || index > 7)
{
string message = $"Index must be between 0 and 7. Supplied index: {index}";
throw new ArgumentOutOfRangeException(nameof(index), message);
}
return Bits[index];
}
/// <summary>
/// Gets the value of the specified bits within the byte.
/// </summary>
/// <param name="startIndex">The zero-based index of the first bit to get.</param>
/// <param name="length">The number of bits to get.</param>
/// <returns>
/// The value of the specified bits within the byte.
/// </returns>
public int GetBits(int startIndex, int length)
{
if (startIndex < 0 || startIndex > 7)
{
string message = $"Start index must be between 0 and 7. Supplied index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(startIndex), message);
}
if (length < 1 || startIndex + length > 8)
{
string message = "Length must be greater than zero and the sum of length and start index must be less than 8. "
+ $"Supplied length: {length}. Supplied start index: {startIndex}";
throw new ArgumentOutOfRangeException(nameof(length), message);
}
int returnValue = 0;
int bitShift = length - 1;
for (int i = startIndex; i < startIndex + length; i++)
{
int bitValue = (Bits[i] ? 1 : 0) << bitShift;
returnValue += bitValue;
bitShift--;
}
return returnValue;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
PackedField? field = obj as PackedField?;
return this.Byte == field?.Byte;
}
/// <inheritdoc/>
public bool Equals(PackedField other)
{
return this.Byte.Equals(other.Byte);
}
/// <inheritdoc/>
public override string ToString()
{
return $"PackedField [ Byte={this.Byte} ]";
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.Byte.GetHashCode();
}
}
}

4
src/ImageProcessorCore/Formats/Gif/README.md

@ -0,0 +1,4 @@
Encoder/Decoder adapted and extended from:
https://github.com/yufeih/Nine.Imaging/
https://imagetools.codeplex.com/

42
src/ImageProcessorCore/Formats/Gif/Sections/GifGraphicsControlExtension.cs

@ -0,0 +1,42 @@
// <copyright file="GifGraphicsControlExtension.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// The Graphic Control Extension contains parameters used when
/// processing a graphic rendering block.
/// </summary>
internal sealed class GifGraphicsControlExtension
{
/// <summary>
/// Gets or sets the disposal method which indicates the way in which the
/// graphic is to be treated after being displayed.
/// </summary>
public DisposalMethod DisposalMethod { get; set; }
/// <summary>
/// Gets or sets a value indicating whether transparency flag is to be set.
/// This indicates whether a transparency index is given in the Transparent Index field.
/// (This field is the least significant bit of the byte.)
/// </summary>
public bool TransparencyFlag { get; set; }
/// <summary>
/// Gets or sets the transparency index.
/// The Transparency Index is such that when encountered, the corresponding pixel
/// of the display device is not modified and processing goes on to the next pixel.
/// </summary>
public int TransparencyIndex { get; set; }
/// <summary>
/// Gets or sets the delay time.
/// If not 0, 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 DelayTime { get; set; }
}
}

59
src/ImageProcessorCore/Formats/Gif/Sections/GifImageDescriptor.cs

@ -0,0 +1,59 @@
// <copyright file="GifImageDescriptor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Each image in the Data Stream is composed of an Image Descriptor,
/// an optional Local Color Table, and the image data.
/// Each image must fit within the boundaries of the
/// Logical Screen, as defined in the Logical Screen Descriptor.
/// </summary>
internal sealed class GifImageDescriptor
{
/// <summary>
/// Gets or sets the column number, in pixels, of the left edge of the image,
/// with respect to the left edge of the Logical Screen.
/// Leftmost column of the Logical Screen is 0.
/// </summary>
public short Left { get; set; }
/// <summary>
/// Gets or sets the row number, in pixels, of the top edge of the image with
/// respect to the top edge of the Logical Screen.
/// Top row of the Logical Screen is 0.
/// </summary>
public short Top { get; set; }
/// <summary>
/// Gets or sets the width of the image in pixels.
/// </summary>
public short Width { get; set; }
/// <summary>
/// Gets or sets the height of the image in pixels.
/// </summary>
public short Height { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the presence of a Local Color Table immediately
/// follows this Image Descriptor.
/// </summary>
public bool LocalColorTableFlag { get; set; }
/// <summary>
/// Gets or sets the local color table size.
/// If the Local Color Table Flag is set to 1, the value in this field
/// is used to calculate the number of bytes contained in the Local Color Table.
/// </summary>
public int LocalColorTableSize { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image is to be interlaced.
/// An image is interlaced in a four-pass interlace pattern.
/// </summary>
public bool InterlaceFlag { get; set; }
}
}

55
src/ImageProcessorCore/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs

@ -0,0 +1,55 @@
// <copyright file="GifLogicalScreenDescriptor.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// The Logical Screen Descriptor contains the parameters
/// necessary to define the area of the display device
/// within which the images will be rendered
/// </summary>
internal sealed class GifLogicalScreenDescriptor
{
/// <summary>
/// Gets or sets the width, in pixels, of the Logical Screen where the images will
/// be rendered in the displaying device.
/// </summary>
public short Width { get; set; }
/// <summary>
/// Gets or sets the height, in pixels, of the Logical Screen where the images will be
/// rendered in the displaying device.
/// </summary>
public short Height { get; set; }
/// <summary>
/// Gets or sets the index at the Global Color Table for the Background Color.
/// The Background Color is the color used for those
/// pixels on the screen that are not covered by an image.
/// </summary>
public byte BackgroundColorIndex { get; set; }
/// <summary>
/// Gets or sets the pixel aspect ratio. Default to 0.
/// </summary>
public byte PixelAspectRatio { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a flag denoting the presence of a Global Color Table
/// should be set.
/// If the flag is set, the Global Color Table will immediately
/// follow the Logical Screen Descriptor.
/// </summary>
public bool GlobalColorTableFlag { get; set; }
/// <summary>
/// Gets or sets the global color table size.
/// If the Global Color Table Flag is set to 1,
/// the value in this field is used to calculate the number of
/// bytes contained in the Global Color Table.
/// </summary>
public int GlobalColorTableSize { get; set; }
}
}

3
src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs

@ -218,8 +218,7 @@ namespace ImageProcessorCore.Formats
if (this.Quantizer == null)
{
//this.Quantizer = new WuQuantizer<T, TP> { Threshold = this.Threshold };
this.Quantizer = new OctreeQuantizer<T, TP> { Threshold = this.Threshold };
this.Quantizer = new WuQuantizer<T, TP> { Threshold = this.Threshold };
}
// Quantize the image returning a palette. This boxing is icky.

66
src/ImageProcessorCore/Image/ImageExtensions.cs

@ -16,24 +16,35 @@ namespace ImageProcessorCore
/// </summary>
public static partial class ImageExtensions
{
///// <summary>
///// Saves the image to the given stream with the bmp format.
///// </summary>
///// <param name="source">The image this method extends.</param>
///// <param name="stream">The stream to save the image to.</param>
///// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
//public static void SaveAsBmp(this ImageBase source, Stream stream) => new BmpEncoder().Encode(source, stream);
/// <summary>
/// Saves the image to the given stream with the bmp format.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsBmp<T, TP>(this ImageBase<T, TP> source, Stream stream)
where T : IPackedVector<TP>
where TP : struct
=> new BmpEncoder().Encode(source, stream);
///// <summary>
///// Saves the image to the given stream with the png format.
///// </summary>
///// <param name="source">The image this method extends.</param>
///// <param name="stream">The stream to save the image to.</param>
///// <param name="quality">The quality to save the image to representing the number of colors.
///// Anything equal to 256 and below will cause the encoder to save the image in an indexed format.
///// </param>
///// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
//public static void SaveAsPng(this ImageBase source, Stream stream, int quality = Int32.MaxValue) => new PngEncoder { Quality = quality }.Encode(source, stream);
/// <summary>
/// Saves the image to the given stream with the png format.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="quality">The quality to save the image to representing the number of colors.
/// Anything equal to 256 and below will cause the encoder to save the image in an indexed format.
/// </param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsPng<T, TP>(this ImageBase<T, TP> source, Stream stream, int quality = int.MaxValue)
where T : IPackedVector<TP>
where TP : struct
=> new PngEncoder { Quality = quality }.Encode(source, stream);
///// <summary>
///// Saves the image to the given stream with the jpeg format.
@ -44,14 +55,19 @@ namespace ImageProcessorCore
///// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
//public static void SaveAsJpeg(this ImageBase source, Stream stream, int quality = 75) => new JpegEncoder { Quality = quality }.Encode(source, stream);
///// <summary>
///// Saves the image to the given stream with the gif format.
///// </summary>
///// <param name="source">The image this method extends.</param>
///// <param name="stream">The stream to save the image to.</param>
///// <param name="quality">The quality to save the image to representing the number of colors. Between 1 and 256.</param>
///// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
//public static void SaveAsGif(this ImageBase source, Stream stream, int quality = 256) => new GifEncoder { Quality = quality }.Encode(source, stream);
/// <summary>
/// Saves the image to the given stream with the gif format.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="quality">The quality to save the image to representing the number of colors. Between 1 and 256.</param>
/// <exception cref="ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsGif<T, TP>(this ImageBase<T, TP> source, Stream stream, int quality = 256)
where T : IPackedVector<TP>
where TP : struct
=> new GifEncoder { Quality = quality }.Encode(source, stream);
/// <summary>
/// Applies the collection of processors to the image.

5
src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs

@ -106,7 +106,10 @@ namespace ImageProcessorCore.Quantizers
List<T> palette = this.octree.Palletize(Math.Max(this.colors, 1));
int diff = this.colors - palette.Count;
palette.AddRange(Enumerable.Repeat(default(T), diff));
if (diff > 0)
{
palette.AddRange(Enumerable.Repeat(default(T), diff));
}
this.TransparentIndex = this.colors;
return palette;

6
tests/ImageProcessorCore.Tests/FileTestBase.cs

@ -25,14 +25,14 @@ namespace ImageProcessorCore.Tests
//"TestImages/Formats/Jpg/progress.jpg", // Perf: Enable for local testing only
//"TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg", // Perf: Enable for local testing only
"TestImages/Formats/Bmp/Car.bmp",
// "TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only
//"TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only
//"TestImages/Formats/Png/blur.png", // Perf: Enable for local testing only
//"TestImages/Formats/Png/indexed.png", // Perf: Enable for local testing only
"TestImages/Formats/Png/splash.png",
//"TestImages/Formats/Gif/rings.gif",
"TestImages/Formats/Gif/rings.gif",
//"TestImages/Formats/Gif/giphy.gif" // Perf: Enable for local testing only
};
protected void ProgressUpdate(object sender, ProgressEventArgs e)
{
Assert.InRange(e.RowsProcessed, 1, e.TotalRows);

2
tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs

@ -114,7 +114,7 @@ namespace ImageProcessorCore.Tests
{
string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file);
Image image = new Image(stream);
Image image = new Image(stream) {Quality=256};
using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}"))
{
image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate)

Loading…
Cancel
Save