Browse Source

Moar gif updates

Former-commit-id: e3548b3f5fb82d262a1054c6d2ff89c24f86206d
Former-commit-id: d9ba2a7a4c74231ba7b8f7c90a87567c41b0605a
Former-commit-id: a6b6bac2b10088c73838b5177636b5c9db6424c4
pull/17/head
James Jackson-South 11 years ago
parent
commit
82d65ea57f
  1. 68
      src/ImageProcessor/Formats/Gif/GifConstants.cs
  2. 8
      src/ImageProcessor/Formats/Gif/GifDecoder.cs
  3. 47
      src/ImageProcessor/Formats/Gif/GifDecoderCore.cs
  4. 109
      src/ImageProcessor/Formats/Gif/GifEncoder.cs
  5. 1
      src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs
  6. 35
      src/ImageProcessor/Formats/Gif/LzwEncoder.cs
  7. 12
      src/ImageProcessor/Image.cs
  8. 4
      src/ImageProcessor/ImageBase.cs
  9. 5
      tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs

68
src/ImageProcessor/Formats/Gif/GifConstants.cs

@ -0,0 +1,68 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="GifConstants.cs" company="James South">
// Copyright © James South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
// <summary>
// Constants that define specific points within a gif.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ImageProcessor.Formats
{
/// <summary>
/// Constants that define specific points within a gif.
/// </summary>
internal sealed class GifConstants
{
/// <summary>
/// The maximum comment length.
/// </summary>
public const int MaxCommentLength = 1024 * 8;
/// <summary>
/// The extension block introducer <value>!</value>.
/// </summary>
public const byte ExtensionIntroducer = 0x21;
/// <summary>
/// The terminator.
/// </summary>
public const byte Terminator = 0;
/// <summary>
/// The image label introducer <value>,</value>.
/// </summary>
public const byte ImageLabel = 0x2C;
/// <summary>
/// The end introducer trailer <value>;</value>.
/// </summary>
public const byte EndIntroducer = 0x3B;
/// <summary>
/// The application extension label.
/// </summary>
public const byte ApplicationExtensionLabel = 0xFF;
/// <summary>
/// The comment label.
/// </summary>
public const byte CommentLabel = 0xFE;
/// <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 graphic control label.
/// </summary>
public const byte GraphicControlLabel = 0xF9;
}
}

8
src/ImageProcessor/Formats/Gif/GifDecoder.cs

@ -24,6 +24,9 @@ namespace ImageProcessor.Formats
/// <value>The size of the header.</value>
public int HeaderSize => 6;
internal GifDecoderCore CoreDecoder { get; private set; }
/// <summary>
/// Returns a value indicating whether the <see cref="IImageDecoder"/> supports the specified
/// file header.
@ -34,7 +37,7 @@ namespace ImageProcessor.Formats
/// </returns>
public bool IsSupportedFileExtension(string extension)
{
Guard.NotNullOrEmpty(extension, "extension");
Guard.NotNullOrEmpty(extension, nameof(extension));
extension = extension.StartsWith(".") ? extension.Substring(1) : extension;
return extension.Equals("GIF", StringComparison.OrdinalIgnoreCase);
@ -66,7 +69,8 @@ namespace ImageProcessor.Formats
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
public void Decode(Image image, Stream stream)
{
new GifDecoderCore().Decode(image, stream);
this.CoreDecoder = new GifDecoderCore();
this.CoreDecoder.Decode(image, stream);
}
}
}

47
src/ImageProcessor/Formats/Gif/GifDecoderCore.cs

