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