Browse Source

Merge pull request #712 from SixLabors/js/multi-bit-png-encoding

Add 1, 2, and 4 bit Png Encoding
af/merge-core
James Jackson-South 8 years ago
committed by GitHub
parent
commit
e123d820a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  2. 539
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  3. 2
      src/ImageSharp/Formats/Png/PngEncoder.cs
  4. 185
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  5. 600
      src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
  6. 12
      src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs
  7. 160
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  8. 6
      tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs
  9. 8
      tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs

2
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Gets the filter method.
/// </summary>
PngFilterMethod FilterMethod { get; }
PngFilterMethod? FilterMethod { get; }
/// <summary>
/// Gets the compression level 1-9.

539
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -92,6 +92,11 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private readonly bool ignoreMetadata;
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The stream to decode from.
/// </summary>
@ -200,12 +205,11 @@ namespace SixLabors.ImageSharp.Formats.Png
public PngDecoderCore(Configuration configuration, IPngDecoderOptions options)
{
this.configuration = configuration ?? Configuration.Default;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.textEncoding = options.TextEncoding ?? PngConstants.DefaultEncoding;
this.ignoreMetadata = options.IgnoreMetadata;
}
private MemoryAllocator MemoryAllocator => this.configuration.MemoryAllocator;
/// <summary>
/// Decodes the stream to the image.
/// </summary>
@ -391,19 +395,19 @@ namespace SixLabors.ImageSharp.Formats.Png
return false;
}
buffer = this.MemoryAllocator.AllocateManagedByteBuffer(bytesPerScanline * 8 / bits, AllocationOptions.Clean);
byte[] result = buffer.Array;
buffer = this.memoryAllocator.AllocateManagedByteBuffer(bytesPerScanline * 8 / bits, AllocationOptions.Clean);
ref byte sourceRef = ref MemoryMarshal.GetReference(source);
ref byte resultRef = ref buffer.Array[0];
int mask = 0xFF >> (8 - bits);
int resultOffset = 0;
for (int i = 0; i < bytesPerScanline - 1; i++)
for (int i = 0; i < bytesPerScanline; i++)
{
byte b = source[i];
byte b = Unsafe.Add(ref sourceRef, i);
for (int shift = 0; shift < 8; shift += bits)
{
int colorIndex = (b >> (8 - bits - shift)) & mask;
result[resultOffset] = (byte)colorIndex;
Unsafe.Add(ref resultRef, resultOffset) = (byte)colorIndex;
resultOffset++;
}
}
@ -470,7 +474,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.bytesPerSample = this.header.BitDepth / 8;
}
this.previousScanline = this.MemoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
this.previousScanline = this.memoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
this.scanline = this.configuration.MemoryAllocator.AllocateManagedByteBuffer(this.bytesPerScanline, AllocationOptions.Clean);
}
@ -612,7 +616,7 @@ namespace SixLabors.ImageSharp.Formats.Png
throw new ImageFormatException("Unknown filter type.");
}
this.ProcessDefilteredScanline(this.scanline.Array, image);
this.ProcessDefilteredScanline(scanlineSpan, image);
this.SwapBuffers();
this.currentRow++;
@ -718,14 +722,13 @@ namespace SixLabors.ImageSharp.Formats.Png
private void ProcessDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, ImageFrame<TPixel> pixels)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
Span<TPixel> rowSpan = pixels.GetPixelRowSpan(this.currentRow);
// Trim the first marker byte from the buffer
ReadOnlySpan<byte> trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1);
// Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent.
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline, this.header.BitDepth, out IManagedByteBuffer buffer)
ReadOnlySpan<byte> scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline - 1, this.header.BitDepth, out IManagedByteBuffer buffer)
? buffer.GetSpan()
: trimmed;
@ -733,196 +736,60 @@ namespace SixLabors.ImageSharp.Formats.Png
{
case PngColorType.Grayscale:
int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1);
if (!this.hasTrans)
{
if (this.header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = 0, o = 0; x < this.header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.R = luminance;
rgb48.G = luminance;
rgb48.B = luminance;
pixel.PackFromRgb48(rgb48);
rowSpan[x] = pixel;
}
}
else
{
// TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method.
var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = 0; x < this.header.Width; x++)
{
byte luminance = (byte)(scanlineSpan[x] * factor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
}
else
{
if (this.header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < this.header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(this.luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = 0; x < this.header.Width; x++)
{
byte luminance = (byte)(scanlineSpan[x] * factor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = luminance.Equals(this.luminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
}
PngScanlineProcessor.ProcessGrayscaleScanline(
this.header,
scanlineSpan,
rowSpan,
this.hasTrans,
this.luminance16Trans,
this.luminanceTrans);
break;
case PngColorType.GrayscaleWithAlpha:
if (this.header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < this.header.Width; x++, o += 4)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = alpha;
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = 0; x < this.header.Width; x++)
{
int offset = x * this.bytesPerPixel;
byte luminance = scanlineSpan[offset];
byte alpha = scanlineSpan[offset + this.bytesPerSample];
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = alpha;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
PngScanlineProcessor.ProcessGrayscaleWithAlphaScanline(
this.header,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
this.bytesPerSample);
break;
case PngColorType.Palette:
this.ProcessScanlineFromPalette(scanlineSpan, rowSpan);
PngScanlineProcessor.ProcessPaletteScanline(
this.header,
scanlineSpan,
rowSpan,
this.palette,
this.paletteAlpha);
break;
case PngColorType.Rgb:
if (!this.hasTrans)
{
if (this.header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = 0, o = 0; x < this.header.Width; x++, o += 6)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2));
pixel.PackFromRgb48(rgb48);
rowSpan[x] = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgb24Bytes(scanlineSpan, rowSpan, this.header.Width);
}
}
else
{
if (this.header.BitDepth == 16)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < this.header.Width; x++, o += 6)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(this.rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(scanlineSpan);
for (int x = 0; x < this.header.Width; x++)
{
ref readonly Rgb24 rgb24 = ref rgb24Span[x];
Rgba32 rgba32 = default;
rgba32.Rgb = rgb24;
rgba32.A = rgb24.Equals(this.rgb24Trans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
}
PngScanlineProcessor.ProcessRgbScanline(
this.header,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
this.bytesPerSample,
this.hasTrans,
this.rgb48Trans,
this.rgb24Trans);
break;
case PngColorType.RgbWithAlpha:
if (this.header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < this.header.Width; x++, o += 8)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 6, 2));
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgba32Bytes(scanlineSpan, rowSpan, this.header.Width);
}
PngScanlineProcessor.ProcessRgbaScanline(
this.header,
scanlineSpan,
rowSpan,
this.bytesPerPixel,
this.bytesPerSample);
break;
}
@ -941,8 +808,6 @@ namespace SixLabors.ImageSharp.Formats.Png
private void ProcessInterlacedDefilteredScanline<TPixel>(ReadOnlySpan<byte> defilteredScanline, Span<TPixel> rowSpan, int pixelOffset = 0, int increment = 1)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
// Trim the first marker byte from the buffer
ReadOnlySpan<byte> trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1);
@ -955,244 +820,70 @@ namespace SixLabors.ImageSharp.Formats.Png
{
case PngColorType.Grayscale:
int factor = 255 / (ImageMaths.GetColorCountForBitDepth(this.header.BitDepth) - 1);
if (!this.hasTrans)
{
if (this.header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.R = luminance;
rgb48.G = luminance;
rgb48.B = luminance;
pixel.PackFromRgb48(rgb48);
rowSpan[x] = pixel;
}
}
else
{
// TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method.
var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o++)
{
byte luminance = (byte)(scanlineSpan[o] * factor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
}
else
{
if (this.header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(this.luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < this.header.Width; x += increment)
{
byte luminance = (byte)(scanlineSpan[x] * factor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = luminance.Equals(this.luminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
}
PngScanlineProcessor.ProcessInterlacedGrayscaleScanline(
this.header,
scanlineSpan,
rowSpan,
pixelOffset,
increment,
this.hasTrans,
this.luminance16Trans,
this.luminanceTrans);
break;
case PngColorType.GrayscaleWithAlpha:
if (this.header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 4)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = alpha;
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < this.header.Width; x += increment)
{
int offset = x * this.bytesPerPixel;
byte luminance = scanlineSpan[offset];
byte alpha = scanlineSpan[offset + this.bytesPerSample];
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = alpha;
pixel.PackFromRgba32(rgba32);
rowSpan[x] = pixel;
}
}
PngScanlineProcessor.ProcessInterlacedGrayscaleWithAlphaScanline(
this.header,
scanlineSpan,
rowSpan,
pixelOffset,
increment,
this.bytesPerPixel,
this.bytesPerSample);
break;
case PngColorType.Palette:
Span<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(this.palette);
if (this.paletteAlpha?.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o++)
{
int index = scanlineSpan[o];
rgba.A = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : byte.MaxValue;
rgba.Rgb = palettePixels[index];
pixel.PackFromRgba32(rgba);
rowSpan[x] = pixel;
}
}
else
{
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o++)
{
int index = scanlineSpan[o];
rgba.Rgb = palettePixels[index];
pixel.PackFromRgba32(rgba);
rowSpan[x] = pixel;
}
}
PngScanlineProcessor.ProcessInterlacedPaletteScanline(
this.header,
scanlineSpan,
rowSpan,
pixelOffset,
increment,
this.palette,
this.paletteAlpha);
break;
case PngColorType.Rgb:
if (this.header.BitDepth == 16)
{
if (this.hasTrans)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 6)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(this.rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
Rgb48 rgb48 = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 6)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2));
pixel.PackFromRgb48(rgb48);
rowSpan[x] = pixel;
}
}
}
else
{
if (this.hasTrans)
{
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += this.bytesPerPixel)
{
rgba.R = scanlineSpan[o];
rgba.G = scanlineSpan[o + this.bytesPerSample];
rgba.B = scanlineSpan[o + (2 * this.bytesPerSample)];
rgba.A = this.rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba);
rowSpan[x] = pixel;
}
}
else
{
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += this.bytesPerPixel)
{
rgba.R = scanlineSpan[o];
rgba.G = scanlineSpan[o + this.bytesPerSample];
rgba.B = scanlineSpan[o + (2 * this.bytesPerSample)];
pixel.PackFromRgba32(rgba);
rowSpan[x] = pixel;
}
}
}
PngScanlineProcessor.ProcessInterlacedRgbScanline(
this.header,
scanlineSpan,
rowSpan,
pixelOffset,
increment,
this.bytesPerPixel,
this.bytesPerSample,
this.hasTrans,
this.rgb48Trans,
this.rgb24Trans);
break;
case PngColorType.RgbWithAlpha:
if (this.header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += 8)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 4, 2));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 6, 2));
pixel.PackFromRgba64(rgba64);
rowSpan[x] = pixel;
}
}
else
{
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < this.header.Width; x += increment, o += this.bytesPerPixel)
{
rgba.R = scanlineSpan[o];
rgba.G = scanlineSpan[o + this.bytesPerSample];
rgba.B = scanlineSpan[o + (2 * this.bytesPerSample)];
rgba.A = scanlineSpan[o + (3 * this.bytesPerSample)];
pixel.PackFromRgba32(rgba);
rowSpan[x] = pixel;
}
}
PngScanlineProcessor.ProcessInterlacedRgbaScanline(
this.header,
scanlineSpan,
rowSpan,
pixelOffset,
increment,
this.bytesPerPixel,
this.bytesPerSample);
break;
}
@ -1245,50 +936,6 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
/// <summary>
/// Processes a scanline that uses a palette
/// </summary>
/// <typeparam name="TPixel">The type of pixel we are expanding to</typeparam>
/// <param name="scanline">The defiltered scanline</param>
/// <param name="row">The current output image row</param>
private void ProcessScanlineFromPalette<TPixel>(ReadOnlySpan<byte> scanline, Span<TPixel> row)
where TPixel : struct, IPixel<TPixel>
{
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(this.palette);
var color = default(TPixel);
if (this.paletteAlpha?.Length > 0)
{
Rgba32 rgba = default;
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
for (int x = 0; x < this.header.Width; x++)
{
int index = scanline[x];
rgba.A = this.paletteAlpha.Length > index ? this.paletteAlpha[index] : byte.MaxValue;
rgba.Rgb = palettePixels[index];
color.PackFromRgba32(rgba);
row[x] = color;
}
}
else
{
// TODO: We should have PackFromRgb24.
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = 0; x < this.header.Width; x++)
{
int index = scanline[x];
rgba.Rgb = palettePixels[index];
color.PackFromRgba32(rgba);
row[x] = color;
}
}
}
/// <summary>
/// Reads a header chunk from the data.
/// </summary>