@ -24,8 +24,9 @@
private Stream currentStream;
private byte[] globalColorTable;
private byte[] currentFrame;
private GifLogicalScreenDescriptor logicalScreenDescriptor;
private GifGraphicsControlExtension graphicsControlExtension;
internal GifLogicalScreenDescriptor LogicalScreenDescriptor { get; set; }
internal GifGraphicsControlExtension GraphicsControlExtension { get; set; }
public void Decode(Image image, Stream stream)
{
@ -37,16 +38,16 @@
this.currentStream.Seek(6, SeekOrigin.Current);
this.ReadLogicalScreenDescriptor();
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
if (this.LogicalScreenDescriptor.GlobalColorTableFlag)
{
this.globalColorTable = new byte[this.logicalScreenDescriptor.GlobalColorTableSize * 3];
this.globalColorTable = new byte[this.LogicalScreenDescriptor.GlobalColorTableSize * 3];
// Read the global color table from the stream
stream.Read(this.globalColorTable, 0, this.globalColorTable.Length);
}
int nextFlag = stream.ReadByte();
while (nextFlag != 0)
while (nextFlag != Terminator)
{
if (nextFlag == ImageLabel)
{
@ -89,7 +90,7 @@
byte packed = buffer[1];
this.graphicsControlExtension = new GifGraphicsControlExtension
this.GraphicsControlExtension = new GifGraphicsControlExtension
{
DelayTime = BitConverter.ToInt16(buffer, 2),
TransparencyIndex = buffer[4],
@ -134,7 +135,7 @@
byte packed = buffer[4];
this.logicalScreenDescriptor = new GifLogicalScreenDescriptor
this.LogicalScreenDescriptor = new GifLogicalScreenDescriptor
{
Width = BitConverter.ToInt16(buffer, 0),
Height = BitConverter.ToInt16(buffer, 2),
@ -144,16 +145,16 @@
GlobalColorTableSize = 2 << (packed & 0x07)
};
if (this.logicalScreenDescriptor.GlobalColorTableSize > 255 * 4)
if (this.LogicalScreenDescriptor.GlobalColorTableSize > 255 * 4)
{
throw new ImageFormatException(
$"Invalid gif colormap size '{this.logicalScreenDescriptor.GlobalColorTableSize}'");
$"Invalid gif colormap size '{this.LogicalScreenDescriptor.GlobalColorTableSize}'");
}
if (this.logicalScreenDescriptor.Width > ImageBase.MaxWidth || this.logicalScreenDescriptor.Height > ImageBase.MaxHeight)
if (this.LogicalScreenDescriptor.Width > ImageBase.MaxWidth || this.LogicalScreenDescriptor.Height > ImageBase.MaxHeight)
{
throw new ArgumentOutOfRangeException(
$"The input gif '{this.logicalScreenDescriptor.Width}x{this.logicalScreenDescriptor.Height}' is bigger then the max allowed size '{ImageBase.MaxWidth}x{ImageBase.MaxHeight}'");
$"The input gif '{this.LogicalScreenDescriptor.Width}x{this.LogicalScreenDescriptor.Height}' is bigger then the max allowed size '{ImageBase.MaxWidth}x{ImageBase.MaxHeight}'");
}
}
@ -232,8 +233,8 @@
private void ReadFrameColors(byte[] indices, byte[] colorTable, GifImageDescriptor descriptor)
{
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
int imageWidth = this.LogicalScreenDescriptor.Width;
int imageHeight = this.LogicalScreenDescriptor.Height;
if (this.currentFrame == null)
{
@ -242,8 +243,8 @@
byte[] lastFrame = null;
if (this.graphicsControlExtension != null &&
this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
if (this.GraphicsControlExtension != null &&
this.GraphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
{
lastFrame = new byte[imageWidth * imageHeight * 4];
@ -299,9 +300,9 @@
index = indices[i];
if (this.graphicsControlExtension == null ||
this.graphicsControlExtension.TransparencyFlag == false ||
this.graphicsControlExtension.TransparencyIndex != index)
if (this.GraphicsControlExtension == null ||
this.GraphicsControlExtension.TransparencyFlag == false ||
this.GraphicsControlExtension.TransparencyIndex != index)
{
this.currentFrame[offset * 4 + 0] = colorTable[index * 3 + 2];
this.currentFrame[offset * 4 + 1] = colorTable[index * 3 + 1];
@ -324,9 +325,9 @@
currentImage = this.image;
currentImage.SetPixels(imageWidth, imageHeight, pixels);
if (this.graphicsControlExtension != null && this.graphicsControlExtension.DelayTime > 0)
if (this.GraphicsControlExtension != null && this.GraphicsControlExtension.DelayTime > 0)
{
this.image.FrameDelay = this.graphicsControlExtension.DelayTime;
this.image.FrameDelay = this.GraphicsControlExtension.DelayTime;
}
}
else
@ -339,9 +340,9 @@
this.image.Frames.Add(frame);
}
if (this.graphicsControlExtension != null)
if (this.GraphicsControlExtension != null)
{
if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground)
if (this.GraphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground)
{
for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++)
{
@ -356,7 +357,7 @@
}
}
}
else if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
else if (this.GraphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious)
{
this.currentFrame = lastFrame;
}

109
src/ImageProcessor/Formats/Gif/GifEncoder.cs

@ -11,7 +11,7 @@ namespace ImageProcessor.Formats
/// </summary>
private int quality = 256;
private ImageBase image;
private Image image;
/// <summary>
/// Gets or sets the quality of output for images.
@ -58,48 +58,65 @@ namespace ImageProcessor.Formats
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode(ImageBase image, Stream stream)
{
Guard.NotNull(image, "image");
Guard.NotNull(stream, "stream");
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.image = image;
this.image = (Image)image;
// Write the header.
// File Header signature and version.
this.WriteString(stream, "GIF");
this.WriteString(stream, "89a");
GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor
{
Width = (short)image.Width,
Height = (short)image.Height,
GlobalColorTableFlag = true,
GlobalColorTableSize = this.Quality
};
this.WriteGlobalLogicalScreenDescriptor(stream, descriptor);
int bitdepth = this.GetBitsNeededForColorDepth(this.Quality) - 1;
this.WriteGlobalLogicalScreenDescriptor(stream, bitdepth);
foreach (ImageFrame frame in this.image.Frames)
{
this.WriteColorTable(stream, bitdepth);
this.WriteGraphicalControlExtension(stream);
}
throw new System.NotImplementedException();
}
private void WriteGlobalLogicalScreenDescriptor(Stream stream, GifLogicalScreenDescriptor descriptor)
private void WriteGlobalLogicalScreenDescriptor(Stream stream, int bitDepth)
{
IImageDecoder decoder = ((Image)this.image).Decoder;
GifLogicalScreenDescriptor descriptor;
// Try and grab an existing descriptor.
if (decoder.GetType() == typeof(GifDecoder))
{
// Ensure the dimensions etc are up to date.
descriptor = ((GifDecoder)decoder).CoreDecoder.LogicalScreenDescriptor;
descriptor.Width = (short)this.image.Width;
descriptor.Height = (short)this.image.Height;
descriptor.GlobalColorTableSize = this.Quality;
}
else
{
descriptor = new GifLogicalScreenDescriptor
{
Width = (short)this.image.Width,
Height = (short)this.image.Height,
GlobalColorTableFlag = true,
GlobalColorTableSize = this.Quality
};
}
this.WriteShort(stream, descriptor.Width);
this.WriteShort(stream, descriptor.Width);
int size = descriptor.GlobalColorTableSize;
int bitdepth = this.GetBitsNeededForColorDepth(size) - 1;
int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used)
0x70 | // 2-4 : color resolution
0x00 | // 5 : GCT sort flag = 0
bitdepth; // 6-8 : GCT size assume 1:1
bitDepth; // 6-8 : GCT size assume 1:1
this.WriteByte(stream, packed);
this.WriteByte(stream, descriptor.BackgroundColorIndex); // Background Color Index
this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio
// Write the global color table.
this.WriteColorTable(stream, bitdepth);
}
private void WriteColorTable(Stream stream, int bitDepth)
@ -127,6 +144,54 @@ namespace ImageProcessor.Formats
stream.Write(colorTable, 0, colorTableLength);
}
private void WriteGraphicalControlExtension(Stream stream)
{
Image i = ((Image)this.image);
IImageDecoder decoder = i.Decoder;
GifGraphicsControlExtension extension;
// Try and grab an existing descriptor.
// TODO: Check whether we need to.
if (decoder.GetType() == typeof(GifDecoder))
{
// Ensure the dimensions etc are up to date.
extension = ((GifDecoder)decoder).CoreDecoder.GraphicsControlExtension;
extension.TransparencyFlag = this.Quality > 1;
extension.TransparencyIndex = this.Quality - 1;
extension.DelayTime = i.FrameDelay;
}
else
{
bool hasTransparent = this.Quality > 1;
DisposalMethod disposalMethod = hasTransparent
? DisposalMethod.RestoreToBackground
: DisposalMethod.Unspecified;
extension = new GifGraphicsControlExtension()
{
DisposalMethod = disposalMethod,
TransparencyFlag = hasTransparent,
TransparencyIndex = this.Quality - 1, // Quantizer set last as transparent.
DelayTime = i.FrameDelay
};
}
this.WriteByte(stream, GifConstants.ExtensionIntroducer);
this.WriteByte(stream, GifConstants.GraphicControlLabel);
this.WriteByte(stream, 4); // Size
int packed = 0 | // 1-3 : Reserved
(int)extension.DisposalMethod | // 4-6 : Disposal
0 | // 7 : User input - 0 = none
extension.TransparencyIndex;
this.WriteByte(stream, packed);
this.WriteShort(stream, extension.DelayTime);
this.WriteByte(stream, GifConstants.Terminator);
}
private void WriteApplicationExtension(Stream stream)
{
// TODO: Implement
@ -146,7 +211,7 @@ namespace ImageProcessor.Formats
}
/// <summary>
/// Writes a short to the given stream.
/// Writes a byte to the given stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="value">The value to write.</param>

1
src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs

@ -42,7 +42,6 @@ namespace ImageProcessor.Formats
/// 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.
/// This field may be used in conjunction with the User Input Flag field.
/// </summary>
public int DelayTime { get; set; }
}

