diff --git a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs index 67d71b2c8..3ae9e2628 100644 --- a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs @@ -26,8 +26,11 @@ private byte[] currentFrame; internal GifLogicalScreenDescriptor LogicalScreenDescriptor { get; set; } + internal GifGraphicsControlExtension GraphicsControlExtension { get; set; } + internal byte Quality { get; set; } + public void Decode(Image image, Stream stream) { this.image = image; @@ -324,6 +327,7 @@ { currentImage = this.image; currentImage.SetPixels(imageWidth, imageHeight, pixels); + currentImage.Quality = colorTable.Length / 3; if (this.GraphicsControlExtension != null && this.GraphicsControlExtension.DelayTime > 0) { @@ -336,6 +340,7 @@ currentImage = frame; currentImage.SetPixels(imageWidth, imageHeight, pixels); + currentImage.Quality = colorTable.Length / 3; if (this.GraphicsControlExtension != null && this.GraphicsControlExtension.DelayTime > 0) { diff --git a/src/ImageProcessor/Formats/Gif/GifEncoder.cs b/src/ImageProcessor/Formats/Gif/GifEncoder.cs index f671b8427..549333ac4 100644 --- a/src/ImageProcessor/Formats/Gif/GifEncoder.cs +++ b/src/ImageProcessor/Formats/Gif/GifEncoder.cs @@ -14,31 +14,20 @@ namespace ImageProcessor.Formats public class GifEncoder : IImageEncoder { /// - /// The quality. + /// The gif decoder if any used to decode the original image. /// - private int quality = 256; + private GifDecoder gifDecoder; /// - /// The gif decoder if any used to decode the original image. + /// The currently processed image. /// - private GifDecoder gifDecoder; + private ImageBase currentImage; /// /// Gets or sets the quality of output for images. /// /// For gifs the value ranges from 1 to 256. - public int Quality - { - get - { - return this.quality; - } - - set - { - this.quality = value.Clamp(1, 256); - } - } + public int Quality { get; set; } /// /// Gets the default file extension for this encoder. @@ -85,34 +74,46 @@ namespace ImageProcessor.Formats this.WriteString(stream, GifConstants.FileType); this.WriteString(stream, GifConstants.FileVersion); - int bitdepth = this.GetBitsNeededForColorDepth(this.Quality) - 1; + // Calculate the quality. + int quality = this.Quality > 0 ? this.Quality : imageBase.Quality; + quality = quality > 0 ? quality.Clamp(1, 256) : 256; + + // Get the number of bits. + int bitdepth = this.GetBitsNeededForColorDepth(quality); // 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) { - this.WriteColorTable(imageBase, stream, bitdepth); + 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); } this.WriteGraphicalControlExtension(imageBase, stream); // TODO: Write Comments - this.WriteApplicationExtension(stream, image.RepeatCount); + 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) { - this.WriteColorTable(frame, stream, bitdepth); this.WriteGraphicalControlExtension(frame, stream); - // TODO: Write Image Info + this.WriteImageDescriptor(imageBase, quality, stream, false); } - throw new System.NotImplementedException(); - // Cleanup - this.Quality = 256; this.gifDecoder = null; } @@ -127,7 +128,8 @@ namespace ImageProcessor.Formats descriptor = this.gifDecoder.CoreDecoder.LogicalScreenDescriptor; descriptor.Width = (short)image.Width; descriptor.Height = (short)image.Height; - descriptor.GlobalColorTableSize = this.Quality; + descriptor.GlobalColorTableFlag = true; + descriptor.GlobalColorTableSize = bitDepth; } else { @@ -136,29 +138,29 @@ namespace ImageProcessor.Formats Width = (short)image.Width, Height = (short)image.Height, GlobalColorTableFlag = true, - GlobalColorTableSize = this.Quality + GlobalColorTableSize = bitDepth }; } this.WriteShort(stream, descriptor.Width); - this.WriteShort(stream, descriptor.Width); + this.WriteShort(stream, descriptor.Height); int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used) - 0x70 | // 2-4 : color resolution + bitDepth - 1 | // 2-4 : color resolution 0x00 | // 5 : GCT sort flag = 0 - bitDepth; // 6-8 : GCT size assume 1:1 + bitDepth - 1; // 6-8 : GCT size TODO: Check this. this.WriteByte(stream, packed); this.WriteByte(stream, descriptor.BackgroundColorIndex); // Background Color Index - this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio + this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio. Assume 1:1 return descriptor.GlobalColorTableFlag; } - private void WriteColorTable(ImageBase image, Stream stream, int bitDepth) + private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality, int bitDepth) { // Quantize the image returning a pallete. - IQuantizer quantizer = new OctreeQuantizer(Math.Max(1, this.quality - 1), bitDepth); + IQuantizer quantizer = new OctreeQuantizer(quality.Clamp(1, 255), bitDepth); QuantizedImage quantizedImage = quantizer.Quantize(image); // Grab the pallete and write it to the stream. @@ -169,34 +171,40 @@ namespace ImageProcessor.Formats for (int i = 0; i < pixelCount; i++) { - int offset = i * 4; + int offset = i * 3; Bgra color = pallete[i]; - colorTable[offset + 0] = color.B; + colorTable[offset + 2] = color.B; colorTable[offset + 1] = color.G; - colorTable[offset + 2] = color.R; + colorTable[offset + 0] = color.R; } stream.Write(colorTable, 0, colorTableLength); + + return quantizedImage; } 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 = this.Quality > 1; - extension.TransparencyIndex = this.Quality - 1; // Quantizer set last as transparent. + extension.TransparencyFlag = quality > 1; + extension.TransparencyIndex = quality - 1; // Quantizer set last as transparent. extension.DelayTime = image.FrameDelay; } else { // TODO: Check transparency logic. - bool hasTransparent = this.Quality > 1; + bool hasTransparent = quality > 1; DisposalMethod disposalMethod = hasTransparent ? DisposalMethod.RestoreToBackground : DisposalMethod.Unspecified; @@ -205,7 +213,7 @@ namespace ImageProcessor.Formats { DisposalMethod = disposalMethod, TransparencyFlag = hasTransparent, - TransparencyIndex = this.Quality - 1, + TransparencyIndex = quality - 1, DelayTime = image.FrameDelay }; } @@ -215,19 +223,20 @@ namespace ImageProcessor.Formats this.WriteByte(stream, 4); // Size int packed = 0 | // 1-3 : Reserved - (int)extension.DisposalMethod | // 4-6 : Disposal + (int)extension.DisposalMethod << 2 | // 4-6 : Disposal 0 | // 7 : User input - 0 = none - extension.TransparencyIndex; + (extension.TransparencyFlag ? 1 : 0); // 8: Has transparent. this.WriteByte(stream, packed); this.WriteShort(stream, extension.DelayTime); + this.WriteByte(stream, extension.TransparencyIndex); this.WriteByte(stream, GifConstants.Terminator); } - private void WriteApplicationExtension(Stream stream, ushort repeatCount) + private void WriteApplicationExtension(Stream stream, ushort repeatCount, int frames) { // Application Extension Header - if (repeatCount != 1) + if (repeatCount != 1 && frames > 0) { // 0 means loop indefinitely. count is set as play n + 1 times. // TODO: Check this as the correct value might be pulled from the decoder. @@ -245,6 +254,57 @@ namespace ImageProcessor.Formats } } + private void WriteImageDescriptor(ImageBase image, int quality, Stream stream, bool first) + { + 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); + + if (first) + { + // Calculate the quality. + int bitdepth = this.GetBitsNeededForColorDepth(quality); + + // 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); + } + + this.WriteByte(stream, GifConstants.EndIntroducer); + } + + private void WriteImageData(ImageBase image, Stream stream, int bitDepth) + { + LzwEncoder encoder = new LzwEncoder(image.Pixels, (byte)bitDepth); + encoder.Encode(stream); + + this.WriteByte(stream, GifConstants.Terminator); + } + /// /// Writes a short to the given stream. /// diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs b/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs index d6a0801d4..6dc5dec03 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs @@ -17,7 +17,6 @@ namespace ImageProcessor.Formats /// public class QuantizedImage { - /// /// Initializes a new instance of the class. /// @@ -73,6 +72,7 @@ namespace ImageProcessor.Formats public Image ToImage() { Image image = new Image(); + int pixelCount = this.Pixels.Length; byte[] bgraPixels = new byte[pixelCount * 4]; diff --git a/src/ImageProcessor/Image.cs b/src/ImageProcessor/Image.cs index 0f2a51f6d..ded47cc3f 100644 --- a/src/ImageProcessor/Image.cs +++ b/src/ImageProcessor/Image.cs @@ -63,7 +63,8 @@ namespace ImageProcessor { new BmpEncoder(), new JpegEncoder(), - new PngEncoder() + new PngEncoder(), + new GifEncoder(), }); /// diff --git a/src/ImageProcessor/ImageBase.cs b/src/ImageProcessor/ImageBase.cs index 897e72bc5..22362f0e5 100644 --- a/src/ImageProcessor/ImageBase.cs +++ b/src/ImageProcessor/ImageBase.cs @@ -110,6 +110,11 @@ namespace ImageProcessor /// public Rectangle Bounds => new Rectangle(0, 0, this.Width, this.Height); + /// + /// Gets or sets th quality of the image. This affects the output quality of lossy image formats. + /// + public int Quality { get; set; } + /// /// Gets or sets the frame delay for animated images. /// If not 0, this field specifies the number of hundredths (1/100) of a second to diff --git a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs index c986b1c75..86732b1e9 100644 --- a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs +++ b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs @@ -13,8 +13,9 @@ [Theory] //[InlineData("TestImages/Car.bmp")] //[InlineData("TestImages/Portrait.png")] - [InlineData("../../TestImages/Formats/Jpg/Backdrop.jpg")] - [InlineData("../../TestImages/Formats/Gif/leaf.gif")] + //[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")] @@ -29,35 +30,13 @@ Stopwatch watch = Stopwatch.StartNew(); Image image = new Image(stream); - OctreeQuantizer quantizer = new OctreeQuantizer(); - var o = quantizer.Quantize(image); - - using (MemoryStream s2 = new MemoryStream()) - { - LzwEncoder2 enc2 = new LzwEncoder2(image.Width, image.Height, o.Pixels, 8); - enc2.Encode(s2); - using (MemoryStream s = new MemoryStream()) - { - LzwEncoder enc = new LzwEncoder(o.Pixels, 8); - enc.Encode(s); - - var x = s.ToArray(); - var y = s2.ToArray(); - - var a = x.Skip(1080); - var b = y.Skip(1080); - - Assert.Equal(s.ToArray(), s2.ToArray()); - } - } - - string encodedFilename = "Encoded/" + Path.GetFileNameWithoutExtension(filename) + ".jpg"; + string encodedFilename = "Encoded/" + Path.GetFileName(filename); //if (!image.IsAnimated) //{ using (FileStream output = File.OpenWrite(encodedFilename)) { - IImageEncoder encoder = Image.Encoders.First(e => e.IsSupportedFileExtension(".jpg")); + IImageEncoder encoder = Image.Encoders.First(e => e.IsSupportedFileExtension(Path.GetExtension(filename))); encoder.Encode(image, output); } //} diff --git a/tests/ImageProcessor.Tests/TestImages/Formats/Gif/a.gif b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/a.gif new file mode 100644 index 000000000..84cf79071 --- /dev/null +++ b/tests/ImageProcessor.Tests/TestImages/Formats/Gif/a.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7f4cb970d8f3c1705e190b6dbe398592a89986bf94b060b59bfe716f045e6f6 +size 69