// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // namespace ImageSharp.Formats { using System; using System.Buffers; using System.IO; /// /// Performs the gif decoding operation. /// /// The pixel format. internal class GifDecoderCore where TColor : struct, IPackedPixel, IEquatable { /// /// The temp buffer used to reduce allocations. /// private readonly byte[] buffer = new byte[16]; /// /// 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 global color table length /// private int globalColorTableLength; /// /// The previous frame. /// private ImageFrame previousFrame; /// /// The area to restore. /// private Rectangle? restoreArea; /// /// 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) { try { this.decodedImage = image; this.currentStream = stream; // Skip the identifier this.currentStream.Skip(6); this.ReadLogicalScreenDescriptor(); if (this.logicalScreenDescriptor.GlobalColorTableFlag) { this.globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; this.globalColorTable = ArrayPool.Shared.Rent(this.globalColorTableLength); // Read the global color table from the stream stream.Read(this.globalColorTable, 0, this.globalColorTableLength); } // 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(); } } finally { if (this.globalColorTable != null) { ArrayPool.Shared.Return(this.globalColorTable); } } } /// /// Reads the graphic control extension. /// private void ReadGraphicalControlExtension() { this.currentStream.Read(this.buffer, 0, 6); byte packed = this.buffer[1]; this.graphicsControlExtension = new GifGraphicsControlExtension { DelayTime = BitConverter.ToInt16(this.buffer, 2), TransparencyIndex = this.buffer[4], TransparencyFlag = (packed & 0x01) == 1, DisposalMethod = (DisposalMethod)((packed & 0x1C) >> 2) }; } /// /// Reads the image descriptor /// /// private GifImageDescriptor ReadImageDescriptor() { this.currentStream.Read(this.buffer, 0, 9); byte packed = this.buffer[8]; GifImageDescriptor imageDescriptor = new GifImageDescriptor { Left = BitConverter.ToInt16(this.buffer, 0), Top = BitConverter.ToInt16(this.buffer, 2), Width = BitConverter.ToInt16(this.buffer, 4), Height = BitConverter.ToInt16(this.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() { this.currentStream.Read(this.buffer, 0, 7); byte packed = this.buffer[4]; this.logicalScreenDescriptor = new GifLogicalScreenDescriptor { Width = BitConverter.ToInt16(this.buffer, 0), Height = BitConverter.ToInt16(this.buffer, 2), BackgroundColorIndex = this.buffer[5], PixelAspectRatio = this.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.Skip(length); int flag; while ((flag = this.currentStream.ReadByte()) != 0) { this.currentStream.Skip(flag); } } /// /// 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[] flagBuffer = ArrayPool.Shared.Rent(flag); try { this.currentStream.Read(flagBuffer, 0, flag); this.decodedImage.Properties.Add(new ImageProperty("Comments", BitConverter.ToString(flagBuffer, 0, flag))); } finally { ArrayPool.Shared.Return(flagBuffer); } } } /// /// Reads an individual gif frame. /// private void ReadFrame() { GifImageDescriptor imageDescriptor = this.ReadImageDescriptor(); byte[] localColorTable = null; byte[] indices = null; try { // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. int length = this.globalColorTableLength; if (imageDescriptor.LocalColorTableFlag) { length = imageDescriptor.LocalColorTableSize * 3; localColorTable = ArrayPool.Shared.Rent(length); this.currentStream.Read(localColorTable, 0, length); } indices = ArrayPool.Shared.Rent(imageDescriptor.Width * imageDescriptor.Height); this.ReadFrameIndices(imageDescriptor, indices); this.ReadFrameColors(indices, localColorTable ?? this.globalColorTable, length, imageDescriptor); // Skip any remaining blocks this.Skip(0); } finally { if (localColorTable != null) { ArrayPool.Shared.Return(localColorTable); } ArrayPool.Shared.Return(indices); } } /// /// Reads the frame indices marking the color to use for each pixel. /// /// The . /// The pixel array to write to. private void ReadFrameIndices(GifImageDescriptor imageDescriptor, byte[] indices) { int dataSize = this.currentStream.ReadByte(); using (LzwDecoder lzwDecoder = new LzwDecoder(this.currentStream)) { lzwDecoder.DecodePixels(imageDescriptor.Width, imageDescriptor.Height, dataSize, indices); } } /// /// Reads the frames colors, mapping indices to colors. /// /// The indexed pixels. /// The color table containing the available colors. /// The color table length. /// The private unsafe void ReadFrameColors(byte[] indices, byte[] colorTable, int colorTableLength, GifImageDescriptor descriptor) { int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; ImageFrame previousFrame = null; ImageFrame currentFrame = null; ImageBase image; if (this.previousFrame == null) { image = this.decodedImage; image.Quality = colorTableLength / 3; // This initializes the image to become fully transparent because the alpha channel is zero. image.InitPixels(imageWidth, imageHeight); } else { if (this.graphicsControlExtension != null && this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToPrevious) { previousFrame = this.previousFrame; } currentFrame = this.previousFrame.Clone(); image = currentFrame; this.RestoreToBackground(image); this.decodedImage.Frames.Add(currentFrame); } if (this.graphicsControlExtension != null && this.graphicsControlExtension.DelayTime > 0) { image.FrameDelay = this.graphicsControlExtension.DelayTime; } int i = 0; int interlacePass = 0; // The interlace pass int interlaceIncrement = 8; // The interlacing line increment int interlaceY = 0; // The current interlaced line using (PixelAccessor pixelAccessor = image.Lock()) { 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++) { int index = indices[i]; if (this.graphicsControlExtension == null || this.graphicsControlExtension.TransparencyFlag == false || this.graphicsControlExtension.TransparencyIndex != index) { int indexOffset = index * 3; TColor pixel = default(TColor); pixel.PackFromBytes(colorTable[indexOffset], colorTable[indexOffset + 1], colorTable[indexOffset + 2], 255); pixelAccessor[x, writeY] = pixel; } i++; } } } if (previousFrame != null) { this.previousFrame = previousFrame; return; } this.previousFrame = currentFrame == null ? this.decodedImage.ToFrame() : currentFrame; if (this.graphicsControlExtension != null && this.graphicsControlExtension.DisposalMethod == DisposalMethod.RestoreToBackground) { this.restoreArea = new Rectangle(descriptor.Left, descriptor.Top, descriptor.Width, descriptor.Height); } } /// /// Restores the current frame area to the background. /// /// The frame. private void RestoreToBackground(ImageBase frame) { if (this.restoreArea == null) { return; } // Optimization for when the size of the frame is the same as the image size. if (this.restoreArea.Value.Width == this.decodedImage.Width && this.restoreArea.Value.Height == this.decodedImage.Height) { using (PixelAccessor pixelAccessor = frame.Lock()) { pixelAccessor.Reset(); } } else { using (PixelArea emptyRow = new PixelArea(this.restoreArea.Value.Width, ComponentOrder.Xyzw)) { using (PixelAccessor pixelAccessor = frame.Lock()) { for (int y = this.restoreArea.Value.Top; y < this.restoreArea.Value.Top + this.restoreArea.Value.Height; y++) { pixelAccessor.CopyFrom(emptyRow, y, this.restoreArea.Value.Left); } } } } this.restoreArea = null; } } }