2
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary>
/// Gets or sets the filter method.
/// </summary>
public PngFilterMethod FilterMethod { get; set; } = PngFilterMethod.Paeth;
public PngFilterMethod? FilterMethod { get; set; }
/// <summary>
/// Gets or sets the compression level 1-9.

185
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -3,7 +3,11 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Png.Filters;
@ -21,6 +25,21 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
internal sealed class PngEncoderCore : IDisposable
{
/// <summary>
/// The dictionary of available color types.
/// </summary>
private static readonly Dictionary<PngColorType, byte[]> ColorTypes = new Dictionary<PngColorType, byte[]>()
{
[PngColorType.Grayscale] = new byte[] { 1, 2, 4, 8, 16 },
[PngColorType.Rgb] = new byte[] { 8, 16 },
[PngColorType.Palette] = new byte[] { 1, 2, 4, 8 },
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 },
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
};
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
@ -158,7 +177,11 @@ namespace SixLabors.ImageSharp.Formats.Png
this.memoryAllocator = memoryAllocator;
this.pngBitDepth = options.BitDepth;
this.pngColorType = options.ColorType;
this.pngFilterMethod = options.FilterMethod;
// Specification recommends default filter method None for paletted images and Paeth for others.
this.pngFilterMethod = options.FilterMethod ?? (options.ColorType.Equals(PngColorType.Palette)
? PngFilterMethod.None
: PngFilterMethod.Paeth);
this.compressionLevel = options.CompressionLevel;
this.gamma = options.Gamma;
this.quantizer = options.Quantizer;
@ -189,28 +212,37 @@ namespace SixLabors.ImageSharp.Formats.Png
this.pngBitDepth = this.pngBitDepth ?? pngMetaData.BitDepth;
this.use16Bit = this.pngBitDepth.Equals(PngBitDepth.Bit16);
// Ensure we are not allowing impossible combinations.
if (!ColorTypes.ContainsKey(this.pngColorType.Value))
{
throw new NotSupportedException("Color type is not supported or not valid.");
}
stream.Write(PngConstants.HeaderBytes, 0, PngConstants.HeaderBytes.Length);
QuantizedFrame<TPixel> quantized = null;
ReadOnlySpan<byte> quantizedPixelsSpan = default;
if (this.pngColorType == PngColorType.Palette)
{
byte bits;
byte bits = (byte)this.pngBitDepth;
if (!ColorTypes[this.pngColorType.Value].Contains(bits))
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
// Use the metadata to determine what quantization depth to use if no quantizer has been set.
if (this.quantizer == null)
{
bits = (byte)Math.Min(8u, (short)this.pngBitDepth);
int colorSize = ImageMaths.GetColorCountForBitDepth(bits);
this.quantizer = new WuQuantizer(colorSize);
this.quantizer = new WuQuantizer(ImageMaths.GetColorCountForBitDepth(bits));
}
// Create quantized frame returning the palette and set the bit depth.
quantized = this.quantizer.CreateFrameQuantizer<TPixel>().QuantizeFrame(image.Frames.RootFrame);
quantizedPixelsSpan = quantized.GetPixelSpan();
bits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
byte quantizedBits = (byte)ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
bits = Math.Max(bits, quantizedBits);
// Png only supports in four pixel depths: 1, 2, 4, and 8 bits when using the PLTE chunk
// We check again for the bit depth as the bit depth of the color palette from a given quantizer might not
// be within the acceptable range.
if (bits == 3)
{
bits = 4;
@ -224,7 +256,11 @@ namespace SixLabors.ImageSharp.Formats.Png
}
else
{
this.bitDepth = (byte)(this.use16Bit ? 16 : 8);
this.bitDepth = (byte)this.pngBitDepth;
if (!ColorTypes[this.pngColorType.Value].Contains(this.bitDepth))
{
throw new NotSupportedException("Bit depth is not supported or not valid.");
}
}
this.bytesPerPixel = this.CalculateBytesPerPixel();
@ -249,7 +285,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePhysicalChunk(stream, metaData);
this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, metaData);
this.WriteDataChunks(image.Frames.RootFrame, quantizedPixelsSpan, stream);
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
this.WriteEndChunk(stream);
stream.Flush();
@ -280,32 +316,55 @@ namespace SixLabors.ImageSharp.Formats.Png
const float RX = .2126F;
const float GX = .7152F;
const float BX = .0722F;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
Span<byte> rawScanlineSpan = this.rawScanline.GetSpan();
ref byte rawScanlineSpanRef = ref MemoryMarshal.GetReference(rawScanlineSpan);
if (this.pngColorType.Equals(PngColorType.Grayscale))
{
// TODO: Realistically we should support 1, 2, 4, 8, and 16 bit grayscale images.
// we currently do the other types via palette. Maybe RC as I don't understand how the data is packed yet
// for 1, 2, and 4 bit grayscale images.
// TODO: Research and add support for grayscale plus tRNS
if (this.use16Bit)
{
// 16 bit grayscale
Rgb48 rgb = default;
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2)
{
rowSpan[x].ToRgb48(ref rgb);
Unsafe.Add(ref rowSpanRef, x).ToRgb48(ref rgb);
ushort luminance = (ushort)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance);
}
}
else
{
// 8 bit grayscale
Rgb24 rgb = default;
for (int x = 0; x < rowSpan.Length; x++)
if (this.bitDepth == 8)
{
rowSpan[x].ToRgb24(ref rgb);
rawScanlineSpan[x] = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
// 8 bit grayscale
Rgb24 rgb = default;
for (int x = 0; x < rowSpan.Length; x++)
{
Unsafe.Add(ref rowSpanRef, x).ToRgb24(ref rgb);
Unsafe.Add(ref rawScanlineSpanRef, x) = (byte)((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B));
}
}
else
{
// 1, 2, and 4 bit grayscale
using (IManagedByteBuffer temp = this.memoryAllocator.AllocateManagedByteBuffer(rowSpan.Length, AllocationOptions.Clean))
{
int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(this.bitDepth) - 1);
Span<byte> tempSpan = temp.GetSpan();
ref byte tempSpanRef = ref MemoryMarshal.GetReference(tempSpan);
Rgb24 rgb = default;
for (int x = 0; x < rowSpan.Length; x++)
{
Unsafe.Add(ref rowSpanRef, x).ToRgb24(ref rgb);
float luminance = ((RX * rgb.R) + (GX * rgb.G) + (BX * rgb.B)) / scaleFactor;
Unsafe.Add(ref tempSpanRef, x) = (byte)luminance;
this.ScaleDownFrom8BitArray(tempSpan, rawScanlineSpan, this.bitDepth);
}
}
}
}
}
@ -317,7 +376,7 @@ namespace SixLabors.ImageSharp.Formats.Png
Rgba64 rgba = default;
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 4)
{
rowSpan[x].ToRgba64(ref rgba);
Unsafe.Add(ref rowSpanRef, x).ToRgba64(ref rgba);
ushort luminance = (ushort)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), luminance);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.A);
@ -329,9 +388,9 @@ namespace SixLabors.ImageSharp.Formats.Png
Rgba32 rgba = default;
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 2)
{
rowSpan[x].ToRgba32(ref rgba);
rawScanlineSpan[o] = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
rawScanlineSpan[o + 1] = rgba.A;
Unsafe.Add(ref rowSpanRef, x).ToRgba32(ref rgba);
Unsafe.Add(ref rawScanlineSpanRef, o) = (byte)((RX * rgba.R) + (GX * rgba.G) + (BX * rgba.B));
Unsafe.Add(ref rawScanlineSpanRef, o + 1) = rgba.A;
}
}
}
@ -367,9 +426,10 @@ namespace SixLabors.ImageSharp.Formats.Png
{
// 16 bit Rgba
Rgba64 rgba = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8)
{
rowSpan[x].ToRgba64(ref rgba);
Unsafe.Add(ref rowSpanRef, x).ToRgba64(ref rgba);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B);
@ -383,9 +443,10 @@ namespace SixLabors.ImageSharp.Formats.Png
{
// 16 bit Rgb
Rgb48 rgb = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6)
{
rowSpan[x].ToRgb48(ref rgb);
Unsafe.Add(ref rowSpanRef, x).ToRgb48(ref rgb);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B);
@ -402,18 +463,25 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="rowSpan">The row span.</param>
/// <param name="quantizedPixelsSpan">The span of quantized pixels. Can be null.</param>
/// <param name="quantized">The quantized pixels. Can be null.</param>
/// <param name="row">The row.</param>
/// <returns>The <see cref="IManagedByteBuffer"/></returns>
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, ReadOnlySpan<byte> quantizedPixelsSpan, int row)
private IManagedByteBuffer EncodePixelRow<TPixel>(ReadOnlySpan<TPixel> rowSpan, QuantizedFrame<TPixel> quantized, int row)
where TPixel : struct, IPixel<TPixel>
{
switch (this.pngColorType)
{
case PngColorType.Palette:
int stride = this.rawScanline.Length();
quantizedPixelsSpan.Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan());
if (this.bitDepth < 8)
{
this.ScaleDownFrom8BitArray(quantized.GetRowSpan(row), this.rawScanline.GetSpan(), this.bitDepth);
}
else
{
int stride = this.rawScanline.Length();
quantized.GetPixelSpan().Slice(row * stride, stride).CopyTo(this.rawScanline.GetSpan());
}
break;
case PngColorType.Grayscale:
@ -569,8 +637,8 @@ namespace SixLabors.ImageSharp.Formats.Png
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
using (IManagedByteBuffer alphaTable = this.memoryAllocator.AllocateManagedByteBuffer(paletteLength))
{
Span<byte> colorTableSpan = colorTable.GetSpan();
Span<byte> alphaTableSpan = alphaTable.GetSpan();
ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan());
ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan());
Span<byte> quantizedSpan = quantized.GetPixelSpan();
for (int i = 0; i < paletteLength; i++)
@ -582,9 +650,9 @@ namespace SixLabors.ImageSharp.Formats.Png
byte alpha = rgba.A;
colorTableSpan[offset] = rgba.R;
colorTableSpan[offset + 1] = rgba.G;
colorTableSpan[offset + 2] = rgba.B;
Unsafe.Add(ref colorTableRef, offset) = rgba.R;
Unsafe.Add(ref colorTableRef, offset + 1) = rgba.G;
Unsafe.Add(ref colorTableRef, offset + 2) = rgba.B;
if (alpha > this.threshold)
{
@ -592,7 +660,7 @@ namespace SixLabors.ImageSharp.Formats.Png
}
anyAlpha = anyAlpha || alpha < byte.MaxValue;
alphaTableSpan[i] = alpha;
Unsafe.Add(ref alphaTableRef, i) = alpha;
}
}
@ -696,9 +764,9 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="pixels">The image.</param>
/// <param name="quantizedPixelsSpan">The span of quantized pixel data. Can be null.</param>
/// <param name="quantized">The quantized pixel data. Can be null.</param>
/// <param name="stream">The stream.</param>
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, ReadOnlySpan<byte> quantizedPixelsSpan, Stream stream)
private void WriteDataChunks<TPixel>(ImageFrame<TPixel> pixels, QuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
this.bytesPerScanline = this.CalculateScanlineLength(this.width);
@ -750,7 +818,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{
for (int y = 0; y < this.height; y++)
{
IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan<TPixel>)pixels.GetPixelRowSpan(y), quantizedPixelsSpan, y);
IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan<TPixel>)pixels.GetPixelRowSpan(y), quantized, y);
deflateStream.Write(r.Array, 0, resultLength);
IManagedByteBuffer temp = this.rawScanline;
@ -830,6 +898,47 @@ namespace SixLabors.ImageSharp.Formats.Png
stream.Write(this.buffer, 0, 4); // write the crc
}
/// <summary>
/// Packs the given 8 bit array into and array of <paramref name="bits"/> depths.
/// </summary>
/// <param name="source">The source span in 8 bits.</param>
/// <param name="result">The resultant span in <paramref name="bits"/>.</param>
/// <param name="bits">The bit depth.</param>
private void ScaleDownFrom8BitArray(ReadOnlySpan<byte> source, Span<byte> result, int bits)
{
ref byte sourceRef = ref MemoryMarshal.GetReference(source);
ref byte resultRef = ref MemoryMarshal.GetReference(result);
byte mask = (byte)(0xFF >> (8 - bits));
byte shift0 = (byte)(8 - bits);
int shift = 8 - bits;
int v = 0;
int resultOffset = 0;
for (int i = 0; i < source.Length; i++)
{
int value = Unsafe.Add(ref sourceRef, i) & mask;
v |= value << shift;
if (shift == 0)
{
shift = shift0;
Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
resultOffset++;
v = 0;
}
else
{
shift -= bits;
}
}
if (shift != shift0)
{
Unsafe.Add(ref resultRef, resultOffset) = (byte)v;
}
}
/// <summary>
/// Calculates the scanline length.
/// </summary>

