Browse Source

Add png encoder

Former-commit-id: b222f96370adf0a3f87d3e2d762e270fac829a5a
Former-commit-id: eecf02f023c9d43ff3e54dd4fcfbfbfdde22f543
Former-commit-id: 03bf7ed3356ee33b96e8dcff9b7a53b26e8f919f
af/merge-core
James Jackson-South 10 years ago
parent
commit
8187f2ad82
  1. 2
      src/ImageProcessorCore/Bootstrapper.cs
  2. 6
      src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs
  3. 76
      src/ImageProcessorCore/Formats/Png/GrayscaleReader.cs
  4. 29
      src/ImageProcessorCore/Formats/Png/IColorReader.cs
  5. 95
      src/ImageProcessorCore/Formats/Png/PaletteIndexReader.cs
  6. 39
      src/ImageProcessorCore/Formats/Png/PngChunk.cs
  7. 62
      src/ImageProcessorCore/Formats/Png/PngChunkTypes.cs
  8. 61
      src/ImageProcessorCore/Formats/Png/PngColorTypeInformation.cs
  9. 89
      src/ImageProcessorCore/Formats/Png/PngDecoder.cs
  10. 543
      src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs
  11. 87
      src/ImageProcessorCore/Formats/Png/PngEncoder.cs
  12. 504
      src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs
  13. 19
      src/ImageProcessorCore/Formats/Png/PngFormat.cs
  14. 62
      src/ImageProcessorCore/Formats/Png/PngHeader.cs
  15. 6
      src/ImageProcessorCore/Formats/Png/README.md
  16. 81
      src/ImageProcessorCore/Formats/Png/TrueColorReader.cs
  17. 174
      src/ImageProcessorCore/Formats/Png/Zlib/Adler32.cs
  18. 180
      src/ImageProcessorCore/Formats/Png/Zlib/Crc32.cs
  19. 60
      src/ImageProcessorCore/Formats/Png/Zlib/IChecksum.cs
  20. 2
      src/ImageProcessorCore/Formats/Png/Zlib/README.md
  21. 210
      src/ImageProcessorCore/Formats/Png/Zlib/ZlibDeflateStream.cs
  22. 205
      src/ImageProcessorCore/Formats/Png/Zlib/ZlibInflateStream.cs
  23. 8
      src/ImageProcessorCore/Image.cs
  24. 32
      src/ImageProcessorCore/Quantizers/IQuantizer.cs
  25. 102
      src/ImageProcessorCore/Quantizers/QuantizedImage.cs
  26. 58
      src/ImageProcessorCore/Quantizers/Wu/Box.cs
  27. 800
      src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs
  28. 2
      tests/ImageProcessorCore.Tests/FileTestBase.cs

2
src/ImageProcessorCore/Bootstrapper.cs

@ -39,7 +39,7 @@ namespace ImageProcessorCore
{
new BmpFormat(),
//new JpegFormat(),
//new PngFormat(),
new PngFormat(),
//new GifFormat()
};

6
src/ImageProcessorCore/Formats/Bmp/BmpDecoder.cs

@ -70,11 +70,11 @@ namespace ImageProcessorCore.Formats
}
/// <summary>
/// Decodes the image from the specified stream to the <see cref="ImageBase{T}"/>.
/// Decodes the image from the specified stream to the <see cref="ImageBase{T,TP}"/>.
/// </summary>
/// <param name="image">The <see cref="ImageBase{T}"/> to decode to.</param>
/// <param name="image">The <see cref="ImageBase{T,TP}"/> to decode to.</param>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
public void Decode<T,TP>(Image<T,TP> image, Stream stream)
public void Decode<T, TP>(Image<T, TP> image, Stream stream)
where T : IPackedVector<TP>, new()
where TP : struct
{

76
src/ImageProcessorCore/Formats/Png/GrayscaleReader.cs

@ -0,0 +1,76 @@
// <copyright file="GrayscaleReader.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Color reader for reading grayscale colors from a png file.
/// </summary>
internal sealed class GrayscaleReader : IColorReader
{
/// <summary>
/// Whether t also read the alpha channel.
/// </summary>
private readonly bool useAlpha;
/// <summary>
/// The current row.
/// </summary>
private int row;
/// <summary>
/// Initializes a new instance of the <see cref="GrayscaleReader"/> class.
/// </summary>
/// <param name="useAlpha">
/// If set to <c>true</c> the color reader will also read the
/// alpha channel from the scanline.
/// </param>
public GrayscaleReader(bool useAlpha)
{
this.useAlpha = useAlpha;
}
/// <inheritdoc/>
public void ReadScanline<T, TP>(byte[] scanline, T[] pixels, PngHeader header)
where T : IPackedVector<TP>, 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++;
}
}
}

29
src/ImageProcessorCore/Formats/Png/IColorReader.cs

@ -0,0 +1,29 @@
// <copyright file="IColorReader.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Encapsulates methods for color readers, which are responsible for reading
/// different color formats from a png file.
/// </summary>
public interface IColorReader
{
/// <summary>
/// Reads the specified scanline.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="scanline">The scanline.</param>
/// <param name="pixels">The pixels to read the image row to.</param>
/// <param name="header">
/// The header, which contains information about the png file, like
/// the width of the image and the height.
/// </param>
void ReadScanline<T, TP>(byte[] scanline, T[] pixels, PngHeader header)
where T : IPackedVector<TP>, new()
where TP : struct;
}
}

