mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Former-commit-id: 085c4bc99388de35847b2b8dd26b20d518e272de Former-commit-id: e8b3af949ca0f63f001483188dbab20cd263c10f Former-commit-id: 8660e1a08e5da2bfebcc269d75ee20488542d4f6af/merge-core
21 changed files with 2160 additions and 33 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
Encoder/Decoder adapted and extended from: |
||||
|
|
||||
|
https://github.com/yufeih/Nine.Imaging/ |
||||
|
https://imagetools.codeplex.com/ |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue