diff --git a/src/ImageProcessorCore/Bootstrapper.cs b/src/ImageProcessorCore/Bootstrapper.cs index 0f47526b3..a1e91e386 100644 --- a/src/ImageProcessorCore/Bootstrapper.cs +++ b/src/ImageProcessorCore/Bootstrapper.cs @@ -39,7 +39,7 @@ namespace ImageProcessorCore { new BmpFormat(), //new JpegFormat(), - //new PngFormat(), + new PngFormat(), //new GifFormat() }; diff --git a/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs b/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs index 916bddce7..48ee205f9 100644 --- a/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs @@ -70,11 +70,11 @@ namespace ImageProcessorCore.Formats } /// - /// Decodes the image from the specified stream to the . + /// Decodes the image from the specified stream to the . /// - /// The to decode to. + /// The to decode to. /// The containing image data. - public void Decode(Image image, Stream stream) + public void Decode(Image image, Stream stream) where T : IPackedVector, new() where TP : struct { diff --git a/src/ImageProcessorCore/Formats/Png/GrayscaleReader.cs b/src/ImageProcessorCore/Formats/Png/GrayscaleReader.cs new file mode 100644 index 000000000..f8884ae43 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/GrayscaleReader.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Color reader for reading grayscale colors from a png file. + /// + internal 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; + } + + /// + public void ReadScanline(byte[] scanline, T[] pixels, PngHeader header) + where T : IPackedVector, new() + where TP : struct + { + int offset; + + byte[] newScanline = scanline.ToArrayByBitsLength(header.BitDepth); + + // Stored in r-> g-> b-> a order. + if (this.useAlpha) + { + for (int x = 0; x < header.Width / 2; x++) + { + offset = (this.row * header.Width) + x; + + byte rgb = newScanline[x * 2]; + byte a = newScanline[(x * 2) + 1]; + + T color = default(T); + color.PackBytes(rgb, rgb, rgb, a); + pixels[offset] = color; + } + } + else + { + for (int x = 0; x < header.Width; x++) + { + offset = (this.row * header.Width) + x; + byte rgb = newScanline[x]; + + T color = default(T); + color.PackBytes(rgb, rgb, rgb, 255); + + pixels[offset] = color; + } + } + + this.row++; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/IColorReader.cs b/src/ImageProcessorCore/Formats/Png/IColorReader.cs new file mode 100644 index 000000000..c28dd3c05 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/IColorReader.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.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 pixel format. + /// The packed format. long, float. + /// The scanline. + /// The pixels to read the image row to. + /// + /// The header, which contains information about the png file, like + /// the width of the image and the height. + /// + void ReadScanline(byte[] scanline, T[] pixels, PngHeader header) + where T : IPackedVector, new() + where TP : struct; + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PaletteIndexReader.cs b/src/ImageProcessorCore/Formats/Png/PaletteIndexReader.cs new file mode 100644 index 000000000..b7e1f2cfb --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PaletteIndexReader.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// A color reader for reading palette indices from the png file. + /// + internal 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; + } + + /// + public void ReadScanline(byte[] scanline, T[] pixels, PngHeader header) + where T : IPackedVector, new() + where TP : struct + { + 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; + int pixelOffset = index * 3; + + byte r = this.palette[pixelOffset]; + byte g = this.palette[pixelOffset + 1]; + byte b = this.palette[pixelOffset + 2]; + byte a = this.paletteAlpha.Length > index + ? this.paletteAlpha[index] + : (byte)255; + + T color = default(T); + color.PackBytes(r, g, b, a); + pixels[offset] = color; + } + } + else + { + for (int i = 0; i < header.Width; i++) + { + index = newScanline[i]; + + offset = (this.row * header.Width) + i; + int pixelOffset = index * 3; + + byte r = this.palette[pixelOffset]; + byte g = this.palette[pixelOffset + 1]; + byte b = this.palette[pixelOffset + 2]; + + T color = default(T); + color.PackBytes(r, g, b, 255); + pixels[offset] = color; + } + } + + this.row++; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngChunk.cs b/src/ImageProcessorCore/Formats/Png/PngChunk.cs new file mode 100644 index 000000000..31ea703a6 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngChunk.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.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/ImageProcessorCore/Formats/Png/PngChunkTypes.cs b/src/ImageProcessorCore/Formats/Png/PngChunkTypes.cs new file mode 100644 index 000000000..5c35b3d4d --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngChunkTypes.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.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/ImageProcessorCore/Formats/Png/PngColorTypeInformation.cs b/src/ImageProcessorCore/Formats/Png/PngColorTypeInformation.cs new file mode 100644 index 000000000..9909cf47c --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngColorTypeInformation.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.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/ImageProcessorCore/Formats/Png/PngDecoder.cs b/src/ImageProcessorCore/Formats/Png/PngDecoder.cs new file mode 100644 index 000000000..d77e46e1e --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngDecoder.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + /// + /// Encoder for generating an image out of a png encoded 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 => 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) + where T : IPackedVector, new() + where TP : struct + { + new PngDecoderCore().Decode(image, stream); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs new file mode 100644 index 000000000..3cf7fabf4 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs @@ -0,0 +1,543 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + + /// + /// Performs the png decoding operation. + /// + internal class PngDecoderCore + { + /// + /// The dictionary of available color types. + /// + private static readonly Dictionary ColorTypes + = new Dictionary(); + + /// + /// The image to decode. + /// + //private IImage currentImage; + + /// + /// The stream to decode from. + /// + 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 pixel format. + /// The packed format. long, float. + /// 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) + where T : IPackedVector, new() + where TP : struct + { + Image 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(currentImage, 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(currentImage, currentChunk.Data); + } + else if (currentChunk.Type == PngChunkTypes.End) + { + isEndChunkReached = true; + } + } + + if (this.header.Width > image.MaxWidth || this.header.Height > image.MaxHeight) + { + throw new ArgumentOutOfRangeException( + $"The input png '{this.header.Width}x{this.header.Height}' is bigger than the " + + $"max allowed size '{image.MaxWidth}x{image.MaxHeight}'"); + } + + T[] pixels = new T[this.header.Width * this.header.Height]; + + 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); + } + } + + /// + /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses + /// as predictor the neighboring pixel closest to the computed value. + /// + /// The left neighbour pixel. + /// The above neighbour pixel. + /// The upper left neighbour pixel. + /// + /// The . + /// + private static byte PaethPredicator(byte left, byte above, byte upperLeft) + { + byte predicator; + + int p = left + above - upperLeft; + int pa = Math.Abs(p - left); + int pb = Math.Abs(p - above); + int pc = Math.Abs(p - upperLeft); + + if (pa <= pb && pa <= pc) + { + predicator = left; + } + else if (pb <= pc) + { + predicator = above; + } + else + { + predicator = upperLeft; + } + + return predicator; + } + + /// + /// Reads the data chunk containing physical dimension data. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to read to. + /// The data containing physical data. + private void ReadPhysicalChunk(Image image, byte[] data) + where T : IPackedVector, new() + where TP : struct + { + Array.Reverse(data, 0, 4); + Array.Reverse(data, 4, 4); + + // 39.3700787 = inches in a meter. + image.HorizontalResolution = BitConverter.ToInt32(data, 0) / 39.3700787d; + image.VerticalResolution = 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, T[] pixels, IColorReader colorReader, PngColorTypeInformation colorTypeInformation) + where T : IPackedVector, new() + where TP : struct + { + 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 (ZlibInflateStream compressedStream = new ZlibInflateStream(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] + PaethPredicator(a, b, c)); + } + + column++; + + if (column == scanlineLength) + { + colorReader.ReadScanline(currentScanline, pixels, this.header); + column = -1; + + this.Swap(ref currentScanline, ref lastScanline); + } + } + } + } + } + + /// + /// Reads a text chunk containing image properties from the data. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to decode to. + /// The containing data. + private void ReadTextChunk(Image image, byte[] data) + where T : IPackedVector, new() + where TP : struct + { + int zeroIndex = 0; + + for (int i = 0; i < data.Length; i++) + { + if (data[i] == 0) + { + zeroIndex = i; + break; + } + } + + string name = Encoding.Unicode.GetString(data, 0, zeroIndex); + string value = Encoding.Unicode.GetString(data, zeroIndex + 1, data.Length - zeroIndex - 1); + + image.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 >= 1 && numBytes <= 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. + private void ReadChunkData(PngChunk chunk) + { + 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 >= 1 && numBytes <= 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 >= 1 && numBytes <= 3) + { + throw new ImageFormatException("Image stream is not valid!"); + } + + Array.Reverse(lengthBuffer); + + chunk.Length = BitConverter.ToInt32(lengthBuffer, 0); + + return numBytes; + } + + /// + /// Swaps two references. + /// + /// The type of the references to swap. + /// The first reference. + /// The second reference. + private void Swap(ref TRef lhs, ref TRef rhs) + where TRef : class + { + TRef tmp = lhs; + + lhs = rhs; + rhs = tmp; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoder.cs b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs new file mode 100644 index 000000000..5e34bfa88 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngEncoder.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + using System.IO; + + using ImageProcessorCore.Quantizers; + + /// + /// Image encoder for writing image data to a stream in png format. + /// + public class PngEncoder : IImageEncoder + { + /// + /// Gets or sets the quality of output for images. + /// + public int Quality { get; set; } + + /// + public string MimeType => "image/png"; + + /// + public string Extension => "png"; + + /// + /// The compression level 1-9. + /// Defaults to 6. + /// + public int CompressionLevel { get; set; } = 6; + + /// + /// 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 float Gamma { get; set; } = 2.2F; + + /// + /// The quantizer for reducing the color count. + /// + public IQuantizer Quantizer { get; set; } + + /// + /// Gets or sets the transparency threshold. + /// + public byte Threshold { get; set; } = 128; + + /// + /// Gets or sets a value indicating whether this instance should write + /// gamma information to the stream. The default value is false. + /// + public bool WriteGamma { get; set; } + + /// + public bool IsSupportedFileExtension(string extension) + { + Guard.NotNullOrEmpty(extension, nameof(extension)); + + extension = extension.StartsWith(".") ? extension.Substring(1) : extension; + + return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase); + } + + /// + public void Encode(ImageBase image, Stream stream) + where T : IPackedVector, new() + where TP : struct + { + PngEncoderCore encoder = new PngEncoderCore + { + CompressionLevel = this.CompressionLevel, + Gamma = this.Gamma, + Quality = this.Quality, + Quantizer = this.Quantizer, + WriteGamma = this.WriteGamma, + Threshold = this.Threshold + }; + + encoder.Encode(image, stream); + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs new file mode 100644 index 000000000..15ed7793a --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs @@ -0,0 +1,504 @@ +// +// 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.Threading.Tasks; + + using ImageProcessorCore.Quantizers; + + /// + /// Performs the png encoding operation. + /// TODO: Perf. There's lots of array parsing going on here. This should be unmanaged. + /// + internal sealed class PngEncoderCore + { + /// + /// The maximum block size, defaults at 64k for uncompressed blocks. + /// + private const int MaxBlockSize = 65535; + + /// + /// The number of bits required to encode the colors in the png. + /// + private byte bitDepth; + + /// + /// The quantized image result. + /// + //private QuantizedImage quantized; + + /// + /// Gets or sets the quality of output for images. + /// + public int Quality { get; set; } + + /// + /// The compression level 1-9. + /// Defaults to 6. + /// + public int CompressionLevel { get; set; } = 6; + + /// + /// Gets or sets a value indicating whether this instance should write + /// gamma information to the stream. The default value is false. + /// + public bool WriteGamma { 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 float Gamma { get; set; } = 2.2F; + + /// + /// The quantizer for reducing the color count. + /// + public IQuantizer Quantizer { get; set; } + + /// + /// Gets or sets the transparency threshold. + /// + public byte Threshold { get; set; } = 128; + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The packed format. long, float. + /// The to encode from. + /// The to encode the image data to. + public void Encode(ImageBase image, Stream stream) + where T : IPackedVector, new() + where TP : struct + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + // Write the png header. + stream.Write( + new byte[] + { + 0x89, // Set the high bit. + 0x50, // P + 0x4E, // N + 0x47, // G + 0x0D, // Line ending CRLF + 0x0A, // Line ending CRLF + 0x1A, // EOF + 0x0A // LF + }, + 0, + 8); + + // 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)(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 + if (this.bitDepth == 3) + { + this.bitDepth = 4; + } + else if (this.bitDepth >= 5 || this.bitDepth <= 7) + { + this.bitDepth = 8; + } + + // TODO: Add more color options here. + PngHeader header = new PngHeader + { + Width = image.Width, + Height = image.Height, + ColorType = (byte)(this.Quality <= 256 ? 3 : 6), // 3 = indexed, 6= Each pixel is an R,G,B triple, followed by an alpha sample. + BitDepth = this.bitDepth, + FilterMethod = 0, // None + CompressionMethod = 0, + InterlaceMethod = 0 + }; + + this.WriteHeaderChunk(stream, header); + QuantizedImage quantized = this.WritePaletteChunk(stream, header, image); + this.WritePhysicalChunk(stream, image); + this.WriteGammaChunk(stream); + + using (IPixelAccessor pixels = image.Lock()) + { + this.WriteDataChunks(stream, pixels, quantized); + } + + 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 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 the palette chunk to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The containing image data. + /// The . + /// The image to encode. + private QuantizedImage WritePaletteChunk(Stream stream, PngHeader header, ImageBase image) + where T : IPackedVector, new() + where TP : struct + { + if (this.Quality > 256) + { + return null; + } + + if (this.Quantizer == null) + { + this.Quantizer = new WuQuantizer { Threshold = this.Threshold }; + } + + // Quantize the image returning a palette. + QuantizedImage quantized = Quantizer.Quantize(image, this.Quality); + + // Grab the palette and write it to the stream. + T[] palette = quantized.Palette; + int pixelCount = palette.Length; + + // Get max colors for bit depth. + int colorTableLength = (int)Math.Pow(2, header.BitDepth) * 3; + byte[] colorTable = new byte[colorTableLength]; + + Parallel.For(0, pixelCount, + i => + { + int offset = i * 3; + byte[] color = palette[i].ToBytes(); + + // Expected format r->g->b + colorTable[offset] = color[0]; + colorTable[offset + 1] = color[1]; + colorTable[offset + 2] = color[2]; + }); + + this.WriteChunk(stream, PngChunkTypes.Palette, colorTable); + + // Write the transparency data + if (quantized.TransparentIndex > -1) + { + this.WriteChunk(stream, PngChunkTypes.PaletteAlpha, new[] { (byte)quantized.TransparentIndex }); + } + + return quantized; + } + + /// + /// Writes the physical dimension information to the stream. + /// + /// The pixel format. + /// The packed format. long, float. + /// The containing image data. + /// The image base. + private void WritePhysicalChunk(Stream stream, ImageBase imageBase) + where T : IPackedVector, new() + where TP : struct + { + Image image = imageBase as Image; + if (image != null && image.HorizontalResolution > 0 && image.VerticalResolution > 0) + { + // 39.3700787 = inches in a meter. + int dpmX = (int)Math.Round(image.HorizontalResolution * 39.3700787D); + int dpmY = (int)Math.Round(image.VerticalResolution * 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.WriteGamma) + { + 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 pixel format. + /// The packed format. long, float. + /// The containing image data. + /// The image pixels. + /// The quantized image. + private void WriteDataChunks(Stream stream, IPixelAccessor pixels, QuantizedImage quantized) + where T : IPackedVector, new() + where TP : struct + { + byte[] data; + int imageWidth = pixels.Width; + int imageHeight = pixels.Height; + + // Indexed image. + if (this.Quality <= 256) + { + int rowLength = imageWidth + 1; + data = new byte[rowLength * imageHeight]; + + Parallel.For( + 0, + imageHeight, + //Bootstrapper.Instance.ParallelOptions, + y => + { + int dataOffset = (y * rowLength); + byte compression = 0; + if (y > 0) + { + compression = 2; + } + data[dataOffset++] = compression; + for (int x = 0; x < imageWidth; x++) + { + data[dataOffset++] = quantized.Pixels[(y * imageWidth) + x]; + if (y > 0) + { + data[dataOffset - 1] -= quantized.Pixels[((y - 1) * imageWidth) + x]; + } + } + }); + } + else + { + // TrueColor image. + data = new byte[(imageWidth * imageHeight * 4) + pixels.Height]; + + int rowLength = (imageWidth * 4) + 1; + + Parallel.For( + 0, + imageHeight, + Bootstrapper.Instance.ParallelOptions, + y => + { + byte compression = 0; + if (y > 0) + { + compression = 2; + } + + data[y * rowLength] = compression; + + for (int x = 0; x < imageWidth; x++) + { + byte[] color = pixels[x, y].ToBytes(); + + // Calculate the offset for the new array. + int dataOffset = (y * rowLength) + (x * 4) + 1; + + // Expected format + data[dataOffset] = color[0]; + data[dataOffset + 1] = color[1]; + data[dataOffset + 2] = color[2]; + data[dataOffset + 3] = color[3]; + + if (y > 0) + { + color = pixels[x, y - 1].ToBytes(); + + data[dataOffset] -= color[0]; + data[dataOffset + 1] -= color[1]; + data[dataOffset + 2] -= color[2]; + data[dataOffset + 3] -= color[3]; + } + } + }); + } + + byte[] buffer; + int bufferLength; + + MemoryStream memoryStream = null; + try + { + memoryStream = new MemoryStream(); + + using (ZlibDeflateStream deflateStream = new ZlibDeflateStream(memoryStream, this.CompressionLevel)) + { + deflateStream.Write(data, 0, data.Length); + } + + bufferLength = (int)memoryStream.Length; + buffer = memoryStream.ToArray(); + } + finally + { + 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 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?.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/ImageProcessorCore/Formats/Png/PngFormat.cs b/src/ImageProcessorCore/Formats/Png/PngFormat.cs new file mode 100644 index 000000000..38a0a7c38 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngFormat.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Encapsulates the means to encode and decode png images. + /// + public class PngFormat : IImageFormat + { + /// + public IImageDecoder Decoder => new PngDecoder(); + + /// + public IImageEncoder Encoder => new PngEncoder(); + } +} diff --git a/src/ImageProcessorCore/Formats/Png/PngHeader.cs b/src/ImageProcessorCore/Formats/Png/PngHeader.cs new file mode 100644 index 000000000..dfa30794a --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/PngHeader.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.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/ImageProcessorCore/Formats/Png/README.md b/src/ImageProcessorCore/Formats/Png/README.md new file mode 100644 index 000000000..8ade37956 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/README.md @@ -0,0 +1,6 @@ +Encoder/Decoder adapted from: + +https://github.com/yufeih/Nine.Imaging/ +https://imagetools.codeplex.com/ +https://github.com/leonbloy/pngcs + diff --git a/src/ImageProcessorCore/Formats/Png/TrueColorReader.cs b/src/ImageProcessorCore/Formats/Png/TrueColorReader.cs new file mode 100644 index 000000000..0fd135c4a --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/TrueColorReader.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.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. + /// + internal 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; + } + + /// + public void ReadScanline(byte[] scanline, T[] pixels, PngHeader header) + where T : IPackedVector, new() + where TP : struct + { + 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); + + // We want to convert to premultiplied alpha here. + byte r = newScanline[x]; + byte g = newScanline[x + 1]; + byte b = newScanline[x + 2]; + byte a = newScanline[x + 3]; + + T color = default(T); + color.PackBytes(r, g, b, a); + + pixels[offset] = color; + } + } + else + { + for (int x = 0; x < newScanline.Length / 3; x++) + { + offset = (this.row * header.Width) + x; + int pixelOffset = x * 3; + + byte r = newScanline[pixelOffset]; + byte g = newScanline[pixelOffset + 1]; + byte b = newScanline[pixelOffset + 2]; + + T color = default(T); + color.PackBytes(r, g, b, 255); + pixels[offset] = color; + } + } + + this.row++; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Zlib/Adler32.cs b/src/ImageProcessorCore/Formats/Png/Zlib/Adler32.cs new file mode 100644 index 000000000..f58ec34c2 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Zlib/Adler32.cs @@ -0,0 +1,174 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + + /// + /// Computes Adler32 checksum for a stream of data. An Adler32 + /// checksum is not as reliable as a CRC32 checksum, but a lot faster to + /// compute. + /// + /// + /// The specification for Adler32 may be found in RFC 1950. + /// ZLIB Compressed Data Format Specification version 3.3) + /// + /// + /// From that document: + /// + /// "ADLER32 (Adler-32 checksum) + /// This contains a checksum value of the uncompressed data + /// (excluding any dictionary data) computed according to Adler-32 + /// algorithm. This algorithm is a 32-bit extension and improvement + /// of the Fletcher algorithm, used in the ITU-T X.224 / ISO 8073 + /// standard. + /// + /// Adler-32 is composed of two sums accumulated per byte: s1 is + /// the sum of all bytes, s2 is the sum of all s1 values. Both sums + /// are done modulo 65521. s1 is initialized to 1, s2 to zero. The + /// Adler-32 checksum is stored as s2*65536 + s1 in most- + /// significant-byte first (network) order." + /// + /// "8.2. The Adler-32 algorithm + /// + /// The Adler-32 algorithm is much faster than the CRC32 algorithm yet + /// still provides an extremely low probability of undetected errors. + /// + /// The modulo on unsigned long accumulators can be delayed for 5552 + /// bytes, so the modulo operation time is negligible. If the bytes + /// are a, b, c, the second sum is 3a + 2b + c + 3, and so is position + /// and order sensitive, unlike the first sum, which is just a + /// checksum. That 65521 is prime is important to avoid a possible + /// large class of two-byte errors that leave the check unchanged. + /// (The Fletcher checksum uses 255, which is not prime and which also + /// makes the Fletcher check insensitive to single byte changes 0 - + /// 255.) + /// + /// The sum s1 is initialized to 1 instead of zero to make the length + /// of the sequence part of s2, so that the length does not have to be + /// checked separately. (Any sequence of zeroes has a Fletcher + /// checksum of zero.)" + /// + /// + /// + internal sealed class Adler32 : IChecksum + { + /// + /// largest prime smaller than 65536 + /// + private const uint Base = 65521; + + /// + /// The checksum calculated to far. + /// + private uint checksum; + + /// + /// Initializes a new instance of the class. + /// The checksum starts off with a value of 1. + /// + public Adler32() + { + this.Reset(); + } + + /// + public long Value => this.checksum; + + /// + public void Reset() + { + this.checksum = 1; + } + + /// + /// Updates the checksum with a byte value. + /// + /// + /// The data value to add. The high byte of the int is ignored. + /// + public void Update(int value) + { + // We could make a length 1 byte array and call update again, but I + // would rather not have that overhead + uint s1 = this.checksum & 0xFFFF; + uint s2 = this.checksum >> 16; + + s1 = (s1 + ((uint)value & 0xFF)) % Base; + s2 = (s1 + s2) % Base; + + this.checksum = (s2 << 16) + s1; + } + + /// + public void Update(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + this.Update(buffer, 0, buffer.Length); + } + + /// + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "cannot be negative"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "cannot be negative"); + } + + if (offset >= buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), "not a valid index into buffer"); + } + + if (offset + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count), "exceeds buffer size"); + } + + // (By Per Bothner) + uint s1 = this.checksum & 0xFFFF; + uint s2 = this.checksum >> 16; + + while (count > 0) + { + // We can defer the modulo operation: + // s1 maximally grows from 65521 to 65521 + 255 * 3800 + // s2 maximally grows by 3800 * median(s1) = 2090079800 < 2^31 + int n = 3800; + if (n > count) + { + n = count; + } + + count -= n; + while (--n >= 0) + { + s1 = s1 + (uint)(buffer[offset++] & 0xff); + s2 = s2 + s1; + } + + s1 %= Base; + s2 %= Base; + } + + this.checksum = (s2 << 16) | s1; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Zlib/Crc32.cs b/src/ImageProcessorCore/Formats/Png/Zlib/Crc32.cs new file mode 100644 index 000000000..da42e8dae --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Zlib/Crc32.cs @@ -0,0 +1,180 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + using System; + + /// + /// Generate a table for a byte-wise 32-bit CRC calculation on the polynomial: + /// x^32+x^26+x^23+x^22+x^16+x^12+x^11+x^10+x^8+x^7+x^5+x^4+x^2+x+1. + /// + /// + /// + /// Polynomials over GF(2) are represented in binary, one bit per coefficient, + /// with the lowest powers in the most significant bit. Then adding polynomials + /// is just exclusive-or, and multiplying a polynomial by x is a right shift by + /// one. If we call the above polynomial p, and represent a byte as the + /// polynomial q, also with the lowest power in the most significant bit (so the + /// byte 0xb1 is the polynomial x^7+x^3+x+1), then the CRC is (q*x^32) mod p, + /// where a mod b means the remainder after dividing a by b. + /// + /// + /// This calculation is done using the shift-register method of multiplying and + /// taking the remainder. The register is initialized to zero, and for each + /// incoming bit, x^32 is added mod p to the register if the bit is a one (where + /// x^32 mod p is p+x^32 = x^26+...+1), and the register is multiplied mod p by + /// x (which is shifting right by one and adding x^32 mod p if the bit shifted + /// out is a one). We start with the highest power (least significant bit) of + /// q and repeat for all eight bits of q. + /// + /// + /// The table is simply the CRC of all possible eight bit values. This is all + /// the information needed to generate CRC's on data a byte at a time for all + /// combinations of CRC register values and incoming bytes. + /// + /// + internal sealed class Crc32 : IChecksum + { + /// + /// The cycle redundancy check seed + /// + private const uint CrcSeed = 0xFFFFFFFF; + + /// + /// The table of all possible eight bit values for fast lookup. + /// + private static readonly uint[] CrcTable = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, + 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, + 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, + 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, + 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, + 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, + 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, + 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, + 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, + 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, + 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, + 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, + 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, + 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, + 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, + 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, + 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, + 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, + 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, + 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, + 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, + 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, + 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, + 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, + 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, + 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, + 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, + 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, + 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, + 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, + 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, + 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, + 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, + 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, + 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, + 0x2D02EF8D + }; + + /// + /// The data checksum so far. + /// + private uint crc; + + /// + public long Value + { + get + { + return this.crc; + } + + set + { + this.crc = (uint)value; + } + } + + /// + public void Reset() + { + this.crc = 0; + } + + /// + /// Updates the checksum with the given value. + /// + /// The byte is taken as the lower 8 bits of value. + public void Update(int value) + { + this.crc ^= CrcSeed; + this.crc = CrcTable[(this.crc ^ value) & 0xFF] ^ (this.crc >> 8); + this.crc ^= CrcSeed; + } + + /// + public void Update(byte[] buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + this.Update(buffer, 0, buffer.Length); + } + + /// + public void Update(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be less than zero"); + } + + if (offset < 0 || offset + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + + this.crc ^= CrcSeed; + + while (--count >= 0) + { + this.crc = CrcTable[(this.crc ^ buffer[offset++]) & 0xFF] ^ (this.crc >> 8); + } + + this.crc ^= CrcSeed; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Zlib/IChecksum.cs b/src/ImageProcessorCore/Formats/Png/Zlib/IChecksum.cs new file mode 100644 index 000000000..077a5ad2a --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Zlib/IChecksum.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Formats +{ + /// + /// Interface to compute a data checksum used by checked input/output streams. + /// A data checksum can be updated by one byte or with a byte array. After each + /// update the value of the current checksum can be returned by calling + /// Value. The complete checksum object can also be reset + /// so it can be used again with new data. + /// + public interface IChecksum + { + /// + /// Gets the data checksum computed so far. + /// + long Value + { + get; + } + + /// + /// Resets the data checksum as if no update was ever called. + /// + void Reset(); + + /// + /// Adds one byte to the data checksum. + /// + /// + /// The data value to add. The high byte of the integer is ignored. + /// + void Update(int value); + + /// + /// Updates the data checksum with the bytes taken from the array. + /// + /// + /// buffer an array of bytes + /// + void Update(byte[] buffer); + + /// + /// Adds the byte array to the data checksum. + /// + /// + /// The buffer which contains the data + /// + /// + /// The offset in the buffer where the data starts + /// + /// + /// the number of data bytes to add. + /// + void Update(byte[] buffer, int offset, int count); + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Zlib/README.md b/src/ImageProcessorCore/Formats/Png/Zlib/README.md new file mode 100644 index 000000000..c297a91d5 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Zlib/README.md @@ -0,0 +1,2 @@ +Adler32.cs and Crc32.cs have been copied from +https://github.com/ygrenier/SharpZipLib.Portable diff --git a/src/ImageProcessorCore/Formats/Png/Zlib/ZlibDeflateStream.cs b/src/ImageProcessorCore/Formats/Png/Zlib/ZlibDeflateStream.cs new file mode 100644 index 000000000..a2c0ca202 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Zlib/ZlibDeflateStream.cs @@ -0,0 +1,210 @@ +// +// 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.IO.Compression; + + /// + /// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm. + /// + internal sealed class ZlibDeflateStream : Stream + { + /// + /// The raw stream containing the uncompressed image data. + /// + private readonly Stream rawStream; + + /// + /// Computes the checksum for the data stream. + /// + private readonly Adler32 adler32 = new Adler32(); + + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + // The stream responsible for decompressing the input stream. + private DeflateStream deflateStream; + + /// + /// Initializes a new instance of + /// + /// The stream to compress. + /// The compression level. + public ZlibDeflateStream(Stream stream, int compressionLevel) + { + this.rawStream = stream; + + // Write the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = 0x78; + int flg = 218; + + // http://stackoverflow.com/a/2331025/277304 + if (compressionLevel >= 5 && compressionLevel <= 6) + { + flg = 156; + } + else if (compressionLevel >= 3 && compressionLevel <= 4) + { + flg = 94; + } + + else if (compressionLevel <= 2) + { + flg = 1; + } + + // Just in case + flg -= (cmf * 256 + flg) % 31; + + if (flg < 0) + { + flg += 31; + } + + this.rawStream.WriteByte((byte)cmf); + this.rawStream.WriteByte((byte)flg); + + // Initialize the deflate Stream. + CompressionLevel level = CompressionLevel.Optimal; + + if (compressionLevel >= 1 && compressionLevel <= 5) + { + level = CompressionLevel.Fastest; + } + + else if (compressionLevel == 0) + { + level = CompressionLevel.NoCompression; + } + + this.deflateStream = new DeflateStream(this.rawStream, level, true); + } + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + /// + public override long Position + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + public override void Flush() + { + this.deflateStream?.Flush(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + this.deflateStream.Write(buffer, offset, count); + this.adler32.Update(buffer, offset, count); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + if (this.deflateStream != null) + { + this.deflateStream.Dispose(); + this.deflateStream = null; + } + else { + + // Hack: empty input? + this.rawStream.WriteByte(3); + this.rawStream.WriteByte(0); + } + + // Add the crc + uint crc = (uint)this.adler32.Value; + this.rawStream.WriteByte((byte)((crc >> 24) & 0xFF)); + this.rawStream.WriteByte((byte)((crc >> 16) & 0xFF)); + this.rawStream.WriteByte((byte)((crc >> 8) & 0xFF)); + this.rawStream.WriteByte((byte)((crc) & 0xFF)); + } + + base.Dispose(disposing); + + // Call the appropriate methods to clean up + // unmanaged resources here. + // Note disposing is done. + this.isDisposed = true; + } + } +} diff --git a/src/ImageProcessorCore/Formats/Png/Zlib/ZlibInflateStream.cs b/src/ImageProcessorCore/Formats/Png/Zlib/ZlibInflateStream.cs new file mode 100644 index 000000000..4373b5fd1 --- /dev/null +++ b/src/ImageProcessorCore/Formats/Png/Zlib/ZlibInflateStream.cs @@ -0,0 +1,205 @@ +// +// 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.IO.Compression; + + /// + /// Provides methods and properties for decompressing streams by using the Zlib Deflate algorithm. + /// + internal sealed class ZlibInflateStream : Stream + { + /// + /// A value indicating whether this instance of the given entity has been disposed. + /// + /// if this instance has been disposed; otherwise, . + /// + /// If the entity is disposed, it must not be disposed a second + /// time. The isDisposed field is set the first time the entity + /// is disposed. If the isDisposed field is true, then the Dispose() + /// method will not dispose again. This help not to prolong the entity's + /// life in the Garbage Collector. + /// + private bool isDisposed; + + /// + /// The raw stream containing the uncompressed image data. + /// + private readonly Stream rawStream; + + /// + /// The read crc data. + /// + private byte[] crcread; + + // The stream responsible for decompressing the input stream. + private DeflateStream deflateStream; + + public ZlibInflateStream(Stream stream) + { + // The DICT dictionary identifier identifying the used dictionary. + + // The preset dictionary. + bool fdict; + this.rawStream = stream; + + // Read the zlib header : http://tools.ietf.org/html/rfc1950 + // CMF(Compression Method and flags) + // This byte is divided into a 4 - bit compression method and a + // 4-bit information field depending on the compression method. + // bits 0 to 3 CM Compression method + // bits 4 to 7 CINFO Compression info + // + // 0 1 + // +---+---+ + // |CMF|FLG| + // +---+---+ + int cmf = this.rawStream.ReadByte(); + int flag = this.rawStream.ReadByte(); + if (cmf == -1 || flag == -1) + { + return; + } + + if ((cmf & 0x0f) != 8) + { + throw new Exception($"Bad compression method for ZLIB header: cmf={cmf}"); + } + + // CINFO is the base-2 logarithm of the LZ77 window size, minus eight. + // int cinfo = ((cmf & (0xf0)) >> 8); + fdict = (flag & 32) != 0; + + if (fdict) + { + // The DICT dictionary identifier identifying the used dictionary. + byte[] dictId = new byte[4]; + + for (int i = 0; i < 4; i++) + { + // We consume but don't use this. + dictId[i] = (byte)this.rawStream.ReadByte(); + } + } + + // Initialize the deflate Stream. + this.deflateStream = new DeflateStream(this.rawStream, CompressionMode.Decompress, true); + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + /// + public override long Position + { + get + { + throw new NotSupportedException(); + } + + set + { + throw new NotSupportedException(); + } + } + + /// + public override void Flush() + { + this.deflateStream?.Flush(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + // We dont't check CRC on reading + int read = this.deflateStream.Read(buffer, offset, count); + if (read < 1 && this.crcread == null) + { + // The deflater has ended. We try to read the next 4 bytes from raw stream (crc) + this.crcread = new byte[4]; + for (int i = 0; i < 4; i++) + { + // we dont really check/use this + this.crcread[i] = (byte)this.rawStream.ReadByte(); + } + } + + return read; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.isDisposed) + { + return; + } + + if (disposing) + { + // dispose managed resources + if (this.deflateStream != null) + { + this.deflateStream.Dispose(); + this.deflateStream = null; + + if (this.crcread == null) + { + // Consume the trailing 4 bytes + this.crcread = new byte[4]; + for (int i = 0; i < 4; i++) + { + this.crcread[i] = (byte)this.rawStream.ReadByte(); + } + } + } + } + + base.Dispose(disposing); + + // Call the appropriate methods to clean up + // unmanaged resources here. + // Note disposing is done. + this.isDisposed = true; + } + } +} diff --git a/src/ImageProcessorCore/Image.cs b/src/ImageProcessorCore/Image.cs index e57129d29..9ab3e73ba 100644 --- a/src/ImageProcessorCore/Image.cs +++ b/src/ImageProcessorCore/Image.cs @@ -13,6 +13,14 @@ namespace ImageProcessorCore /// public class Image : Image { + /// + /// Initializes a new instance of the class + /// with the height and the width of the image. + /// + public Image() + { + } + /// /// Initializes a new instance of the class /// with the height and the width of the image. diff --git a/src/ImageProcessorCore/Quantizers/IQuantizer.cs b/src/ImageProcessorCore/Quantizers/IQuantizer.cs new file mode 100644 index 000000000..3f555236a --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/IQuantizer.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Quantizers +{ + /// + /// Provides methods for allowing quantization of images pixels. + /// + public interface IQuantizer + { + /// + /// Gets or sets the transparency threshold. + /// + byte Threshold { get; set; } + + /// + /// Quantize an image and return the resulting output pixels. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image to quantize. + /// The maximum number of colors to return. + /// + /// A representing a quantized version of the image pixels. + /// + QuantizedImage Quantize(ImageBase image, int maxColors) + where T : IPackedVector, new() + where TP : struct; + } +} diff --git a/src/ImageProcessorCore/Quantizers/QuantizedImage.cs b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs new file mode 100644 index 000000000..ddadc099e --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/QuantizedImage.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Quantizers +{ + using System; + using System.Threading.Tasks; + + /// + /// Represents a quantized image where the pixels indexed by a color palette. + /// + /// The pixel format. + /// The packed format. long, float. + public class QuantizedImage + where T : IPackedVector, new() + where TP : struct + { + /// + /// Initializes a new instance of the class. + /// + /// The image width. + /// The image height. + /// The color palette. + /// The quantized pixels. + /// The transparency index. + public QuantizedImage(int width, int height, T[] palette, byte[] pixels, int transparentIndex = -1) + { + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.NotNull(palette, nameof(palette)); + Guard.NotNull(pixels, nameof(pixels)); + + if (pixels.Length != width * height) + { + throw new ArgumentException( + $"Pixel array size must be {nameof(width)} * {nameof(height)}", nameof(pixels)); + } + + this.Width = width; + this.Height = height; + this.Palette = palette; + this.Pixels = pixels; + this.TransparentIndex = transparentIndex; + } + + /// + /// Gets the width of this . + /// + public int Width { get; } + + /// + /// Gets the height of this . + /// + public int Height { get; } + + /// + /// Gets the color palette of this . + /// + public T[] Palette { get; } + + /// + /// Gets the pixels of this . + /// + public byte[] Pixels { get; } + + /// + /// Gets the transparent index + /// + public int TransparentIndex { get; } + + /// + /// Converts this quantized image to a normal image. + /// + /// + /// The + /// + public Image ToImage() + { + Image image = new Image(); + + int pixelCount = this.Pixels.Length; + int palletCount = this.Palette.Length - 1; + T[] pixels = new T[pixelCount]; + + Parallel.For( + 0, + pixelCount, + Bootstrapper.Instance.ParallelOptions, + i => + { + int offset = i * 4; + T color = this.Palette[Math.Min(palletCount, this.Pixels[i])]; + pixels[offset] = color; + }); + + image.SetPixels(this.Width, this.Height, pixels); + return image; + } + } +} diff --git a/src/ImageProcessorCore/Quantizers/Wu/Box.cs b/src/ImageProcessorCore/Quantizers/Wu/Box.cs new file mode 100644 index 000000000..b9300b087 --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/Wu/Box.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Quantizers +{ + /// + /// Represents a box color cube. + /// + internal sealed class Box + { + /// + /// Gets or sets the min red value, exclusive. + /// + public int R0 { get; set; } + + /// + /// Gets or sets the max red value, inclusive. + /// + public int R1 { get; set; } + + /// + /// Gets or sets the min green value, exclusive. + /// + public int G0 { get; set; } + + /// + /// Gets or sets the max green value, inclusive. + /// + public int G1 { get; set; } + + /// + /// Gets or sets the min blue value, exclusive. + /// + public int B0 { get; set; } + + /// + /// Gets or sets the max blue value, inclusive. + /// + public int B1 { get; set; } + + /// + /// Gets or sets the min alpha value, exclusive. + /// + public int A0 { get; set; } + + /// + /// Gets or sets the max alpha value, inclusive. + /// + public int A1 { get; set; } + + /// + /// Gets or sets the volume. + /// + public int Volume { get; set; } + } +} diff --git a/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs new file mode 100644 index 000000000..95e0b0772 --- /dev/null +++ b/src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs @@ -0,0 +1,800 @@ +// +// Copyright © James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Quantizers +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + /// + /// An implementation of Wu's color quantizer with alpha channel. + /// + /// + /// + /// Based on C Implementation of Xiaolin Wu's Color Quantizer (v. 2) + /// (see Graphics Gems volume II, pages 126-133) + /// (). + /// + /// + /// This adaptation is based on the excellent JeremyAnsel.ColorQuant by Jérémy Ansel + /// + /// + /// + /// Algorithm: Greedy orthogonal bipartition of RGB space for variance + /// minimization aided by inclusion-exclusion tricks. + /// For speed no nearest neighbor search is done. Slightly + /// better performance can be expected by more sophisticated + /// but more expensive versions. + /// + /// + public sealed class WuQuantizer : IQuantizer + { + /// + /// The epsilon for comparing floating point numbers. + /// + private const float Epsilon = 0.001f; + + /// + /// The index bits. + /// + private const int IndexBits = 6; + + /// + /// The index alpha bits. + /// + private const int IndexAlphaBits = 3; + + /// + /// The index count. + /// + private const int IndexCount = (1 << IndexBits) + 1; + + /// + /// The index alpha count. + /// + private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1; + + /// + /// The table length. + /// + private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; + + /// + /// Moment of P(c). + /// + private readonly long[] vwt; + + /// + /// Moment of r*P(c). + /// + private readonly long[] vmr; + + /// + /// Moment of g*P(c). + /// + private readonly long[] vmg; + + /// + /// Moment of b*P(c). + /// + private readonly long[] vmb; + + /// + /// Moment of a*P(c). + /// + private readonly long[] vma; + + /// + /// Moment of c^2*P(c). + /// + private readonly double[] m2; + + /// + /// Color space tag. + /// + private readonly byte[] tag; + + /// + /// Initializes a new instance of the class. + /// + public WuQuantizer() + { + this.vwt = new long[TableLength]; + this.vmr = new long[TableLength]; + this.vmg = new long[TableLength]; + this.vmb = new long[TableLength]; + this.vma = new long[TableLength]; + this.m2 = new double[TableLength]; + this.tag = new byte[TableLength]; + } + + /// + public byte Threshold { get; set; } + + /// + public QuantizedImage Quantize(ImageBase image, int maxColors) + where T : IPackedVector, new() + where TP : struct + { + Guard.NotNull(image, nameof(image)); + + int colorCount = maxColors.Clamp(1, 256); + + this.Clear(); + + using (IPixelAccessor imagePixels = image.Lock()) + { + this.Build3DHistogram(imagePixels); + this.Get3DMoments(); + + Box[] cube; + this.BuildCube(out cube, ref colorCount); + + return this.GenerateResult(imagePixels, colorCount, cube); + } + } + + /// + /// Gets an index. + /// + /// The red value. + /// The green value. + /// The blue value. + /// The alpha value. + /// The index. + private static int GetPaletteIndex(int r, int g, int b, int a) + { + return (r << ((IndexBits * 2) + IndexAlphaBits)) + + (r << (IndexBits + IndexAlphaBits + 1)) + + (g << (IndexBits + IndexAlphaBits)) + + (r << (IndexBits * 2)) + + (r << (IndexBits + 1)) + + (g << IndexBits) + + ((r + g + b) << IndexAlphaBits) + + r + g + b + a; + } + + /// + /// Computes sum over a box of any given statistic. + /// + /// The cube. + /// The moment. + /// The result. + private static double Volume(Box cube, long[] moment) + { + return moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A1)] + - moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] + - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A1)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + } + + /// + /// Computes part of Volume(cube, moment) that doesn't depend on r1, g1, or b1 (depending on direction). + /// + /// The cube. + /// The direction. + /// The moment. + /// The result. + private static long Bottom(Box cube, int direction, long[] moment) + { + switch (direction) + { + // Red + case 0: + return -moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + + // Green + case 1: + return -moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A1)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + + // Blue + case 2: + return -moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + + // Alpha + case 3: + return -moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + + default: + throw new ArgumentOutOfRangeException(nameof(direction)); + } + } + + /// + /// Computes remainder of Volume(cube, moment), substituting position for r1, g1, or b1 (depending on direction). + /// + /// The cube. + /// The direction. + /// The position. + /// The moment. + /// The result. + private static long Top(Box cube, int direction, int position, long[] moment) + { + switch (direction) + { + // Red + case 0: + return moment[GetPaletteIndex(position, cube.G1, cube.B1, cube.A1)] + - moment[GetPaletteIndex(position, cube.G1, cube.B1, cube.A0)] + - moment[GetPaletteIndex(position, cube.G1, cube.B0, cube.A1)] + + moment[GetPaletteIndex(position, cube.G1, cube.B0, cube.A0)] + - moment[GetPaletteIndex(position, cube.G0, cube.B1, cube.A1)] + + moment[GetPaletteIndex(position, cube.G0, cube.B1, cube.A0)] + + moment[GetPaletteIndex(position, cube.G0, cube.B0, cube.A1)] + - moment[GetPaletteIndex(position, cube.G0, cube.B0, cube.A0)]; + + // Green + case 1: + return moment[GetPaletteIndex(cube.R1, position, cube.B1, cube.A1)] + - moment[GetPaletteIndex(cube.R1, position, cube.B1, cube.A0)] + - moment[GetPaletteIndex(cube.R1, position, cube.B0, cube.A1)] + + moment[GetPaletteIndex(cube.R1, position, cube.B0, cube.A0)] + - moment[GetPaletteIndex(cube.R0, position, cube.B1, cube.A1)] + + moment[GetPaletteIndex(cube.R0, position, cube.B1, cube.A0)] + + moment[GetPaletteIndex(cube.R0, position, cube.B0, cube.A1)] + - moment[GetPaletteIndex(cube.R0, position, cube.B0, cube.A0)]; + + // Blue + case 2: + return moment[GetPaletteIndex(cube.R1, cube.G1, position, cube.A1)] + - moment[GetPaletteIndex(cube.R1, cube.G1, position, cube.A0)] + - moment[GetPaletteIndex(cube.R1, cube.G0, position, cube.A1)] + + moment[GetPaletteIndex(cube.R1, cube.G0, position, cube.A0)] + - moment[GetPaletteIndex(cube.R0, cube.G1, position, cube.A1)] + + moment[GetPaletteIndex(cube.R0, cube.G1, position, cube.A0)] + + moment[GetPaletteIndex(cube.R0, cube.G0, position, cube.A1)] + - moment[GetPaletteIndex(cube.R0, cube.G0, position, cube.A0)]; + + // Alpha + case 3: + return moment[GetPaletteIndex(cube.R1, cube.G1, cube.B1, position)] + - moment[GetPaletteIndex(cube.R1, cube.G1, cube.B0, position)] + - moment[GetPaletteIndex(cube.R1, cube.G0, cube.B1, position)] + + moment[GetPaletteIndex(cube.R1, cube.G0, cube.B0, position)] + - moment[GetPaletteIndex(cube.R0, cube.G1, cube.B1, position)] + + moment[GetPaletteIndex(cube.R0, cube.G1, cube.B0, position)] + + moment[GetPaletteIndex(cube.R0, cube.G0, cube.B1, position)] + - moment[GetPaletteIndex(cube.R0, cube.G0, cube.B0, position)]; + + default: + throw new ArgumentOutOfRangeException(nameof(direction)); + } + } + + /// + /// Clears the tables. + /// + private void Clear() + { + Array.Clear(this.vwt, 0, TableLength); + Array.Clear(this.vmr, 0, TableLength); + Array.Clear(this.vmg, 0, TableLength); + Array.Clear(this.vmb, 0, TableLength); + Array.Clear(this.vma, 0, TableLength); + Array.Clear(this.m2, 0, TableLength); + + Array.Clear(this.tag, 0, TableLength); + } + + /// + /// Builds a 3-D color histogram of counts, r/g/b, c^2. + /// + /// The pixel format. + /// The packed format. long, float. + /// The pixel accessor. + private void Build3DHistogram(IPixelAccessor pixels) + where T : IPackedVector, new() + where TP : struct + { + for (int y = 0; y < pixels.Height; y++) + { + for (int x = 0; x < pixels.Width; x++) + { + // Colors are expected in r->g->b->a format + byte[] color = pixels[x, y].ToBytes(); + + byte r = color[0]; + byte g = color[1]; + byte b = color[2]; + byte a = color[3]; + + int inr = r >> (8 - IndexBits); + int ing = g >> (8 - IndexBits); + int inb = b >> (8 - IndexBits); + int ina = a >> (8 - IndexAlphaBits); + + int ind = GetPaletteIndex(inr + 1, ing + 1, inb + 1, ina + 1); + + this.vwt[ind]++; + this.vmr[ind] += r; + this.vmg[ind] += g; + this.vmb[ind] += b; + this.vma[ind] += a; + this.m2[ind] += (r * r) + (g * g) + (b * b) + (a * a); + } + } + } + + /// + /// Converts the histogram into moments so that we can rapidly calculate + /// the sums of the above quantities over any desired box. + /// + private void Get3DMoments() + { + long[] volume = new long[IndexCount * IndexAlphaCount]; + long[] volumeR = new long[IndexCount * IndexAlphaCount]; + long[] volumeG = new long[IndexCount * IndexAlphaCount]; + long[] volumeB = new long[IndexCount * IndexAlphaCount]; + long[] volumeA = new long[IndexCount * IndexAlphaCount]; + double[] volume2 = new double[IndexCount * IndexAlphaCount]; + + long[] area = new long[IndexAlphaCount]; + long[] areaR = new long[IndexAlphaCount]; + long[] areaG = new long[IndexAlphaCount]; + long[] areaB = new long[IndexAlphaCount]; + long[] areaA = new long[IndexAlphaCount]; + double[] area2 = new double[IndexAlphaCount]; + + for (int r = 1; r < IndexCount; r++) + { + Array.Clear(volume, 0, IndexCount * IndexAlphaCount); + Array.Clear(volumeR, 0, IndexCount * IndexAlphaCount); + Array.Clear(volumeG, 0, IndexCount * IndexAlphaCount); + Array.Clear(volumeB, 0, IndexCount * IndexAlphaCount); + Array.Clear(volumeA, 0, IndexCount * IndexAlphaCount); + Array.Clear(volume2, 0, IndexCount * IndexAlphaCount); + + for (int g = 1; g < IndexCount; g++) + { + Array.Clear(area, 0, IndexAlphaCount); + Array.Clear(areaR, 0, IndexAlphaCount); + Array.Clear(areaG, 0, IndexAlphaCount); + Array.Clear(areaB, 0, IndexAlphaCount); + Array.Clear(areaA, 0, IndexAlphaCount); + Array.Clear(area2, 0, IndexAlphaCount); + + for (int b = 1; b < IndexCount; b++) + { + long line = 0; + long lineR = 0; + long lineG = 0; + long lineB = 0; + long lineA = 0; + double line2 = 0; + + for (int a = 1; a < IndexAlphaCount; a++) + { + int ind1 = GetPaletteIndex(r, g, b, a); + + line += this.vwt[ind1]; + lineR += this.vmr[ind1]; + lineG += this.vmg[ind1]; + lineB += this.vmb[ind1]; + lineA += this.vma[ind1]; + line2 += this.m2[ind1]; + + area[a] += line; + areaR[a] += lineR; + areaG[a] += lineG; + areaB[a] += lineB; + areaA[a] += lineA; + area2[a] += line2; + + int inv = (b * IndexAlphaCount) + a; + + volume[inv] += area[a]; + volumeR[inv] += areaR[a]; + volumeG[inv] += areaG[a]; + volumeB[inv] += areaB[a]; + volumeA[inv] += areaA[a]; + volume2[inv] += area2[a]; + + int ind2 = ind1 - GetPaletteIndex(1, 0, 0, 0); + + this.vwt[ind1] = this.vwt[ind2] + volume[inv]; + this.vmr[ind1] = this.vmr[ind2] + volumeR[inv]; + this.vmg[ind1] = this.vmg[ind2] + volumeG[inv]; + this.vmb[ind1] = this.vmb[ind2] + volumeB[inv]; + this.vma[ind1] = this.vma[ind2] + volumeA[inv]; + this.m2[ind1] = this.m2[ind2] + volume2[inv]; + } + } + } + } + } + + /// + /// Computes the weighted variance of a box cube. + /// + /// The cube. + /// The . + private double Variance(Box cube) + { + double dr = Volume(cube, this.vmr); + double dg = Volume(cube, this.vmg); + double db = Volume(cube, this.vmb); + double da = Volume(cube, this.vma); + + double xx = + this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A1)] + - this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B1, cube.A0)] + - this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A1)] + + this.m2[GetPaletteIndex(cube.R1, cube.G1, cube.B0, cube.A0)] + - this.m2[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A1)] + + this.m2[GetPaletteIndex(cube.R1, cube.G0, cube.B1, cube.A0)] + + this.m2[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A1)] + - this.m2[GetPaletteIndex(cube.R1, cube.G0, cube.B0, cube.A0)] + - this.m2[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A1)] + + this.m2[GetPaletteIndex(cube.R0, cube.G1, cube.B1, cube.A0)] + + this.m2[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A1)] + - this.m2[GetPaletteIndex(cube.R0, cube.G1, cube.B0, cube.A0)] + + this.m2[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A1)] + - this.m2[GetPaletteIndex(cube.R0, cube.G0, cube.B1, cube.A0)] + - this.m2[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A1)] + + this.m2[GetPaletteIndex(cube.R0, cube.G0, cube.B0, cube.A0)]; + + return xx - (((dr * dr) + (dg * dg) + (db * db) + (da * da)) / Volume(cube, this.vwt)); + } + + /// + /// We want to minimize the sum of the variances of two sub-boxes. + /// The sum(c^2) terms can be ignored since their sum over both sub-boxes + /// is the same (the sum for the whole box) no matter where we split. + /// The remaining terms have a minus sign in the variance formula, + /// so we drop the minus sign and maximize the sum of the two terms. + /// + /// The cube. + /// The direction. + /// The first position. + /// The last position. + /// The cutting point. + /// The whole red. + /// The whole green. + /// The whole blue. + /// The whole alpha. + /// The whole weight. + /// The . + private double Maximize(Box cube, int direction, int first, int last, out int cut, double wholeR, double wholeG, double wholeB, double wholeA, double wholeW) + { + long baseR = Bottom(cube, direction, this.vmr); + long baseG = Bottom(cube, direction, this.vmg); + long baseB = Bottom(cube, direction, this.vmb); + long baseA = Bottom(cube, direction, this.vma); + long baseW = Bottom(cube, direction, this.vwt); + + double max = 0.0; + cut = -1; + + for (int i = first; i < last; i++) + { + double halfR = baseR + Top(cube, direction, i, this.vmr); + double halfG = baseG + Top(cube, direction, i, this.vmg); + double halfB = baseB + Top(cube, direction, i, this.vmb); + double halfA = baseA + Top(cube, direction, i, this.vma); + double halfW = baseW + Top(cube, direction, i, this.vwt); + + double temp; + + if (Math.Abs(halfW) < Epsilon) + { + continue; + } + + temp = ((halfR * halfR) + (halfG * halfG) + (halfB * halfB) + (halfA * halfA)) / halfW; + + halfR = wholeR - halfR; + halfG = wholeG - halfG; + halfB = wholeB - halfB; + halfA = wholeA - halfA; + halfW = wholeW - halfW; + + if (Math.Abs(halfW) < Epsilon) + { + continue; + } + + temp += ((halfR * halfR) + (halfG * halfG) + (halfB * halfB) + (halfA * halfA)) / halfW; + + if (temp > max) + { + max = temp; + cut = i; + } + } + + return max; + } + + /// + /// Cuts a box. + /// + /// The first set. + /// The second set. + /// Returns a value indicating whether the box has been split. + private bool Cut(Box set1, Box set2) + { + double wholeR = Volume(set1, this.vmr); + double wholeG = Volume(set1, this.vmg); + double wholeB = Volume(set1, this.vmb); + double wholeA = Volume(set1, this.vma); + double wholeW = Volume(set1, this.vwt); + + int cutr; + int cutg; + int cutb; + int cuta; + + double maxr = this.Maximize(set1, 0, set1.R0 + 1, set1.R1, out cutr, wholeR, wholeG, wholeB, wholeA, wholeW); + double maxg = this.Maximize(set1, 1, set1.G0 + 1, set1.G1, out cutg, wholeR, wholeG, wholeB, wholeA, wholeW); + double maxb = this.Maximize(set1, 2, set1.B0 + 1, set1.B1, out cutb, wholeR, wholeG, wholeB, wholeA, wholeW); + double maxa = this.Maximize(set1, 3, set1.A0 + 1, set1.A1, out cuta, wholeR, wholeG, wholeB, wholeA, wholeW); + + int dir; + + if ((maxr >= maxg) && (maxr >= maxb) && (maxr >= maxa)) + { + dir = 0; + + if (cutr < 0) + { + return false; + } + } + else if ((maxg >= maxr) && (maxg >= maxb) && (maxg >= maxa)) + { + dir = 1; + } + else if ((maxb >= maxr) && (maxb >= maxg) && (maxb >= maxa)) + { + dir = 2; + } + else + { + dir = 3; + } + + set2.R1 = set1.R1; + set2.G1 = set1.G1; + set2.B1 = set1.B1; + set2.A1 = set1.A1; + + switch (dir) + { + // Red + case 0: + set2.R0 = set1.R1 = cutr; + set2.G0 = set1.G0; + set2.B0 = set1.B0; + set2.A0 = set1.A0; + break; + + // Green + case 1: + set2.G0 = set1.G1 = cutg; + set2.R0 = set1.R0; + set2.B0 = set1.B0; + set2.A0 = set1.A0; + break; + + // Blue + case 2: + set2.B0 = set1.B1 = cutb; + set2.R0 = set1.R0; + set2.G0 = set1.G0; + set2.A0 = set1.A0; + break; + + // Alpha + case 3: + set2.A0 = set1.A1 = cuta; + set2.R0 = set1.R0; + set2.G0 = set1.G0; + set2.B0 = set1.B0; + break; + } + + set1.Volume = (set1.R1 - set1.R0) * (set1.G1 - set1.G0) * (set1.B1 - set1.B0) * (set1.A1 - set1.A0); + set2.Volume = (set2.R1 - set2.R0) * (set2.G1 - set2.G0) * (set2.B1 - set2.B0) * (set2.A1 - set2.A0); + + return true; + } + + /// + /// Marks a color space tag. + /// + /// The cube. + /// A label. + private void Mark(Box cube, byte label) + { + for (int r = cube.R0 + 1; r <= cube.R1; r++) + { + for (int g = cube.G0 + 1; g <= cube.G1; g++) + { + for (int b = cube.B0 + 1; b <= cube.B1; b++) + { + for (int a = cube.A0 + 1; a <= cube.A1; a++) + { + this.tag[GetPaletteIndex(r, g, b, a)] = label; + } + } + } + } + } + + /// + /// Builds the cube. + /// + /// The cube. + /// The color count. + private void BuildCube(out Box[] cube, ref int colorCount) + { + cube = new Box[colorCount]; + double[] vv = new double[colorCount]; + + for (int i = 0; i < colorCount; i++) + { + cube[i] = new Box(); + } + + cube[0].R0 = cube[0].G0 = cube[0].B0 = cube[0].A0 = 0; + cube[0].R1 = cube[0].G1 = cube[0].B1 = IndexCount - 1; + cube[0].A1 = IndexAlphaCount - 1; + + int next = 0; + + for (int i = 1; i < colorCount; i++) + { + if (this.Cut(cube[next], cube[i])) + { + vv[next] = cube[next].Volume > 1 ? this.Variance(cube[next]) : 0.0; + vv[i] = cube[i].Volume > 1 ? this.Variance(cube[i]) : 0.0; + } + else + { + vv[next] = 0.0; + i--; + } + + next = 0; + + double temp = vv[0]; + for (int k = 1; k <= i; k++) + { + if (vv[k] > temp) + { + temp = vv[k]; + next = k; + } + } + + if (temp <= 0.0) + { + colorCount = i + 1; + break; + } + } + } + + /// + /// Generates the quantized result. + /// + /// The pixel format. + /// The packed format. long, float. + /// The image pixels. + /// The color count. + /// The cube. + /// The result. + private QuantizedImage GenerateResult(IPixelAccessor imagePixels, int colorCount, Box[] cube) + where T : IPackedVector, new() + where TP : struct + { + List pallette = new List(); + byte[] pixels = new byte[imagePixels.Width * imagePixels.Height]; + int transparentIndex = -1; + int width = imagePixels.Width; + int height = imagePixels.Height; + + for (int k = 0; k < colorCount; k++) + { + this.Mark(cube[k], (byte)k); + + double weight = Volume(cube[k], this.vwt); + + if (Math.Abs(weight) > Epsilon) + { + byte r = (byte)(Volume(cube[k], this.vmr) / weight); + byte g = (byte)(Volume(cube[k], this.vmg) / weight); + byte b = (byte)(Volume(cube[k], this.vmb) / weight); + byte a = (byte)(Volume(cube[k], this.vma) / weight); + + T color = default(T); + color.PackBytes(r, g, b, a); + + if (color.Equals(default(T))) + { + transparentIndex = k; + } + + pallette.Add(color); + } + else + { + pallette.Add(default(T)); + transparentIndex = k; + } + } + + Parallel.For( + 0, + height, + Bootstrapper.Instance.ParallelOptions, + y => + { + for (int x = 0; x < width; x++) + { + // Expected order r->g->b->a + byte[] color = imagePixels[x, y].ToBytes(); + int r = color[0] >> (8 - IndexBits); + int g = color[1] >> (8 - IndexBits); + int b = color[2] >> (8 - IndexBits); + int a = color[3] >> (8 - IndexAlphaBits); + + if (transparentIndex > -1 && color[3] <= this.Threshold) + { + pixels[(y * width) + x] = (byte)transparentIndex; + continue; + } + + int ind = GetPaletteIndex(r + 1, g + 1, b + 1, a + 1); + pixels[(y * width) + x] = this.tag[ind]; + } + }); + + + return new QuantizedImage(width, height, pallette.ToArray(), pixels, transparentIndex); + } + } +} \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/FileTestBase.cs b/tests/ImageProcessorCore.Tests/FileTestBase.cs index 05136a1c8..280e0e512 100644 --- a/tests/ImageProcessorCore.Tests/FileTestBase.cs +++ b/tests/ImageProcessorCore.Tests/FileTestBase.cs @@ -28,7 +28,7 @@ namespace ImageProcessorCore.Tests // "TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only //"TestImages/Formats/Png/blur.png", // Perf: Enable for local testing only //"TestImages/Formats/Png/indexed.png", // Perf: Enable for local testing only - //"TestImages/Formats/Png/splash.png", + "TestImages/Formats/Png/splash.png", //"TestImages/Formats/Gif/rings.gif", //"TestImages/Formats/Gif/giphy.gif" // Perf: Enable for local testing only };