95
src/ImageProcessorCore/Formats/Png/PaletteIndexReader.cs

@ -0,0 +1,95 @@
// <copyright file="PaletteIndexReader.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// A color reader for reading palette indices from the png file.
/// </summary>
internal sealed class PaletteIndexReader : IColorReader
{
/// <summary>
/// The palette.
/// </summary>
private readonly byte[] palette;
/// <summary>
/// The alpha palette.
/// </summary>
private readonly byte[] paletteAlpha;
/// <summary>
/// The current row.
/// </summary>
private int row;
/// <summary>
/// Initializes a new instance of the <see cref="PaletteIndexReader"/> class.
/// </summary>
/// <param name="palette">The palette as simple byte array. It will contains 3 values for each
/// color, which represents the red-, the green- and the blue channel.</param>
/// <param name="paletteAlpha">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.</param>
public PaletteIndexReader(byte[] palette, byte[] paletteAlpha)
{
this.palette = palette;
this.paletteAlpha = paletteAlpha;
}
/// <inheritdoc/>
public void ReadScanline<T, TP>(byte[] scanline, T[] pixels, PngHeader header)
where T : IPackedVector<TP>, 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++;
}
}
}

39
src/ImageProcessorCore/Formats/Png/PngChunk.cs

@ -0,0 +1,39 @@
// <copyright file="PngChunk.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Stores header information about a chunk.
/// </summary>
internal sealed class PngChunk
{
/// <summary>
/// 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
/// </summary>
public int Length { get; set; }
/// <summary>
/// Gets or sets the chunk type as string with 4 chars.
/// </summary>
public string Type { get; set; }
/// <summary>
/// Gets or sets the data bytes appropriate to the chunk type, if any.
/// This field can be of zero length.
/// </summary>
public byte[] Data { get; set; }
/// <summary>
/// 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
/// </summary>
public uint Crc { get; set; }
}
}

62
src/ImageProcessorCore/Formats/Png/PngChunkTypes.cs

@ -0,0 +1,62 @@
// <copyright file="PngChunkTypes.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Contains a list of possible chunk type identifiers.
/// </summary>
internal static class PngChunkTypes
{
/// <summary>
/// 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.
/// </summary>
public const string Header = "IHDR";
/// <summary>
/// The PLTE chunk contains from 1 to 256 palette entries, each a three byte
/// series in the RGB format.
/// </summary>
public const string Palette = "PLTE";
/// <summary>
/// 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.
/// </summary>
public const string Data = "IDAT";
/// <summary>
/// This chunk must appear last. It marks the end of the PNG data stream.
/// The chunk's data field is empty.
/// </summary>
public const string End = "IEND";
/// <summary>
/// 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).
/// </summary>
public const string PaletteAlpha = "tRNS";
/// <summary>
/// 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.
/// </summary>
public const string Text = "tEXt";
/// <summary>
/// This chunk specifies the relationship between the image samples and the desired
/// display output intensity.
/// </summary>
public const string Gamma = "gAMA";
/// <summary>
/// The pHYs chunk specifies the intended pixel size or aspect ratio for display of the image.
/// </summary>
public const string Physical = "pHYs";
}
}

61
src/ImageProcessorCore/Formats/Png/PngColorTypeInformation.cs

@ -0,0 +1,61 @@
// <copyright file="PngColorTypeInformation.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
/// <summary>
/// Contains information that are required when loading a png with a specific color type.
/// </summary>
internal sealed class PngColorTypeInformation
{
/// <summary>
/// Initializes a new instance of the <see cref="PngColorTypeInformation"/> class with
/// the scanline factory, the function to create the color reader and the supported bit depths.
/// </summary>
/// <param name="scanlineFactor">The scanline factor.</param>
/// <param name="supportedBitDepths">The supported bit depths.</param>
/// <param name="scanlineReaderFactory">The factory to create the color reader.</param>
public PngColorTypeInformation(int scanlineFactor, int[] supportedBitDepths, Func<byte[], byte[], IColorReader> scanlineReaderFactory)
{
this.ChannelsPerColor = scanlineFactor;
this.ScanlineReaderFactory = scanlineReaderFactory;
this.SupportedBitDepths = supportedBitDepths;
}
/// <summary>
/// Gets an array with the bit depths that are supported for the color type
/// where this object is created for.
/// </summary>
/// <value>The supported bit depths that can be used in combination with this color type.</value>
public int[] SupportedBitDepths { get; private set; }
/// <summary>
/// Gets a function that is used the create the color reader for the color type where
/// this object is created for.
/// </summary>
/// <value>The factory function to create the color type.</value>
public Func<byte[], byte[], IColorReader> ScanlineReaderFactory { get; private set; }
/// <summary>
/// Gets a factor that is used when iterating through the scan lines.
/// </summary>
/// <value>The scanline factor.</value>
public int ChannelsPerColor { get; private set; }
/// <summary>
/// Creates the color reader for the color type where this object is create for.
/// </summary>
/// <param name="palette">The palette of the image. Can be null when no palette is used.</param>
/// <param name="paletteAlpha">The alpha palette of the image. Can be null when
/// no palette is used for the image or when the image has no alpha.</param>
/// <returns>The color reader for the image.</returns>
public IColorReader CreateColorReader(byte[] palette, byte[] paletteAlpha)
{
return this.ScanlineReaderFactory(palette, paletteAlpha);
}
}
}

89
src/ImageProcessorCore/Formats/Png/PngDecoder.cs

