mirror of https://github.com/SixLabors/ImageSharp
Browse Source
Former-commit-id: b222f96370adf0a3f87d3e2d762e270fac829a5a Former-commit-id: eecf02f023c9d43ff3e54dd4fcfbfbfdde22f543 Former-commit-id: 03bf7ed3356ee33b96e8dcff9b7a53b26e8f919faf/merge-core
28 changed files with 3589 additions and 5 deletions
@ -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++; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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++; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
Encoder/Decoder adapted from: |
|||
|
|||
https://github.com/yufeih/Nine.Imaging/ |
|||
https://imagetools.codeplex.com/ |
|||
https://github.com/leonbloy/pngcs |
|||
|
|||
@ -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++; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
Adler32.cs and Crc32.cs have been copied from |
|||
https://github.com/ygrenier/SharpZipLib.Portable |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue