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
};