@ -0,0 +1,89 @@
// <copyright file="PngDecoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
/// <summary>
/// Encoder for generating an image out of a png encoded stream.
/// </summary>
/// <remarks>
/// At the moment the following features are supported:
/// <para>
/// <b>Filters:</b> all filters are supported.
/// </para>
/// <para>
/// <b>Pixel formats:</b>
/// <list type="bullet">
/// <item>RGBA (True color) with alpha (8 bit).</item>
/// <item>RGB (True color) without alpha (8 bit).</item>
/// <item>Greyscale with alpha (8 bit).</item>
/// <item>Greyscale without alpha (8 bit).</item>
/// <item>Palette Index with alpha (8 bit).</item>
/// <item>Palette Index without alpha (8 bit).</item>
/// </list>
/// </para>
/// </remarks>
public class PngDecoder : IImageDecoder
{
/// <summary>
/// Gets the size of the header for this image type.
/// </summary>
/// <value>The size of the header.</value>
public int HeaderSize => 8;
/// <summary>
/// Returns a value indicating whether the <see cref="IImageDecoder"/> supports the specified
/// file header.
/// </summary>
/// <param name="extension">The <see cref="string"/> containing the file extension.</param>
/// <returns>
/// True if the decoder supports the file extension; otherwise, false.
/// </returns>
public bool IsSupportedFileExtension(string extension)
{
Guard.NotNullOrEmpty(extension, "extension");
extension = extension.StartsWith(".") ? extension.Substring(1) : extension;
return extension.Equals("PNG", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Returns a value indicating whether the <see cref="IImageDecoder"/> supports the specified
/// file header.
/// </summary>
/// <param name="header">The <see cref="T:byte[]"/> containing the file header.</param>
/// <returns>
/// True if the decoder supports the file header; otherwise, false.
/// </returns>
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
}
/// <summary>
/// Decodes the image from the specified stream to the <see cref="ImageBase{T,TP}"/>.
/// </summary>
/// <param name="image">The <see cref="ImageBase{T,TP}"/> to decode to.</param>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
public void Decode<T, TP>(Image<T, TP> image, Stream stream)
where T : IPackedVector<TP>, new()
where TP : struct
{
new PngDecoderCore().Decode(image, stream);
}
}
}

543
src/ImageProcessorCore/Formats/Png/PngDecoderCore.cs

@ -0,0 +1,543 @@
// <copyright file="PngDecoderCore.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
/// <summary>
/// Performs the png decoding operation.
/// </summary>
internal class PngDecoderCore
{
/// <summary>
/// The dictionary of available color types.
/// </summary>
private static readonly Dictionary<int, PngColorTypeInformation> ColorTypes
= new Dictionary<int, PngColorTypeInformation>();
/// <summary>
/// The image to decode.
/// </summary>
//private IImage currentImage;
/// <summary>
/// The stream to decode from.
/// </summary>
private Stream currentStream;
/// <summary>
/// The png header.
/// </summary>
private PngHeader header;
/// <summary>
/// Initializes static members of the <see cref="PngDecoderCore"/> class.
/// </summary>
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)));
}
/// <summary>
/// Decodes the stream to the image.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The image to decode to.</param>
/// <param name="stream">The stream containing image data. </param>
/// <exception cref="ImageFormatException">
/// Thrown if the stream does not contain and end chunk.
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the image is larger than the maximum allowable size.
/// </exception>
public void Decode<T, TP>(Image<T, TP> image, Stream stream)
where T : IPackedVector<TP>, new()
where TP : struct
{
Image<T, TP> 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<T, TP>(dataStream, pixels, colorReader, colorTypeInformation);
}
image.SetPixels(this.header.Width, this.header.Height, pixels);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="left">The left neighbour pixel.</param>
/// <param name="above">The above neighbour pixel.</param>
/// <param name="upperLeft">The upper left neighbour pixel.</param>
/// <returns>
/// The <see cref="byte"/>.
/// </returns>
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;
}
/// <summary>
/// Reads the data chunk containing physical dimension data.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The image to read to.</param>
/// <param name="data">The data containing physical data.</param>
private void ReadPhysicalChunk<T, TP>(Image<T, TP> image, byte[] data)
where T : IPackedVector<TP>, 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;
}
/// <summary>
/// Calculates the scanline length.
/// </summary>
/// <param name="colorTypeInformation">The color type information.</param>
/// <returns>The <see cref="int"/> representing the length.</returns>
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;
}
/// <summary>
/// Calculates a scanline step.
/// </summary>
/// <param name="colorTypeInformation">The color type information.</param>
/// <returns>The <see cref="int"/> representing the length of each step.</returns>
private int CalculateScanlineStep(PngColorTypeInformation colorTypeInformation)
{
int scanlineStep = 1;
if (this.header.BitDepth >= 8)
{
scanlineStep = (colorTypeInformation.ChannelsPerColor * this.header.BitDepth) / 8;
}
return scanlineStep;
}
/// <summary>
/// Reads the scanlines within the image.
/// </summary>
/// <param name="dataStream">The <see cref="MemoryStream"/> containing data.</param>
/// <param name="pixels">
/// The <see cref="T:float[]"/> containing pixel data.</param>
/// <param name="colorReader">The color reader.</param>
/// <param name="colorTypeInformation">The color type information.</param>
private void ReadScanlines<T, TP>(MemoryStream dataStream, T[] pixels, IColorReader colorReader, PngColorTypeInformation colorTypeInformation)
where T : IPackedVector<TP>, 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<T, TP>(currentScanline, pixels, this.header);
column = -1;
this.Swap(ref currentScanline, ref lastScanline);
}
}
}
}
}
/// <summary>
/// Reads a text chunk containing image properties from the data.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The image to decode to.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
private void ReadTextChunk<T, TP>(Image<T, TP> image, byte[] data)
where T : IPackedVector<TP>, 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));
}
/// <summary>
/// Reads a header chunk from the data.
/// </summary>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
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];
}
/// <summary>
/// Validates the png header.
/// </summary>
/// <exception cref="ImageFormatException">
/// Thrown if the image does pass validation.
/// </exception>
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.");
}
}
/// <summary>
/// Reads a chunk from the stream.
/// </summary>
/// <returns>
/// The <see cref="PngChunk"/>.
/// </returns>
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;
}
/// <summary>
/// Reads the cycle redundancy chunk from the data.
/// </summary>
/// <param name="chunk">The chunk.</param>
/// <param name="typeBuffer">The type buffer.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid or corrupt.
/// </exception>
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!");
}
}
/// <summary>
/// Reads the chunk data from the stream.
/// </summary>
/// <param name="chunk">The chunk.</param>
private void ReadChunkData(PngChunk chunk)
{
chunk.Data = new byte[chunk.Length];
this.currentStream.Read(chunk.Data, 0, chunk.Length);
}
/// <summary>
/// Identifies the chunk type from the chunk.
/// </summary>
/// <param name="chunk">The chunk.</param>
/// <returns>
/// The <see cref="T:byte[]"/> containing identifying information.
/// </returns>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
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;
}
/// <summary>
/// Calculates the length of the given chunk.
/// </summary>
/// <param name="chunk">he chunk.</param>
/// <returns>
/// The <see cref="int"/> representing the chunk length.
/// </returns>
/// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid.
/// </exception>
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;
}
/// <summary>
/// Swaps two references.
/// </summary>
/// <typeparam name="TRef">The type of the references to swap.</typeparam>
/// <param name="lhs">The first reference.</param>
/// <param name="rhs">The second reference.</param>
private void Swap<TRef>(ref TRef lhs, ref TRef rhs)
where TRef : class
{
TRef tmp = lhs;
lhs = rhs;
rhs = tmp;
}
}
}

87
src/ImageProcessorCore/Formats/Png/PngEncoder.cs

@ -0,0 +1,87 @@
// <copyright file="PngEncoder.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
using ImageProcessorCore.Quantizers;
/// <summary>
/// Image encoder for writing image data to a stream in png format.
/// </summary>
public class PngEncoder : IImageEncoder
{
/// <summary>
/// Gets or sets the quality of output for images.
/// </summary>
public int Quality { get; set; }
/// <inheritdoc/>
public string MimeType => "image/png";
/// <inheritdoc/>
public string Extension => "png";
/// <summary>
/// The compression level 1-9.
/// <remarks>Defaults to 6.</remarks>
/// </summary>
public int CompressionLevel { get; set; } = 6;
/// <summary>
/// Gets or sets the gamma value, that will be written
/// the the stream, when the <see cref="WriteGamma"/> property
/// is set to true. The default value is 2.2F.
/// </summary>
/// <value>The gamma value of the image.</value>
public float Gamma { get; set; } = 2.2F;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 128;
/// <summary>
/// Gets or sets a value indicating whether this instance should write
/// gamma information to the stream. The default value is false.
/// </summary>
public bool WriteGamma { get; set; }
/// <inheritdoc/>
public bool IsSupportedFileExtension(string extension)
{
Guard.NotNullOrEmpty(extension, nameof(extension));
extension = extension.StartsWith(".") ? extension.Substring(1) : extension;
return extension.Equals(this.Extension, StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc/>
public void Encode<T, TP>(ImageBase<T, TP> image, Stream stream)
where T : IPackedVector<TP>, 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);
}
}
}

504
src/ImageProcessorCore/Formats/Png/PngEncoderCore.cs

