From b66e71a47d65380776148e2d2fb94c20d386c058 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 25 Sep 2015 00:37:30 +1000 Subject: [PATCH] Moar gif updates Former-commit-id: 40454c7dbf848e389d6566b83308aad3fa7e8e1d Former-commit-id: d35c141b9e7174b30d51459069918471a85fe36c Former-commit-id: d026c277ae32c95b96105c7e6a9244a88aadab6e --- .../Formats/Gif/GifConstants.cs | 68 +++++++++++ src/ImageProcessor/Formats/Gif/GifDecoder.cs | 8 +- .../Formats/Gif/GifDecoderCore.cs | 47 ++++---- src/ImageProcessor/Formats/Gif/GifEncoder.cs | 109 ++++++++++++++---- .../Gif/GifGraphicsControlExtension.cs | 1 - src/ImageProcessor/Formats/Gif/LzwEncoder.cs | 35 +++--- src/ImageProcessor/Image.cs | 12 +- src/ImageProcessor/ImageBase.cs | 4 +- .../Colors/ColorConversionTests.cs | 5 +- 9 files changed, 216 insertions(+), 73 deletions(-) create mode 100644 src/ImageProcessor/Formats/Gif/GifConstants.cs diff --git a/src/ImageProcessor/Formats/Gif/GifConstants.cs b/src/ImageProcessor/Formats/Gif/GifConstants.cs new file mode 100644 index 0000000000..70f84f42ad --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/GifConstants.cs @@ -0,0 +1,68 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Constants that define specific points within a gif. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Constants that define specific points within a gif. + /// + internal sealed class GifConstants + { + /// + /// The maximum comment length. + /// + public const int MaxCommentLength = 1024 * 8; + + /// + /// The extension block introducer !. + /// + public const byte ExtensionIntroducer = 0x21; + + /// + /// The terminator. + /// + public const byte Terminator = 0; + + /// + /// The image label introducer ,. + /// + public const byte ImageLabel = 0x2C; + + /// + /// The end introducer trailer ;. + /// + public const byte EndIntroducer = 0x3B; + + /// + /// The application extension label. + /// + public const byte ApplicationExtensionLabel = 0xFF; + + /// + /// The comment label. + /// + public const byte CommentLabel = 0xFE; + + /// + /// The image descriptor label ,. + /// + public const byte ImageDescriptorLabel = 0x2C; + + /// + /// The plain text label. + /// + public const byte PlainTextLabel = 0x01; + + /// + /// The graphic control label. + /// + public const byte GraphicControlLabel = 0xF9; + } +} diff --git a/src/ImageProcessor/Formats/Gif/GifDecoder.cs b/src/ImageProcessor/Formats/Gif/GifDecoder.cs index 00c59db849..d4c33fd2bc 100644 --- a/src/ImageProcessor/Formats/Gif/GifDecoder.cs +++ b/src/ImageProcessor/Formats/Gif/GifDecoder.cs @@ -24,6 +24,9 @@ namespace ImageProcessor.Formats /// The size of the header. public int HeaderSize => 6; + internal GifDecoderCore CoreDecoder { get; private set; } + + /// /// Returns a value indicating whether the supports the specified /// file header. @@ -34,7 +37,7 @@ namespace ImageProcessor.Formats /// public bool IsSupportedFileExtension(string extension) { - Guard.NotNullOrEmpty(extension, "extension"); + Guard.NotNullOrEmpty(extension, nameof(extension)); extension = extension.StartsWith(".") ? extension.Substring(1) : extension; return extension.Equals("GIF", StringComparison.OrdinalIgnoreCase); @@ -66,7 +69,8 @@ namespace ImageProcessor.Formats /// The containing image data. public void Decode(Image image, Stream stream) { - new GifDecoderCore().Decode(image, stream); + this.CoreDecoder = new GifDecoderCore(); + this.CoreDecoder.Decode(image, stream); } } } diff --git a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs index 28f55b0471..92382d8bc7 100644 --- a/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageProcessor/Formats/Gif/GifDecoderCore.cs @@ -24,8 +24,9 @@ private Stream currentStream; private byte[] globalColorTable; private byte[] currentFrame; - private GifLogicalScreenDescriptor logicalScreenDescriptor; - private GifGraphicsControlExtension graphicsControlExtension; + + internal GifLogicalScreenDescriptor LogicalScreenDescriptor { get; set; } + internal GifGraphicsControlExtension GraphicsControlExtension { get; set; } public void Decode(Image image, Stream stream) { @@ -37,16 +38,16 @@ this.currentStream.Seek(6, SeekOrigin.Current); this.ReadLogicalScreenDescriptor(); - if (this.logicalScreenDescriptor.GlobalColorTableFlag) + if (this.LogicalScreenDescriptor.GlobalColorTableFlag) { - this.globalColorTable = new byte[this.logicalScreenDescriptor.GlobalColorTableSize * 3]; + this.globalColorTable = new byte[this.LogicalScreenDescriptor.GlobalColorTableSize * 3]; // Read the global color table from the stream stream.Read(this.globalColorTable, 0, this.globalColorTable.Length); } int nextFlag = stream.ReadByte(); - while (nextFlag != 0) + while (nextFlag != Terminator) { if (nextFlag == ImageLabel) { @@ -89,7 +90,7 @@ byte packed = buffer[1]; - this.graphicsControlExtension = new GifGraphicsControlExtension + this.GraphicsControlExtension = new GifGraphicsControlExtension { DelayTime = BitConverter.ToInt16(buffer, 2), TransparencyIndex = buffer[4], @@ -134,7 +135,7 @@ byte packed = buffer[4]; - this.logicalScreenDescriptor = new GifLogicalScreenDescriptor + this.LogicalScreenDescriptor = new GifLogicalScreenDescriptor { Width = BitConverter.ToInt16(buffer, 0), Height = BitConverter.ToInt16(buffer, 2), @@ -144,16 +145,16 @@ GlobalColorTableSize = 2 << (packed & 0x07) }; - if (this.logicalScreenDescriptor.GlobalColorTableSize > 255 * 4) + if (this.LogicalScreenDescriptor.GlobalColorTableSize > 255 * 4) { throw new ImageFormatException( - $"Invalid gif colormap size '{this.logicalScreenDescriptor.GlobalColorTableSize}'"); + $"Invalid gif colormap size '{this.LogicalScreenDescriptor.GlobalColorTableSize}'"); } - if (this.logicalScreenDescriptor.Width > ImageBase.MaxWidth || this.logicalScreenDescriptor.Height > ImageBase.MaxHeight) + if (this.LogicalScreenDescriptor.Width > ImageBase.MaxWidth || this.LogicalScreenDescriptor.Height > ImageBase.MaxHeight) { throw new ArgumentOutOfRangeException( - $"The input gif '{this.logicalScreenDescriptor.Width}x{this.logicalScreenDescriptor.Height}' is bigger then the max allowed size '{ImageBase.MaxWidth}x{ImageBase.MaxHeight}'"); + $"The input gif '{this.LogicalScreenDescriptor.Width}x{this.LogicalScreenDescriptor.Height}' is bigger then the max allowed size '{ImageBase.MaxWidth}x{ImageBase.MaxHeight}'"); } } @@ -232,8 +233,8 @@ private void ReadFrameColors(byte[] indices, byte[] colorTable, GifImageDescriptor descriptor) { - int imageWidth = this.logicalScreenDescriptor.Width; - int imageHeight = this.logicalScreenDescriptor.Height; + int imageWidth = this.LogicalScreenDescriptor.Width; + int imageHeight = this.LogicalScreenDescriptor.Height; if (this.currentFrame == null) { @@ -242,8 +243,8 @@ byte[] lastFrame = null; - if (this.graphicsControlExtension != null && - this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) + if (this.GraphicsControlExtension != null && + this.GraphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) { lastFrame = new byte[imageWidth * imageHeight * 4]; @@ -299,9 +300,9 @@ index = indices[i]; - if (this.graphicsControlExtension == null || - this.graphicsControlExtension.TransparencyFlag == false || - this.graphicsControlExtension.TransparencyIndex != index) + if (this.GraphicsControlExtension == null || + this.GraphicsControlExtension.TransparencyFlag == false || + this.GraphicsControlExtension.TransparencyIndex != index) { this.currentFrame[offset * 4 + 0] = colorTable[index * 3 + 2]; this.currentFrame[offset * 4 + 1] = colorTable[index * 3 + 1]; @@ -324,9 +325,9 @@ currentImage = this.image; currentImage.SetPixels(imageWidth, imageHeight, pixels); - if (this.graphicsControlExtension != null && this.graphicsControlExtension.DelayTime > 0) + if (this.GraphicsControlExtension != null && this.GraphicsControlExtension.DelayTime > 0) { - this.image.FrameDelay = this.graphicsControlExtension.DelayTime; + this.image.FrameDelay = this.GraphicsControlExtension.DelayTime; } } else @@ -339,9 +340,9 @@ this.image.Frames.Add(frame); } - if (this.graphicsControlExtension != null) + if (this.GraphicsControlExtension != null) { - if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) + if (this.GraphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) { for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++) { @@ -356,7 +357,7 @@ } } } - else if (this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) + else if (this.GraphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) { this.currentFrame = lastFrame; } diff --git a/src/ImageProcessor/Formats/Gif/GifEncoder.cs b/src/ImageProcessor/Formats/Gif/GifEncoder.cs index e903d9ac32..dcce4ad1aa 100644 --- a/src/ImageProcessor/Formats/Gif/GifEncoder.cs +++ b/src/ImageProcessor/Formats/Gif/GifEncoder.cs @@ -11,7 +11,7 @@ namespace ImageProcessor.Formats /// private int quality = 256; - private ImageBase image; + private Image image; /// /// Gets or sets the quality of output for images. @@ -58,48 +58,65 @@ namespace ImageProcessor.Formats /// The to encode the image data to. public void Encode(ImageBase image, Stream stream) { - Guard.NotNull(image, "image"); - Guard.NotNull(stream, "stream"); + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); - this.image = image; + this.image = (Image)image; // Write the header. // File Header signature and version. this.WriteString(stream, "GIF"); this.WriteString(stream, "89a"); - GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor - { - Width = (short)image.Width, - Height = (short)image.Height, - GlobalColorTableFlag = true, - GlobalColorTableSize = this.Quality - }; - - this.WriteGlobalLogicalScreenDescriptor(stream, descriptor); - + int bitdepth = this.GetBitsNeededForColorDepth(this.Quality) - 1; + this.WriteGlobalLogicalScreenDescriptor(stream, bitdepth); + + foreach (ImageFrame frame in this.image.Frames) + { + this.WriteColorTable(stream, bitdepth); + this.WriteGraphicalControlExtension(stream); + } throw new System.NotImplementedException(); } - private void WriteGlobalLogicalScreenDescriptor(Stream stream, GifLogicalScreenDescriptor descriptor) + private void WriteGlobalLogicalScreenDescriptor(Stream stream, int bitDepth) { + IImageDecoder decoder = ((Image)this.image).Decoder; + GifLogicalScreenDescriptor descriptor; + + // Try and grab an existing descriptor. + if (decoder.GetType() == typeof(GifDecoder)) + { + // Ensure the dimensions etc are up to date. + descriptor = ((GifDecoder)decoder).CoreDecoder.LogicalScreenDescriptor; + descriptor.Width = (short)this.image.Width; + descriptor.Height = (short)this.image.Height; + descriptor.GlobalColorTableSize = this.Quality; + } + else + { + descriptor = new GifLogicalScreenDescriptor + { + Width = (short)this.image.Width, + Height = (short)this.image.Height, + GlobalColorTableFlag = true, + GlobalColorTableSize = this.Quality + }; + } + this.WriteShort(stream, descriptor.Width); this.WriteShort(stream, descriptor.Width); - int size = descriptor.GlobalColorTableSize; - int bitdepth = this.GetBitsNeededForColorDepth(size) - 1; + int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used) 0x70 | // 2-4 : color resolution 0x00 | // 5 : GCT sort flag = 0 - bitdepth; // 6-8 : GCT size assume 1:1 + bitDepth; // 6-8 : GCT size assume 1:1 this.WriteByte(stream, packed); this.WriteByte(stream, descriptor.BackgroundColorIndex); // Background Color Index this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio - - // Write the global color table. - this.WriteColorTable(stream, bitdepth); } private void WriteColorTable(Stream stream, int bitDepth) @@ -127,6 +144,54 @@ namespace ImageProcessor.Formats stream.Write(colorTable, 0, colorTableLength); } + private void WriteGraphicalControlExtension(Stream stream) + { + Image i = ((Image)this.image); + IImageDecoder decoder = i.Decoder; + GifGraphicsControlExtension extension; + + // Try and grab an existing descriptor. + // TODO: Check whether we need to. + if (decoder.GetType() == typeof(GifDecoder)) + { + // Ensure the dimensions etc are up to date. + extension = ((GifDecoder)decoder).CoreDecoder.GraphicsControlExtension; + extension.TransparencyFlag = this.Quality > 1; + extension.TransparencyIndex = this.Quality - 1; + extension.DelayTime = i.FrameDelay; + } + else + { + bool hasTransparent = this.Quality > 1; + DisposalMethod disposalMethod = hasTransparent + ? DisposalMethod.RestoreToBackground + : DisposalMethod.Unspecified; + + extension = new GifGraphicsControlExtension() + { + DisposalMethod = disposalMethod, + TransparencyFlag = hasTransparent, + TransparencyIndex = this.Quality - 1, // Quantizer set last as transparent. + DelayTime = i.FrameDelay + }; + } + + this.WriteByte(stream, GifConstants.ExtensionIntroducer); + this.WriteByte(stream, GifConstants.GraphicControlLabel); + this.WriteByte(stream, 4); // Size + + int packed = 0 | // 1-3 : Reserved + (int)extension.DisposalMethod | // 4-6 : Disposal + 0 | // 7 : User input - 0 = none + extension.TransparencyIndex; + + this.WriteByte(stream, packed); + this.WriteShort(stream, extension.DelayTime); + this.WriteByte(stream, GifConstants.Terminator); + } + + + private void WriteApplicationExtension(Stream stream) { // TODO: Implement @@ -146,7 +211,7 @@ namespace ImageProcessor.Formats } /// - /// Writes a short to the given stream. + /// Writes a byte to the given stream. /// /// The containing image data. /// The value to write. diff --git a/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs b/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs index 0004347163..7b7bfc153b 100644 --- a/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs +++ b/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs @@ -42,7 +42,6 @@ namespace ImageProcessor.Formats /// 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. - /// This field may be used in conjunction with the User Input Flag field. /// public int DelayTime { get; set; } } diff --git a/src/ImageProcessor/Formats/Gif/LzwEncoder.cs b/src/ImageProcessor/Formats/Gif/LzwEncoder.cs index b6af73e5d0..b9244f6493 100644 --- a/src/ImageProcessor/Formats/Gif/LzwEncoder.cs +++ b/src/ImageProcessor/Formats/Gif/LzwEncoder.cs @@ -63,7 +63,7 @@ // General DEFINEs - private const int BITS = 12; + private const int Bits = 12; private const int HSIZE = 5003; // 80% occupancy @@ -79,12 +79,12 @@ // Joe Orost (decvax!vax135!petsd!joe) private int numberOfBits; // number of bits/code - private int maxbits = BITS; // user settable max # bits/code + private int maxbits = Bits; // user settable max # bits/code private int maxcode; // maximum code, given n_bits - private int maxmaxcode = 1 << BITS; // should NEVER generate this code + private int maxmaxcode = 1 << Bits; // should NEVER generate this code - private int[] htab = new int[HSIZE]; - private int[] codetab = new int[HSIZE]; + private readonly int[] hashTable = new int[HSIZE]; + private readonly int[] codeTable = new int[HSIZE]; private int hsize = HSIZE; // for dynamic table sizing @@ -168,7 +168,7 @@ { for (int i = 0; i < hsize; ++i) { - this.htab[i] = -1; + this.hashTable[i] = -1; } } @@ -210,21 +210,21 @@ this.Output(this.ClearCode, outs); - // TODO: Refactor this. Goto is baaaaaaad! - outer_loop: + // TODO: Refactor this. Goto is baaaaaaad! + // outer_loop: while ((c = this.NextPixel()) != EOF) { fcode = (c << this.maxbits) + ent; int i = c << hshift ^ ent; - if (this.htab[i] == fcode) + if (this.hashTable[i] == fcode) { - ent = this.codetab[i]; + ent = this.codeTable[i]; continue; } // non-empty slot - if (this.htab[i] >= 0) + if (this.hashTable[i] >= 0) { disp = hsize_reg - i; // secondary hash (after G. Knott) if (i == 0) @@ -239,13 +239,14 @@ i += hsize_reg; } - if (this.htab[i] == fcode) + if (this.hashTable[i] == fcode) { - ent = this.codetab[i]; - goto outer_loop; + ent = this.codeTable[i]; + // goto outer_loop; + break; } } - while (this.htab[i] >= 0); + while (this.hashTable[i] >= 0); } this.Output(ent, outs); @@ -253,8 +254,8 @@ if (this.freeEntry < this.maxmaxcode) { - this.codetab[i] = this.freeEntry++; // code -> hashtable - this.htab[i] = fcode; + this.codeTable[i] = this.freeEntry++; // code -> hashtable + this.hashTable[i] = fcode; } else { diff --git a/src/ImageProcessor/Image.cs b/src/ImageProcessor/Image.cs index f6e6a0d3dc..d27c77833d 100644 --- a/src/ImageProcessor/Image.cs +++ b/src/ImageProcessor/Image.cs @@ -98,7 +98,7 @@ namespace ImageProcessor public Image(Image other) : base(other) { - Guard.NotNull(other, "other", "Other image cannot be null."); + Guard.NotNull(other, nameof(other), "Other image cannot be null."); foreach (ImageFrame frame in other.Frames) { @@ -120,7 +120,7 @@ namespace ImageProcessor /// public Image(Stream stream) { - Guard.NotNull(stream, "stream"); + Guard.NotNull(stream, nameof(stream)); this.Load(stream, Decoders); } @@ -154,9 +154,8 @@ namespace ImageProcessor /// 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. - /// This field may be used in conjunction with the User Input Flag field. /// - public int? FrameDelay { get; set; } + public int FrameDelay { get; set; } /// /// Gets or sets the resolution of the image in x- direction. It is defined as @@ -240,6 +239,8 @@ namespace ImageProcessor /// A list of image properties. public IList Properties { get; } = new List(); + internal IImageDecoder Decoder { get; set; } + /// /// Loads the image from the given stream. /// @@ -279,7 +280,8 @@ namespace ImageProcessor IImageDecoder decoder = decoders.FirstOrDefault(x => x.IsSupportedFileFormat(header)); if (decoder != null) { - decoder.Decode(this, stream); + this.Decoder = decoder; + this.Decoder.Decode(this, stream); return; } } diff --git a/src/ImageProcessor/ImageBase.cs b/src/ImageProcessor/ImageBase.cs index 0773d63e85..a8116b2d2b 100644 --- a/src/ImageProcessor/ImageBase.cs +++ b/src/ImageProcessor/ImageBase.cs @@ -40,8 +40,8 @@ namespace ImageProcessor /// protected ImageBase(int width, int height) { - Guard.MustBeGreaterThan(width, 0, "width"); - Guard.MustBeGreaterThan(height, 0, "height"); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); this.Width = width; this.Height = height; diff --git a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs index c175ba0d0a..a5d584cc88 100644 --- a/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs +++ b/tests/ImageProcessor.Tests/Colors/ColorConversionTests.cs @@ -88,7 +88,7 @@ namespace ImageProcessor.Tests } /// - /// Tests the implicit conversion from to . + /// Tests the implicit conversion from to . /// [Fact] [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", @@ -128,6 +128,9 @@ namespace ImageProcessor.Tests Assert.Equal(80, hsv3.V, 1); } + /// + /// Tests the implicit conversion from to . + /// [Fact] public void HsvToBgr() {