diff --git a/src/ImageProcessorCore/Bootstrapper.cs b/src/ImageProcessorCore/Bootstrapper.cs index 56eee634e3..3294086fe4 100644 --- a/src/ImageProcessorCore/Bootstrapper.cs +++ b/src/ImageProcessorCore/Bootstrapper.cs @@ -40,7 +40,7 @@ namespace ImageProcessorCore new BmpFormat(), //new JpegFormat(), new PngFormat(), - //new GifFormat() + new GifFormat() }; this.pixelAccessors = new Dictionary> diff --git a/src/ImageProcessorCore/Formats/Gif/BitEncoder.cs b/src/ImageProcessorCore/Formats/Gif/BitEncoder.cs new file mode 100644 index 0000000000..a0c633a194 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/BitEncoder.cs @@ -0,0 +1,132 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System.Collections.Generic; + + /// + /// Handles the encoding of bits for compression. + /// + internal class BitEncoder + { + /// + /// The inner list for collecting the bits. + /// + private readonly List list = new List(); + + /// + /// The current working bit. + /// + private int currentBit; + + /// + /// The current value. + /// + private int currentValue; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The initial bits. + /// + public BitEncoder(int initial) + { + this.IntitialBit = initial; + } + + /// + /// Gets or sets the intitial bit. + /// + public int IntitialBit { get; set; } + + /// + /// The number of bytes in the encoder. + /// + public int Length => this.list.Count; + + /// + /// Adds the current byte to the end of the encoder. + /// + /// + /// The byte to add. + /// + 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); + } + } + + /// + /// Adds the collection of bytes to the end of the encoder. + /// + /// + /// The collection of bytes to add. + /// The collection itself cannot be null but can contain elements that are null. + public void AddRange(byte[] collection) + { + this.list.AddRange(collection); + } + + /// + /// Copies a range of elements from the encoder to a compatible one-dimensional array, + /// starting at the specified index of the target array. + /// + /// + /// The zero-based index in the source at which copying begins. + /// + /// + /// The one-dimensional Array that is the destination of the elements copied + /// from . The Array must have zero-based indexing + /// + /// The zero-based index in array at which copying begins. + /// The number of bytes to copy. + public void CopyTo(int index, byte[] array, int arrayIndex, int count) + { + this.list.CopyTo(index, array, arrayIndex, count); + } + + /// + /// Removes all the bytes from the encoder. + /// + public void Clear() + { + this.list.Clear(); + } + + /// + /// Copies the bytes into a new array. + /// + /// + public byte[] ToArray() + { + return this.list.ToArray(); + } + + /// + /// The end. + /// + 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); + } + } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/DisposalMethod.cs b/src/ImageProcessorCore/Formats/Gif/DisposalMethod.cs new file mode 100644 index 0000000000..4b0a019734 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/DisposalMethod.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Provides enumeration for instructing the decoder what to do with the last image + /// in an animation sequence. + /// section 23 + /// + public enum DisposalMethod + { + /// + /// No disposal specified. The decoder is not required to take any action. + /// + Unspecified = 0, + + /// + /// Do not dispose. The graphic is to be left in place. + /// + NotDispose = 1, + + /// + /// Restore to background color. The area used by the graphic must be restored to + /// the background color. + /// + RestoreToBackground = 2, + + /// + /// Restore to previous. The decoder is required to restore the area overwritten by the + /// graphic with what was there prior to rendering the graphic. + /// + RestoreToPrevious = 3 + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/GifConstants.cs b/src/ImageProcessorCore/Formats/Gif/GifConstants.cs new file mode 100644 index 0000000000..42949cf168 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifConstants.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Constants that define specific points within a gif. + /// + internal sealed class GifConstants + { + /// + /// The file type. + /// + public const string FileType = "GIF"; + + /// + /// The file version. + /// + public const string FileVersion = "89a"; + + /// + /// The extension block introducer !. + /// + public const byte ExtensionIntroducer = 0x21; + + /// + /// The graphic control label. + /// + public const byte GraphicControlLabel = 0xF9; + + /// + /// The application extension label. + /// + public const byte ApplicationExtensionLabel = 0xFF; + + /// + /// The application identification. + /// + public const string ApplicationIdentification = "NETSCAPE2.0"; + + /// + /// The application block size. + /// + public const byte ApplicationBlockSize = 0x0b; + + /// + /// The comment label. + /// + public const byte CommentLabel = 0xFE; + + /// + /// The maximum comment length. + /// + public const int MaxCommentLength = 1024 * 8; + + /// + /// The image descriptor label ,. + /// + public const byte ImageDescriptorLabel = 0x2C; + + /// + /// The plain text label. + /// + public const byte PlainTextLabel = 0x01; + + /// + /// The image label introducer ,. + /// + public const byte ImageLabel = 0x2C; + + /// + /// The terminator. + /// + public const byte Terminator = 0; + + /// + /// The end introducer trailer ;. + /// + public const byte EndIntroducer = 0x3B; + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/GifDecoder.cs b/src/ImageProcessorCore/Formats/Gif/GifDecoder.cs new file mode 100644 index 0000000000..3f73510ca7 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifDecoder.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Decoder for generating an image out of a gif encoded stream. + /// + public class GifDecoder : IImageDecoder + { + /// + /// Gets the size of the header for this image type. + /// + /// The size of the header. + public int HeaderSize => 6; + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file extension. + /// + /// True if the decoder supports the file extension; otherwise, false. + /// + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, nameof(extension)); + + extension = extension.StartsWith(".") ? extension.Substring(1) : extension; + return extension.Equals("GIF", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file header. + /// + /// True if the decoder supports the file header; otherwise, false. + /// + 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 + } + + /// + public void Decode(Image image, Stream stream) + where T : IPackedVector + where TP : struct + { + new GifDecoderCore().Decode(image, stream); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/GifDecoderCore.cs b/src/ImageProcessorCore/Formats/Gif/GifDecoderCore.cs new file mode 100644 index 0000000000..f431d3a0a3 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifDecoderCore.cs @@ -0,0 +1,426 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Performs the gif decoding operation. + /// + /// The pixel format. + /// The packed format. long, float. + internal class GifDecoderCore + where T : IPackedVector + where TP : struct + { + /// + /// The image to decode the information to. + /// + private Image decodedImage; + + /// + /// The currently loaded stream. + /// + private Stream currentStream; + + /// + /// The global color table. + /// + private byte[] globalColorTable; + + /// + /// The current frame. + /// + private T[] currentFrame; + + /// + /// The logical screen descriptor. + /// + private GifLogicalScreenDescriptor logicalScreenDescriptor; + + /// + /// The graphics control extension. + /// + private GifGraphicsControlExtension graphicsControlExtension; + + /// + /// Decodes the stream to the image. + /// + /// The image to decode to. + /// The stream containing image data. + public void Decode(Image 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(); + } + } + + /// + /// Reads the graphic control extension. + /// + 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) + }; + } + + /// + /// Reads the image descriptor + /// + /// + 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; + } + + /// + /// Reads the logical screen descriptor. + /// + 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}'"); + } + } + + /// + /// Skips the designated number of bytes in the stream. + /// + /// The number of bytes to skip. + 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); + } + } + + /// + /// Reads the gif comments. + /// + 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))); + } + } + + /// + /// Reads an individual gif frame. + /// + 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); + } + + /// + /// Reads the frame indices marking the color to use for each pixel. + /// + /// The . + /// The + 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; + } + + /// + /// Reads the local color table from the current frame. + /// + /// The . + /// The + 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; + } + + /// + /// Reads the frames colors, mapping indices to colors. + /// + /// The indexed pixels. + /// The color table containing the available colors. + /// The + 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 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 frame = new ImageFrame(); + + 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; + } + } + } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs b/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs new file mode 100644 index 0000000000..4ece1c769c --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + using ImageProcessorCore.Quantizers; + + /// + /// Image encoder for writing image data to a stream in gif format. + /// + public class GifEncoder : IImageEncoder + { + /// + /// Gets or sets the quality of output for images. + /// + /// For gifs the value ranges from 1 to 256. + public int Quality { get; set; } + + /// + /// Gets or sets the transparency threshold. + /// + public byte Threshold { get; set; } = 128; + + /// + /// The quantizer for reducing the color count. + /// + public IQuantizer Quantizer { get; set; } + + /// + public string Extension => "gif"; + + /// + public string MimeType => "image/gif"; + + /// + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, nameof(extension)); + + extension = extension.StartsWith(".") ? extension.Substring(1) : extension; + return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase); + } + + /// + public void Encode(ImageBase image, Stream stream) + where T : IPackedVector + where TP : struct + { + GifEncoderCore encoder = new GifEncoderCore + { + Quality = this.Quality, + Quantizer = this.Quantizer, + Threshold = this.Threshold + }; + + encoder.Encode(image, stream); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs b/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs new file mode 100644 index 0000000000..9ac7c2d584 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs @@ -0,0 +1,313 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using IO; + using Quantizers; + + /// + /// Performs the gif encoding operation. + /// + internal sealed class GifEncoderCore + { + /// + /// The number of bits requires to store the image palette. + /// + private int bitDepth; + + /// + /// Gets or sets the quality of output for images. + /// + /// For gifs the value ranges from 1 to 256. + public int Quality { get; set; } + + /// + /// Gets or sets the transparency threshold. + /// + public byte Threshold { get; set; } = 128; + + /// + /// The quantizer for reducing the color count. + /// + public IQuantizer Quantizer { get; set; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The packed format. long, float. + /// The to encode from. + /// The to encode the image data to. + public void Encode(ImageBase imageBase, Stream stream) + where T : IPackedVector + where TP : struct + { + Guard.NotNull(imageBase, nameof(imageBase)); + Guard.NotNull(stream, nameof(stream)); + + Image image = (Image)imageBase; + + if (this.Quantizer == null) + { + this.Quantizer = new OctreeQuantizer { 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 quantized = ((IQuantizer)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 frame in image.Frames) + { + QuantizedImage quantizedFrame = ((IQuantizer)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); + } + + /// + /// Writes the file header signature and version to the stream. + /// + /// The writer to write to the stream with. + private void WriteHeader(EndianBinaryWriter writer) + { + writer.Write((GifConstants.FileType + GifConstants.FileVersion).ToCharArray()); + } + + /// + /// Writes the logical screen descriptor to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to encode. + /// The writer to write to the stream with. + /// The transparency index to set the default backgound index to. + private void WriteLogicalScreenDescriptor(Image image, EndianBinaryWriter writer, int tranparencyIndex) + where T : IPackedVector + 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); + } + + /// + /// Writes the application exstension to the stream. + /// + /// The writer to write to the stream with. + /// The animated image repeat count. + /// Th number of image frames. + 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 + } + } + + /// + /// Writes the graphics control extension to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The to encode. + /// The stream to write to. + /// The index of the color in the color palette to make transparent. + private void WriteGraphicalControlExtension(ImageBase image, EndianBinaryWriter writer, int transparencyIndex) + where T : IPackedVector + 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); + } + + /// + /// Writes the image descriptor to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The to be encoded. + /// The stream to write to. + private void WriteImageDescriptor(ImageBase image, EndianBinaryWriter writer) + where T : IPackedVector + 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); + } + + /// + /// Writes the color table to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The to encode. + /// The writer to write to the stream with. + private void WriteColorTable(QuantizedImage image, EndianBinaryWriter writer) + where T : IPackedVector + 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); + } + + /// + /// Writes the image pixel data to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The containing indexed pixels. + /// The stream to write to. + private void WriteImageData(QuantizedImage image, EndianBinaryWriter writer) + where T : IPackedVector + where TP : struct + { + byte[] indexedPixels = image.Pixels; + + LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)this.bitDepth); + encoder.Encode(writer.BaseStream); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/GifFormat.cs b/src/ImageProcessorCore/Formats/Gif/GifFormat.cs new file mode 100644 index 0000000000..572815630f --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifFormat.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Encapsulates the means to encode and decode gif images. + /// + public class GifFormat : IImageFormat + { + /// + public IImageDecoder Decoder => new GifDecoder(); + + /// + public IImageEncoder Encoder => new GifEncoder(); + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/LzwDecoder.cs b/src/ImageProcessorCore/Formats/Gif/LzwDecoder.cs new file mode 100644 index 0000000000..75d590673a --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/LzwDecoder.cs @@ -0,0 +1,231 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Decompresses and decodes data using the dynamic LZW algorithms. + /// + internal sealed class LzwDecoder + { + /// + /// The max decoder pixel stack size. + /// + private const int MaxStackSize = 4096; + + /// + /// The null code. + /// + private const int NullCode = -1; + + /// + /// The stream to decode. + /// + private readonly Stream stream; + + /// + /// Initializes a new instance of the class + /// and sets the stream, where the compressed data should be read from. + /// + /// The stream to read from. + /// is null. + public LzwDecoder(Stream stream) + { + Guard.NotNull(stream, nameof(stream)); + + this.stream = stream; + } + + /// + /// Decodes and decompresses all pixel indices from the stream. + /// + /// + /// + /// The width of the pixel index array. + /// The height of the pixel index array. + /// Size of the data. + /// The decoded and uncompressed array. + 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; + } + + /// + /// 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. + /// + /// + /// The . + /// + private byte[] ReadBlock() + { + int blockSize = this.stream.ReadByte(); + return this.ReadBytes(blockSize); + } + + /// + /// Reads the specified number of bytes from the data stream. + /// + /// + /// The number of bytes to read. + /// + /// + /// The . + /// + private byte[] ReadBytes(int length) + { + byte[] buffer = new byte[length]; + this.stream.Read(buffer, 0, length); + return buffer; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/LzwEncoder.cs b/src/ImageProcessorCore/Formats/Gif/LzwEncoder.cs new file mode 100644 index 0000000000..a9681d2c56 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/LzwEncoder.cs @@ -0,0 +1,385 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Encodes and compresses the image data using dynamic Lempel-Ziv compression. + /// + /// + /// Adapted from Jef Poskanzer's Java port by way of J. M. G. Elliott. K Weiner 12/00 + /// + /// GIFCOMPR.C - GIF Image compression routines + /// + /// Lempel-Ziv compression based on 'compress'. GIF modifications by + /// David Rowley (mgardi@watdcsu.waterloo.edu) + /// + /// + /// 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) + /// + /// + 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; + + /// + /// Number of bits/code + /// + private int bitCount; + + /// + /// User settable max # bits/code + /// + 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]; + + /// + /// For dynamic table sizing + /// + private int hsize = HashSize; + + /// + /// First unused entry + /// + private int freeEntry; + + /// + /// Block compression parameters -- after all codes are used up, + /// and compression rate changes, start over. + /// + 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 + }; + + /// + /// Number of characters so far in this 'packet' + /// + private int accumulatorCount; + + /// + /// Define the storage for the packet accumulator. + /// + private readonly byte[] accumulators = new byte[256]; + + /// + /// Initializes a new instance of the class. + /// + /// The array of indexed pixels. + /// The color depth in bits. + public LzwEncoder(byte[] indexedPixels, int colorDepth) + { + this.pixelArray = indexedPixels; + this.initialCodeSize = Math.Max(2, colorDepth); + } + + /// + /// Encodes and compresses the indexed pixels to the stream. + /// + /// The stream to write to. + 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); + } + + /// + /// Gets the maximum code value + /// + /// The number of bits + /// See + private static int GetMaxcode(int bitCount) + { + return (1 << bitCount) - 1; + } + + /// + /// Add a character to the end of the current packet, and if it is 254 characters, + /// flush the packet to disk. + /// + /// The character to add. + /// The stream to write to. + private void AddCharacter(byte c, Stream stream) + { + this.accumulators[this.accumulatorCount++] = c; + if (this.accumulatorCount >= 254) + { + this.FlushPacket(stream); + } + } + + /// + /// Table clear for block compress + /// + /// The output stream. + private void ClearBlock(Stream stream) + { + this.ResetCodeTable(this.hsize); + this.freeEntry = this.clearCode + 2; + this.clearFlag = true; + + this.Output(this.clearCode, stream); + } + + /// + /// Reset the code table. + /// + /// The hash size. + private void ResetCodeTable(int size) + { + for (int i = 0; i < size; ++i) + { + this.hashTable[i] = -1; + } + } + + /// + /// Compress the packets to the stream. + /// + /// The inital bits. + /// The stream to write to. + 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; + } + } + + /// + /// Return the next pixel from the image + /// + /// + /// The + /// + 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; + } + + /// + /// Output the current code to the stream. + /// + /// The code. + /// The stream to write to. + 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); + } + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Formats/Gif/PackedField.cs b/src/ImageProcessorCore/Formats/Gif/PackedField.cs new file mode 100644 index 0000000000..0141d36c6a --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/PackedField.cs @@ -0,0 +1,194 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + + /// + /// Represents a byte of data in a GIF data stream which contains a number + /// of data items. + /// + internal struct PackedField : IEquatable + { + /// + /// The individual bits representing the packed byte. + /// + private static readonly bool[] Bits = new bool[8]; + + /// + /// Gets the byte which represents the data items held in this instance. + /// + 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); + } + } + + /// + /// Returns a new with the bits in the packed fields to + /// the corresponding bits from the supplied byte. + /// + /// The value to pack. + /// The + public static PackedField FromInt(byte value) + { + PackedField packed = new PackedField(); + packed.SetBits(0, 8, value); + return packed; + } + + /// + /// Sets the specified bit within the packed fields to the supplied + /// value. + /// + /// + /// The zero-based index within the packed fields of the bit to set. + /// + /// + /// The value to set the bit to. + /// + 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; + } + + /// + /// Sets the specified bits within the packed fields to the supplied + /// value. + /// + /// The zero-based index within the packed fields of the first bit to set. + /// The number of bits to set. + /// The value to set the bits to. + 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--; + } + } + + /// + /// Gets the value of the specified bit within the byte. + /// + /// The zero-based index of the bit to get. + /// + /// The value of the specified bit within the byte. + /// + 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]; + } + + /// + /// Gets the value of the specified bits within the byte. + /// + /// The zero-based index of the first bit to get. + /// The number of bits to get. + /// + /// The value of the specified bits within the byte. + /// + 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; + } + + /// + public override bool Equals(object obj) + { + PackedField? field = obj as PackedField?; + + return this.Byte == field?.Byte; + } + + /// + public bool Equals(PackedField other) + { + return this.Byte.Equals(other.Byte); + } + + /// + public override string ToString() + { + return $"PackedField [ Byte={this.Byte} ]"; + } + + /// + public override int GetHashCode() + { + return this.Byte.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Formats/Gif/README.md b/src/ImageProcessorCore/Formats/Gif/README.md new file mode 100644 index 0000000000..d47a4c6836 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/README.md @@ -0,0 +1,4 @@ +Encoder/Decoder adapted and extended from: + +https://github.com/yufeih/Nine.Imaging/ +https://imagetools.codeplex.com/ diff --git a/src/ImageProcessorCore/Formats/Gif/Sections/GifGraphicsControlExtension.cs b/src/ImageProcessorCore/Formats/Gif/Sections/GifGraphicsControlExtension.cs new file mode 100644 index 0000000000..071dc62c84 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/Sections/GifGraphicsControlExtension.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// The Graphic Control Extension contains parameters used when + /// processing a graphic rendering block. + /// + internal sealed class GifGraphicsControlExtension + { + /// + /// Gets or sets the disposal method which indicates the way in which the + /// graphic is to be treated after being displayed. + /// + public DisposalMethod DisposalMethod { get; set; } + + /// + /// 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.) + /// + public bool TransparencyFlag { get; set; } + + /// + /// 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. + /// + public int TransparencyIndex { get; set; } + + /// + /// 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. + /// + public int DelayTime { get; set; } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/Sections/GifImageDescriptor.cs b/src/ImageProcessorCore/Formats/Gif/Sections/GifImageDescriptor.cs new file mode 100644 index 0000000000..62737de660 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/Sections/GifImageDescriptor.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// 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. + /// + internal sealed class GifImageDescriptor + { + /// + /// 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. + /// + public short Left { get; set; } + + /// + /// 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. + /// + public short Top { get; set; } + + /// + /// Gets or sets the width of the image in pixels. + /// + public short Width { get; set; } + + /// + /// Gets or sets the height of the image in pixels. + /// + public short Height { get; set; } + + /// + /// Gets or sets a value indicating whether the presence of a Local Color Table immediately + /// follows this Image Descriptor. + /// + public bool LocalColorTableFlag { get; set; } + + /// + /// 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. + /// + public int LocalColorTableSize { get; set; } + + /// + /// Gets or sets a value indicating whether the image is to be interlaced. + /// An image is interlaced in a four-pass interlace pattern. + /// + public bool InterlaceFlag { get; set; } + } +} diff --git a/src/ImageProcessorCore/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs b/src/ImageProcessorCore/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs new file mode 100644 index 0000000000..8c0400f24d --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/Sections/GifLogicalScreenDescriptor.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// The Logical Screen Descriptor contains the parameters + /// necessary to define the area of the display device + /// within which the images will be rendered + /// + internal sealed class GifLogicalScreenDescriptor + { + /// + /// Gets or sets the width, in pixels, of the Logical Screen where the images will + /// be rendered in the displaying device. + /// + public short Width { get; set; } + + /// + /// Gets or sets the height, in pixels, of the Logical Screen where the images will be + /// rendered in the displaying device. + /// + public short Height { get; set; } + + /// + /// 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. + /// + public byte BackgroundColorIndex { get; set; } + + /// + /// Gets or sets the pixel aspect ratio. Default to 0. + /// + public byte PixelAspectRatio { get; set; } + + /// + /// 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. + /// + public bool GlobalColorTableFlag { get; set; } + + /// + /// 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. + /// + public int GlobalColorTableSize { get; set; } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index 4c76a5c00b..eeb7c651f4 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -218,8 +218,7 @@ namespace ImageProcessorCore.Formats if (this.Quantizer == null) { - //this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; - this.Quantizer = new OctreeQuantizer { Threshold = this.Threshold }; + this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; } // Quantize the image returning a palette. This boxing is icky. diff --git a/src/ImageProcessorCore/Image/ImageExtensions.cs b/src/ImageProcessorCore/Image/ImageExtensions.cs index 544c346596..53133207fc 100644 --- a/src/ImageProcessorCore/Image/ImageExtensions.cs +++ b/src/ImageProcessorCore/Image/ImageExtensions.cs @@ -16,24 +16,35 @@ namespace ImageProcessorCore /// public static partial class ImageExtensions { - ///// - ///// Saves the image to the given stream with the bmp format. - ///// - ///// The image this method extends. - ///// The stream to save the image to. - ///// Thrown if the stream is null. - //public static void SaveAsBmp(this ImageBase source, Stream stream) => new BmpEncoder().Encode(source, stream); + /// + /// Saves the image to the given stream with the bmp format. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsBmp(this ImageBase source, Stream stream) + where T : IPackedVector + where TP : struct + => new BmpEncoder().Encode(source, stream); - ///// - ///// Saves the image to the given stream with the png format. - ///// - ///// The image this method extends. - ///// The stream to save the image to. - ///// The quality to save the image to representing the number of colors. - ///// Anything equal to 256 and below will cause the encoder to save the image in an indexed format. - ///// - ///// Thrown if the stream is null. - //public static void SaveAsPng(this ImageBase source, Stream stream, int quality = Int32.MaxValue) => new PngEncoder { Quality = quality }.Encode(source, stream); + + /// + /// Saves the image to the given stream with the png format. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The stream to save the image to. + /// The quality to save the image to representing the number of colors. + /// Anything equal to 256 and below will cause the encoder to save the image in an indexed format. + /// + /// Thrown if the stream is null. + public static void SaveAsPng(this ImageBase source, Stream stream, int quality = int.MaxValue) + where T : IPackedVector + where TP : struct + => new PngEncoder { Quality = quality }.Encode(source, stream); ///// ///// Saves the image to the given stream with the jpeg format. @@ -44,14 +55,19 @@ namespace ImageProcessorCore ///// Thrown if the stream is null. //public static void SaveAsJpeg(this ImageBase source, Stream stream, int quality = 75) => new JpegEncoder { Quality = quality }.Encode(source, stream); - ///// - ///// Saves the image to the given stream with the gif format. - ///// - ///// The image this method extends. - ///// The stream to save the image to. - ///// The quality to save the image to representing the number of colors. Between 1 and 256. - ///// Thrown if the stream is null. - //public static void SaveAsGif(this ImageBase source, Stream stream, int quality = 256) => new GifEncoder { Quality = quality }.Encode(source, stream); + /// + /// Saves the image to the given stream with the gif format. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image this method extends. + /// The stream to save the image to. + /// The quality to save the image to representing the number of colors. Between 1 and 256. + /// Thrown if the stream is null. + public static void SaveAsGif(this ImageBase source, Stream stream, int quality = 256) + where T : IPackedVector + where TP : struct + => new GifEncoder { Quality = quality }.Encode(source, stream); /// /// Applies the collection of processors to the image. diff --git a/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs index 9d94869ceb..344a1607e5 100644 --- a/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs +++ b/src/ImageProcessorCore/Quantizers/Octree/OctreeQuantizer.cs @@ -106,7 +106,10 @@ namespace ImageProcessorCore.Quantizers List palette = this.octree.Palletize(Math.Max(this.colors, 1)); int diff = this.colors - palette.Count; - palette.AddRange(Enumerable.Repeat(default(T), diff)); + if (diff > 0) + { + palette.AddRange(Enumerable.Repeat(default(T), diff)); + } this.TransparentIndex = this.colors; return palette; diff --git a/tests/ImageProcessorCore.Tests/FileTestBase.cs b/tests/ImageProcessorCore.Tests/FileTestBase.cs index 280e0e5120..0ef3403a18 100644 --- a/tests/ImageProcessorCore.Tests/FileTestBase.cs +++ b/tests/ImageProcessorCore.Tests/FileTestBase.cs @@ -25,14 +25,14 @@ namespace ImageProcessorCore.Tests //"TestImages/Formats/Jpg/progress.jpg", // Perf: Enable for local testing only //"TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg", // Perf: Enable for local testing only "TestImages/Formats/Bmp/Car.bmp", - // "TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only + //"TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only //"TestImages/Formats/Png/blur.png", // Perf: Enable for local testing only //"TestImages/Formats/Png/indexed.png", // Perf: Enable for local testing only "TestImages/Formats/Png/splash.png", - //"TestImages/Formats/Gif/rings.gif", + "TestImages/Formats/Gif/rings.gif", //"TestImages/Formats/Gif/giphy.gif" // Perf: Enable for local testing only }; - + protected void ProgressUpdate(object sender, ProgressEventArgs e) { Assert.InRange(e.RowsProcessed, 1, e.TotalRows); diff --git a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs index ba12cdf107..9d9726a9c6 100644 --- a/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs +++ b/tests/ImageProcessorCore.Tests/Processors/Samplers/SamplerTests.cs @@ -114,7 +114,7 @@ namespace ImageProcessorCore.Tests { string filename = Path.GetFileNameWithoutExtension(file) + "-" + name + Path.GetExtension(file); - Image image = new Image(stream); + Image image = new Image(stream) {Quality=256}; using (FileStream output = File.OpenWrite($"TestOutput/Resize/{filename}")) { image.Resize(image.Width / 2, image.Height / 2, sampler, false, this.ProgressUpdate)