diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 77bc9f7a05..7e5a9fa6b8 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Gets the filter method. /// - PngFilterMethod FilterMethod { get; } + PngFilterMethod? FilterMethod { get; } /// /// Gets the compression level 1-9. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index ef2f226556..1bfba68ed6 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -92,6 +92,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private readonly bool ignoreMetadata; + /// + /// Used the manage memory allocations. + /// + private readonly MemoryAllocator memoryAllocator; + /// /// The stream to decode from. /// @@ -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; - /// /// Decodes the stream to the image. /// @@ -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(ReadOnlySpan defilteredScanline, ImageFrame pixels) where TPixel : struct, IPixel { - TPixel pixel = default; Span rowSpan = pixels.GetPixelRowSpan(this.currentRow); // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1); // Convert 1, 2, and 4 bit pixel data into the 8 bit equivalent. - ReadOnlySpan scanlineSpan = this.TryScaleUpTo8BitArray(trimmed, this.bytesPerScanline, this.header.BitDepth, out IManagedByteBuffer buffer) + ReadOnlySpan 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.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 rgb24Span = MemoryMarshal.Cast(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.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(ReadOnlySpan defilteredScanline, Span rowSpan, int pixelOffset = 0, int increment = 1) where TPixel : struct, IPixel { - TPixel pixel = default; - // Trim the first marker byte from the buffer ReadOnlySpan 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 palettePixels = MemoryMarshal.Cast(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 } } - /// - /// Processes a scanline that uses a palette - /// - /// The type of pixel we are expanding to - /// The defiltered scanline - /// The current output image row - private void ProcessScanlineFromPalette(ReadOnlySpan scanline, Span row) - where TPixel : struct, IPixel - { - ReadOnlySpan palettePixels = MemoryMarshal.Cast(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; - } - } - } - /// /// Reads a header chunk from the data. /// diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index f47a6518f0..96e97a305f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Gets or sets the filter method. /// - public PngFilterMethod FilterMethod { get; set; } = PngFilterMethod.Paeth; + public PngFilterMethod? FilterMethod { get; set; } /// /// Gets or sets the compression level 1-9. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e4d2fc510d..603162fbe8 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/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 /// internal sealed class PngEncoderCore : IDisposable { + /// + /// The dictionary of available color types. + /// + private static readonly Dictionary ColorTypes = new Dictionary() + { + [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 } + }; + + /// + /// Used the manage memory allocations. + /// private readonly MemoryAllocator memoryAllocator; /// @@ -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 quantized = null; - ReadOnlySpan 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().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 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 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 /// /// The pixel format. /// The row span. - /// The span of quantized pixels. Can be null. + /// The quantized pixels. Can be null. /// The row. /// The - private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, ReadOnlySpan quantizedPixelsSpan, int row) + private IManagedByteBuffer EncodePixelRow(ReadOnlySpan rowSpan, QuantizedFrame quantized, int row) where TPixel : struct, IPixel { 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 colorTableSpan = colorTable.GetSpan(); - Span alphaTableSpan = alphaTable.GetSpan(); + ref byte colorTableRef = ref MemoryMarshal.GetReference(colorTable.GetSpan()); + ref byte alphaTableRef = ref MemoryMarshal.GetReference(alphaTable.GetSpan()); Span 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 /// /// The pixel format. /// The image. - /// The span of quantized pixel data. Can be null. + /// The quantized pixel data. Can be null. /// The stream. - private void WriteDataChunks(ImageFrame pixels, ReadOnlySpan quantizedPixelsSpan, Stream stream) + private void WriteDataChunks(ImageFrame pixels, QuantizedFrame quantized, Stream stream) where TPixel : struct, IPixel { 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)pixels.GetPixelRowSpan(y), quantizedPixelsSpan, y); + IManagedByteBuffer r = this.EncodePixelRow((ReadOnlySpan)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 } + /// + /// Packs the given 8 bit array into and array of depths. + /// + /// The source span in 8 bits. + /// The resultant span in . + /// The bit depth. + private void ScaleDownFrom8BitArray(ReadOnlySpan source, Span 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; + } + } + /// /// Calculates the scanline length. /// diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs new file mode 100644 index 0000000000..6c81ba76c2 --- /dev/null +++ b/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 +{ + /// + /// Provides methods to allow the decoding of raw scanlines to image rows of different pixel formats. + /// + internal static class PngScanlineProcessor + { + public static void ProcessGrayscaleScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + bool hasTrans, + ushort luminance16Trans, + byte luminanceTrans) + where TPixel : struct, IPixel + { + 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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + bool hasTrans, + ushort luminance16Trans, + byte luminanceTrans) + where TPixel : struct, IPixel + { + 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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + 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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + 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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + ReadOnlySpan palette, + byte[] paletteAlpha) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + ReadOnlySpan palettePixels = MemoryMarshal.Cast(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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + ReadOnlySpan palette, + byte[] paletteAlpha) + where TPixel : struct, IPixel + { + TPixel pixel = default; + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + ReadOnlySpan palettePixels = MemoryMarshal.Cast(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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample, + bool hasTrans, + Rgb48 rgb48Trans, + Rgb24 rgb24Trans) + where TPixel : struct, IPixel + { + 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.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 rgb24Span = MemoryMarshal.Cast(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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + int bytesPerPixel, + int bytesPerSample, + bool hasTrans, + Rgb48 rgb48Trans, + Rgb24 rgb24Trans) + where TPixel : struct, IPixel + { + 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( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + 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.Instance.PackFromRgba32Bytes(scanlineSpan, rowSpan, header.Width); + } + } + + public static void ProcessInterlacedRgbaScanline( + in PngHeader header, + ReadOnlySpan scanlineSpan, + Span rowSpan, + int pixelOffset, + int increment, + int bytesPerPixel, + int bytesPerSample) + where TPixel : struct, IPixel + { + 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; + } + } + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs index 2e3bb2c419..38862ef446 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizedFrame{TPixel}.cs +++ b/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 . /// /// The + [MethodImpl(InliningOptions.ShortMethod)] public Span GetPixelSpan() => this.pixels.GetSpan(); + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the the first pixel on that row. + /// + /// The row. + /// The + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetRowSpan(int rowIndex) => this.GetPixelSpan().Slice(rowIndex * this.Width, this.Width); + /// public void Dispose() { diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 0508ac8c26..5d328db361 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/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 PngBitDepthFiles = new TheoryData { @@ -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(TestImageProvider 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(TestImageProvider provider, PngColorType pngColorType, PngBitDepth pngBitDepth) where TPixel : struct, IPixel { 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( - TestImageProvider 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 - { - using (Image 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(actualOutputFile, referenceDecoder)) - { - // TODO: Do we still need the reference output files? - Image referenceImage = referenceOutputFileExists - ? Image.Load(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(TestImageProvider provider) @@ -321,5 +248,54 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } } + + private static void TestPngEncoderCore( + TestImageProvider 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 + { + using (Image 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(actualOutputFile, new PngDecoder())) + using (var referenceImage = Image.Load(actualOutputFile, referenceDecoder)) + { + ImageComparer.Exact.VerifySimilarity(referenceImage, imageSharpImage); + } + } + } } } \ No newline at end of file diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 8cfc2472f5..7e942691e9 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/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.Instance.PackFromRgba32Bytes(data, resultPixels, resultPixels.Length); } else if (magickImage.Depth == 16) { - ushort[] data = pixels.ToShortArray("RGBA"); + ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); PixelOperations.Instance.PackFromRgba64Bytes(bytes, resultPixels, resultPixels.Length); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs index db651886f2..b60439b488 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/MagickReferenceCodecTests.cs +++ b/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(TestImageProvider dummyProvider, string testImage) where TPixel : struct, IPixel { @@ -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(path, magickDecoder)) using (var sdImage = Image.Load(path, sdDecoder)) {