diff --git a/src/ImageProcessor/Formats/Gif/GifEncoder.cs b/src/ImageProcessor/Formats/Gif/GifEncoder.cs index 549333ac4..46bb8dd31 100644 --- a/src/ImageProcessor/Formats/Gif/GifEncoder.cs +++ b/src/ImageProcessor/Formats/Gif/GifEncoder.cs @@ -7,22 +7,13 @@ namespace ImageProcessor.Formats { using System; using System.IO; + using System.Linq; /// /// The Gif encoder /// public class GifEncoder : IImageEncoder { - /// - /// The gif decoder if any used to decode the original image. - /// - private GifDecoder gifDecoder; - - /// - /// The currently processed image. - /// - private ImageBase currentImage; - /// /// Gets or sets the quality of output for images. /// @@ -62,13 +53,6 @@ namespace ImageProcessor.Formats Image image = (Image)imageBase; - // Try to grab and assign an image decoder. - IImageDecoder decoder = image.CurrentDecoder; - if (decoder.GetType() == typeof(GifDecoder)) - { - this.gifDecoder = (GifDecoder)decoder; - } - // Write the header. // File Header signature and version. this.WriteString(stream, GifConstants.FileType); @@ -84,63 +68,41 @@ namespace ImageProcessor.Formats // Write the LSD and check to see if we need a global color table. // Always true just now. bool globalColor = this.WriteGlobalLogicalScreenDescriptor(image, stream, bitdepth); - - QuantizedImage quantized; - - // Should always be true. - if (globalColor) - { - quantized = this.WriteColorTable(imageBase, stream, quality, bitdepth); - } - else - { - // Quantize the image returning a pallete. - IQuantizer quantizer = new OctreeQuantizer(quality.Clamp(1, 255), bitdepth + 1); - quantized = quantizer.Quantize(image); - } + QuantizedImage quantized = this.WriteColorTable(imageBase, stream, quality, bitdepth); this.WriteGraphicalControlExtension(imageBase, stream); + this.WriteImageDescriptor(quantized, quality, stream); - // TODO: Write Comments - this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count); - this.WriteImageDescriptor(quantized.ToImage(), quality, stream, true); - - // TODO: Write Image Info - - foreach (ImageFrame frame in image.Frames) + if (image.Frames.Any()) { - this.WriteGraphicalControlExtension(frame, stream); - this.WriteImageDescriptor(imageBase, quality, stream, false); + this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count); + foreach (ImageFrame frame in image.Frames) + { + this.WriteGraphicalControlExtension(frame, stream); + this.WriteFrameImageDescriptor(frame, stream); + } } - // Cleanup - this.gifDecoder = null; + // TODO: Write Comments extension etc + this.WriteByte(stream, GifConstants.EndIntroducer); } + /// + /// Writes the logical screen descriptor to the stream. + /// + /// The image to encode. + /// The stream to write to. + /// The bit depth. + /// The private bool WriteGlobalLogicalScreenDescriptor(Image image, Stream stream, int bitDepth) { - GifLogicalScreenDescriptor descriptor; - - // Try and grab an existing descriptor. - if (this.gifDecoder != null) + GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor { - // Ensure the dimensions etc are up to date. - descriptor = this.gifDecoder.CoreDecoder.LogicalScreenDescriptor; - descriptor.Width = (short)image.Width; - descriptor.Height = (short)image.Height; - descriptor.GlobalColorTableFlag = true; - descriptor.GlobalColorTableSize = bitDepth; - } - else - { - descriptor = new GifLogicalScreenDescriptor - { - Width = (short)image.Width, - Height = (short)image.Height, - GlobalColorTableFlag = true, - GlobalColorTableSize = bitDepth - }; - } + Width = (short)image.Width, + Height = (short)image.Height, + GlobalColorTableFlag = true, + GlobalColorTableSize = bitDepth + }; this.WriteShort(stream, descriptor.Width); this.WriteShort(stream, descriptor.Height); @@ -157,6 +119,14 @@ namespace ImageProcessor.Formats return descriptor.GlobalColorTableFlag; } + /// + /// Writes the color table to the stream. + /// + /// The to encode. + /// The stream to write to. + /// The quality (number of colors) to encode the image to. + /// The bit depth. + /// The private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality, int bitDepth) { // Quantize the image returning a pallete. @@ -166,7 +136,9 @@ namespace ImageProcessor.Formats // Grab the pallete and write it to the stream. Bgra[] pallete = quantizedImage.Palette; int pixelCount = pallete.Length; - int colorTableLength = pixelCount * 3; + + // Get max colors for bit depth. + int colorTableLength = (int)Math.Pow(2, bitDepth) * 3; byte[] colorTable = new byte[colorTableLength]; for (int i = 0; i < pixelCount; i++) @@ -183,40 +155,30 @@ namespace ImageProcessor.Formats return quantizedImage; } + /// + /// Writes the graphics control extension to the stream. + /// + /// The to encode. + /// The stream to write to. private void WriteGraphicalControlExtension(ImageBase image, Stream stream) { - GifGraphicsControlExtension extension; - // Calculate the quality. int quality = this.Quality > 0 ? this.Quality : image.Quality; quality = quality > 0 ? quality.Clamp(1, 256) : 256; - // Try and grab an existing descriptor. - // TODO: Check whether we need to. - if (this.gifDecoder != null) - { - // Ensure the dimensions etc are up to date. - extension = this.gifDecoder.CoreDecoder.GraphicsControlExtension; - extension.TransparencyFlag = quality > 1; - extension.TransparencyIndex = quality - 1; // Quantizer set last as transparent. - extension.DelayTime = image.FrameDelay; - } - else - { - // TODO: Check transparency logic. - bool hasTransparent = quality > 1; - DisposalMethod disposalMethod = hasTransparent - ? DisposalMethod.RestoreToBackground - : DisposalMethod.Unspecified; + // TODO: Check transparency logic. + bool hasTransparent = quality > 1; + DisposalMethod disposalMethod = hasTransparent + ? DisposalMethod.RestoreToBackground + : DisposalMethod.Unspecified; - extension = new GifGraphicsControlExtension() - { - DisposalMethod = disposalMethod, - TransparencyFlag = hasTransparent, - TransparencyIndex = quality - 1, - DelayTime = image.FrameDelay - }; - } + GifGraphicsControlExtension extension = new GifGraphicsControlExtension() + { + DisposalMethod = disposalMethod, + TransparencyFlag = hasTransparent, + TransparencyIndex = quality - 1, // Quantizer sets last index as transparent. + DelayTime = image.FrameDelay + }; this.WriteByte(stream, GifConstants.ExtensionIntroducer); this.WriteByte(stream, GifConstants.GraphicControlLabel); @@ -233,6 +195,12 @@ namespace ImageProcessor.Formats this.WriteByte(stream, GifConstants.Terminator); } + /// + /// Writes the application exstension to the stream. + /// + /// The stream to write to. + /// The animated image repeat count. + /// Th number of image frames. private void WriteApplicationExtension(Stream stream, ushort repeatCount, int frames) { // Application Extension Header @@ -254,7 +222,13 @@ namespace ImageProcessor.Formats } } - private void WriteImageDescriptor(ImageBase image, int quality, Stream stream, bool first) + /// + /// Writes the image descriptor to the stream. + /// + /// The containing indexed pixels. + /// The quality (number of colors) to encode the image to. + /// The stream to write to. + private void WriteImageDescriptor(QuantizedImage image, int quality, Stream stream) { this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c // TODO: Can we capture this? @@ -263,43 +237,59 @@ namespace ImageProcessor.Formats this.WriteShort(stream, image.Width); this.WriteShort(stream, image.Height); - if (first) - { - // Calculate the quality. - int bitdepth = this.GetBitsNeededForColorDepth(quality); + // Calculate the quality. + int bitdepth = this.GetBitsNeededForColorDepth(quality); - // No LCT use GCT. - this.WriteByte(stream, 0); + // No LCT use GCT. + this.WriteByte(stream, 0); - // Write the image data. Pixels have already been quantized. - this.WriteImageData(image, stream, bitdepth); - } - else - { - // Calculate the quality. - quality = this.Quality > 0 ? this.Quality : image.Quality; - quality = quality > 0 ? quality.Clamp(1, 256) : 256; - int bitdepth = this.GetBitsNeededForColorDepth(quality); - - int packed = 0x80 | // 1: Local color table flag = 1 (LCT used) - 0x00 | // 2: Interlace flag 0 - 0x00 | // 3: Sort flag 0 - 0 | // 4-5: Reserved - bitdepth - 1; - - this.WriteByte(stream, packed); - - // Now immediately follow with the color table. - QuantizedImage quantized = this.WriteColorTable(image, stream, quality, bitdepth); - this.WriteImageData(quantized.ToImage(), stream, bitdepth); - } + // Write the image data.. + this.WriteImageData(image, stream, bitdepth); + } - this.WriteByte(stream, GifConstants.EndIntroducer); + /// + /// Writes the image descriptor to the stream. + /// + /// The to be encoded. + /// The stream to write to. + private void WriteFrameImageDescriptor(ImageBase image, Stream stream) + { + this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c + // TODO: Can we capture this? + this.WriteShort(stream, 0); // Left position + this.WriteShort(stream, 0); // Top position + this.WriteShort(stream, image.Width); + this.WriteShort(stream, image.Height); + + // Calculate the quality. + int quality = this.Quality > 0 ? this.Quality : image.Quality; + quality = quality > 0 ? quality.Clamp(1, 256) : 256; + int bitdepth = this.GetBitsNeededForColorDepth(quality); + + int packed = 0x80 | // 1: Local color table flag = 1 (LCT used) + 0x00 | // 2: Interlace flag 0 + 0x00 | // 3: Sort flag 0 + 0 | // 4-5: Reserved + bitdepth - 1; + + this.WriteByte(stream, packed); + + // Now immediately follow with the color table. + QuantizedImage quantized = this.WriteColorTable(image, stream, quality, bitdepth); + this.WriteImageData(quantized, stream, bitdepth); } - private void WriteImageData(ImageBase image, Stream stream, int bitDepth) + /// + /// Writes the image pixel data to the stream. + /// + /// The containing indexed pixels. + /// The stream to write to. + /// The bit depth of the image. + private void WriteImageData(QuantizedImage image, Stream stream, int bitDepth) { - LzwEncoder encoder = new LzwEncoder(image.Pixels, (byte)bitDepth); + byte[] indexedPixels = image.Pixels; + + LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)bitDepth); encoder.Encode(stream); this.WriteByte(stream, GifConstants.Terminator); diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs index a7ebe8ec3..1cf697589 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/OctreeQuantizer.cs @@ -338,7 +338,7 @@ namespace ImageProcessor.Formats private int paletteIndex; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// The level in the tree = 0 - 7 @@ -415,7 +415,7 @@ namespace ImageProcessor.Formats if (null == child) { - // Create a new child node & store in the array + // Create a new child node and store it in the array child = new OctreeNode(level + 1, colorBits, octree); this.children[index] = child; } @@ -511,8 +511,8 @@ namespace ImageProcessor.Formats { int shift = 7 - level; int pixelIndex = ((pixel.R & Mask[level]) >> (shift - 2)) | - ((pixel.G & Mask[level]) >> (shift - 1)) | - ((pixel.B & Mask[level]) >> shift); + ((pixel.G & Mask[level]) >> (shift - 1)) | + ((pixel.B & Mask[level]) >> shift); if (null != this.children[pixelIndex]) { diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs index ec00bf508..9efe62ec3 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs @@ -61,9 +61,11 @@ namespace ImageProcessor.Formats byte[] quantizedPixels = new byte[width * height]; + List palette = this.GetPalette(); + this.SecondPass(imageBase, quantizedPixels, width, height); - return new QuantizedImage(width, height, this.GetPalette().ToArray(), quantizedPixels); + return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels); } /// @@ -158,4 +160,4 @@ namespace ImageProcessor.Formats /// protected abstract List GetPalette(); } -} +} \ No newline at end of file diff --git a/src/ImageProcessor/Image.cs b/src/ImageProcessor/Image.cs index ded47cc3f..8a4adb1f2 100644 --- a/src/ImageProcessor/Image.cs +++ b/src/ImageProcessor/Image.cs @@ -232,11 +232,6 @@ namespace ImageProcessor /// A list of image properties. public IList Properties { get; } = new List(); - /// - /// The current decoder - /// - internal IImageDecoder CurrentDecoder { get; set; } - /// /// Loads the image from the given stream. /// @@ -276,8 +271,7 @@ namespace ImageProcessor IImageDecoder decoder = decoders.FirstOrDefault(x => x.IsSupportedFileFormat(header)); if (decoder != null) { - this.CurrentDecoder = decoder; - this.CurrentDecoder.Decode(this, stream); + decoder.Decode(this, stream); return; } } diff --git a/src/ImageProcessor/ImageBase.cs b/src/ImageProcessor/ImageBase.cs index 22362f0e5..c90516861 100644 --- a/src/ImageProcessor/ImageBase.cs +++ b/src/ImageProcessor/ImageBase.cs @@ -145,7 +145,7 @@ namespace ImageProcessor throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width."); } - if ((y < 0) || (y >= this.Width)) + if ((y < 0) || (y >= this.Height)) { throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height."); } @@ -163,7 +163,7 @@ namespace ImageProcessor throw new ArgumentOutOfRangeException(nameof(x), "Value cannot be less than zero or greater than the bitmap width."); } - if ((y < 0) || (y >= this.Width)) + if ((y < 0) || (y >= this.Height)) { throw new ArgumentOutOfRangeException(nameof(y), "Value cannot be less than zero or greater than the bitmap height."); } diff --git a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs index 86732b1e9..dae8f8763 100644 --- a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs +++ b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs @@ -11,14 +11,14 @@ public class EncoderDecoderTests { [Theory] - //[InlineData("TestImages/Car.bmp")] - //[InlineData("TestImages/Portrait.png")] - //[InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")] + [InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")] [InlineData("../../TestImages/Formats/Gif/a.gif")] - //[InlineData("../../TestImages/Formats/Gif/leaf.gif")] - //[InlineData("TestImages/Windmill.gif")] - //[InlineData("../../TestImages/Formats/Bmp/Car.bmp")] - //[InlineData("../../TestImages/Formats/Png/cmyk.png")] + [InlineData("../../TestImages/Formats/Gif/leaf.gif")] + [InlineData("../../TestImages/Formats/Gif/ani.gif")] + [InlineData("../../TestImages/Formats/Gif/ani2.gif")] + [InlineData("../../TestImages/Formats/Gif/giphy.gif")] + [InlineData("../../TestImages/Formats/Bmp/Car.bmp")] + [InlineData("../../TestImages/Formats/Png/cmyk.png")] public void DecodeThenEncodeImageFromStreamShouldSucceed(string filename) { if (!Directory.Exists("Encoded")) @@ -29,7 +29,7 @@ FileStream stream = File.OpenRead(filename); Stopwatch watch = Stopwatch.StartNew(); Image image = new Image(stream); - + string encodedFilename = "Encoded/" + Path.GetFileName(filename); //if (!image.IsAnimated) @@ -62,8 +62,8 @@ [Theory] [InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")] - [InlineData("../../TestImages/Formats/Bmp/Car.bmp")] - [InlineData("../../TestImages/Formats/Png/cmyk.png")] + //[InlineData("../../TestImages/Formats/Bmp/Car.bmp")] + //[InlineData("../../TestImages/Formats/Png/cmyk.png")] public void QuantizedImageShouldPreserveMaximumColorPrecision(string filename) { if (!Directory.Exists("Quantized")) @@ -74,7 +74,7 @@ Image image = new Image(File.OpenRead(filename)); IQuantizer quantizer = new OctreeQuantizer(); QuantizedImage quantizedImage = quantizer.Quantize(image); - + var pixel = quantizedImage.Pixels; using (FileStream output = File.OpenWrite($"Quantized/{ Path.GetFileName(filename) }")) { IImageEncoder encoder = Image.Encoders.First(e => e.IsSupportedFileExtension(Path.GetExtension(filename))); diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani.gif.REMOVED.git-id b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani.gif.REMOVED.git-id new file mode 100644 index 000000000..9b07936a0 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani.gif.REMOVED.git-id @@ -0,0 +1 @@ +a0cc93222effb5feec0d1a1dc45efd0c5af77450 \ No newline at end of file diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani2.gif.REMOVED.git-id b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani2.gif.REMOVED.git-id new file mode 100644 index 000000000..c8fe9de52 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/ani2.gif.REMOVED.git-id @@ -0,0 +1 @@ +c2c7a5fcc0f00cdef39dacd7df0816137b3d63a3 \ No newline at end of file diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Gif/giphy.gif b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/giphy.gif new file mode 100644 index 000000000..1f2618fba Binary files /dev/null and b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/giphy.gif differ