600
src/ImageSharp/Formats/Png/PngScanlineProcessor.cs

@ -0,0 +1,600 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Provides methods to allow the decoding of raw scanlines to image rows of different pixel formats.
/// </summary>
internal static class PngScanlineProcessor
{
public static void ProcessGrayscaleScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
bool hasTrans,
ushort luminance16Trans,
byte luminanceTrans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(header.BitDepth) - 1);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.R = luminance;
rgb48.G = luminance;
rgb48.B = luminance;
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
// TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method.
var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = 0; x < header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = 0; x < header.Width; x++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = luminance.Equals(luminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedGrayscaleScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
bool hasTrans,
ushort luminance16Trans,
byte luminanceTrans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
int scaleFactor = 255 / (ImageMaths.GetColorCountForBitDepth(header.BitDepth) - 1);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgb48.R = luminance;
rgb48.G = luminance;
rgb48.B = luminance;
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
// TODO: We should really be using Rgb24 here but IPixel does not have a PackFromRgb24 method.
var rgba32 = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 2)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = luminance.Equals(luminance16Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < header.Width; x += increment)
{
byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * scaleFactor);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = luminance.Equals(luminanceTrans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += 4)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = alpha;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
int bps = bytesPerSample;
for (int x = 0; x < header.Width; x++)
{
int offset = x * bytesPerPixel;
byte luminance = Unsafe.Add(ref scanlineSpanRef, offset);
byte alpha = Unsafe.Add(ref scanlineSpanRef, offset + bps);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = alpha;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedGrayscaleWithAlphaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += 4)
{
ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2));
ushort alpha = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + 2, 2));
rgba64.R = luminance;
rgba64.G = luminance;
rgba64.B = luminance;
rgba64.A = alpha;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba32 = default;
for (int x = pixelOffset; x < header.Width; x += increment)
{
int offset = x * bytesPerPixel;
byte luminance = Unsafe.Add(ref scanlineSpanRef, offset);
byte alpha = Unsafe.Add(ref scanlineSpanRef, offset + bytesPerSample);
rgba32.R = luminance;
rgba32.G = luminance;
rgba32.B = luminance;
rgba32.A = alpha;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessPaletteScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
ReadOnlySpan<byte> palette,
byte[] paletteAlpha)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
if (paletteAlpha?.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
ref byte paletteAlphaRef = ref paletteAlpha[0];
for (int x = 0; x < header.Width; x++)
{
int index = Unsafe.Add(ref scanlineSpanRef, x);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
// TODO: We should have PackFromRgb24.
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = 0; x < header.Width; x++)
{
int index = Unsafe.Add(ref scanlineSpanRef, x);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedPaletteScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
ReadOnlySpan<byte> palette,
byte[] paletteAlpha)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan<Rgb24> palettePixels = MemoryMarshal.Cast<byte, Rgb24>(palette);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
if (paletteAlpha?.Length > 0)
{
// If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha
// channel and we should try to read it.
Rgba32 rgba = default;
ref byte paletteAlphaRef = ref paletteAlpha[0];
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++)
{
int index = Unsafe.Add(ref scanlineSpanRef, o);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o++)
{
int index = Unsafe.Add(ref scanlineSpanRef, o);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessRgbScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample,
bool hasTrans,
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (!hasTrans)
{
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgb24Bytes(scanlineSpan, rowSpan, header.Width);
}
return;
}
if (header.BitDepth == 16)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
ReadOnlySpan<Rgb24> rgb24Span = MemoryMarshal.Cast<byte, Rgb24>(scanlineSpan);
ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span);
for (int x = 0; x < header.Width; x++)
{
ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x);
Rgba32 rgba32 = default;
rgba32.Rgb = rgb24;
rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba32);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessInterlacedRgbScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
int bytesPerPixel,
int bytesPerSample,
bool hasTrans,
Rgb48 rgb48Trans,
Rgb24 rgb24Trans)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
if (hasTrans)
{
Rgb48 rgb48 = default;
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.Rgb = rgb48;
rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue;
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgb48 rgb48 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
pixel.PackFromRgb48(rgb48);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
return;
}
if (hasTrans)
{
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample);
rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample));
rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue;
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
var rgba = new Rgba32(0, 0, 0, byte.MaxValue);
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample);
rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample));
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
public static void ProcessRgbaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = 0, o = 0; x < header.Width; x++, o += bytesPerPixel)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
PixelOperations<TPixel>.Instance.PackFromRgba32Bytes(scanlineSpan, rowSpan, header.Width);
}
}
public static void ProcessInterlacedRgbaScanline<TPixel>(
in PngHeader header,
ReadOnlySpan<byte> scanlineSpan,
Span<TPixel> rowSpan,
int pixelOffset,
int increment,
int bytesPerPixel,
int bytesPerSample)
where TPixel : struct, IPixel<TPixel>
{
TPixel pixel = default;
ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan);
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
if (header.BitDepth == 16)
{
Rgba64 rgba64 = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba64.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample));
rgba64.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample));
rgba64.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample));
rgba64.A = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample));
pixel.PackFromRgba64(rgba64);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
else
{
Rgba32 rgba = default;
for (int x = pixelOffset, o = 0; x < header.Width; x += increment, o += bytesPerPixel)
{
rgba.R = Unsafe.Add(ref scanlineSpanRef, o);
rgba.G = Unsafe.Add(ref scanlineSpanRef, o + bytesPerSample);
rgba.B = Unsafe.Add(ref scanlineSpanRef, o + (2 * bytesPerSample));
rgba.A = Unsafe.Add(ref scanlineSpanRef, o + (3 * bytesPerSample));
pixel.PackFromRgba32(rgba);
Unsafe.Add(ref rowSpanRef, x) = pixel;
}
}
}
}
}