35
src/ImageProcessor/Formats/Gif/LzwEncoder.cs

@ -63,7 +63,7 @@
// General DEFINEs
private const int BITS = 12;
private const int Bits = 12;
private const int HSIZE = 5003; // 80% occupancy
@ -79,12 +79,12 @@
// Joe Orost (decvax!vax135!petsd!joe)
private int numberOfBits; // number of bits/code
private int maxbits = BITS; // user settable max # bits/code
private int maxbits = Bits; // user settable max # bits/code
private int maxcode; // maximum code, given n_bits
private int maxmaxcode = 1 << BITS; // should NEVER generate this code
private int maxmaxcode = 1 << Bits; // should NEVER generate this code
private int[] htab = new int[HSIZE];
private int[] codetab = new int[HSIZE];
private readonly int[] hashTable = new int[HSIZE];
private readonly int[] codeTable = new int[HSIZE];
private int hsize = HSIZE; // for dynamic table sizing
@ -168,7 +168,7 @@
{
for (int i = 0; i < hsize; ++i)
{
this.htab[i] = -1;
this.hashTable[i] = -1;
}
}
@ -210,21 +210,21 @@
this.Output(this.ClearCode, outs);
// TODO: Refactor this. Goto is baaaaaaad!
outer_loop:
// TODO: Refactor this. Goto is baaaaaaad!
// outer_loop:
while ((c = this.NextPixel()) != EOF)
{
fcode = (c << this.maxbits) + ent;
int i = c << hshift ^ ent;
if (this.htab[i] == fcode)
if (this.hashTable[i] == fcode)
{
ent = this.codetab[i];
ent = this.codeTable[i];
continue;
}
// non-empty slot
if (this.htab[i] >= 0)
if (this.hashTable[i] >= 0)
{
disp = hsize_reg - i; // secondary hash (after G. Knott)
if (i == 0)
@ -239,13 +239,14 @@
i += hsize_reg;
}
if (this.htab[i] == fcode)
if (this.hashTable[i] == fcode)
{
ent = this.codetab[i];
goto outer_loop;
ent = this.codeTable[i];
// goto outer_loop;
break;
}
}
while (this.htab[i] >= 0);
while (this.hashTable[i] >= 0);
}
this.Output(ent, outs);
@ -253,8 +254,8 @@
if (this.freeEntry < this.maxmaxcode)
{
this.codetab[i] = this.freeEntry++; // code -> hashtable
this.htab[i] = fcode;
this.codeTable[i] = this.freeEntry++; // code -> hashtable
this.hashTable[i] = fcode;
}
else
{

12
src/ImageProcessor/Image.cs

@ -98,7 +98,7 @@ namespace ImageProcessor
public Image(Image other)
: base(other)
{
Guard.NotNull(other, "other", "Other image cannot be null.");
Guard.NotNull(other, nameof(other), "Other image cannot be null.");
foreach (ImageFrame frame in other.Frames)
{
@ -120,7 +120,7 @@ namespace ImageProcessor
/// </param>
public Image(Stream stream)
{
Guard.NotNull(stream, "stream");
Guard.NotNull(stream, nameof(stream));
this.Load(stream, Decoders);
}
@ -154,9 +154,8 @@ namespace ImageProcessor
/// 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.
/// This field may be used in conjunction with the User Input Flag field.
/// </summary>
public int? FrameDelay { get; set; }
public int FrameDelay { get; set; }
/// <summary>
/// Gets or sets the resolution of the image in x- direction. It is defined as
@ -240,6 +239,8 @@ namespace ImageProcessor
/// <value>A list of image properties.</value>
public IList<ImageProperty> Properties { get; } = new List<ImageProperty>();
internal IImageDecoder Decoder { get; set; }
/// <summary>
/// Loads the image from the given stream.
/// </summary>
@ -279,7 +280,8 @@ namespace ImageProcessor
IImageDecoder decoder = decoders.FirstOrDefault(x => x.IsSupportedFileFormat(header));
if (decoder != null)
{
decoder.Decode(this, stream);
this.Decoder = decoder;
this.Decoder.Decode(this, stream);
return;
}
}

4
src/ImageProcessor/ImageBase.cs

@ -40,8 +40,8 @@ namespace ImageProcessor
/// </exception>
protected ImageBase(int width, int height)
{
Guard.MustBeGreaterThan(width, 0, "width");
Guard.MustBeGreaterThan(height, 0, "height");
Guard.MustBeGreaterThan(width, 0, nameof(width));
Guard.MustBeGreaterThan(height, 0, nameof(height));
this.Width = width;
this.Height = height;

5
tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs

@ -88,7 +88,7 @@ namespace ImageProcessor.Tests
}
/// <summary>
/// Tests the implicit conversion from <see cref="YCbCr"/> to <see cref="Bgra"/>.
/// Tests the implicit conversion from <see cref="Bgra"/> to <see cref="Hsv"/>.
/// </summary>
[Fact]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation",
@ -128,6 +128,9 @@ namespace ImageProcessor.Tests
Assert.Equal(80, hsv3.V, 1);
}
/// <summary>
/// Tests the implicit conversion from <see cref="Hsv"/> to <see cref="Bgra"/>.
/// </summary>
[Fact]
public void HsvToBgr()
{

Loading…
Cancel
Save