@ -0,0 +1,504 @@
// <copyright file="PngEncoderCore.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
using System.Threading.Tasks;
using ImageProcessorCore.Quantizers;
/// <summary>
/// Performs the png encoding operation.
/// TODO: Perf. There's lots of array parsing going on here. This should be unmanaged.
/// </summary>
internal sealed class PngEncoderCore
{
/// <summary>
/// The maximum block size, defaults at 64k for uncompressed blocks.
/// </summary>
private const int MaxBlockSize = 65535;
/// <summary>
/// The number of bits required to encode the colors in the png.
/// </summary>
private byte bitDepth;
/// <summary>
/// The quantized image result.
/// </summary>
//private QuantizedImage<T,P> quantized;
/// <summary>
/// Gets or sets the quality of output for images.
/// </summary>
public int Quality { get; set; }
/// <summary>
/// The compression level 1-9.
/// <remarks>Defaults to 6.</remarks>
/// </summary>
public int CompressionLevel { get; set; } = 6;
/// <summary>
/// Gets or sets a value indicating whether this instance should write
/// gamma information to the stream. The default value is false.
/// </summary>
public bool WriteGamma { get; set; }
/// <summary>
/// Gets or sets the gamma value, that will be written
/// the the stream, when the <see cref="WriteGamma"/> property
/// is set to true. The default value is 2.2F.
/// </summary>
/// <value>The gamma value of the image.</value>
public float Gamma { get; set; } = 2.2F;
/// <summary>
/// The quantizer for reducing the color count.
/// </summary>
public IQuantizer Quantizer { get; set; }
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
public byte Threshold { get; set; } = 128;
/// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageBase{T,TP}"/>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The <see cref="ImageBase{T,TP}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<T, TP>(ImageBase<T, TP> image, Stream stream)
where T : IPackedVector<TP>, 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<T, TP> quantized = this.WritePaletteChunk(stream, header, image);
this.WritePhysicalChunk(stream, image);
this.WriteGammaChunk(stream);
using (IPixelAccessor<T, TP> pixels = image.Lock())
{
this.WriteDataChunks(stream, pixels, quantized);
}
this.WriteEndChunk(stream);
stream.Flush();
}
/// <summary>
/// Writes an integer to the byte array.
/// </summary>
/// <param name="data">The <see cref="T:byte[]"/> containing image data.</param>
/// <param name="offset">The amount to offset by.</param>
/// <param name="value">The value to write.</param>
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);
}
/// <summary>
/// Writes an integer to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="value">The value to write.</param>
private static void WriteInteger(Stream stream, int value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
stream.Write(buffer, 0, 4);
}
/// <summary>
/// Writes an unsigned integer to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="value">The value to write.</param>
private static void WriteInteger(Stream stream, uint value)
{
byte[] buffer = BitConverter.GetBytes(value);
Array.Reverse(buffer);
stream.Write(buffer, 0, 4);
}
/// <summary>
/// Writes the header chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
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);
}
/// <summary>
/// Writes the palette chunk to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="header">The <see cref="PngHeader"/>.</param>
/// <param name="image">The image to encode.</param>
private QuantizedImage<T, TP> WritePaletteChunk<T, TP>(Stream stream, PngHeader header, ImageBase<T, TP> image)
where T : IPackedVector<TP>, 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<T, TP> 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;
}
/// <summary>
/// Writes the physical dimension information to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="imageBase">The image base.</param>
private void WritePhysicalChunk<T, TP>(Stream stream, ImageBase<T, TP> imageBase)
where T : IPackedVector<TP>, new()
where TP : struct
{
Image<T, TP> image = imageBase as Image<T, TP>;
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);
}
}
/// <summary>
/// Writes the gamma information to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
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);
}
}
/// <summary>
/// Writes the pixel information to the stream.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="pixels">The image pixels.</param>
/// <param name="quantized">The quantized image.</param>
private void WriteDataChunks<T, TP>(Stream stream, IPixelAccessor<T, TP> pixels, QuantizedImage<T, TP> quantized)
where T : IPackedVector<TP>, 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);
}
}
/// <summary>
/// Writes the chunk end to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
private void WriteEndChunk(Stream stream)
{
this.WriteChunk(stream, PngChunkTypes.End, null);
}
/// <summary>
/// Writes a chunk to the stream.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
private void WriteChunk(Stream stream, string type, byte[] data)
{
this.WriteChunk(stream, type, data, 0, data?.Length ?? 0);
}
/// <summary>
/// Writes a chunk of a specified length to the stream at the given offset.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="type">The type of chunk to write.</param>
/// <param name="data">The <see cref="T:byte[]"/> containing data.</param>
/// <param name="offset">The position to offset the data at.</param>
/// <param name="length">The of the data to write.</param>
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);
}
}
}

19
src/ImageProcessorCore/Formats/Png/PngFormat.cs

@ -0,0 +1,19 @@
// <copyright file="PngFormat.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Encapsulates the means to encode and decode png images.
/// </summary>
public class PngFormat : IImageFormat
{
/// <inheritdoc/>
public IImageDecoder Decoder => new PngDecoder();
/// <inheritdoc/>
public IImageEncoder Encoder => new PngEncoder();
}
}

62
src/ImageProcessorCore/Formats/Png/PngHeader.cs

@ -0,0 +1,62 @@
// <copyright file="PngHeader.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// Represents the png header chunk.
/// </summary>
public sealed class PngHeader
{
/// <summary>
/// Gets or sets the dimension in x-direction of the image in pixels.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets the dimension in y-direction of the image in pixels.
/// </summary>
public int Height { get; set; }
/// <summary>
/// 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.
/// </summary>
public byte BitDepth { get; set; }
/// <summary>
/// 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).
/// </summary>
public byte ColorType { get; set; }
/// <summary>
/// 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.
/// </summary>
public byte CompressionMethod { get; set; }
/// <summary>
/// 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.
/// </summary>
public byte FilterMethod { get; set; }
/// <summary>
/// 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).
/// </summary>
public byte InterlaceMethod { get; set; }
}
}

6
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

81
src/ImageProcessorCore/Formats/Png/TrueColorReader.cs

@ -0,0 +1,81 @@
// <copyright file="TrueColorReader.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// 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.
/// </summary>
internal sealed class TrueColorReader : IColorReader
{
/// <summary>
/// Whether t also read the alpha channel.
/// </summary>
private readonly bool useAlpha;
/// <summary>
/// The current row.
/// </summary>
private int row;
/// <summary>
/// Initializes a new instance of the <see cref="TrueColorReader"/> class.
/// </summary>
/// <param name="useAlpha">if set to <c>true</c> the color reader will also read the
/// alpha channel from the scanline.</param>
public TrueColorReader(bool useAlpha)
{
this.useAlpha = useAlpha;
}
/// <inheritdoc/>
public void ReadScanline<T, TP>(byte[] scanline, T[] pixels, PngHeader header)
where T : IPackedVector<TP>, 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++;
}
}
}

