diff --git a/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs b/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs index 207ff61a77..662a41f1b2 100644 --- a/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs +++ b/src/ImageProcessorCore/Common/Helpers/ImageMaths.cs @@ -19,6 +19,19 @@ namespace ImageProcessorCore // ReSharper disable once InconsistentNaming public const float PI = 3.1415926535897931f; + /// + /// Returns how many bits are required to store the specified number of colors. + /// Performs a Log2() on the value. + /// + /// The number of colors. + /// + /// The + /// + public static int GetBitsNeededForColorDepth(int colors) + { + return (int)Math.Ceiling(Math.Log(colors, 2)); + } + /// /// Implementation of 1D Gaussian G(x) function /// diff --git a/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs b/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs index 4dd43f30f7..7a1c81c2b7 100644 --- a/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs +++ b/src/ImageProcessorCore/Formats/Gif/GifEncoder.cs @@ -7,8 +7,6 @@ namespace ImageProcessorCore.Formats { using System; using System.IO; - using System.Linq; - using System.Threading.Tasks; using ImageProcessorCore.Quantizers; @@ -49,308 +47,16 @@ namespace ImageProcessorCore.Formats } /// - public void Encode(ImageBase imageBase, Stream stream) + public void Encode(ImageBase image, Stream stream) { - Guard.NotNull(imageBase, nameof(imageBase)); - Guard.NotNull(stream, nameof(stream)); - - Image image = (Image)imageBase; - - if (this.Quantizer == null) - { - this.Quantizer = new OctreeQuantizer { Threshold = this.Threshold }; - } - - // Write the header. - // File Header signature and version. - this.WriteString(stream, GifConstants.FileType); - this.WriteString(stream, GifConstants.FileVersion); - - // Calculate the quality. - int quality = this.Quality > 0 ? this.Quality : imageBase.Quality; - quality = quality > 0 ? quality.Clamp(1, 256) : 256; - - // Get the number of bits. - int bitDepth = this.GetBitsNeededForColorDepth(quality); - - // Write the LSD and check to see if we need a global color table. - // Always true just now. - this.WriteGlobalLogicalScreenDescriptor(image, stream, bitDepth); - QuantizedImage quantized = this.WriteColorTable(imageBase, stream, quality, bitDepth); - - this.WriteGraphicalControlExtension(imageBase, stream, quantized.TransparentIndex); - this.WriteImageDescriptor(quantized, quality, stream); - - if (image.Frames.Any()) - { - this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count); - foreach (ImageFrame frame in image.Frames) - { - this.WriteGraphicalControlExtension(frame, stream, quantized.TransparentIndex); - this.WriteFrameImageDescriptor(frame, stream); - } - } - - // TODO: Write Comments extension etc - this.WriteByte(stream, GifConstants.EndIntroducer); - } - - /// - /// Writes the logical screen descriptor to the stream. - /// - /// The image to encode. - /// The stream to write to. - /// The bit depth. - /// The - private bool WriteGlobalLogicalScreenDescriptor(Image image, Stream stream, int bitDepth) - { - Guard.NotNull(image, nameof(image)); - Guard.NotNull(stream, nameof(stream)); - - GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor - { - Width = (short)image.Width, - Height = (short)image.Height, - GlobalColorTableFlag = true, - GlobalColorTableSize = bitDepth - }; - - this.WriteShort(stream, descriptor.Width); - this.WriteShort(stream, descriptor.Height); - - int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used) - bitDepth - 1 | // 2-4 : color resolution - 0x00 | // 5 : GCT sort flag = 0 - bitDepth - 1; // 6-8 : GCT size TODO: Check this. - - this.WriteByte(stream, packed); - this.WriteByte(stream, descriptor.BackgroundColorIndex); // Background Color Index - this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio. Assume 1:1 - - return descriptor.GlobalColorTableFlag; - } - - /// - /// Writes the color table to the stream. - /// - /// The to encode. - /// The stream to write to. - /// The quality (number of colors) to encode the image to. - /// The bit depth. - /// The - private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality, int bitDepth) - { - // Quantize the image returning a palette. - QuantizedImage quantizedImage = this.Quantizer.Quantize(image, quality); - - // Grab the palette and write it to the stream. - Bgra32[] palette = quantizedImage.Palette; - int pixelCount = palette.Length; - - // Get max colors for bit depth. - int colorTableLength = (int)Math.Pow(2, bitDepth) * 3; - byte[] colorTable = new byte[colorTableLength]; - - Parallel.For(0, pixelCount, - i => - { - int offset = i * 3; - Bgra32 color = palette[i]; - - colorTable[offset] = color.R; - colorTable[offset + 1] = color.G; - colorTable[offset + 2] = color.B; - }); - - stream.Write(colorTable, 0, colorTableLength); - - return quantizedImage; - } - - /// - /// Writes the graphics control extension to the stream. - /// - /// The to encode. - /// The stream to write to. - /// The index of the color in the color palette to make transparent. - private void WriteGraphicalControlExtension(ImageBase image, Stream stream, int transparencyIndex) - { - // TODO: Check transparency logic. - bool hasTransparent = transparencyIndex > -1; - DisposalMethod disposalMethod = hasTransparent - ? DisposalMethod.RestoreToBackground - : DisposalMethod.Unspecified; - - GifGraphicsControlExtension extension = new GifGraphicsControlExtension() + GifEncoderCore encoder = new GifEncoderCore { - DisposalMethod = disposalMethod, - TransparencyFlag = hasTransparent, - TransparencyIndex = transparencyIndex, - DelayTime = image.FrameDelay + Quality = this.Quality, + Quantizer = this.Quantizer, + Threshold = this.Threshold }; - this.WriteByte(stream, GifConstants.ExtensionIntroducer); - this.WriteByte(stream, GifConstants.GraphicControlLabel); - this.WriteByte(stream, 4); // Size - - int packed = 0 | // 1-3 : Reserved - (int)extension.DisposalMethod << 2 | // 4-6 : Disposal - 0 | // 7 : User input - 0 = none - (extension.TransparencyFlag ? 1 : 0); // 8: Has transparent. - - this.WriteByte(stream, packed); - this.WriteShort(stream, extension.DelayTime); - this.WriteByte(stream, extension.TransparencyIndex == -1 ? 255 : extension.TransparencyIndex); - this.WriteByte(stream, GifConstants.Terminator); - } - - /// - /// Writes the application exstension to the stream. - /// - /// The stream to write to. - /// The animated image repeat count. - /// Th number of image frames. - private void WriteApplicationExtension(Stream stream, ushort repeatCount, int frames) - { - // Application Extension Header - if (repeatCount != 1 && frames > 0) - { - // 0 means loop indefinitely. count is set as play n + 1 times. - // TODO: Check this as the correct value might be pulled from the decoder. - repeatCount = (ushort)Math.Max(0, repeatCount - 1); - this.WriteByte(stream, GifConstants.ExtensionIntroducer); // NETSCAPE2.0 - this.WriteByte(stream, GifConstants.ApplicationExtensionLabel); - this.WriteByte(stream, GifConstants.ApplicationBlockSize); - - this.WriteString(stream, GifConstants.ApplicationIdentification); - this.WriteByte(stream, 3); // Application block length - this.WriteByte(stream, 1); // Data sub-block index (always 1) - this.WriteShort(stream, repeatCount); // Repeat count for images. - - this.WriteByte(stream, GifConstants.Terminator); // Terminator - } - } - - /// - /// Writes the image descriptor to the stream. - /// - /// The containing indexed pixels. - /// The quality (number of colors) to encode the image to. - /// The stream to write to. - private void WriteImageDescriptor(QuantizedImage image, int quality, Stream stream) - { - this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c - // TODO: Can we capture this? - this.WriteShort(stream, 0); // Left position - this.WriteShort(stream, 0); // Top position - this.WriteShort(stream, image.Width); - this.WriteShort(stream, image.Height); - - // Calculate the quality. - int bitDepth = this.GetBitsNeededForColorDepth(quality); - - // No LCT use GCT. - this.WriteByte(stream, 0); - - // Write the image data. - this.WriteImageData(image, stream, bitDepth); - } - - /// - /// Writes the image descriptor to the stream. - /// - /// The to be encoded. - /// The stream to write to. - private void WriteFrameImageDescriptor(ImageBase image, Stream stream) - { - this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c - // TODO: Can we capture this? - this.WriteShort(stream, 0); // Left position - this.WriteShort(stream, 0); // Top position - this.WriteShort(stream, image.Width); - this.WriteShort(stream, image.Height); - - // Calculate the quality. - int quality = this.Quality > 0 ? this.Quality : image.Quality; - quality = quality > 0 ? quality.Clamp(1, 256) : 256; - int bitDepth = this.GetBitsNeededForColorDepth(quality); - - int packed = 0x80 | // 1: Local color table flag = 1 (LCT used) - 0x00 | // 2: Interlace flag 0 - 0x00 | // 3: Sort flag 0 - 0 | // 4-5: Reserved - bitDepth - 1; - - this.WriteByte(stream, packed); - - // Now immediately follow with the color table. - QuantizedImage quantized = this.WriteColorTable(image, stream, quality, bitDepth); - this.WriteImageData(quantized, stream, bitDepth); - } - - /// - /// Writes the image pixel data to the stream. - /// - /// The containing indexed pixels. - /// The stream to write to. - /// The bit depth of the image. - private void WriteImageData(QuantizedImage image, Stream stream, int bitDepth) - { - byte[] indexedPixels = image.Pixels; - - LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)bitDepth); - encoder.Encode(stream); - - this.WriteByte(stream, GifConstants.Terminator); - } - - /// - /// Writes a short to the given stream. - /// - /// The containing image data. - /// The value to write. - private void WriteShort(Stream stream, int value) - { - // Leave only one significant byte. - stream.WriteByte(Convert.ToByte(value & 0xff)); - stream.WriteByte(Convert.ToByte((value >> 8) & 0xff)); - } - - /// - /// Writes a byte to the given stream. - /// - /// The containing image data. - /// The value to write. - private void WriteByte(Stream stream, int value) - { - stream.WriteByte(Convert.ToByte(value)); - } - - /// - /// Writes a string to the given stream. - /// - /// The containing image data. - /// The value to write. - private void WriteString(Stream stream, string value) - { - char[] chars = value.ToCharArray(); - foreach (char c in chars) - { - stream.WriteByte((byte)c); - } - } - - /// - /// Returns how many bits are required to store the specified number of colors. - /// Performs a Log2() on the value. - /// - /// The number of colors. - /// - /// The - /// - private int GetBitsNeededForColorDepth(int colors) - { - return (int)Math.Ceiling(Math.Log(colors, 2)); + encoder.Encode(image, stream); } } } diff --git a/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs b/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs new file mode 100644 index 0000000000..19e2f1df99 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Gif/GifEncoderCore.cs @@ -0,0 +1,343 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + + using ImageProcessorCore.Quantizers; + + /// + /// Performs the gif encoding operation. + /// + internal sealed class GifEncoderCore + { + /// + /// The number of bits requires to store the image palette. + /// + private int bitDepth; + + /// + /// Gets or sets the quality of output for images. + /// + /// For gifs the value ranges from 1 to 256. + public int Quality { get; set; } + + /// + /// Gets or sets the transparency threshold. + /// + public byte Threshold { get; set; } = 128; + + /// + /// The quantizer for reducing the color count. + /// + public IQuantizer Quantizer { get; set; } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The to encode from. + /// The to encode the image data to. + public void Encode(ImageBase imageBase, Stream stream) + { + Guard.NotNull(imageBase, nameof(imageBase)); + Guard.NotNull(stream, nameof(stream)); + + Image image = (Image)imageBase; + + if (this.Quantizer == null) + { + this.Quantizer = new OctreeQuantizer { Threshold = this.Threshold }; + } + + // Write the header. + // File Header signature and version. + this.WriteString(stream, GifConstants.FileType); + this.WriteString(stream, GifConstants.FileVersion); + + // Ensure that quality can be set but has a fallback. + int quality = this.Quality > 0 ? this.Quality : imageBase.Quality; + quality = quality > 0 ? quality.Clamp(1, 256) : 256; + + // Get the number of bits. + this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quality); + + // Write the LSD and check to see if we need a global color table. + // Always true just now. + this.WriteGlobalLogicalScreenDescriptor(image, stream); + QuantizedImage quantized = this.WriteColorTable(imageBase, stream, quality); + + this.WriteGraphicalControlExtension(imageBase, stream, quantized.TransparentIndex); + this.WriteImageDescriptor(quantized, quality, stream); + + if (image.Frames.Any()) + { + this.WriteApplicationExtension(stream, image.RepeatCount, image.Frames.Count); + foreach (ImageFrame frame in image.Frames) + { + this.WriteGraphicalControlExtension(frame, stream, quantized.TransparentIndex); + this.WriteFrameImageDescriptor(frame, stream); + } + } + + // TODO: Write Comments extension etc + this.WriteByte(stream, GifConstants.EndIntroducer); + } + + /// + /// Writes the logical screen descriptor to the stream. + /// + /// The image to encode. + /// The stream to write to. + /// The + private bool WriteGlobalLogicalScreenDescriptor(Image image, Stream stream) + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + GifLogicalScreenDescriptor descriptor = new GifLogicalScreenDescriptor + { + Width = (short)image.Width, + Height = (short)image.Height, + GlobalColorTableFlag = true, + GlobalColorTableSize = this.bitDepth + }; + + this.WriteShort(stream, descriptor.Width); + this.WriteShort(stream, descriptor.Height); + + int packed = 0x80 | // 1 : Global color table flag = 1 (GCT used) + this.bitDepth - 1 | // 2-4 : color resolution + 0x00 | // 5 : GCT sort flag = 0 + this.bitDepth - 1; // 6-8 : GCT size TODO: Check this. + + this.WriteByte(stream, packed); + this.WriteByte(stream, descriptor.BackgroundColorIndex); // Background Color Index + this.WriteByte(stream, descriptor.PixelAspectRatio); // Pixel aspect ratio. Assume 1:1 + + return descriptor.GlobalColorTableFlag; + } + + /// + /// Writes the color table to the stream. + /// + /// The to encode. + /// The stream to write to. + /// The quality (number of colors) to encode the image to. + /// The + private QuantizedImage WriteColorTable(ImageBase image, Stream stream, int quality) + { + // Quantize the image returning a palette. + QuantizedImage quantizedImage = this.Quantizer.Quantize(image, quality); + + // Grab the palette and write it to the stream. + Bgra32[] palette = quantizedImage.Palette; + int pixelCount = palette.Length; + + // Get max colors for bit depth. + int colorTableLength = (int)Math.Pow(2, this.bitDepth) * 3; + byte[] colorTable = new byte[colorTableLength]; + + Parallel.For(0, pixelCount, + i => + { + int offset = i * 3; + Bgra32 color = palette[i]; + + colorTable[offset] = color.R; + colorTable[offset + 1] = color.G; + colorTable[offset + 2] = color.B; + }); + + stream.Write(colorTable, 0, colorTableLength); + + return quantizedImage; + } + + /// + /// Writes the graphics control extension to the stream. + /// + /// The to encode. + /// The stream to write to. + /// The index of the color in the color palette to make transparent. + private void WriteGraphicalControlExtension(ImageBase image, Stream stream, int transparencyIndex) + { + // TODO: Check transparency logic. + bool hasTransparent = transparencyIndex > -1; + DisposalMethod disposalMethod = hasTransparent + ? DisposalMethod.RestoreToBackground + : DisposalMethod.Unspecified; + + GifGraphicsControlExtension extension = new GifGraphicsControlExtension() + { + DisposalMethod = disposalMethod, + TransparencyFlag = hasTransparent, + TransparencyIndex = transparencyIndex, + DelayTime = image.FrameDelay + }; + + this.WriteByte(stream, GifConstants.ExtensionIntroducer); + this.WriteByte(stream, GifConstants.GraphicControlLabel); + this.WriteByte(stream, 4); // Size + + int packed = 0 | // 1-3 : Reserved + (int)extension.DisposalMethod << 2 | // 4-6 : Disposal + 0 | // 7 : User input - 0 = none + (extension.TransparencyFlag ? 1 : 0); // 8: Has transparent. + + this.WriteByte(stream, packed); + this.WriteShort(stream, extension.DelayTime); + this.WriteByte(stream, extension.TransparencyIndex == -1 ? 255 : extension.TransparencyIndex); + this.WriteByte(stream, GifConstants.Terminator); + } + + /// + /// Writes the application exstension to the stream. + /// + /// The stream to write to. + /// The animated image repeat count. + /// Th number of image frames. + private void WriteApplicationExtension(Stream stream, ushort repeatCount, int frames) + { + // Application Extension Header + if (repeatCount != 1 && frames > 0) + { + // 0 means loop indefinitely. count is set as play n + 1 times. + // TODO: Check this as the correct value might be pulled from the decoder. + repeatCount = (ushort)Math.Max(0, repeatCount - 1); + this.WriteByte(stream, GifConstants.ExtensionIntroducer); // NETSCAPE2.0 + this.WriteByte(stream, GifConstants.ApplicationExtensionLabel); + this.WriteByte(stream, GifConstants.ApplicationBlockSize); + + this.WriteString(stream, GifConstants.ApplicationIdentification); + this.WriteByte(stream, 3); // Application block length + this.WriteByte(stream, 1); // Data sub-block index (always 1) + this.WriteShort(stream, repeatCount); // Repeat count for images. + + this.WriteByte(stream, GifConstants.Terminator); // Terminator + } + } + + /// + /// Writes the image descriptor to the stream. + /// + /// The containing indexed pixels. + /// The quality (number of colors) to encode the image to. + /// The stream to write to. + private void WriteImageDescriptor(QuantizedImage image, int quality, Stream stream) + { + this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c + // TODO: Can we capture this? + this.WriteShort(stream, 0); // Left position + this.WriteShort(stream, 0); // Top position + this.WriteShort(stream, image.Width); + this.WriteShort(stream, image.Height); + + // No LCT use GCT. + this.WriteByte(stream, 0); + + // Write the image data. + this.WriteImageData(image, stream); + } + + /// + /// Writes the image descriptor to the stream. + /// + /// The to be encoded. + /// The stream to write to. + private void WriteFrameImageDescriptor(ImageBase image, Stream stream) + { + this.WriteByte(stream, GifConstants.ImageDescriptorLabel); // 2c + // TODO: Can we capture this? + this.WriteShort(stream, 0); // Left position + this.WriteShort(stream, 0); // Top position + this.WriteShort(stream, image.Width); + this.WriteShort(stream, image.Height); + + // Calculate the quality. + int quality = this.Quality > 0 ? this.Quality : image.Quality; + quality = quality > 0 ? quality.Clamp(1, 256) : 256; + + int packed = 0x80 | // 1: Local color table flag = 1 (LCT used) + 0x00 | // 2: Interlace flag 0 + 0x00 | // 3: Sort flag 0 + 0 | // 4-5: Reserved + this.bitDepth - 1; + + this.WriteByte(stream, packed); + + // Now immediately follow with the color table. + QuantizedImage quantized = this.WriteColorTable(image, stream, quality); + this.WriteImageData(quantized, stream); + } + + /// + /// Writes the image pixel data to the stream. + /// + /// The containing indexed pixels. + /// The stream to write to. + private void WriteImageData(QuantizedImage image, Stream stream) + { + byte[] indexedPixels = image.Pixels; + + LzwEncoder encoder = new LzwEncoder(indexedPixels, (byte)this.bitDepth); + encoder.Encode(stream); + + this.WriteByte(stream, GifConstants.Terminator); + } + + /// + /// Writes a short to the given stream. + /// + /// The containing image data. + /// The value to write. + private void WriteShort(Stream stream, int value) + { + // Leave only one significant byte. + stream.WriteByte(Convert.ToByte(value & 0xff)); + stream.WriteByte(Convert.ToByte((value >> 8) & 0xff)); + } + + /// + /// Writes a byte to the given stream. + /// + /// The containing image data. + /// The value to write. + private void WriteByte(Stream stream, int value) + { + stream.WriteByte(Convert.ToByte(value)); + } + + /// + /// Writes a string to the given stream. + /// + /// The containing image data. + /// The value to write. + private void WriteString(Stream stream, string value) + { + char[] chars = value.ToCharArray(); + foreach (char c in chars) + { + stream.WriteByte((byte)c); + } + } + + /// + /// Returns how many bits are required to store the specified number of colors. + /// Performs a Log2() on the value. + /// + /// The number of colors. + /// + /// The + /// + private int GetBitsNeededForColorDepth(int colors) + { + return (int)Math.Ceiling(Math.Log(colors, 2)); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs index c5c03991dd..89e3883b35 100644 --- a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -15,7 +15,7 @@ namespace ImageProcessorCore.Formats /// Performs the png encoding operation. /// TODO: Perf. There's lots of array parsing going on here. This should be unmanaged. /// - internal class PngEncoderCore + internal sealed class PngEncoderCore { /// /// The maximum block size, defaults at 64k for uncompressed blocks. @@ -89,14 +89,12 @@ namespace ImageProcessorCore.Formats 0, 8); - if (image.Quality > 0) - { - - this.Quality = image.Quality.Clamp(1, int.MaxValue); - } + // Ensure that quality can be set but has a fallback. + int quality = this.Quality > 0 ? this.Quality : image.Quality; + this.Quality = quality > 0 ? quality.Clamp(1, int.MaxValue) : int.MaxValue; this.bitDepth = this.Quality <= 256 - ? (byte)(this.GetBitsNeededForColorDepth(this.Quality).Clamp(1, 8)) + ? (byte)(ImageMaths.GetBitsNeededForColorDepth(this.Quality).Clamp(1, 8)) : (byte)8; // Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk @@ -463,18 +461,5 @@ namespace ImageProcessorCore.Formats WriteInteger(stream, (uint)crc32.Value); } - - /// - /// Returns how many bits are required to store the specified number of colors. - /// Performs a Log2() on the value. - /// - /// The number of colors. - /// - /// The - /// - private int GetBitsNeededForColorDepth(int colors) - { - return (int)Math.Ceiling(Math.Log(colors, 2)); - } } }