From f5a5cac667db63cd2c65923bedd5304e506f04ff Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 24 Aug 2015 23:13:26 +1000 Subject: [PATCH] More work on the gif decoder/encoder Former-commit-id: 780d7b2478cbafd42d2230fc245d8ab5320dc245 Former-commit-id: 16c380a0287b7b1822b617eae826554c4109c4d8 Former-commit-id: 81a821fdd7bf09f715fb956c4486468ddce2e92e --- .../Common/Extensions/ByteExtensions.cs | 15 +++- .../Formats/Gif/GifDecoderCore.cs | 71 ++++++++++--------- src/ImageProcessor/Formats/Gif/GifEncoder.cs | 34 ++++++++- src/ImageProcessor/Formats/Gif/LzwDecoder.cs | 2 +- .../Formats/Gif/Quantizer/QuantizedImage.cs | 31 ++++---- .../Formats/Gif/Quantizer/Quantizer.cs | 4 +- src/ImageProcessor/Image.cs | 6 ++ src/ImageProcessor/ImageBase.cs | 2 +- .../Formats/EncoderDecoderTests.cs | 2 +- 9 files changed, 112 insertions(+), 55 deletions(-) diff --git a/src/ImageProcessor/Common/Extensions/ByteExtensions.cs b/src/ImageProcessor/Common/Extensions/ByteExtensions.cs index 0c0bc6b1f..958326f17 100644 --- a/src/ImageProcessor/Common/Extensions/ByteExtensions.cs +++ b/src/ImageProcessor/Common/Extensions/ByteExtensions.cs @@ -1,7 +1,20 @@ -namespace ImageProcessor +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Extension methods for the struct. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor { using System; + /// + /// Extension methods for the struct. + /// internal static class ByteExtensions { /// diff --git a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs index e8df9337d..d3c7d7155 100644 --- a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs @@ -25,7 +25,7 @@ private byte[] globalColorTable; private byte[] _currentFrame; private GifLogicalScreenDescriptor logicalScreenDescriptor; - private GifGraphicsControlExtension _graphicsControl; + private GifGraphicsControlExtension graphicsControlExtension; public void Decode(Image image, Stream stream) { @@ -64,6 +64,7 @@ this.ReadComments(); break; case ApplicationExtensionLabel: + // TODO: Read Application extension this.Skip(12); break; case PlainTextLabel: @@ -88,7 +89,7 @@ byte packed = buffer[1]; - _graphicsControl = new GifGraphicsControlExtension + this.graphicsControlExtension = new GifGraphicsControlExtension { DelayTime = BitConverter.ToInt16(buffer, 2), TransparencyIndex = buffer[4], @@ -97,6 +98,12 @@ }; } + private void ReadApplicationBlockExtension() + { + // TODO: Implement + throw new NotImplementedException(); + } + private GifImageDescriptor ReadImageDescriptor() { byte[] buffer = new byte[9]; @@ -105,14 +112,16 @@ byte packed = buffer[8]; - GifImageDescriptor imageDescriptor = new GifImageDescriptor(); - imageDescriptor.Left = BitConverter.ToInt16(buffer, 0); - imageDescriptor.Top = BitConverter.ToInt16(buffer, 2); - imageDescriptor.Width = BitConverter.ToInt16(buffer, 4); - imageDescriptor.Height = BitConverter.ToInt16(buffer, 6); - imageDescriptor.LocalColorTableFlag = ((packed & 0x80) >> 7) == 1; - imageDescriptor.LocalColorTableSize = 2 << (packed & 0x07); - imageDescriptor.InterlaceFlag = ((packed & 0x40) >> 6) == 1; + 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; } @@ -137,18 +146,14 @@ if (this.logicalScreenDescriptor.GlobalColorTableSize > 255 * 4) { - throw new ImageFormatException(string.Format("Invalid gif colormap size '{0}'", this.logicalScreenDescriptor.GlobalColorTableSize)); + throw new ImageFormatException( + $"Invalid gif colormap size '{this.logicalScreenDescriptor.GlobalColorTableSize}'"); } if (this.logicalScreenDescriptor.Width > ImageBase.MaxWidth || this.logicalScreenDescriptor.Height > ImageBase.MaxHeight) { throw new ArgumentOutOfRangeException( - string.Format( - "The input gif '{0}x{1}' is bigger then the max allowed size '{2}x{3}'", - this.logicalScreenDescriptor.Width, - this.logicalScreenDescriptor.Height, - ImageBase.MaxWidth, - ImageBase.MaxHeight)); + $"The input gif '{this.logicalScreenDescriptor.Width}x{this.logicalScreenDescriptor.Height}' is bigger then the max allowed size '{ImageBase.MaxWidth}x{ImageBase.MaxHeight}'"); } } @@ -156,7 +161,7 @@ { this.currentStream.Seek(length, SeekOrigin.Current); - int flag = 0; + int flag; while ((flag = this.currentStream.ReadByte()) != 0) { @@ -166,13 +171,13 @@ private void ReadComments() { - int flag = 0; + int flag; while ((flag = this.currentStream.ReadByte()) != 0) { if (flag > MaxCommentLength) { - throw new ImageFormatException(string.Format("Gif comment length '{0}' exceeds max '{1}'", flag, MaxCommentLength)); + throw new ImageFormatException($"Gif comment length '{flag}' exceeds max '{MaxCommentLength}'"); } byte[] buffer = new byte[flag]; @@ -230,19 +235,19 @@ int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; - if (_currentFrame == null) + if (this._currentFrame == null) { - _currentFrame = new byte[imageWidth * imageHeight * 4]; + this._currentFrame = new byte[imageWidth * imageHeight * 4]; } byte[] lastFrame = null; - if (_graphicsControl != null && - _graphicsControl.DisposalMethod == DisposalMethod.RestoreToPrevious) + if (this.graphicsControlExtension != null && + this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) { lastFrame = new byte[imageWidth * imageHeight * 4]; - Array.Copy(_currentFrame, lastFrame, lastFrame.Length); + Array.Copy(this._currentFrame, lastFrame, lastFrame.Length); } int offset = 0, i = 0, index = -1; @@ -294,9 +299,9 @@ index = indices[i]; - if (_graphicsControl == null || - _graphicsControl.TransparencyFlag == false || - _graphicsControl.TransparencyIndex != index) + if (this.graphicsControlExtension == null || + this.graphicsControlExtension.TransparencyFlag == false || + this.graphicsControlExtension.TransparencyIndex != index) { _currentFrame[offset * 4 + 0] = colorTable[index * 3 + 2]; _currentFrame[offset * 4 + 1] = colorTable[index * 3 + 1]; @@ -319,9 +324,9 @@ currentImage = this.image; currentImage.SetPixels(imageWidth, imageHeight, pixels); - if (_graphicsControl != null && _graphicsControl.DelayTime > 0) + if (this.graphicsControlExtension != null && this.graphicsControlExtension.DelayTime > 0) { - this.image.FrameDelay = _graphicsControl.DelayTime; + this.image.FrameDelay = this.graphicsControlExtension.DelayTime; } } else @@ -334,9 +339,9 @@ this.image.Frames.Add(frame); } - if (_graphicsControl != null) + if (this.graphicsControlExtension != null) { - if (_graphicsControl.DisposalMethod == DisposalMethod.RestoreToBackground) + if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) { for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++) { @@ -351,7 +356,7 @@ } } } - else if (_graphicsControl.DisposalMethod == DisposalMethod.RestoreToPrevious) + else if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) { _currentFrame = lastFrame; } diff --git a/src/ImageProcessor/Formats/Gif/GifEncoder.cs b/src/ImageProcessor/Formats/Gif/GifEncoder.cs index 8d630f808..e903d9ac3 100644 --- a/src/ImageProcessor/Formats/Gif/GifEncoder.cs +++ b/src/ImageProcessor/Formats/Gif/GifEncoder.cs @@ -11,6 +11,8 @@ namespace ImageProcessor.Formats /// private int quality = 256; + private ImageBase image; + /// /// Gets or sets the quality of output for images. /// @@ -59,6 +61,8 @@ namespace ImageProcessor.Formats Guard.NotNull(image, "image"); Guard.NotNull(stream, "stream"); + this.image = image; + // Write the header. // File Header signature and version. this.WriteString(stream, "GIF"); @@ -95,12 +99,38 @@ namespace ImageProcessor.Formats this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio // Write the global color table. - this.WriteColorTable(stream, size); + this.WriteColorTable(stream, bitdepth); } - private void WriteColorTable(Stream stream, int size) + private void WriteColorTable(Stream stream, int bitDepth) { + // Quantize the image returning a pallete. + IQuantizer quantizer = new OctreeQuantizer(Math.Max(1, this.quality - 1), bitDepth); + QuantizedImage quantizedImage = quantizer.Quantize(this.image); + this.image = quantizedImage.ToImage(); + + // Grab the pallete and write it to the stream. + Bgra[] pallete = quantizedImage.Palette; + int pixelCount = pallete.Length; + int colorTableLength = pixelCount * 3; + byte[] colorTable = new byte[colorTableLength]; + + for (int i = 0; i < pixelCount; i++) + { + int offset = i * 4; + Bgra color = pallete[i]; + colorTable[offset + 0] = color.B; + colorTable[offset + 1] = color.G; + colorTable[offset + 2] = color.R; + } + stream.Write(colorTable, 0, colorTableLength); + } + + private void WriteApplicationExtension(Stream stream) + { + // TODO: Implement + throw new NotImplementedException(); } /// diff --git a/src/ImageProcessor/Formats/Gif/LzwDecoder.cs b/src/ImageProcessor/Formats/Gif/LzwDecoder.cs index 8fa2e7880..1c3d5d28b 100644 --- a/src/ImageProcessor/Formats/Gif/LzwDecoder.cs +++ b/src/ImageProcessor/Formats/Gif/LzwDecoder.cs @@ -56,7 +56,7 @@ namespace ImageProcessor.Formats /// The decoded and uncompressed array. public byte[] DecodePixels(int width, int height, int dataSize) { - Guard.LessThan(dataSize, int.MaxValue, "dataSize"); + Guard.LessThan(dataSize, int.MaxValue, nameof(dataSize)); // The resulting index table. byte[] pixels = new byte[width * height]; diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs b/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs index 1d4d3b3f7..627eee2ac 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/QuantizedImage.cs @@ -42,11 +42,16 @@ namespace ImageProcessor.Formats /// public QuantizedImage(int width, int height, Bgra[] palette, byte[] pixels) { - if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width)); - if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height)); - if (palette == null) throw new ArgumentNullException(nameof(palette)); - if (pixels == null) throw new ArgumentNullException(nameof(pixels)); - if (pixels.Length != width * height) throw new ArgumentException("Pixel array size must be width * height", nameof(pixels)); + Guard.GreaterThan(width, 0, nameof(width)); + Guard.GreaterThan(height, 0, nameof(height)); + Guard.NotNull(palette, nameof(palette)); + Guard.NotNull(pixels, nameof(pixels)); + + if (pixels.Length != width * height) + { + throw new ArgumentException( + $"Pixel array size must be {nameof(width)} * {nameof(height)}", nameof(pixels)); + } this.Width = width; this.Height = height; @@ -61,20 +66,20 @@ namespace ImageProcessor.Formats public Image ToImage() { Image image = new Image(); - int pixelCount = Pixels.Length; + int pixelCount = this.Pixels.Length; byte[] bgraPixels = new byte[pixelCount * 4]; for (int i = 0; i < pixelCount; i++) { - int j = i * 4; - Bgra color = Palette[Pixels[i]]; - bgraPixels[j + 0] = color.B; - bgraPixels[j + 1] = color.G; - bgraPixels[j + 2] = color.R; - bgraPixels[j + 3] = color.A; + int offset = i * 4; + Bgra color = this.Palette[this.Pixels[i]]; + bgraPixels[offset + 0] = color.B; + bgraPixels[offset + 1] = color.G; + bgraPixels[offset + 2] = color.R; + bgraPixels[offset + 3] = color.A; } - image.SetPixels(Width, Height, bgraPixels); + image.SetPixels(this.Width, this.Height, bgraPixels); return image; } } diff --git a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs index bcb8e79fb..ec00bf508 100644 --- a/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs +++ b/src/ImageProcessor/Formats/Gif/Quantizer/Quantizer.cs @@ -61,11 +61,9 @@ namespace ImageProcessor.Formats byte[] quantizedPixels = new byte[width * height]; - List palette = GetPalette(); - this.SecondPass(imageBase, quantizedPixels, width, height); - return new QuantizedImage(width, height, palette.ToArray(), quantizedPixels); + return new QuantizedImage(width, height, this.GetPalette().ToArray(), quantizedPixels); } /// diff --git a/src/ImageProcessor/Image.cs b/src/ImageProcessor/Image.cs index 0e8113167..f6e6a0d3d 100644 --- a/src/ImageProcessor/Image.cs +++ b/src/ImageProcessor/Image.cs @@ -222,6 +222,12 @@ namespace ImageProcessor /// public bool IsAnimated => this.Frames.Count > 0; + /// + /// Gets or sets the number of times any animation is repeated. + /// 0 means to repeat indefinitely. + /// + public ushort RepeatCount { get; set; } + /// /// Gets the other frames for the animation. /// diff --git a/src/ImageProcessor/ImageBase.cs b/src/ImageProcessor/ImageBase.cs index 19c1fd809..517ac184f 100644 --- a/src/ImageProcessor/ImageBase.cs +++ b/src/ImageProcessor/ImageBase.cs @@ -60,7 +60,7 @@ namespace ImageProcessor /// protected ImageBase(ImageBase other) { - Guard.NotNull(other, "other", "Other image cannot be null."); + Guard.NotNull(other, nameof(other), "Other image cannot be null."); byte[] pixels = other.Pixels; diff --git a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs index 5c0c5bf0c..b6162a840 100644 --- a/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs +++ b/tests/ImageProcessor.Tests/Formats/EncoderDecoderTests.cs @@ -55,7 +55,7 @@ // } //} - Trace.WriteLine(string.Format("{0} : {1}ms", filename, watch.ElapsedMilliseconds)); + Trace.WriteLine($"{filename} : {watch.ElapsedMilliseconds}ms"); } [Theory]