174
src/ImageProcessorCore/Formats/Png/Zlib/Adler32.cs

@ -0,0 +1,174 @@
// <copyright file="Adler32.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.)"
/// </remarks>
/// <see cref="ZlibInflateStream"/>
/// <see cref="ZlibDeflateStream"/>
internal sealed class Adler32 : IChecksum
{
/// <summary>
/// largest prime smaller than 65536
/// </summary>
private const uint Base = 65521;
/// <summary>
/// The checksum calculated to far.
/// </summary>
private uint checksum;
/// <summary>
/// Initializes a new instance of the <see cref="Adler32"/> class.
/// The checksum starts off with a value of 1.
/// </summary>
public Adler32()
{
this.Reset();
}
/// <inheritdoc/>
public long Value => this.checksum;
/// <inheritdoc/>
public void Reset()
{
this.checksum = 1;
}
/// <summary>
/// Updates the checksum with a byte value.
/// </summary>
/// <param name="value">
/// The data value to add. The high byte of the int is ignored.
/// </param>
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;
}
/// <inheritdoc/>
public void Update(byte[] buffer)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
this.Update(buffer, 0, buffer.Length);
}
/// <inheritdoc/>
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;
}
}
}

180
src/ImageProcessorCore/Formats/Png/Zlib/Crc32.cs

@ -0,0 +1,180 @@
// <copyright file="Crc32.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
internal sealed class Crc32 : IChecksum
{
/// <summary>
/// The cycle redundancy check seed
/// </summary>
private const uint CrcSeed = 0xFFFFFFFF;
/// <summary>
/// The table of all possible eight bit values for fast lookup.
/// </summary>
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
};
/// <summary>
/// The data checksum so far.
/// </summary>
private uint crc;
/// <inheritdoc/>
public long Value
{
get
{
return this.crc;
}
set
{
this.crc = (uint)value;
}
}
/// <inheritdoc/>
public void Reset()
{
this.crc = 0;
}
/// <summary>
/// Updates the checksum with the given value.
/// </summary>
/// <param name="value">The byte is taken as the lower 8 bits of value.</param>
public void Update(int value)
{
this.crc ^= CrcSeed;
this.crc = CrcTable[(this.crc ^ value) & 0xFF] ^ (this.crc >> 8);
this.crc ^= CrcSeed;
}
/// <inheritdoc/>
public void Update(byte[] buffer)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
this.Update(buffer, 0, buffer.Length);
}
/// <inheritdoc/>
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;
}
}
}

60
src/ImageProcessorCore/Formats/Png/Zlib/IChecksum.cs

@ -0,0 +1,60 @@
// <copyright file="IChecksum.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
/// <summary>
/// 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
/// <code>Value</code>. The complete checksum object can also be reset
/// so it can be used again with new data.
/// </summary>
public interface IChecksum
{
/// <summary>
/// Gets the data checksum computed so far.
/// </summary>
long Value
{
get;
}
/// <summary>
/// Resets the data checksum as if no update was ever called.
/// </summary>
void Reset();
/// <summary>
/// Adds one byte to the data checksum.
/// </summary>
/// <param name = "value">
/// The data value to add. The high byte of the integer is ignored.
/// </param>
void Update(int value);
/// <summary>
/// Updates the data checksum with the bytes taken from the array.
/// </summary>
/// <param name="buffer">
/// buffer an array of bytes
/// </param>
void Update(byte[] buffer);
/// <summary>
/// Adds the byte array to the data checksum.
/// </summary>
/// <param name = "buffer">
/// The buffer which contains the data
/// </param>
/// <param name = "offset">
/// The offset in the buffer where the data starts
/// </param>
/// <param name = "count">
/// the number of data bytes to add.
/// </param>
void Update(byte[] buffer, int offset, int count);
}
}

2
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

210
src/ImageProcessorCore/Formats/Png/Zlib/ZlibDeflateStream.cs

@ -0,0 +1,210 @@
// <copyright file="ZlibDeflateStream.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
using System.IO.Compression;
/// <summary>
/// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm.
/// </summary>
internal sealed class ZlibDeflateStream : Stream
{
/// <summary>
/// The raw stream containing the uncompressed image data.
/// </summary>
private readonly Stream rawStream;
/// <summary>
/// Computes the checksum for the data stream.
/// </summary>
private readonly Adler32 adler32 = new Adler32();
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// 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.
/// </remarks>
private bool isDisposed;
// The stream responsible for decompressing the input stream.
private DeflateStream deflateStream;
/// <summary>
/// Initializes a new instance of <see cref="ZlibDeflateStream"/>
/// </summary>
/// <param name="stream">The stream to compress.</param>
/// <param name="compressionLevel">The compression level.</param>
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);
}
/// <inheritdoc/>
public override bool CanRead => false;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => true;
/// <inheritdoc/>
public override long Length
{
get
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override void Flush()
{
this.deflateStream?.Flush();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
this.deflateStream.Write(buffer, offset, count);
this.adler32.Update(buffer, offset, count);
}
/// <inheritdoc/>
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;
}
}
}

205
src/ImageProcessorCore/Formats/Png/Zlib/ZlibInflateStream.cs

