diff --git a/src/ImageProcessor/Common/Extensions/ByteExtensions.cs b/src/ImageProcessor/Common/Extensions/ByteExtensions.cs new file mode 100644 index 000000000..2b180e77e --- /dev/null +++ b/src/ImageProcessor/Common/Extensions/ByteExtensions.cs @@ -0,0 +1,51 @@ +namespace ImageProcessor +{ + using System; + + internal static class ByteExtensions + { + /// + /// Converts a byte array to a new array where each value in the original array is represented + /// by a the specified number of bits. + /// + /// The bytes to convert from. Cannot be null. + /// The number of bits per value. + /// The resulting array. Is never null. + /// is null. + /// is less than or equals than zero. + public static byte[] ToArrayByBitsLength(this byte[] bytes, int bits) + { + Guard.NotNull(bytes, "bytes"); + Guard.GreaterThan(bits, 0, "bits"); + + byte[] result; + + if (bits < 8) + { + result = new byte[bytes.Length * 8 / bits]; + + int factor = (int)Math.Pow(2, bits) - 1; + int mask = (0xFF >> (8 - bits)); + int resultOffset = 0; + + foreach (byte b in bytes) + { + for (int shift = 0; shift < 8; shift += bits) + { + int colorIndex = (((b) >> (8 - bits - shift)) & mask) * (255 / factor); + + result[resultOffset] = (byte)colorIndex; + + resultOffset++; + } + } + } + else + { + result = bytes; + } + + return result; + } + } +} diff --git a/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs b/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs new file mode 100644 index 000000000..cc6fd6052 --- /dev/null +++ b/src/ImageProcessor/Common/Extensions/ComparableExtensions.cs @@ -0,0 +1,36 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Extension methods for classes that implement . +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor +{ + using System; + + /// + /// Extension methods for classes that implement . + /// + internal static class ComparableExtensions + { + /// + /// Returns value indicating whether the given number is with in the minimum and maximum + /// given range. + /// + /// The value to to check. + /// The minimum range value. + /// The maximum range value. + /// The to test. + /// + /// True if the value falls within the maximum and minimum; otherwise, false. + /// + public static bool IsBetween(this T value, T min, T max) where T : IComparable + { + return (value.CompareTo(min) > 0) && (value.CompareTo(max) < 0); + } + } +} diff --git a/src/ImageProcessor/Common/Helpers/Guard.cs b/src/ImageProcessor/Common/Helpers/Guard.cs new file mode 100644 index 000000000..fcf85907d --- /dev/null +++ b/src/ImageProcessor/Common/Helpers/Guard.cs @@ -0,0 +1,119 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Provides methods to protect against invalid parameters. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor +{ + using System; + using System.Globalization; + + /// + /// Provides methods to protect against invalid parameters. + /// + internal static class Guard + { + /// + /// Verifies, that the method parameter with specified object value is not null + /// and throws an exception if it is found to be so. + /// + /// + /// The target object, which cannot be null. + /// + /// + /// The name of the parameter that is to be checked. + /// + /// + /// The error message, if any to add to the exception. + /// + /// + /// is null + /// + public static void NotNull(object target, string parameterName, string message = "") + { + if (target == null) + { + if (string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentNullException(parameterName, message); + } + + throw new ArgumentNullException(parameterName); + } + } + + /// + /// Verifies, that the string method parameter with specified object value and message + /// is not null, not empty and does not contain only blanks and throws an exception + /// if the object is null. + /// + /// The target string, which should be checked against being null or empty. + /// Name of the parameter. + /// + /// is null. + /// + /// + /// is + /// empty or contains only blanks. + /// + public static void NotNullOrEmpty(string target, string parameterName) + { + if (target == null) + { + throw new ArgumentNullException(parameterName); + } + + if (string.IsNullOrWhiteSpace(target)) + { + throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); + } + } + + /// + /// Verifies that the specified value is less than a maximum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The maximum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is greater than the maximum value. + /// + public static void LessThan(TValue value, TValue max, string parameterName) where TValue : IComparable + { + if (value.CompareTo(max) >= 0) + { + throw new ArgumentOutOfRangeException( + parameterName, + string.Format(CultureInfo.CurrentCulture, "Value must be less than {0}", max)); + } + } + + /// + /// Verifies that the specified value is greater than a minimum value + /// and throws an exception if it is not. + /// + /// The target value, which should be validated. + /// The minimum value. + /// The name of the parameter that is to be checked. + /// The type of the value. + /// + /// is less than the minimum value. + /// + public static void GreaterThan(TValue value, TValue min, string parameterName) where TValue : IComparable + { + if (value.CompareTo(min) <= 0) + { + throw new ArgumentOutOfRangeException( + parameterName, + string.Format(CultureInfo.CurrentCulture, "Value must be greater than {0}", min)); + } + } + } +} diff --git a/src/ImageProcessor/Common/Helpers/Utils.cs b/src/ImageProcessor/Common/Helpers/Utils.cs new file mode 100644 index 000000000..c56129d3e --- /dev/null +++ b/src/ImageProcessor/Common/Helpers/Utils.cs @@ -0,0 +1,34 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// General utility methods. +// TODO: I don't like having classes like this as they turn into a dumping ground. Investigate method usage. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor +{ + /// + /// General utility methods. + /// TODO: I don't like having classes like this as they turn into a dumping ground. Investigate method usage. + /// + internal static class Utils + { + /// + /// Swaps two references. + /// + /// The type of the references to swap. + /// The first reference. + /// The second reference. + public static void Swap(ref TRef lhs, ref TRef rhs) where TRef : class + { + TRef tmp = lhs; + + lhs = rhs; + rhs = tmp; + } + } +} diff --git a/src/ImageProcessor/Formats/Gif/DisposalMethod.cs b/src/ImageProcessor/Formats/Gif/DisposalMethod.cs new file mode 100644 index 000000000..2ce4a0523 --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/DisposalMethod.cs @@ -0,0 +1,42 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Provides enumeration for instructing the decoder what to do with the last image +// in an animation sequence. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Provides enumeration for instructing the decoder what to do with the last image + /// in an animation sequence. + /// + public enum DisposalMethod + { + /// + /// No disposal specified. The decoder is not required to take any action. + /// + Unspecified = 0, + + /// + /// Do not dispose. The graphic is to be left in place. + /// + NotDispose = 1, + + /// + /// Restore to background color. The area used by the graphic must be restored to + /// the background color. + /// + RestoreToBackground = 2, + + /// + /// Restore to previous. The decoder is required to restore the area overwritten by the + /// graphic with what was there prior to rendering the graphic. + /// + RestoreToPrevious = 3 + } +} diff --git a/src/ImageProcessor/Formats/Gif/GifDecoder.cs b/src/ImageProcessor/Formats/Gif/GifDecoder.cs new file mode 100644 index 000000000..056aee7e4 --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/GifDecoder.cs @@ -0,0 +1,382 @@ +namespace ImageProcessor.Formats +{ + using System; + using System.IO; + + public class GifDecoder : IImageDecoder + { + public static int MaxCommentLength = 1024 * 8; + + public int HeaderSize + { + get + { + return 6; + } + } + + public bool IsSupportedFileExtension(string extension) + { + if (extension.StartsWith(".")) extension = extension.Substring(1); + return extension.Equals("GIF", StringComparison.OrdinalIgnoreCase); + } + + public bool IsSupportedFileFormat(byte[] header) + { + return header.Length >= 6 && + header[0] == 0x47 && // G + header[1] == 0x49 && // I + header[2] == 0x46 && // F + header[3] == 0x38 && // 8 + (header[4] == 0x39 || header[4] == 0x37) && // 9 or 7 + header[5] == 0x61; // a + } + + public void Decode(Image image, Stream stream) + { + new GifDecoderCore().Decode(image, stream); + } + + class GifDecoderCore + { + private const byte ExtensionIntroducer = 0x21; + private const byte Terminator = 0; + private const byte ImageLabel = 0x2C; + private const byte EndIntroducer = 0x3B; + private const byte ApplicationExtensionLabel = 0xFF; + private const byte CommentLabel = 0xFE; + private const byte ImageDescriptorLabel = 0x2C; + private const byte PlainTextLabel = 0x01; + private const byte GraphicControlLabel = 0xF9; + + private Image _image; + private Stream _stream; + private GifLogicalScreenDescriptor _descriptor; + private byte[] _globalColorTable; + private byte[] _currentFrame; + private GifGraphicsControlExtension _graphicsControl; + + public void Decode(Image image, Stream stream) + { + _image = image; + + _stream = stream; + _stream.Seek(6, SeekOrigin.Current); + + ReadLogicalScreenDescriptor(); + + if (_descriptor.GlobalColorTableFlag == true) + { + _globalColorTable = new byte[_descriptor.GlobalColorTableSize * 3]; + + // Read the global color table from the stream + stream.Read(_globalColorTable, 0, _globalColorTable.Length); + } + + int nextFlag = stream.ReadByte(); + while (nextFlag != 0) + { + if (nextFlag == ImageLabel) + { + ReadFrame(); + } + else if (nextFlag == ExtensionIntroducer) + { + int gcl = stream.ReadByte(); + switch (gcl) + { + case GraphicControlLabel: + ReadGraphicalControlExtension(); + break; + case CommentLabel: + ReadComments(); + break; + case ApplicationExtensionLabel: + Skip(12); + break; + case PlainTextLabel: + Skip(13); + break; + } + } + else if (nextFlag == EndIntroducer) + { + break; + } + nextFlag = stream.ReadByte(); + } + } + + private void ReadGraphicalControlExtension() + { + byte[] buffer = new byte[6]; + + _stream.Read(buffer, 0, buffer.Length); + + byte packed = buffer[1]; + + _graphicsControl = new GifGraphicsControlExtension(); + _graphicsControl.DelayTime = BitConverter.ToInt16(buffer, 2); + _graphicsControl.TransparencyIndex = buffer[4]; + _graphicsControl.TransparencyFlag = (packed & 0x01) == 1; + _graphicsControl.DisposalMethod = (DisposalMethod)((packed & 0x1C) >> 2); + } + + private GifImageDescriptor ReadImageDescriptor() + { + byte[] buffer = new byte[9]; + + _stream.Read(buffer, 0, buffer.Length); + + 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; + + return imageDescriptor; + } + + private void ReadLogicalScreenDescriptor() + { + byte[] buffer = new byte[7]; + + _stream.Read(buffer, 0, buffer.Length); + + byte packed = buffer[4]; + + _descriptor = new GifLogicalScreenDescriptor(); + _descriptor.Width = BitConverter.ToInt16(buffer, 0); + _descriptor.Height = BitConverter.ToInt16(buffer, 2); + _descriptor.Background = buffer[5]; + _descriptor.GlobalColorTableFlag = ((packed & 0x80) >> 7) == 1; + _descriptor.GlobalColorTableSize = 2 << (packed & 0x07); + + if (_descriptor.GlobalColorTableSize > 255 * 4) + { + throw new ImageFormatException(string.Format("Invalid gif colormap size '{0}'", _descriptor.GlobalColorTableSize)); + } + + if (_descriptor.Width > ImageBase.MaxWidth || _descriptor.Height > ImageBase.MaxHeight) + { + throw new ArgumentOutOfRangeException( + string.Format("The input gif '{0}x{1}' is bigger then the max allowed size '{2}x{3}'", + _descriptor.Width, + _descriptor.Height, + ImageBase.MaxWidth, + ImageBase.MaxHeight)); + } + } + + private void Skip(int length) + { + _stream.Seek(length, SeekOrigin.Current); + + int flag = 0; + + while ((flag = _stream.ReadByte()) != 0) + { + _stream.Seek(flag, SeekOrigin.Current); + } + } + + private void ReadComments() + { + int flag = 0; + + while ((flag = _stream.ReadByte()) != 0) + { + if (flag > MaxCommentLength) + { + throw new ImageFormatException(string.Format("Gif comment length '{0}' exceeds max '{1}'", flag, MaxCommentLength)); + } + + byte[] buffer = new byte[flag]; + + _stream.Read(buffer, 0, flag); + + _image.Properties.Add(new ImageProperty("Comments", BitConverter.ToString(buffer))); + } + } + + private void ReadFrame() + { + GifImageDescriptor imageDescriptor = ReadImageDescriptor(); + + byte[] localColorTable = ReadFrameLocalColorTable(imageDescriptor); + + byte[] indices = ReadFrameIndices(imageDescriptor); + + // Determine the color table for this frame. If there is a local one, use it + // otherwise use the global color table. + byte[] colorTable = localColorTable ?? this._globalColorTable; + + ReadFrameColors(indices, colorTable, imageDescriptor); + + Skip(0); // skip any remaining blocks + } + + private byte[] ReadFrameIndices(GifImageDescriptor imageDescriptor) + { + int dataSize = _stream.ReadByte(); + + LzwDecoder lzwDecoder = new LzwDecoder(_stream); + + byte[] indices = lzwDecoder.DecodePixels(imageDescriptor.Width, imageDescriptor.Height, dataSize); + return indices; + } + + private byte[] ReadFrameLocalColorTable(GifImageDescriptor imageDescriptor) + { + byte[] localColorTable = null; + + if (imageDescriptor.LocalColorTableFlag) + { + localColorTable = new byte[imageDescriptor.LocalColorTableSize * 3]; + + _stream.Read(localColorTable, 0, localColorTable.Length); + } + + return localColorTable; + } + + private void ReadFrameColors(byte[] indices, byte[] colorTable, GifImageDescriptor descriptor) + { + int imageWidth = _descriptor.Width; + int imageHeight = _descriptor.Height; + + if (_currentFrame == null) + { + _currentFrame = new byte[imageWidth * imageHeight * 4]; + } + + byte[] lastFrame = null; + + if (_graphicsControl != null && + _graphicsControl.DisposalMethod == DisposalMethod.RestoreToPrevious) + { + lastFrame = new byte[imageWidth * imageHeight * 4]; + + Array.Copy(_currentFrame, lastFrame, lastFrame.Length); + } + + int offset = 0, i = 0, index = -1; + + int iPass = 0; // the interlace pass + int iInc = 8; // the interlacing line increment + int iY = 0; // the current interlaced line + int writeY = 0; // the target y offset to write to + + for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++) + { + // Check if this image is interlaced. + 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 (iY >= descriptor.Height) + { + iPass++; + switch (iPass) + { + case 1: + iY = 4; + break; + case 2: + iY = 2; + iInc = 4; + break; + case 3: + iY = 1; + iInc = 2; + break; + } + } + + writeY = iY + descriptor.Top; + + iY += iInc; + } + else + { + writeY = y; + } + + for (int x = descriptor.Left; x < descriptor.Left + descriptor.Width; x++) + { + offset = writeY * imageWidth + x; + + index = indices[i]; + + if (_graphicsControl == null || + _graphicsControl.TransparencyFlag == false || + _graphicsControl.TransparencyIndex != index) + { + _currentFrame[offset * 4 + 0] = colorTable[index * 3 + 2]; + _currentFrame[offset * 4 + 1] = colorTable[index * 3 + 1]; + _currentFrame[offset * 4 + 2] = colorTable[index * 3 + 0]; + _currentFrame[offset * 4 + 3] = (byte)255; + } + + i++; + } + } + + byte[] pixels = new byte[imageWidth * imageHeight * 4]; + + Array.Copy(_currentFrame, pixels, pixels.Length); + + ImageBase currentImage = null; + + if (_image.Pixels == null) + { + currentImage = _image; + currentImage.SetPixels(imageWidth, imageHeight, pixels); + + if (_graphicsControl != null && _graphicsControl.DelayTime > 0) + { + _image.DelayTime = _graphicsControl.DelayTime; + } + } + else + { + ImageFrame frame = new ImageFrame(); + + currentImage = frame; + currentImage.SetPixels(imageWidth, imageHeight, pixels); + + _image.Frames.Add(frame); + } + + if (_graphicsControl != null) + { + if (_graphicsControl.DisposalMethod == DisposalMethod.RestoreToBackground) + { + for (int y = descriptor.Top; y < descriptor.Top + descriptor.Height; y++) + { + for (int x = descriptor.Left; x < descriptor.Left + descriptor.Width; x++) + { + offset = y * imageWidth + x; + + _currentFrame[offset * 4 + 0] = 0; + _currentFrame[offset * 4 + 1] = 0; + _currentFrame[offset * 4 + 2] = 0; + _currentFrame[offset * 4 + 3] = 0; + } + } + } + else if (_graphicsControl.DisposalMethod == DisposalMethod.RestoreToPrevious) + { + _currentFrame = lastFrame; + } + } + } + } + } +} diff --git a/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs b/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs new file mode 100644 index 000000000..000434716 --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/GifGraphicsControlExtension.cs @@ -0,0 +1,49 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// The Graphic Control Extension contains parameters used when +// processing a graphic rendering block. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// The Graphic Control Extension contains parameters used when + /// processing a graphic rendering block. + /// + internal sealed class GifGraphicsControlExtension + { + /// + /// Gets or sets the disposal method which indicates the way in which the + /// graphic is to be treated after being displayed. + /// + public DisposalMethod DisposalMethod { get; set; } + + /// + /// Gets or sets a value indicating whether transparency flag is to be set. + /// This indicates whether a transparency index is given in the Transparent Index field. + /// (This field is the least significant bit of the byte.) + /// + public bool TransparencyFlag { get; set; } + + /// + /// Gets or sets the transparency index. + /// The Transparency Index is such that when encountered, the corresponding pixel + /// of the display device is not modified and processing goes on to the next pixel. + /// + public int TransparencyIndex { get; set; } + + /// + /// Gets or sets the delay time. + /// 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/GifImageDescriptor.cs b/src/ImageProcessor/Formats/Gif/GifImageDescriptor.cs new file mode 100644 index 000000000..cdd768250 --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/GifImageDescriptor.cs @@ -0,0 +1,72 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Each image in the Data Stream is composed of an Image Descriptor, +// an optional Local Color Table, and the image data. +// Each image must fit within the boundaries of the +// Logical Screen, as defined in the Logical Screen Descriptor. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Each image in the Data Stream is composed of an Image Descriptor, + /// an optional Local Color Table, and the image data. + /// Each image must fit within the boundaries of the + /// Logical Screen, as defined in the Logical Screen Descriptor. + /// + internal sealed class GifImageDescriptor + { + /// + /// Gets or sets the column number, in pixels, of the left edge of the image, + /// with respect to the left edge of the Logical Screen. + /// Leftmost column of the Logical Screen is 0. + /// + public short Left { get; set; } + + /// + /// Gets or sets the row number, in pixels, of the top edge of the image with + /// respect to the top edge of the Logical Screen. + /// Top row of the Logical Screen is 0. + /// + public short Top { get; set; } + + + /// + /// Gets or sets the width of the image in pixels. + /// + public short Width { get; set; } + + + /// + /// Gets or sets the height of the image in pixels. + /// + public short Height { get; set; } + + + /// + /// Gets or sets a value indicating whether the presence of a Local Color Table immediately + /// follows this Image Descriptor. + /// + public bool LocalColorTableFlag { get; set; } + + + /// + /// Gets or sets the local color table size. + /// If the Local Color Table Flag is set to 1, the value in this field + /// is used to calculate the number of bytes contained in the Local Color Table. + /// + public int LocalColorTableSize { get; set; } + + + /// + /// Gets or sets a value indicating whether the image is to be interlaced. + /// An image is interlaced in a four-pass interlace pattern. + /// + public bool InterlaceFlag { get; set; } + } +} diff --git a/src/ImageProcessor/Formats/Gif/GifLogicalScreenDescriptor.cs b/src/ImageProcessor/Formats/Gif/GifLogicalScreenDescriptor.cs new file mode 100644 index 000000000..f1dbf7ba0 --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/GifLogicalScreenDescriptor.cs @@ -0,0 +1,57 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// The Logical Screen Descriptor contains the parameters +// necessary to define the area of the display device +// within which the images will be rendered +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// The Logical Screen Descriptor contains the parameters + /// necessary to define the area of the display device + /// within which the images will be rendered + /// + internal sealed class GifLogicalScreenDescriptor + { + /// + /// Gets or sets the width, in pixels, of the Logical Screen where the images will + /// be rendered in the displaying device. + /// + public short Width { get; set; } + + /// + /// Gets or sets the height, in pixels, of the Logical Screen where the images will be + /// rendered in the displaying device. + /// + public short Height { get; set; } + + /// + /// Gets or sets the index at the Global Color Table for the Background Color. + /// The Background Color is the color used for those + /// pixels on the screen that are not covered by an image. + /// + public byte Background { get; set; } + + /// + /// Gets or sets a value indicating whether a flag denoting the presence of a Global Color Table + /// should be set. + /// If the flag is set, the Global Color Table will immediately + /// follow the Logical Screen Descriptor. + /// + public bool GlobalColorTableFlag { get; set; } + + /// + /// Gets or sets the global color table size. + /// If the Global Color Table Flag is set to 1, + /// the value in this field is used to calculate the number of + /// bytes contained in the Global Color Table. + /// + public int GlobalColorTableSize { get; set; } + } +} diff --git a/src/ImageProcessor/Formats/Gif/LzwDecoder.cs b/src/ImageProcessor/Formats/Gif/LzwDecoder.cs new file mode 100644 index 000000000..8fa2e7880 --- /dev/null +++ b/src/ImageProcessor/Formats/Gif/LzwDecoder.cs @@ -0,0 +1,270 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Decompresses data using the LZW algorithms. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System; + using System.IO; + + /// + /// Decompresses data using the LZW algorithms. + /// + internal sealed class LzwDecoder + { + /// + /// The stack size. + /// + private const int StackSize = 4096; + + /// + /// The null code. + /// + private const int NullCode = -1; + + /// + /// The stream. + /// + private readonly Stream stream; + + /// + /// Initializes a new instance of the class + /// and sets the stream, where the compressed data should be read from. + /// + /// The stream. where to read from. + /// is null + /// (Nothing in Visual Basic). + public LzwDecoder(Stream stream) + { + Guard.NotNull(stream, "stream"); + + this.stream = stream; + } + + /// + /// Decodes and decompresses all pixel indices from the stream. + /// + /// The width of the pixel index array. + /// The height of the pixel index array. + /// Size of the data. + /// The decoded and uncompressed array. + public byte[] DecodePixels(int width, int height, int dataSize) + { + Guard.LessThan(dataSize, int.MaxValue, "dataSize"); + + // The resulting index table. + byte[] pixels = new byte[width * height]; + + // Calculate the clear code. The value of the clear code is 2 ^ dataSize + int clearCode = 1 << dataSize; + + int codeSize = dataSize + 1; + + // Calculate the end code + int endCode = clearCode + 1; + + // Calculate the available code. + int availableCode = clearCode + 2; + + #region Jillzhangs Code (Not From Me) see: http://giflib.codeplex.com/ + // TODO: It's imperative that this close is cleaned up and commented properly. + // TODO: Unfortunately I can't figure out the character encoding to translate from the original Chinese. + int code; // ÓÃÓÚ´æ´¢µ±Ç°µÄ±àÂëÖµ + int oldCode = NullCode; // ÓÃÓÚ´æ´¢ÉÏÒ»´ÎµÄ±àÂëÖµ + int codeMask = (1 << codeSize) - 1; // ±íʾ±àÂëµÄ×î´óÖµ£¬Èç¹ûcodeSize=5,Ôòcode_mask=31 + int bits = 0; // ÔÚ±àÂëÁ÷ÖÐÊý¾ÝµÄ±£´æÐÎʽΪbyte£¬¶øÊµ¼Ê±àÂë¹ý³ÌÖÐÊÇÕÒʵ¼Ê±àÂëλÀ´´æ´¢µÄ£¬±ÈÈçµ±codeSize=5µÄʱºò£¬ÄÇôʵ¼ÊÉÏ5bitµÄÊý¾Ý¾ÍÓ¦¸Ã¿ÉÒÔ±íʾһ¸ö±àÂ룬ÕâÑùÈ¡³öÀ´µÄ1¸ö×ֽھ͸»ÓàÁË3¸öbit£¬Õâ3¸öbitÓÃÓں͵ڶþ¸ö×ֽڵĺóÁ½¸öbit½øÐÐ×éºÏ£¬ÔÙ´ÎÐγɱàÂëÖµ£¬Èç´ËÀàÍÆ + + + int[] prefix = new int[StackSize]; // ÓÃÓÚ±£´æÇ°×ºµÄ¼¯ºÏ + int[] suffix = new int[StackSize]; // ÓÃÓÚ±£´æºó׺ + int[] pixelStatck = new int[StackSize + 1]; // ÓÃÓÚÁÙʱ±£´æÊý¾ÝÁ÷ + + int top = 0; + int count = 0; // ÔÚÏÂÃæµÄÑ­»·ÖУ¬Ã¿´Î»á»ñȡһ¶¨Á¿µÄ±àÂëµÄ×Ö½ÚÊý×飬¶ø´¦ÀíÕâЩÊý×éµÄʱºòÐèÒª1¸ö¸ö×Ö½ÚÀ´´¦Àí£¬count¾ÍÊDZíʾ»¹Òª´¦ÀíµÄ×Ö½ÚÊýÄ¿ + int bi = 0; // count±íʾ»¹Ê£¶àÉÙ×Ö½ÚÐèÒª´¦Àí£¬¶øbiÔò±íʾ±¾´ÎÒѾ­´¦ÀíµÄ¸öÊý + int xyz = 0; // i´ú±íµ±Ç°´¦ÀíµÃµ½ÏñËØÊý + + int data = 0; // ±íʾµ±Ç°´¦ÀíµÄÊý¾ÝµÄÖµ + int first = 0; // Ò»¸ö×Ö·û´®ÖصĵÚÒ»¸ö×Ö½Ú + int inCode; // ÔÚlzwÖУ¬Èç¹ûÈÏʶÁËÒ»¸ö±àÂëËù´ú±íµÄÊý¾Ýentry£¬Ôò½«±àÂë×÷ΪÏÂÒ»´ÎµÄprefix£¬´Ë´¦inCode´ú±í´«µÝ¸øÏÂÒ»´Î×÷Ϊǰ׺µÄ±àÂëÖµ + + // ÏÈÉú³ÉÔªÊý¾ÝµÄǰ׺¼¯ºÏºÍºó׺¼¯ºÏ£¬ÔªÊý¾ÝµÄǰ׺¾ùΪ0£¬¶øºó׺ÓëÔªÊý¾ÝÏàµÈ£¬Í¬Ê±±àÂëÒ²ÓëÔªÊý¾ÝÏàµÈ + for (code = 0; code < clearCode; code++) + { + // ǰ׺³õʼΪ0 + prefix[code] = 0; + + // ºó׺=ÔªÊý¾Ý=±àÂë + suffix[code] = (byte)code; + } + + byte[] buffer = null; + while (xyz < pixels.Length) + { + // ×î´óÏñËØÊýÒѾ­È·¶¨ÎªpixelCount = width * width + if (top == 0) + { + if (bits < codeSize) + { + // Èç¹ûµ±Ç°µÄÒª´¦ÀíµÄbitÊýСÓÚ±àÂëλ´óС£¬ÔòÐèÒª¼ÓÔØÊý¾Ý + if (count == 0) + { + // Èç¹ûcountΪ0£¬±íʾҪ´Ó±àÂëÁ÷ÖжÁÒ»¸öÊý¾Ý¶ÎÀ´½øÐзÖÎö + buffer = this.ReadBlock(); + count = buffer.Length; + if (count == 0) + { + // ÔÙ´ÎÏë¶ÁÈ¡Êý¾Ý¶Î£¬È´Ã»ÓжÁµ½Êý¾Ý£¬´Ëʱ¾Í±íÃ÷ÒѾ­´¦ÀíÍêÁË + break; + } + + // ÖØÐ¶Áȡһ¸öÊý¾Ý¶Îºó£¬Ó¦¸Ã½«ÒѾ­´¦ÀíµÄ¸öÊýÖÃ0 + bi = 0; + } + + // »ñÈ¡±¾´ÎÒª´¦ÀíµÄÊý¾ÝµÄÖµ + data += buffer[bi] << bits; // ´Ë´¦ÎªºÎÒªÒÆÎ»ÄØ£¬±ÈÈçµÚÒ»´Î´¦ÀíÁË1¸ö×Ö½ÚΪ176£¬µÚÒ»´ÎÖ»Òª´¦Àí5bit¾Í¹»ÁË£¬Ê£ÏÂ3bitÁô¸øÏ¸ö×Ö½Ú½øÐÐ×éºÏ¡£Ò²¾ÍÊǵڶþ¸ö×ֽڵĺóÁ½Î»+µÚÒ»¸ö×Ö½ÚµÄǰÈýλ×é³ÉµÚ¶þ´ÎÊä³öÖµ + bits += 8; // ±¾´ÎÓÖ´¦ÀíÁËÒ»¸ö×Ö½Ú£¬ËùÒÔÐèÒª+8 + bi++; // ½«´¦ÀíÏÂÒ»¸ö×Ö½Ú + count--; // ÒѾ­´¦Àí¹ýµÄ×Ö½ÚÊý+1 + continue; + } + + // Èç¹ûÒѾ­ÓÐ×ã¹»µÄbitÊý¿É¹©´¦Àí£¬ÏÂÃæ¾ÍÊÇ´¦Àí¹ý³Ì + // »ñÈ¡±àÂë + code = data & codeMask; // »ñÈ¡dataÊý¾ÝµÄ±àÂëλ´óСbitµÄÊý¾Ý + data >>= codeSize; // ½«±àÂëÊý¾Ý½ØÈ¡ºó£¬Ô­À´µÄÊý¾Ý¾Íʣϼ¸¸öbitÁË£¬´Ëʱ½«ÕâЩbitÓÒÒÆ£¬ÎªÏ´Î×÷×¼±¸ + bits -= codeSize; // ͬʱÐèÒª½«µ±Ç°Êý¾ÝµÄbitÊý¼õÈ¥±àÂë볤£¬ÒòΪÒѾ­µÃµ½ÁË´¦Àí¡£ + + // ÏÂÃæ¸ù¾Ý»ñÈ¡µÄcodeÖµÀ´½øÐд¦Àí + if (code > availableCode || code == endCode) + { + // µ±±àÂëÖµ´óÓÚ×î´ó±àÂëÖµ»òÕßΪ½áÊø±ê¼ÇµÄʱºò£¬Í£Ö¹´¦Àí + break; + } + + if (code == clearCode) + { + // Èç¹ûµ±Ç°ÊÇÇå³ý±ê¼Ç£¬ÔòÖØÐ³õʼ»¯±äÁ¿£¬ºÃÖØÐÂÔÙÀ´ + codeSize = dataSize + 1; + + // ÖØÐ³õʼ»¯×î´ó±àÂëÖµ + codeMask = (1 << codeSize) - 1; + + // ³õʼ»¯ÏÂÒ»²½Ó¦¸Ã´¦ÀíµÃ±àÂëÖµ + availableCode = clearCode + 2; + + // ½«±£´æµ½old_codeÖеÄÖµÇå³ý£¬ÒÔ±ãÖØÍ·ÔÙÀ´ + oldCode = NullCode; + continue; + } + + // ÏÂÃæÊÇcodeÊôÓÚÄÜѹËõµÄ±àÂ뷶ΧÄڵĵĴ¦Àí¹ý³Ì + if (oldCode == NullCode) + { + // Èç¹ûµ±Ç°±àÂëֵΪ¿Õ,±íʾÊǵÚÒ»´Î»ñÈ¡±àÂë + pixelStatck[top++] = suffix[code]; // »ñÈ¡µ½1¸öÊý¾ÝÁ÷µÄÊý¾Ý + + // ±¾´Î±àÂë´¦ÀíÍê³É£¬½«±àÂëÖµ±£´æµ½old_codeÖÐ + oldCode = code; + + // µÚÒ»¸ö×Ö·ûΪµ±Ç°±àÂë + first = code; + continue; + } + + inCode = code; + if (code == availableCode) + { + // Èç¹ûµ±Ç°±àÂëºÍ±¾´ÎÓ¦¸ÃÉú³ÉµÄ±àÂëÏàͬ + // ÄÇôÏÂÒ»¸öÊý¾Ý×ֽھ͵ÈÓÚµ±Ç°´¦Àí×Ö·û´®µÄµÚÒ»¸ö×Ö½Ú + pixelStatck[top++] = (byte)first; + + code = oldCode; // »ØËݵ½ÉÏÒ»¸ö±àÂë + } + + while (code > clearCode) + { + // Èç¹ûµ±Ç°±àÂë´óÓÚÇå³ý±ê¼Ç£¬±íʾ±àÂëÖµÊÇÄÜѹËõÊý¾ÝµÄ + pixelStatck[top++] = suffix[code]; + code = prefix[code]; // »ØËݵ½ÉÏÒ»¸ö±àÂë + } + + first = suffix[code]; + + // »ñÈ¡ÏÂÒ»¸öÊý¾Ý + pixelStatck[top++] = suffix[code]; + + // Fix for Gifs that have "deferred clear code" as per here : https://bugzilla.mozilla.org/show_bug.cgi?id=55918 + if (availableCode < StackSize) + { + // ÉèÖõ±Ç°Ó¦¸Ã±àÂëλÖõÄǰ׺ + prefix[availableCode] = oldCode; + + // ÉèÖõ±Ç°Ó¦¸Ã±àÂëλÖõĺó׺ + suffix[availableCode] = first; + + // Ï´ÎÓ¦¸ÃµÃµ½µÄ±àÂëÖµ + availableCode++; + if (availableCode == codeMask + 1 && availableCode < StackSize) + { + // Ôö¼Ó±àÂëλÊý + codeSize++; + + // ÖØÉè×î´ó±àÂëÖµ + codeMask = (1 << codeSize) - 1; + } + } + + // »¹Ô­old_code + oldCode = inCode; + } + + // »ØËݵ½ÉÏÒ»¸ö´¦ÀíλÖà + top--; + + // »ñȡԪÊý¾Ý + pixels[xyz++] = (byte)pixelStatck[top]; + } + + #endregion + + return pixels; + } + + /// + /// Reads the next data block from the stream. A data block begins with a byte, + /// which defines the size of the block, followed by the block itself. + /// + /// + /// The . + /// + private byte[] ReadBlock() + { + int blockSize = this.stream.ReadByte(); + return this.ReadBytes(blockSize); + } + + /// + /// Reads the specified number of bytes from the data stream. + /// + /// + /// The number of bytes to read. + /// + /// + /// The . + /// + private byte[] ReadBytes(int length) + { + byte[] buffer = new byte[length]; + this.stream.Read(buffer, 0, length); + return buffer; + } + } +} diff --git a/src/ImageProcessor/Formats/IImageDecoder.cs b/src/ImageProcessor/Formats/IImageDecoder.cs new file mode 100644 index 000000000..4796f6cab --- /dev/null +++ b/src/ImageProcessor/Formats/IImageDecoder.cs @@ -0,0 +1,53 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates properties and methods required for decoding an image from a stream. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System.IO; + + /// + /// Encapsulates properties and methods required for decoding an image from a stream. + /// + public interface IImageDecoder + { + /// + /// Gets the size of the header for this image type. + /// + /// The size of the header. + int HeaderSize { get; } + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file extension. + /// + /// True if the decoder supports the file extension; otherwise, false. + /// + bool IsSupportedFileExtension(string extension); + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file header. + /// + /// True if the decoder supports the file header; otherwise, false. + /// + bool IsSupportedFileFormat(byte[] header); + + /// + /// Decodes the image from the specified stream to the . + /// + /// The to decode to. + /// The containing image data. + void Decode(Image image, Stream stream); + } +} diff --git a/src/ImageProcessor/Formats/IImageEncoder.cs b/src/ImageProcessor/Formats/IImageEncoder.cs new file mode 100644 index 000000000..ab62f3566 --- /dev/null +++ b/src/ImageProcessor/Formats/IImageEncoder.cs @@ -0,0 +1,47 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates properties and methods required for decoding an image to a stream. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System.IO; + + /// + /// Encapsulates properties and methods required for encoding an image to a stream. + /// + public interface IImageEncoder + { + /// + /// Gets or sets the quality of output for images. + /// + int Quality { get; set; } + + /// + /// Gets the default file extension for this encoder. + /// + string Extension { get; } + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file extension. + /// + /// True if the decoder supports the file extension; otherwise, false. + /// + bool IsSupportedFileExtension(string extension); + + /// + /// Encodes the image to the specified stream from the . + /// + /// The to encode from. + /// The to encode the image data to. + void Encode(ImageBase image, Stream stream); + } +} diff --git a/src/ImageProcessor/Formats/IImageFormat.cs b/src/ImageProcessor/Formats/IImageFormat.cs new file mode 100644 index 000000000..596805877 --- /dev/null +++ b/src/ImageProcessor/Formats/IImageFormat.cs @@ -0,0 +1,9 @@ +namespace ImageProcessor.Encoders +{ + public interface IImageFormat + { + IImageEncoder Encoder { get; } + + IImageDecoder Decoder { get; } + } +} diff --git a/src/ImageProcessor/Formats/Png/GrayscaleReader.cs b/src/ImageProcessor/Formats/Png/GrayscaleReader.cs new file mode 100644 index 000000000..0069b8a0d --- /dev/null +++ b/src/ImageProcessor/Formats/Png/GrayscaleReader.cs @@ -0,0 +1,83 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Color reader for reading grayscale colors from a png file. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Color reader for reading grayscale colors from a png file. + /// + public sealed class GrayscaleReader : IColorReader + { + /// + /// Whether t also read the alpha channel. + /// + private readonly bool useAlpha; + + /// + /// The current row. + /// + private int row; + + /// + /// Initializes a new instance of the class. + /// + /// + /// If set to true the color reader will also read the + /// alpha channel from the scanline. + /// + public GrayscaleReader(bool useAlpha) + { + this.useAlpha = useAlpha; + } + + /// + /// Reads the specified scanline. + /// + /// The scanline. + /// The pixels, where the colors should be stored in RGBA format. + /// + /// The header, which contains information about the png file, like + /// the width of the image and the height. + /// + public void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header) + { + int offset; + + byte[] newScanline = scanline.ToArrayByBitsLength(header.BitDepth); + + if (this.useAlpha) + { + for (int x = 0; x < header.Width / 2; x++) + { + offset = ((this.row * header.Width) + x) * 4; + + pixels[offset + 0] = newScanline[x * 2]; + pixels[offset + 1] = newScanline[x * 2]; + pixels[offset + 2] = newScanline[x * 2]; + pixels[offset + 3] = newScanline[(x * 2) + 1]; + } + } + else + { + for (int x = 0; x < header.Width; x++) + { + offset = ((this.row * header.Width) + x) * 4; + + pixels[offset + 0] = newScanline[x]; + pixels[offset + 1] = newScanline[x]; + pixels[offset + 2] = newScanline[x]; + pixels[offset + 3] = 255; + } + } + + this.row++; + } + } +} diff --git a/src/ImageProcessor/Formats/Png/IColorReader.cs b/src/ImageProcessor/Formats/Png/IColorReader.cs new file mode 100644 index 000000000..b2a7e4460 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/IColorReader.cs @@ -0,0 +1,31 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Encapsulates methods for color readers, which are responsible for reading +// different color formats from a png file. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Encapsulates methods for color readers, which are responsible for reading + /// different color formats from a png file. + /// + public interface IColorReader + { + /// + /// Reads the specified scanline. + /// + /// The scanline. + /// The pixels, where the colors should be stored in RGBA format. + /// + /// The header, which contains information about the png file, like + /// the width of the image and the height. + /// + void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header); + } +} diff --git a/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs b/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs new file mode 100644 index 000000000..140da5e5d --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PaletteIndexReader.cs @@ -0,0 +1,95 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// A color reader for reading palette indices from the png file. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// A color reader for reading palette indices from the png file. + /// + public sealed class PaletteIndexReader : IColorReader + { + /// + /// The palette. + /// + private readonly byte[] palette; + + /// + /// The alpha palette. + /// + private readonly byte[] paletteAlpha; + + /// + /// The current row. + /// + private int row; + + /// + /// Initializes a new instance of the class. + /// + /// The palette as simple byte array. It will contains 3 values for each + /// color, which represents the red-, the green- and the blue channel. + /// The alpha palette. Can be null, if the image does not have an + /// alpha channel and can contain less entries than the number of colors in the palette. + public PaletteIndexReader(byte[] palette, byte[] paletteAlpha) + { + this.palette = palette; + this.paletteAlpha = paletteAlpha; + } + + /// + /// Reads the specified scanline. + /// + /// The scanline. + /// The pixels, where the colors should be stored in RGBA format. + /// The header, which contains information about the png file, like + /// the width of the image and the height. + public void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header) + { + byte[] newScanline = scanline.ToArrayByBitsLength(header.BitDepth); + int offset, index; + + if (this.paletteAlpha != null && this.paletteAlpha.Length > 0) + { + // If the alpha palette is not null and does one or + // more entries, this means, that the image contains and alpha + // channel and we should try to read it. + for (int i = 0; i < header.Width; i++) + { + index = newScanline[i]; + + offset = ((this.row * header.Width) + i) * 4; + + pixels[offset + 0] = this.palette[(index * 3) + 2]; + pixels[offset + 1] = this.palette[(index * 3) + 1]; + pixels[offset + 2] = this.palette[(index * 3) + 0]; + pixels[offset + 3] = this.paletteAlpha.Length > index + ? this.paletteAlpha[index] + : (byte)255; + } + } + else + { + for (int i = 0; i < header.Width; i++) + { + index = newScanline[i]; + + offset = ((this.row * header.Width) + i) * 4; + + pixels[offset + 0] = this.palette[(index * 3) + 2]; + pixels[offset + 1] = this.palette[(index * 3) + 1]; + pixels[offset + 2] = this.palette[(index * 3) + 0]; + pixels[offset + 3] = 255; + } + } + + this.row++; + } + } +} diff --git a/src/ImageProcessor/Formats/Png/PngChunk.cs b/src/ImageProcessor/Formats/Png/PngChunk.cs new file mode 100644 index 000000000..554ccbca2 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngChunk.cs @@ -0,0 +1,44 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Stores header information about a chunk. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Stores header information about a chunk. + /// + internal sealed class PngChunk + { + /// + /// Gets or sets the length. + /// An unsigned integer giving the number of bytes in the chunk's + /// data field. The length counts only the data field, not itself, + /// the chunk type code, or the CRC. Zero is a valid length + /// + public int Length { get; set; } + + /// + /// Gets or sets the chunk type as string with 4 chars. + /// + public string Type { get; set; } + + /// + /// Gets or sets the data bytes appropriate to the chunk type, if any. + /// This field can be of zero length. + /// + public byte[] Data { get; set; } + + /// + /// Gets or sets a CRC (Cyclic Redundancy Check) calculated on the preceding bytes in the chunk, + /// including the chunk type code and chunk data fields, but not including the length field. + /// The CRC is always present, even for chunks containing no data + /// + public uint Crc { get; set; } + } +} diff --git a/src/ImageProcessor/Formats/Png/PngChunkTypes.cs b/src/ImageProcessor/Formats/Png/PngChunkTypes.cs new file mode 100644 index 000000000..cc908e48e --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngChunkTypes.cs @@ -0,0 +1,67 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Contains a list of possible chunk type identifiers. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Contains a list of possible chunk type identifiers. + /// + internal static class PngChunkTypes + { + /// + /// The first chunk in a png file. Can only exists once. Contains + /// common information like the width and the height of the image or + /// the used compression method. + /// + public const string Header = "IHDR"; + + /// + /// The PLTE chunk contains from 1 to 256 palette entries, each a three byte + /// series in the RGB format. + /// + public const string Palette = "PLTE"; + + /// + /// The IDAT chunk contains the actual image data. The image can contains more + /// than one chunk of this type. All chunks together are the whole image. + /// + public const string Data = "IDAT"; + + /// + /// This chunk must appear last. It marks the end of the PNG data stream. + /// The chunk's data field is empty. + /// + public const string End = "IEND"; + + /// + /// This chunk specifies that the image uses simple transparency: + /// either alpha values associated with palette entries (for indexed-color images) + /// or a single transparent color (for grayscale and true color images). + /// + public const string PaletteAlpha = "tRNS"; + + /// + /// Textual information that the encoder wishes to record with the image can be stored in + /// tEXt chunks. Each tEXt chunk contains a keyword and a text string. + /// + public const string Text = "tEXt"; + + /// + /// This chunk specifies the relationship between the image samples and the desired + /// display output intensity. + /// + public const string Gamma = "gAMA"; + + /// + /// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image. + /// + public const string Physical = "pHYs"; + } +} diff --git a/src/ImageProcessor/Formats/Png/PngColorTypeInformation.cs b/src/ImageProcessor/Formats/Png/PngColorTypeInformation.cs new file mode 100644 index 000000000..15b3fb7ff --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngColorTypeInformation.cs @@ -0,0 +1,66 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Contains information that are required when loading a png with a specific color type. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System; + + /// + /// Contains information that are required when loading a png with a specific color type. + /// + internal sealed class PngColorTypeInformation + { + /// + /// Initializes a new instance of the class with + /// the scanline factory, the function to create the color reader and the supported bit depths. + /// + /// The scanline factor. + /// The supported bit depths. + /// The factory to create the color reader. + public PngColorTypeInformation(int scanlineFactor, int[] supportedBitDepths, Func scanlineReaderFactory) + { + this.ChannelsPerColor = scanlineFactor; + this.ScanlineReaderFactory = scanlineReaderFactory; + this.SupportedBitDepths = supportedBitDepths; + } + + /// + /// Gets an array with the bit depths that are supported for the color type + /// where this object is created for. + /// + /// The supported bit depths that can be used in combination with this color type. + public int[] SupportedBitDepths { get; private set; } + + /// + /// Gets a function that is used the create the color reader for the color type where + /// this object is created for. + /// + /// The factory function to create the color type. + public Func ScanlineReaderFactory { get; private set; } + + /// + /// Gets a factor that is used when iterating through the scan lines. + /// + /// The scanline factor. + public int ChannelsPerColor { get; private set; } + + /// + /// Creates the color reader for the color type where this object is create for. + /// + /// The palette of the image. Can be null when no palette is used. + /// The alpha palette of the image. Can be null when + /// no palette is used for the image or when the image has no alpha. + /// The color reader for the image. + public IColorReader CreateColorReader(byte[] palette, byte[] paletteAlpha) + { + return this.ScanlineReaderFactory(palette, paletteAlpha); + } + } +} diff --git a/src/ImageProcessor/Formats/Png/PngDecoder.cs b/src/ImageProcessor/Formats/Png/PngDecoder.cs new file mode 100644 index 000000000..8df7d0106 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngDecoder.cs @@ -0,0 +1,98 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Encoder for generating a image out of a png stream. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System; + using System.IO; + + /// + /// Encoder for generating a image out of a png stream. + /// + /// + /// At the moment the following features are supported: + /// + /// Filters: all filters are supported. + /// + /// + /// Pixel formats: + /// + /// RGBA (True color) with alpha (8 bit). + /// RGB (True color) without alpha (8 bit). + /// Greyscale with alpha (8 bit). + /// Greyscale without alpha (8 bit). + /// Palette Index with alpha (8 bit). + /// Palette Index without alpha (8 bit). + /// + /// + /// + public class PngDecoder : IImageDecoder + { + /// + /// Gets the size of the header for this image type. + /// + /// The size of the header. + public int HeaderSize + { + get + { + return 8; + } + } + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file extension. + /// + /// True if the decoder supports the file extension; otherwise, false. + /// + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, "extension"); + + extension = extension.StartsWith(".") ? extension.Substring(1) : extension; + + return extension.Equals("PNG", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Returns a value indicating whether the supports the specified + /// file header. + /// + /// The containing the file header. + /// + /// True if the decoder supports the file header; otherwise, false. + /// + public bool IsSupportedFileFormat(byte[] header) + { + return header.Length >= 8 && + header[0] == 0x89 && + header[1] == 0x50 && // P + header[2] == 0x4E && // N + header[3] == 0x47 && // G + header[4] == 0x0D && // CR + header[5] == 0x0A && // LF + header[6] == 0x1A && // EOF + header[7] == 0x0A; // LF + } + + /// + /// Decodes the image from the specified stream to the . + /// + /// The to decode to. + /// The containing image data. + public void Decode(Image image, Stream stream) + { + new PngDecoderCore().Decode(image, stream); + } + } +} diff --git a/src/ImageProcessor/Formats/Png/PngDecoderCore.cs b/src/ImageProcessor/Formats/Png/PngDecoderCore.cs new file mode 100644 index 000000000..f0fec8d04 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngDecoderCore.cs @@ -0,0 +1,554 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Performs the png decoding operation. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + + using ICSharpCode.SharpZipLib.Checksums; + using ICSharpCode.SharpZipLib.Zip.Compression.Streams; + + /// + /// Performs the png decoding operation. + /// + internal class PngDecoderCore + { + /// + /// The maximum chunk size. + /// + private const int MaxChunkSize = 1024 * 1024; + + /// + /// The dictionary of available color types. + /// + private static readonly Dictionary ColorTypes + = new Dictionary(); + + /// + /// The image to decode. + /// + private Image currentImage; + + /// + /// The stream to decode to. + /// + private Stream currentStream; + + /// + /// The png header. + /// + private PngHeader header; + + /// + /// Initializes static members of the class. + /// + static PngDecoderCore() + { + ColorTypes.Add( + 0, + new PngColorTypeInformation( + 1, + new[] { 1, 2, 4, 8 }, + (p, a) => new GrayscaleReader(false))); + + ColorTypes.Add( + 2, + new PngColorTypeInformation( + 3, + new[] { 8 }, + (p, a) => new TrueColorReader(false))); + + ColorTypes.Add( + 3, + new PngColorTypeInformation( + 1, + new[] { 1, 2, 4, 8 }, + (p, a) => new PaletteIndexReader(p, a))); + + ColorTypes.Add( + 4, + new PngColorTypeInformation( + 2, + new[] { 8 }, + (p, a) => new GrayscaleReader(true))); + + ColorTypes.Add( + 6, + new PngColorTypeInformation( + 4, + new[] { 8 }, + (p, a) => new TrueColorReader(true))); + } + + /// + /// Decodes the stream to the image. + /// + /// The image to decode to. + /// The stream containing image data. + /// + /// Thrown if the stream does not contain and end chunk. + /// + /// + /// Thrown if the image is larger than the maximum allowable size. + /// + public void Decode(Image image, Stream stream) + { + this.currentImage = image; + this.currentStream = stream; + this.currentStream.Seek(8, SeekOrigin.Current); + + bool isEndChunkReached = false; + + byte[] palette = null; + byte[] paletteAlpha = null; + + using (MemoryStream dataStream = new MemoryStream()) + { + PngChunk currentChunk; + while ((currentChunk = this.ReadChunk()) != null) + { + if (isEndChunkReached) + { + throw new ImageFormatException("Image does not end with end chunk."); + } + + if (currentChunk.Type == PngChunkTypes.Header) + { + this.ReadHeaderChunk(currentChunk.Data); + this.ValidateHeader(); + } + else if (currentChunk.Type == PngChunkTypes.Physical) + { + this.ReadPhysicalChunk(currentChunk.Data); + } + else if (currentChunk.Type == PngChunkTypes.Data) + { + dataStream.Write(currentChunk.Data, 0, currentChunk.Data.Length); + } + else if (currentChunk.Type == PngChunkTypes.Palette) + { + palette = currentChunk.Data; + } + else if (currentChunk.Type == PngChunkTypes.PaletteAlpha) + { + paletteAlpha = currentChunk.Data; + } + else if (currentChunk.Type == PngChunkTypes.Text) + { + this.ReadTextChunk(currentChunk.Data); + } + else if (currentChunk.Type == PngChunkTypes.End) + { + isEndChunkReached = true; + } + } + + if (this.header.Width > ImageBase.MaxWidth || this.header.Height > ImageBase.MaxHeight) + { + throw new ArgumentOutOfRangeException( + string.Format( + "The input png '{0}x{1}' is bigger then the max allowed size '{2}x{3}'", + this.header.Width, + this.header.Height, + ImageBase.MaxWidth, + ImageBase.MaxHeight)); + } + + byte[] pixels = new byte[this.header.Width * this.header.Height * 4]; + + PngColorTypeInformation colorTypeInformation = ColorTypes[this.header.ColorType]; + + if (colorTypeInformation != null) + { + IColorReader colorReader = colorTypeInformation.CreateColorReader(palette, paletteAlpha); + + this.ReadScanlines(dataStream, pixels, colorReader, colorTypeInformation); + } + + image.SetPixels(this.header.Width, this.header.Height, pixels); + } + } + + /// + /// Reads the data chunk containing physical dimension data. + /// + /// The data containing physical data. + private void ReadPhysicalChunk(byte[] data) + { + Array.Reverse(data, 0, 4); + Array.Reverse(data, 4, 4); + + this.currentImage.DensityX = BitConverter.ToInt32(data, 0) / 39.3700787d; + this.currentImage.DensityY = BitConverter.ToInt32(data, 4) / 39.3700787d; + } + + /// + /// Calculates the scanline length. + /// + /// The color type information. + /// The representing the length. + private int CalculateScanlineLength(PngColorTypeInformation colorTypeInformation) + { + int scanlineLength = this.header.Width * this.header.BitDepth * colorTypeInformation.ChannelsPerColor; + + int amount = scanlineLength % 8; + if (amount != 0) + { + scanlineLength += 8 - amount; + } + + return scanlineLength / 8; + } + + /// + /// Calculates a scanline step. + /// + /// The color type information. + /// The representing the length of each step. + private int CalculateScanlineStep(PngColorTypeInformation colorTypeInformation) + { + int scanlineStep = 1; + + if (this.header.BitDepth >= 8) + { + scanlineStep = (colorTypeInformation.ChannelsPerColor * this.header.BitDepth) / 8; + } + + return scanlineStep; + } + + /// + /// Reads the scanlines within the image. + /// + /// The containing data. + /// + /// The containing pixel data. + /// The color reader. + /// The color type information. + private void ReadScanlines(MemoryStream dataStream, byte[] pixels, IColorReader colorReader, PngColorTypeInformation colorTypeInformation) + { + dataStream.Position = 0; + + int scanlineLength = this.CalculateScanlineLength(colorTypeInformation); + int scanlineStep = this.CalculateScanlineStep(colorTypeInformation); + + byte[] lastScanline = new byte[scanlineLength]; + byte[] currentScanline = new byte[scanlineLength]; + int filter = 0, column = -1; + + using (InflaterInputStream compressedStream = new InflaterInputStream(dataStream)) + { + int readByte; + while ((readByte = compressedStream.ReadByte()) >= 0) + { + if (column == -1) + { + filter = readByte; + + column++; + } + else + { + currentScanline[column] = (byte)readByte; + + byte a; + byte b; + byte c; + + if (column >= scanlineStep) + { + a = currentScanline[column - scanlineStep]; + c = lastScanline[column - scanlineStep]; + } + else + { + a = 0; + c = 0; + } + + b = lastScanline[column]; + + if (filter == 1) + { + currentScanline[column] = (byte)(currentScanline[column] + a); + } + else if (filter == 2) + { + currentScanline[column] = (byte)(currentScanline[column] + b); + } + else if (filter == 3) + { + currentScanline[column] = (byte)(currentScanline[column] + (byte)((a + b) / 2)); + } + else if (filter == 4) + { + currentScanline[column] = (byte)(currentScanline[column] + PathPredicator(a, b, c)); + } + + column++; + + if (column == scanlineLength) + { + colorReader.ReadScanline(currentScanline, pixels, this.header); + column = -1; + + Utils.Swap(ref currentScanline, ref lastScanline); + } + } + } + } + } + + /// + /// TODO: Document this + /// + /// The a. + /// The b. + /// The c. + /// + /// The . + /// + private static byte PathPredicator(byte a, byte b, byte c) + { + byte predicator; + + int p = a + b - c; + int pa = Math.Abs(p - a); + int pb = Math.Abs(p - b); + int pc = Math.Abs(p - c); + + if (pa <= pb && pa <= pc) + { + predicator = a; + } + else if (pb <= pc) + { + predicator = b; + } + else + { + predicator = c; + } + + return predicator; + } + + /// + /// Reads a text chunk containing image properties from the data. + /// + /// The containing data. + private void ReadTextChunk(byte[] data) + { + int zeroIndex = 0; + + for (int i = 0; i < data.Length; i++) + { + if (data[i] == 0) + { + zeroIndex = i; + break; + } + } + + string name = System.Text.Encoding.Unicode.GetString(data, 0, zeroIndex); + string value = System.Text.Encoding.Unicode.GetString(data, zeroIndex + 1, data.Length - zeroIndex - 1); + + this.currentImage.Properties.Add(new ImageProperty(name, value)); + } + + /// + /// Reads a header chunk from the data. + /// + /// The containing data. + private void ReadHeaderChunk(byte[] data) + { + this.header = new PngHeader(); + + Array.Reverse(data, 0, 4); + Array.Reverse(data, 4, 4); + + this.header.Width = BitConverter.ToInt32(data, 0); + this.header.Height = BitConverter.ToInt32(data, 4); + + this.header.BitDepth = data[8]; + this.header.ColorType = data[9]; + this.header.FilterMethod = data[11]; + this.header.InterlaceMethod = data[12]; + this.header.CompressionMethod = data[10]; + } + + /// + /// Validates the png header. + /// + /// + /// Thrown if the image does pass validation. + /// + private void ValidateHeader() + { + if (!ColorTypes.ContainsKey(this.header.ColorType)) + { + throw new ImageFormatException("Color type is not supported or not valid."); + } + + if (!ColorTypes[this.header.ColorType].SupportedBitDepths.Contains(this.header.BitDepth)) + { + throw new ImageFormatException("Bit depth is not supported or not valid."); + } + + if (this.header.FilterMethod != 0) + { + throw new ImageFormatException("The png specification only defines 0 as filter method."); + } + + if (this.header.InterlaceMethod != 0) + { + throw new ImageFormatException("Interlacing is not supported."); + } + } + + /// + /// Reads a chunk from the stream. + /// + /// + /// The . + /// + private PngChunk ReadChunk() + { + PngChunk chunk = new PngChunk(); + + if (this.ReadChunkLength(chunk) == 0) + { + return null; + } + + byte[] typeBuffer = this.ReadChunkType(chunk); + + this.ReadChunkData(chunk); + this.ReadChunkCrc(chunk, typeBuffer); + + return chunk; + } + + /// + /// Reads the cycle redundancy chunk from the data. + /// + /// The chunk. + /// The type buffer. + /// + /// Thrown if the input stream is not valid or corrupt. + /// + private void ReadChunkCrc(PngChunk chunk, byte[] typeBuffer) + { + byte[] crcBuffer = new byte[4]; + + int numBytes = this.currentStream.Read(crcBuffer, 0, 4); + if (numBytes.IsBetween(1, 3)) + { + throw new ImageFormatException("Image stream is not valid!"); + } + + Array.Reverse(crcBuffer); + + chunk.Crc = BitConverter.ToUInt32(crcBuffer, 0); + + Crc32 crc = new Crc32(); + crc.Update(typeBuffer); + crc.Update(chunk.Data); + + if (crc.Value != chunk.Crc) + { + throw new ImageFormatException("CRC Error. PNG Image chunk is corrupt!"); + } + } + + /// + /// Reads the chunk data from the stream. + /// + /// The chunk. + /// + /// Thrown if the chunk length exceeds the maximum allowable size. + /// + private void ReadChunkData(PngChunk chunk) + { + if (chunk.Length > MaxChunkSize) + { + throw new ArgumentOutOfRangeException( + string.Format( + "Png chunk size '{0}' exceeds the '{1}'", + chunk.Length, + MaxChunkSize)); + } + + chunk.Data = new byte[chunk.Length]; + this.currentStream.Read(chunk.Data, 0, chunk.Length); + } + + /// + /// Identifies the chunk type from the chunk. + /// + /// The chunk. + /// + /// The containing identifying information. + /// + /// + /// Thrown if the input stream is not valid. + /// + private byte[] ReadChunkType(PngChunk chunk) + { + byte[] typeBuffer = new byte[4]; + + int numBytes = this.currentStream.Read(typeBuffer, 0, 4); + if (numBytes.IsBetween(1, 3)) + { + throw new ImageFormatException("Image stream is not valid!"); + } + + char[] chars = new char[4]; + chars[0] = (char)typeBuffer[0]; + chars[1] = (char)typeBuffer[1]; + chars[2] = (char)typeBuffer[2]; + chars[3] = (char)typeBuffer[3]; + + chunk.Type = new string(chars); + + return typeBuffer; + } + + /// + /// Calculates the length of the given chunk. + /// + /// he chunk. + /// + /// The representing the chunk length. + /// + /// + /// Thrown if the input stream is not valid. + /// + private int ReadChunkLength(PngChunk chunk) + { + byte[] lengthBuffer = new byte[4]; + + int numBytes = this.currentStream.Read(lengthBuffer, 0, 4); + if (numBytes.IsBetween(1, 3)) + { + throw new ImageFormatException("Image stream is not valid!"); + } + + Array.Reverse(lengthBuffer); + + chunk.Length = BitConverter.ToInt32(lengthBuffer, 0); + + return numBytes; + } + } +} diff --git a/src/ImageProcessor/Formats/Png/PngEncoder.cs b/src/ImageProcessor/Formats/Png/PngEncoder.cs new file mode 100644 index 000000000..a4e64d6f5 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngEncoder.cs @@ -0,0 +1,488 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Image encoder for writing image data to a stream in png format. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + using System; + using System.IO; + + using ICSharpCode.SharpZipLib.Checksums; + using ICSharpCode.SharpZipLib.Zip.Compression.Streams; + + /// + /// Image encoder for writing image data to a stream in png format. + /// + public class PngEncoder : IImageEncoder + { + /// + /// The maximum block size. + /// + private const int MaxBlockSize = 0xFFFF; + + /// + /// Initializes a new instance of the class. + /// + public PngEncoder() + { + this.Gamma = 2.2f; + } + + /// + /// Gets or sets the quality of output for images. + /// + /// Png is a lossless format so this is not used in this encoder. + public int Quality { get; set; } + + /// + /// Gets or sets a value indicating whether this encoder + /// will write the image uncompressed the stream. + /// + /// + /// true if the image should be written uncompressed to + /// the stream; otherwise, false. + /// + public bool IsWritingUncompressed { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is writing + /// gamma information to the stream. The default value is false. + /// + /// + /// true if this instance is writing gamma + /// information to the stream.; otherwise, false. + /// + public bool IsWritingGamma { get; set; } + + /// + /// Gets or sets the gamma value, that will be written + /// the the stream, when the property + /// is set to true. The default value is 2.2f. + /// + /// The gamma value of the image. + public double Gamma { get; set; } + + /// + /// Gets the default file extension for this encoder. + /// + /// The default file extension for this encoder. + public string Extension + { + get { return "PNG"; } + } + + /// + /// Indicates if the image encoder supports the specified + /// file extension. + /// + /// The file extension. + /// true, if the encoder supports the specified + /// extensions; otherwise false. + /// + /// + /// is null (Nothing in Visual Basic). + /// is a string + /// of length zero or contains only blanks. + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, "extension"); + + extension = extension.StartsWith(".") ? extension.Substring(1) : extension; + + return extension.Equals("PNG", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Encodes the data of the specified image and writes the result to + /// the specified stream. + /// + /// The image, where the data should be get from. + /// Cannot be null (Nothing in Visual Basic). + /// The stream, where the image data should be written to. + /// Cannot be null (Nothing in Visual Basic). + /// + /// is null (Nothing in Visual Basic). + /// - or - + /// is null (Nothing in Visual Basic). + /// + public void Encode(ImageBase image, Stream stream) + { + Guard.NotNull(image, "image"); + Guard.NotNull(stream, "stream"); + + // Write the png header. + stream.Write( + new byte[] + { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A + }, + 0, + 8); + + PngHeader header = new PngHeader + { + Width = image.Width, + Height = image.Height, + ColorType = 6, + BitDepth = 8, + FilterMethod = 0, + CompressionMethod = 0, + InterlaceMethod = 0 + }; + + this.WriteHeaderChunk(stream, header); + this.WritePhysicalChunk(stream, image); + this.WriteGammaChunk(stream); + + if (this.IsWritingUncompressed) + { + this.WriteDataChunksFast(stream, image); + } + else + { + this.WriteDataChunks(stream, image); + } + + this.WriteEndChunk(stream); + stream.Flush(); + } + + /// + /// Writes an integer to the byte array. + /// + /// The containing image data. + /// The amount to offset by. + /// The value to write. + private static void WriteInteger(byte[] data, int offset, int value) + { + byte[] buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + Array.Copy(buffer, 0, data, offset, 4); + } + + /// + /// Writes an integer to the stream. + /// + /// The containing image data. + /// The value to write. + private static void WriteInteger(Stream stream, int value) + { + byte[] buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + + stream.Write(buffer, 0, 4); + } + + /// + /// Writes an unsigned integer to the stream. + /// + /// The containing image data. + /// The value to write. + private static void WriteInteger(Stream stream, uint value) + { + byte[] buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + + stream.Write(buffer, 0, 4); + } + + /// + /// Writes the physical dimension information to the stream. + /// + /// The containing image data. + /// The image base. + private void WritePhysicalChunk(Stream stream, ImageBase imageBase) + { + Image image = imageBase as Image; + if (image != null && image.DensityX > 0 && image.DensityY > 0) + { + int dpmX = (int)Math.Round(image.DensityX * 39.3700787d); + int dpmY = (int)Math.Round(image.DensityY * 39.3700787d); + + byte[] chunkData = new byte[9]; + + WriteInteger(chunkData, 0, dpmX); + WriteInteger(chunkData, 4, dpmY); + + chunkData[8] = 1; + + this.WriteChunk(stream, PngChunkTypes.Physical, chunkData); + } + } + + /// + /// Writes the gamma information to the stream. + /// + /// The containing image data. + private void WriteGammaChunk(Stream stream) + { + if (this.IsWritingGamma) + { + int gammaValue = (int)(this.Gamma * 100000f); + + byte[] fourByteData = new byte[4]; + + byte[] size = BitConverter.GetBytes(gammaValue); + + fourByteData[0] = size[3]; + fourByteData[1] = size[2]; + fourByteData[2] = size[1]; + fourByteData[3] = size[0]; + + this.WriteChunk(stream, PngChunkTypes.Gamma, fourByteData); + } + } + + /// + /// Writes the pixel information to the stream. + /// + /// The containing image data. + /// The image base. + private void WriteDataChunksFast(Stream stream, ImageBase imageBase) + { + byte[] pixels = imageBase.Pixels; + + // Convert the pixel array to a new array for adding + // the filter byte. + byte[] data = new byte[(imageBase.Width * imageBase.Height * 4) + imageBase.Height]; + + int rowLength = (imageBase.Width * 4) + 1; + + for (int y = 0; y < imageBase.Height; y++) + { + data[y * rowLength] = 0; + Array.Copy(pixels, y * imageBase.Width * 4, data, (y * rowLength) + 1, imageBase.Width * 4); + } + + Adler32 adler32 = new Adler32(); + adler32.Update(data); + + using (MemoryStream tempStream = new MemoryStream()) + { + int remainder = data.Length; + + int blockCount; + if ((data.Length % MaxBlockSize) == 0) + { + blockCount = data.Length / MaxBlockSize; + } + else + { + blockCount = (data.Length / MaxBlockSize) + 1; + } + + // Write headers + tempStream.WriteByte(0x78); + tempStream.WriteByte(0xDA); + + for (int i = 0; i < blockCount; i++) + { + // Write the length + ushort length = (ushort)((remainder < MaxBlockSize) ? remainder : MaxBlockSize); + + tempStream.WriteByte(length == remainder ? (byte)0x01 : (byte)0x00); + + tempStream.Write(BitConverter.GetBytes(length), 0, 2); + + // Write one's compliment of length + tempStream.Write(BitConverter.GetBytes((ushort)~length), 0, 2); + + // Write blocks + tempStream.Write(data, i * MaxBlockSize, length); + + // Next block + remainder -= MaxBlockSize; + } + + WriteInteger(tempStream, (int)adler32.Value); + + tempStream.Seek(0, SeekOrigin.Begin); + + byte[] zipData = new byte[tempStream.Length]; + tempStream.Read(zipData, 0, (int)tempStream.Length); + + this.WriteChunk(stream, PngChunkTypes.Data, zipData); + } + } + + /// + /// Writes the pixel information to the stream. + /// + /// The containing image data. + /// The image base. + private void WriteDataChunks(Stream stream, ImageBase imageBase) + { + byte[] pixels = imageBase.Pixels; + + byte[] data = new byte[(imageBase.Width * imageBase.Height * 4) + imageBase.Height]; + + int rowLength = (imageBase.Width * 4) + 1; + + for (int y = 0; y < imageBase.Height; y++) + { + byte compression = 0; + if (y > 0) + { + compression = 2; + } + + data[y * rowLength] = compression; + + for (int x = 0; x < imageBase.Width; x++) + { + // Calculate the offset for the new array. + int dataOffset = (y * rowLength) + (x * 4) + 1; + + // Calculate the offset for the original pixel array. + int pixelOffset = ((y * imageBase.Width) + x) * 4; + + data[dataOffset + 0] = pixels[pixelOffset + 2]; + data[dataOffset + 1] = pixels[pixelOffset + 1]; + data[dataOffset + 2] = pixels[pixelOffset + 0]; + data[dataOffset + 3] = pixels[pixelOffset + 3]; + + if (y > 0) + { + int lastOffset = (((y - 1) * imageBase.Width) + x) * 4; + + data[dataOffset + 0] -= pixels[lastOffset + 2]; + data[dataOffset + 1] -= pixels[lastOffset + 1]; + data[dataOffset + 2] -= pixels[lastOffset + 0]; + data[dataOffset + 3] -= pixels[lastOffset + 3]; + } + } + } + + byte[] buffer; + int bufferLength; + + MemoryStream memoryStream = null; + try + { + memoryStream = new MemoryStream(); + + using (DeflaterOutputStream outputStream = new DeflaterOutputStream(memoryStream)) + { + outputStream.Write(data, 0, data.Length); + outputStream.Flush(); + outputStream.Finish(); + + bufferLength = (int)memoryStream.Length; + buffer = memoryStream.ToArray(); + } + } + finally + { + if (memoryStream != null) + { + memoryStream.Dispose(); + } + } + + int numChunks = bufferLength / MaxBlockSize; + + if (bufferLength % MaxBlockSize != 0) + { + numChunks++; + } + + for (int i = 0; i < numChunks; i++) + { + int length = bufferLength - (i * MaxBlockSize); + + if (length > MaxBlockSize) + { + length = MaxBlockSize; + } + + this.WriteChunk(stream, PngChunkTypes.Data, buffer, i * MaxBlockSize, length); + } + } + + /// + /// Writes the chunk end to the stream. + /// + /// The containing image data. + private void WriteEndChunk(Stream stream) + { + this.WriteChunk(stream, PngChunkTypes.End, null); + } + + /// + /// Writes the header chunk to the stream. + /// + /// The containing image data. + /// The . + private void WriteHeaderChunk(Stream stream, PngHeader header) + { + byte[] chunkData = new byte[13]; + + WriteInteger(chunkData, 0, header.Width); + WriteInteger(chunkData, 4, header.Height); + + chunkData[8] = header.BitDepth; + chunkData[9] = header.ColorType; + chunkData[10] = header.CompressionMethod; + chunkData[11] = header.FilterMethod; + chunkData[12] = header.InterlaceMethod; + + this.WriteChunk(stream, PngChunkTypes.Header, chunkData); + } + + /// + /// Writes a chunk to the stream. + /// + /// The to write to. + /// The type of chunk to write. + /// The containing data. + private void WriteChunk(Stream stream, string type, byte[] data) + { + this.WriteChunk(stream, type, data, 0, data != null ? data.Length : 0); + } + + /// + /// Writes a chunk of a specified length to the stream at the given offset. + /// + /// The to write to. + /// The type of chunk to write. + /// The containing data. + /// The position to offset the data at. + /// The of the data to write. + private void WriteChunk(Stream stream, string type, byte[] data, int offset, int length) + { + WriteInteger(stream, length); + + byte[] typeArray = new byte[4]; + typeArray[0] = (byte)type[0]; + typeArray[1] = (byte)type[1]; + typeArray[2] = (byte)type[2]; + typeArray[3] = (byte)type[3]; + + stream.Write(typeArray, 0, 4); + + if (data != null) + { + stream.Write(data, offset, length); + } + + Crc32 crc32 = new Crc32(); + crc32.Update(typeArray); + + if (data != null) + { + crc32.Update(data, offset, length); + } + + WriteInteger(stream, (uint)crc32.Value); + } + } +} diff --git a/src/ImageProcessor/Formats/Png/PngHeader.cs b/src/ImageProcessor/Formats/Png/PngHeader.cs new file mode 100644 index 000000000..4c2ac2d56 --- /dev/null +++ b/src/ImageProcessor/Formats/Png/PngHeader.cs @@ -0,0 +1,67 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Represents the png header chunk. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Represents the png header chunk. + /// + public sealed class PngHeader + { + /// + /// Gets or sets the dimension in x-direction of the image in pixels. + /// + public int Width { get; set; } + + /// + /// Gets or sets the dimension in y-direction of the image in pixels. + /// + public int Height { get; set; } + + /// + /// Gets or sets the bit depth. + /// Bit depth is a single-byte integer giving the number of bits per sample + /// or per palette index (not per pixel). Valid values are 1, 2, 4, 8, and 16, + /// although not all values are allowed for all color types. + /// + public byte BitDepth { get; set; } + + /// + /// Gets or sets the color type. + /// Color type is a integer that describes the interpretation of the + /// image data. Color type codes represent sums of the following values: + /// 1 (palette used), 2 (color used), and 4 (alpha channel used). + /// + public byte ColorType { get; set; } + + /// + /// Gets or sets the compression method. + /// Indicates the method used to compress the image data. At present, + /// only compression method 0 (deflate/inflate compression with a sliding + /// window of at most 32768 bytes) is defined. + /// + public byte CompressionMethod { get; set; } + + /// + /// Gets or sets the preprocessing method. + /// Indicates the preprocessing method applied to the image + /// data before compression. At present, only filter method 0 + /// (adaptive filtering with five basic filter types) is defined. + /// + public byte FilterMethod { get; set; } + + /// + /// Gets or sets the transmission order. + /// Indicates the transmission order of the image data. + /// Two values are currently defined: 0 (no interlace) or 1 (Adam7 interlace). + /// + public byte InterlaceMethod { get; set; } + } +} diff --git a/src/ImageProcessor/Formats/Png/TrueColorReader.cs b/src/ImageProcessor/Formats/Png/TrueColorReader.cs new file mode 100644 index 000000000..bb78ecf1f --- /dev/null +++ b/src/ImageProcessor/Formats/Png/TrueColorReader.cs @@ -0,0 +1,81 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Color reader for reading true colors from a png file. Only colors +// with 24 or 32 bit (3 or 4 bytes) per pixel are supported at the moment. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor.Formats +{ + /// + /// Color reader for reading true colors from a png file. Only colors + /// with 24 or 32 bit (3 or 4 bytes) per pixel are supported at the moment. + /// + public sealed class TrueColorReader : IColorReader + { + /// + /// Whether t also read the alpha channel. + /// + private readonly bool useAlpha; + + /// + /// The current row. + /// + private int row; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true the color reader will also read the + /// alpha channel from the scanline. + public TrueColorReader(bool useAlpha) + { + this.useAlpha = useAlpha; + } + + /// + /// Reads the specified scanline. + /// + /// The scanline. + /// The pixels, where the colors should be stored in RGBA format. + /// The header, which contains information about the png file, like + /// the width of the image and the height. + public void ReadScanline(byte[] scanline, byte[] pixels, PngHeader header) + { + int offset; + + byte[] newScanline = scanline.ToArrayByBitsLength(header.BitDepth); + + if (this.useAlpha) + { + for (int x = 0; x < newScanline.Length; x += 4) + { + offset = ((this.row * header.Width) + (x >> 2)) * 4; + + pixels[offset + 0] = newScanline[x + 2]; + pixels[offset + 1] = newScanline[x + 1]; + pixels[offset + 2] = newScanline[x + 0]; + pixels[offset + 3] = newScanline[x + 3]; + } + } + else + { + for (int x = 0; x < newScanline.Length / 3; x++) + { + offset = ((this.row * header.Width) + x) * 4; + + pixels[offset + 0] = newScanline[(x * 3) + 2]; + pixels[offset + 1] = newScanline[(x * 3) + 1]; + pixels[offset + 2] = newScanline[(x * 3) + 0]; + pixels[offset + 3] = 255; + } + } + + this.row++; + } + } +} diff --git a/src/ImageProcessor/IImageBase.cs b/src/ImageProcessor/IImageBase.cs deleted file mode 100644 index 15a37b68e..000000000 --- a/src/ImageProcessor/IImageBase.cs +++ /dev/null @@ -1,78 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// Copyright © James South and contributors. -// Licensed under the Apache License, Version 2.0. -// -// -// Encapsulates all the basic properties and methods required to manipulate images. -// -// -------------------------------------------------------------------------------------------------------------------- - -namespace ImageProcessor -{ - /// - /// Encapsulates all the basic properties and methods required to manipulate images. - /// - public interface IImageBase - { - /// - /// Gets the image pixels as byte array. - /// - /// - /// The returned array has a length of Width * Height * 4 bytes - /// and stores the blue, the green, the red and the alpha value for - /// each pixel in this order. - /// - byte[] Pixels { get; } - - /// - /// Gets the width in pixels. - /// - int Width { get; } - - /// - /// Gets the height in pixels. - /// - int Height { get; } - - /// - /// Gets the pixel ratio made up of the width and height. - /// - double PixelRatio { get; } - - /// - /// Gets the representing the bounds of the image. - /// - Rectangle Bounds { get; } - - /// - /// Gets or sets the color of a pixel at the specified position. - /// - /// - /// The x-coordinate of the pixel. Must be greater - /// than zero and smaller than the width of the pixel. - /// - /// - /// The y-coordinate of the pixel. Must be greater - /// than zero and smaller than the width of the pixel. - /// - /// The at the specified position. - Color this[int x, int y] - { - get; - set; - } - - /// - /// Sets the pixel array of the image. - /// - /// - /// The new width of the image. Must be greater than zero. - /// The new height of the image. Must be greater than zero. - /// - /// The array with colors. Must be a multiple - /// of four, width and height. - /// - void SetPixels(int width, int height, byte[] pixels); - } -} diff --git a/src/ImageProcessor/Image.cs b/src/ImageProcessor/Image.cs new file mode 100644 index 000000000..6e8d116ab --- /dev/null +++ b/src/ImageProcessor/Image.cs @@ -0,0 +1,299 @@ +namespace ImageProcessor +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Text; + + using ImageProcessor.Formats; + + /// + /// Image class with stores the pixels and provides common functionality + /// such as loading images from files and streams or operation like resizing or cutting. + /// + /// The image data is alway stored in RGBA format, where the red, the blue, the + /// alpha values are simple bytes. + [DebuggerDisplay("Image: {PixelWidth}x{PixelHeight}")] + public class Image : ImageBase + { + #region Constants + + /// + /// The default density value (dots per inch) in x direction. The default value is 75 dots per inch. + /// + public const double DefaultDensityX = 75; + /// + /// The default density value (dots per inch) in y direction. The default value is 75 dots per inch. + /// + public const double DefaultDensityY = 75; + + private static readonly Lazy> defaultDecoders = new Lazy>(() => new List + { + //new BmpDecoder(), + //new JpegDecoder(), + //new PngDecoder(), + new GifDecoder(), + }); + + private static readonly Lazy> defaultEncoders = new Lazy>(() => new List + { + //new BmpEncoder(), + //new JpegEncoder(), + //new PngEncoder(), + }); + + /// + /// Gets a list of default decoders. + /// + public static IList Decoders + { + get { return defaultDecoders.Value; } + } + + /// + /// Gets a list of default encoders. + /// + public static IList Encoders + { + get { return defaultEncoders.Value; } + } + + #endregion + + #region Fields + + private readonly object _lockObject = new object(); + + #endregion + + /// + /// 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; } + + #region Properties + + /// + /// Gets or sets the resolution of the image in x direction. It is defined as + /// number of dots per inch and should be an positive value. + /// + /// The density of the image in x direction. + public double DensityX { get; set; } + + /// + /// Gets or sets the resolution of the image in y direction. It is defined as + /// number of dots per inch and should be an positive value. + /// + /// The density of the image in y direction. + public double DensityY { get; set; } + + /// + /// Gets the width of the image in inches. It is calculated as the width of the image + /// in pixels multiplied with the density. When the density is equals or less than zero + /// the default value is used. + /// + /// The width of the image in inches. + public double InchWidth + { + get + { + double densityX = DensityX; + + if (densityX <= 0) + { + densityX = DefaultDensityX; + } + + return Width / densityX; + } + } + + /// + /// Gets the height of the image in inches. It is calculated as the height of the image + /// in pixels multiplied with the density. When the density is equals or less than zero + /// the default value is used. + /// + /// The height of the image in inches. + public double InchHeight + { + get + { + double densityY = DensityY; + + if (densityY <= 0) + { + densityY = DefaultDensityY; + } + + return Height / densityY; + } + } + + /// + /// Gets a value indicating whether this image is animated. + /// + /// + /// true if this image is animated; otherwise, false. + /// + public bool IsAnimated + { + get { return _frames.Count > 0; } + } + + private IList _frames = new List(); + /// + /// Get the other frames for the animation. + /// + /// The list of frame images. + public IList Frames + { + get { return _frames; } + } + + private IList _properties = new List(); + /// + /// Gets the list of properties for storing meta information about this image. + /// + /// A list of image properties. + public IList Properties + { + get { return _properties; } + } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + /// The width of the image in pixels. + /// The height of the image in pixels. + public Image(int width, int height) + : base(width, height) + { + DensityX = DefaultDensityX; + DensityY = DefaultDensityY; + } + + /// + /// Initializes a new instance of the class + /// by making a copy from another image. + /// + /// The other image, where the clone should be made from. + /// is null + /// (Nothing in Visual Basic). + public Image(Image other) + : base(other) + { + if (other == null) throw new ArgumentNullException("Other image cannot be null."); + + foreach (ImageFrame frame in other.Frames) + { + if (frame != null) + { + Frames.Add(new ImageFrame(frame)); + } + } + + DensityX = DefaultDensityX; + DensityY = DefaultDensityY; + } + + /// + /// Initializes a new instance of the class. + /// + public Image() + { + DensityX = DefaultDensityX; + DensityY = DefaultDensityY; + } + + /// + /// Initializes a new instance of the class. + /// + public Image(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + Load(stream, Decoders); + } + + /// + /// Initializes a new instance of the class. + /// + public Image(Stream stream, params IImageDecoder[] decoders) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + Load(stream, decoders); + } + + #endregion Constructors + + #region Methods + + private void Load(Stream stream, IList decoders) + { + try + { + if (!stream.CanRead) + { + throw new NotSupportedException("Cannot read from the stream."); + } + + if (!stream.CanSeek) + { + throw new NotSupportedException("The stream does not support seeking."); + } + + if (decoders.Count > 0) + { + int maxHeaderSize = decoders.Max(x => x.HeaderSize); + if (maxHeaderSize > 0) + { + byte[] header = new byte[maxHeaderSize]; + + stream.Read(header, 0, maxHeaderSize); + stream.Position = 0; + + var decoder = decoders.FirstOrDefault(x => x.IsSupportedFileFormat(header)); + if (decoder != null) + { + decoder.Decode(this, stream); + return; + } + } + } + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.AppendLine("Image cannot be loaded. Available decoders:"); + + foreach (IImageDecoder decoder in decoders) + { + stringBuilder.AppendLine("-" + decoder); + } + + throw new NotSupportedException(stringBuilder.ToString()); + } + finally + { + stream.Dispose(); + } + } + + #endregion Methods + } +} diff --git a/src/ImageProcessor/ImageBase.cs b/src/ImageProcessor/ImageBase.cs index 616f879fe..951a6af9d 100644 --- a/src/ImageProcessor/ImageBase.cs +++ b/src/ImageProcessor/ImageBase.cs @@ -14,11 +14,28 @@ namespace ImageProcessor using System; /// - /// The base class of all images. Encapsulates all the properties and methods + /// The base class of all images. Encapsulates the basic properties and methods /// required to manipulate images. /// - public abstract class ImageBase : IImageBase + public abstract class ImageBase { + /// + /// The maximum allowable width in pixels. + /// + private static int maxWidth = int.MaxValue; + + /// + /// The maximum allowable height in pixels. + /// + private static int maxHeight = int.MaxValue; + + /// + /// Initializes a new instance of the class. + /// + protected ImageBase() + { + } + /// /// Initializes a new instance of the class. /// @@ -73,6 +90,38 @@ namespace ImageProcessor Array.Copy(pixels, this.Pixels, pixels.Length); } + /// + /// Gets or sets the maximum allowable width in pixels. + /// + public static int MaxWidth + { + get + { + return maxWidth; + } + + set + { + maxWidth = value; + } + } + + /// + /// Gets or sets the maximum allowable height in pixels. + /// + public static int MaxHeight + { + get + { + return maxHeight; + } + + set + { + maxHeight = value; + } + } + /// /// Gets the image pixels as byte array. /// diff --git a/src/ImageProcessor/ImageFrame.cs b/src/ImageProcessor/ImageFrame.cs new file mode 100644 index 000000000..b2d4f5235 --- /dev/null +++ b/src/ImageProcessor/ImageFrame.cs @@ -0,0 +1,41 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright © James South and contributors. +// Licensed under the Apache License, Version 2.0. +// +// +// Represents a single frame in a animation. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace ImageProcessor +{ + using System; + + /// + /// Represents a single frame in a animation. + /// + public class ImageFrame : ImageBase + { + /// + /// Initializes a new instance of the class. + /// + public ImageFrame() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The other to create this instance from. + /// + /// + /// Thrown if the given is null. + /// + public ImageFrame(ImageFrame other) + : base(other) + { + } + } +} diff --git a/src/ImageProcessor/ImageProcessor.csproj b/src/ImageProcessor/ImageProcessor.csproj index 02f70a50e..1553528c3 100644 --- a/src/ImageProcessor/ImageProcessor.csproj +++ b/src/ImageProcessor/ImageProcessor.csproj @@ -35,21 +35,52 @@ - - - + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + ..\..\packages\SharpZipLib.Portable.0.86.0.0002\lib\portable-net45+netcore45+wp8+win8+wpa81+MonoTouch+MonoAndroid\ICSharpCode.SharpZipLib.Portable.dll + True + + + + +