12
src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs

@ -3,7 +3,7 @@
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
@ -57,8 +57,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization
/// Gets the pixels of this <see cref="QuantizedFrame{TPixel}"/>.
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<byte> GetPixelSpan() => this.pixels.GetSpan();
/// <summary>
/// Gets the representation of the pixels as a <see cref="Span{T}"/> of contiguous memory
/// at row <paramref name="rowIndex"/> beginning from the the first pixel on that row.
/// </summary>
/// <param name="rowIndex">The row.</param>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public Span<byte> GetRowSpan(int rowIndex) => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width);
/// <inheritdoc/>
public void Dispose()
{

160
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.MetaData;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -19,10 +18,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngEncoderTests
{
// This is bull. Failing online for no good reason.
// The images are an exact match. Maybe the submodule isn't updating?
private const float ToleranceThresholdForPaletteEncoder = 1.3F / 100;
public static readonly TheoryData<string, PngBitDepth> PngBitDepthFiles =
new TheoryData<string, PngBitDepth>
{
@ -137,19 +132,32 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
[Theory]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha)]
public void WorksWithBitDepth16<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType)
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Rgb, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.Rgb, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.RgbWithAlpha, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.RgbWithAlpha, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit1)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit2)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit4)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.Palette, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit1)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit2)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit4)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb24, PngColorType.Grayscale, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgb48, PngColorType.Grayscale, PngBitDepth.Bit16)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba32, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8)]
[WithTestPatternImages(24, 24, PixelTypes.Rgba64, PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16)]
public void WorksWithAllBitDepths<TPixel>(TestImageProvider<TPixel> provider, PngColorType pngColorType, PngBitDepth pngBitDepth)
where TPixel : struct, IPixel<TPixel>
{
TestPngEncoderCore(
provider,
pngColorType,
PngFilterMethod.Adaptive,
PngBitDepth.Bit16,
pngBitDepth,
appendPngColorType: true,
appendPixelType: true);
appendPixelType: true,
appendPngBitDepth: true);
}
[Theory]
@ -166,87 +174,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
appendPaletteSize: true);
}
private static bool HasAlpha(PngColorType pngColorType) =>
pngColorType == PngColorType.GrayscaleWithAlpha || pngColorType == PngColorType.RgbWithAlpha;
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,
PngFilterMethod pngFilterMethod,
PngBitDepth bitDepth,
int compressionLevel = 6,
int paletteSize = 255,
bool appendPngColorType = false,
bool appendPngFilterMethod = false,
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
if (!HasAlpha(pngColorType))
{
image.Mutate(c => c.MakeOpaque());
}
var encoder = new PngEncoder
{
ColorType = pngColorType,
FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(paletteSize)
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;
string pngFilterMethodInfo = appendPngFilterMethod ? pngFilterMethod.ToString() : string.Empty;
string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty;
string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty;
string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}";
//string referenceInfo = $"{pngColorTypeInfo}";
// Does DebugSave & load reference CompareToReferenceInput():
string actualOutputFile = ((ITestImageProvider)provider).Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);
if (TestEnvironment.IsMono)
{
// There are bugs in mono's System.Drawing implementation, reference decoders are not always reliable!
return;
}
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
string referenceOutputFile = ((ITestImageProvider)provider).Utility.GetReferenceOutputFileName("png", debugInfo, appendPixelType, true);
bool referenceOutputFileExists = File.Exists(referenceOutputFile);
using (var actualImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
{
// TODO: Do we still need the reference output files?
Image<TPixel> referenceImage = referenceOutputFileExists
? Image.Load<TPixel>(referenceOutputFile, referenceDecoder)
: image;
float paletteToleranceHack = 80f / paletteSize;
paletteToleranceHack *= paletteToleranceHack;
ImageComparer comparer = pngColorType == PngColorType.Palette
? ImageComparer.Tolerant(ToleranceThresholdForPaletteEncoder * paletteToleranceHack)
: ImageComparer.Exact;
try
{
comparer.VerifySimilarity(referenceImage, actualImage);
}
finally
{
if (referenceOutputFileExists)
{
referenceImage.Dispose();
}
}
}
}
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32)]
public void WritesFileMarker<TPixel>(TestImageProvider<TPixel> provider)
@ -321,5 +248,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
}
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,
PngFilterMethod pngFilterMethod,
PngBitDepth bitDepth,
int compressionLevel = 6,
int paletteSize = 255,
bool appendPngColorType = false,
bool appendPngFilterMethod = false,
bool appendPixelType = false,
bool appendCompressionLevel = false,
bool appendPaletteSize = false,
bool appendPngBitDepth = false)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var encoder = new PngEncoder
{
ColorType = pngColorType,
FilterMethod = pngFilterMethod,
CompressionLevel = compressionLevel,
BitDepth = bitDepth,
Quantizer = new WuQuantizer(paletteSize)
};
string pngColorTypeInfo = appendPngColorType ? pngColorType.ToString() : string.Empty;
string pngFilterMethodInfo = appendPngFilterMethod ? pngFilterMethod.ToString() : string.Empty;
string compressionLevelInfo = appendCompressionLevel ? $"_C{compressionLevel}" : string.Empty;
string paletteSizeInfo = appendPaletteSize ? $"_PaletteSize-{paletteSize}" : string.Empty;
string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty;
string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}";
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType);
// Compare to the Magick reference decoder.
IImageDecoder referenceDecoder = TestEnvironment.GetReferenceDecoder(actualOutputFile);
// We compare using both our decoder and the reference decoder as pixel transformation
// occurrs within the encoder itself leaving the input image unaffected.
// This means we are benefiting from testing our decoder also.
using (var imageSharpImage = Image.Load<TPixel>(actualOutputFile, new PngDecoder()))
using (var referenceImage = Image.Load<TPixel>(actualOutputFile, referenceDecoder))
{
ImageComparer.Exact.VerifySimilarity(referenceImage, imageSharpImage);
}
}
}
}
}