@ -0,0 +1,205 @@
// <copyright file="ZlibInflateStream.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Formats
{
using System;
using System.IO;
using System.IO.Compression;
/// <summary>
/// Provides methods and properties for decompressing streams by using the Zlib Deflate algorithm.
/// </summary>
internal sealed class ZlibInflateStream : Stream
{
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// 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.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The raw stream containing the uncompressed image data.
/// </summary>
private readonly Stream rawStream;
/// <summary>
/// The read crc data.
/// </summary>
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);
}
/// <inheritdoc/>
public override bool CanRead => true;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => false;
/// <inheritdoc/>
public override long Length
{
get
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
/// <inheritdoc/>
public override void Flush()
{
this.deflateStream?.Flush();
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
/// <inheritdoc/>
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;
}
}
}

8
src/ImageProcessorCore/Image.cs

@ -13,6 +13,14 @@ namespace ImageProcessorCore
/// </summary>
public class Image : Image<Color, uint>
{
/// <summary>
/// Initializes a new instance of the <see cref="Image"/> class
/// with the height and the width of the image.
/// </summary>
public Image()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Image"/> class
/// with the height and the width of the image.

32
src/ImageProcessorCore/Quantizers/IQuantizer.cs

@ -0,0 +1,32 @@
// <copyright file="IQuantizer.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Quantizers
{
/// <summary>
/// Provides methods for allowing quantization of images pixels.
/// </summary>
public interface IQuantizer
{
/// <summary>
/// Gets or sets the transparency threshold.
/// </summary>
byte Threshold { get; set; }
/// <summary>
/// Quantize an image and return the resulting output pixels.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="image">The image to quantize.</param>
/// <param name="maxColors">The maximum number of colors to return.</param>
/// <returns>
/// A <see cref="T:QuantizedImage"/> representing a quantized version of the image pixels.
/// </returns>
QuantizedImage<T, TP> Quantize<T, TP>(ImageBase<T, TP> image, int maxColors)
where T : IPackedVector<TP>, new()
where TP : struct;
}
}

102
src/ImageProcessorCore/Quantizers/QuantizedImage.cs

@ -0,0 +1,102 @@
// <copyright file="QuantizedImage.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Quantizers
{
using System;
using System.Threading.Tasks;
/// <summary>
/// Represents a quantized image where the pixels indexed by a color palette.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
public class QuantizedImage<T, TP>
where T : IPackedVector<TP>, new()
where TP : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="QuantizedImage{T,TP}"/> class.
/// </summary>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="palette">The color palette.</param>
/// <param name="pixels">The quantized pixels.</param>
/// <param name="transparentIndex">The transparency index.</param>
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;
}
/// <summary>
/// Gets the width of this <see cref="T:QuantizedImage"/>.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the height of this <see cref="T:QuantizedImage"/>.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the color palette of this <see cref="T:QuantizedImage"/>.
/// </summary>
public T[] Palette { get; }
/// <summary>
/// Gets the pixels of this <see cref="T:QuantizedImage"/>.
/// </summary>
public byte[] Pixels { get; }
/// <summary>
/// Gets the transparent index
/// </summary>
public int TransparentIndex { get; }
/// <summary>
/// Converts this quantized image to a normal image.
/// </summary>
/// <returns>
/// The <see cref="Image"/>
/// </returns>
public Image<T, TP> ToImage()
{
Image<T, TP> image = new Image<T, TP>();
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;
}
}
}

58
src/ImageProcessorCore/Quantizers/Wu/Box.cs

@ -0,0 +1,58 @@
// <copyright file="Box.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Quantizers
{
/// <summary>
/// Represents a box color cube.
/// </summary>
internal sealed class Box
{
/// <summary>
/// Gets or sets the min red value, exclusive.
/// </summary>
public int R0 { get; set; }
/// <summary>
/// Gets or sets the max red value, inclusive.
/// </summary>
public int R1 { get; set; }
/// <summary>
/// Gets or sets the min green value, exclusive.
/// </summary>
public int G0 { get; set; }
/// <summary>
/// Gets or sets the max green value, inclusive.
/// </summary>
public int G1 { get; set; }
/// <summary>
/// Gets or sets the min blue value, exclusive.
/// </summary>
public int B0 { get; set; }
/// <summary>
/// Gets or sets the max blue value, inclusive.
/// </summary>
public int B1 { get; set; }
/// <summary>
/// Gets or sets the min alpha value, exclusive.
/// </summary>
public int A0 { get; set; }
/// <summary>
/// Gets or sets the max alpha value, inclusive.
/// </summary>
public int A1 { get; set; }
/// <summary>
/// Gets or sets the volume.
/// </summary>
public int Volume { get; set; }
}
}

800
src/ImageProcessorCore/Quantizers/Wu/WuQuantizer.cs

@ -0,0 +1,800 @@
// <copyright file="WuQuantizer.cs" company="James Jackson-South">
// Copyright © James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Quantizers
{
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
/// <summary>
/// An implementation of Wu's color quantizer with alpha channel.
/// </summary>
/// <remarks>
/// <para>
/// Based on C Implementation of Xiaolin Wu's Color Quantizer (v. 2)
/// (see Graphics Gems volume II, pages 126-133)
/// (<see href="http://www.ece.mcmaster.ca/~xwu/cq.c"/>).
/// </para>
/// <para>
/// This adaptation is based on the excellent JeremyAnsel.ColorQuant by Jérémy Ansel
/// <see href="https://github.com/JeremyAnsel/JeremyAnsel.ColorQuant"/>
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public sealed class WuQuantizer : IQuantizer
{
/// <summary>
/// The epsilon for comparing floating point numbers.
/// </summary>
private const float Epsilon = 0.001f;
/// <summary>
/// The index bits.
/// </summary>
private const int IndexBits = 6;
/// <summary>
/// The index alpha bits.
/// </summary>
private const int IndexAlphaBits = 3;
/// <summary>
/// The index count.
/// </summary>
private const int IndexCount = (1 << IndexBits) + 1;
/// <summary>
/// The index alpha count.
/// </summary>
private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1;
/// <summary>
/// The table length.
/// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
/// <summary>
/// Moment of <c>P(c)</c>.
/// </summary>
private readonly long[] vwt;
/// <summary>
/// Moment of <c>r*P(c)</c>.
/// </summary>
private readonly long[] vmr;
/// <summary>
/// Moment of <c>g*P(c)</c>.
/// </summary>
private readonly long[] vmg;
/// <summary>
/// Moment of <c>b*P(c)</c>.
/// </summary>
private readonly long[] vmb;
/// <summary>
/// Moment of <c>a*P(c)</c>.
/// </summary>
private readonly long[] vma;
/// <summary>
/// Moment of <c>c^2*P(c)</c>.
/// </summary>
private readonly double[] m2;
/// <summary>
/// Color space tag.
/// </summary>
private readonly byte[] tag;
/// <summary>
/// Initializes a new instance of the <see cref="WuQuantizer"/> class.
/// </summary>
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];
}
/// <inheritdoc/>
public byte Threshold { get; set; }
/// <inheritdoc/>
public QuantizedImage<T, TP> Quantize<T, TP>(ImageBase<T, TP> image, int maxColors)
where T : IPackedVector<TP>, new()
where TP : struct
{
Guard.NotNull(image, nameof(image));
int colorCount = maxColors.Clamp(1, 256);
this.Clear();
using (IPixelAccessor<T, TP> imagePixels = image.Lock())
{
this.Build3DHistogram(imagePixels);
this.Get3DMoments();
Box[] cube;
this.BuildCube(out cube, ref colorCount);
return this.GenerateResult(imagePixels, colorCount, cube);
}
}
/// <summary>
/// Gets an index.
/// </summary>
/// <param name="r">The red value.</param>
/// <param name="g">The green value.</param>
/// <param name="b">The blue value.</param>
/// <param name="a">The alpha value.</param>
/// <returns>The index.</returns>
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;
}
/// <summary>
/// Computes sum over a box of any given statistic.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="moment">The moment.</param>
/// <returns>The result.</returns>
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)];
}
/// <summary>
/// Computes part of Volume(cube, moment) that doesn't depend on r1, g1, or b1 (depending on direction).
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="direction">The direction.</param>
/// <param name="moment">The moment.</param>
/// <returns>The result.</returns>
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));
}
}
/// <summary>
/// Computes remainder of Volume(cube, moment), substituting position for r1, g1, or b1 (depending on direction).
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="direction">The direction.</param>
/// <param name="position">The position.</param>
/// <param name="moment">The moment.</param>
/// <returns>The result.</returns>
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));
}
}
/// <summary>
/// Clears the tables.
/// </summary>
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);
}
/// <summary>
/// Builds a 3-D color histogram of <c>counts, r/g/b, c^2</c>.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="pixels">The pixel accessor.</param>
private void Build3DHistogram<T, TP>(IPixelAccessor<T, TP> pixels)
where T : IPackedVector<TP>, 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);
}
}
}
/// <summary>
/// Converts the histogram into moments so that we can rapidly calculate
/// the sums of the above quantities over any desired box.
/// </summary>
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];
}
}
}
}
}
/// <summary>
/// Computes the weighted variance of a box cube.
/// </summary>
/// <param name="cube">The cube.</param>
/// <returns>The <see cref="double"/>.</returns>
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));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="direction">The direction.</param>
/// <param name="first">The first position.</param>
/// <param name="last">The last position.</param>
/// <param name="cut">The cutting point.</param>
/// <param name="wholeR">The whole red.</param>
/// <param name="wholeG">The whole green.</param>
/// <param name="wholeB">The whole blue.</param>
/// <param name="wholeA">The whole alpha.</param>
/// <param name="wholeW">The whole weight.</param>
/// <returns>The <see cref="double"/>.</returns>
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;
}
/// <summary>
/// Cuts a box.
/// </summary>
/// <param name="set1">The first set.</param>
/// <param name="set2">The second set.</param>
/// <returns>Returns a value indicating whether the box has been split.</returns>
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;
}
/// <summary>
/// Marks a color space tag.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="label">A label.</param>
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;
}
}
}
}
}
/// <summary>
/// Builds the cube.
/// </summary>
/// <param name="cube">The cube.</param>
/// <param name="colorCount">The color count.</param>
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;
}
}
}
/// <summary>
/// Generates the quantized result.
/// </summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
/// <param name="imagePixels">The image pixels.</param>
/// <param name="colorCount">The color count.</param>
/// <param name="cube">The cube.</param>
/// <returns>The result.</returns>
private QuantizedImage<T, TP> GenerateResult<T, TP>(IPixelAccessor<T, TP> imagePixels, int colorCount, Box[] cube)
where T : IPackedVector<TP>, new()
where TP : struct
{
List<T> pallette = new List<T>();
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<T, TP>(width, height, pallette.ToArray(), pixels, transparentIndex);
}
}
}

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

Loading…
Cancel
Save