6
tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs

@ -29,13 +29,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs
{
if (magickImage.Depth == 8)
{
byte[] data = pixels.ToByteArray("RGBA");
byte[] data = pixels.ToByteArray(PixelMapping.RGBA);
PixelOperations<TPixel>.Instance.PackFromRgba32Bytes(data, resultPixels, resultPixels.Length);
}
else if (magickImage.Depth == 16)
{
ushort[] data = pixels.ToShortArray("RGBA");
ushort[] data = pixels.ToShortArray(PixelMapping.RGBA);
Span<byte> bytes = MemoryMarshal.Cast<ushort, byte>(data.AsSpan());
PixelOperations<TPixel>.Instance.PackFromRgba64Bytes(bytes, resultPixels, resultPixels.Length);

8
tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs

@ -14,10 +14,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
public class MagickReferenceCodecTests
{
public MagickReferenceCodecTests(ITestOutputHelper output)
{
this.Output = output;
}
public MagickReferenceCodecTests(ITestOutputHelper output) => this.Output = output;
private ITestOutputHelper Output { get; }
@ -61,6 +58,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48Bpp)]
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48BppInterlaced)]
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Rgb48BppTrans)]
[WithBlankImages(1, 1, PixelTypesToTest48, TestImages.Png.Gray16Bit)]
public void MagickDecode_16BitDepthImage_IsApproximatelyEquivalentTo_SystemDrawingResult<TPixel>(TestImageProvider<TPixel> dummyProvider, string testImage)
where TPixel : struct, IPixel<TPixel>
{
@ -71,7 +69,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests
// 1020 == 4 * 255 (Equivalent to manhattan distance of 1+1+1+1=4 in Rgba32 space)
var comparer = ImageComparer.TolerantPercentage(1, 1020);
using (var mImage = Image.Load<TPixel>(path, magickDecoder))
using (var sdImage = Image.Load<TPixel>(path, sdDecoder))
{

Loading…
Cancel
Save