Erik White 2 weeks ago
committed by GitHub
parent
commit
5b154fb9c7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 119
      src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs
  2. 219
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  3. 319
      src/ImageSharp/Formats/Png/PngCgbiProcessor.cs
  4. 347
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  5. 105
      tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs
  6. 22
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  7. 4
      tests/ImageSharp.Tests/TestImages.cs
  8. 3
      tests/Images/Input/Png/cgbi/colors-cgbi-bitdepth16.png
  9. 3
      tests/Images/Input/Png/cgbi/colors-cgbi-palette.png

119
src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs

@ -0,0 +1,119 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// A read-only stream over a sequence of length-delimited segments. Bytes are
/// pulled from the inner stream up to the current segment's remaining length;
/// when the segment is exhausted the supplied delegate is invoked to advance
/// to the next segment and return its length. The inner stream is not owned
/// and is not disposed.
/// </summary>
internal sealed class ChunkedReadStream : Stream
{
private static readonly Func<int> GetDataNoOp = () => 0;
private readonly BufferedReadStream innerStream;
private readonly Func<int> getData;
private int currentDataRemaining;
public ChunkedReadStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
{
}
public ChunkedReadStream(BufferedReadStream innerStream, Func<int> getData)
{
this.innerStream = innerStream;
this.getData = getData;
}
/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
/// <summary>
/// Sets the number of bytes available to read from the current segment.
/// Must be called before reading each segment.
/// </summary>
public void SetCurrentSegmentLength(int bytes) => this.currentDataRemaining = bytes;
/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte()
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return -1;
}
}
int value = this.innerStream.ReadByte();
if (value is not -1)
{
this.currentDataRemaining--;
}
return value;
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// Decrement currentDataRemaining only by bytes actually returned by
// innerStream.Read; a short read otherwise underflows the segment
// counter and triggers getData() before the segment is truly drained.
int totalBytesRead = 0;
while (totalBytesRead < count)
{
if (this.currentDataRemaining is 0)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
break;
}
}
int bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
int bytesRead = this.innerStream.Read(buffer, offset + totalBytesRead, bytesToRead);
if (bytesRead is 0)
{
break;
}
this.currentDataRemaining -= bytesRead;
totalBytesRead += bytesRead;
}
return totalBytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

219
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -8,9 +8,11 @@ using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Compression.Zlib;
/// <summary>
/// Provides methods and properties for deframing streams from PNGs.
/// Reads chunked input, parses the zlib CMF/FLG header, and exposes a
/// <see cref="DeflateStream"/> over the remaining DEFLATE payload. The
/// Adler-32 trailer is not validated.
/// </summary>
internal sealed class ZlibInflateStream : Stream
internal sealed class ZlibInflateStream : IDisposable
{
/// <summary>
/// Used to read the Adler-32 and Crc-32 checksums.
@ -19,94 +21,13 @@ internal sealed class ZlibInflateStream : Stream
/// </summary>
private static readonly byte[] ChecksumBuffer = new byte[4];
/// <summary>
/// A default delegate to get more data from the inner stream.
/// </summary>
private static readonly Func<int> GetDataNoOp = () => 0;
/// <summary>
/// The inner raw memory stream.
/// </summary>
private readonly BufferedReadStream innerStream;
/// <summary>
/// A value indicating whether this instance of the given entity has been disposed.
/// </summary>
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
/// <remarks>
/// If the entity is disposed, it must not be disposed a second
/// time. The isDisposed field is set the first time the entity
/// is disposed. If the isDisposed field is true, then the Dispose()
/// method will not dispose again. This help not to prolong the entity's
/// life in the Garbage Collector.
/// </remarks>
private bool isDisposed;
/// <summary>
/// The current data remaining to be read.
/// </summary>
private int currentDataRemaining;
/// <summary>
/// Delegate to get more data once we've exhausted the current data remaining.
/// </summary>
private readonly Func<int> getData;
/// <summary>
/// When true, the inflated payload is treated as a raw DEFLATE stream with no zlib
/// CMF/FLG header (and no Adler-32 trailer). This is required to decode IDATs in
/// Apple's proprietary CgBI PNG variant.
/// </summary>
private readonly bool noHeader;
private readonly ChunkedReadStream segmentStream;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp, noHeader: false)
{
}
=> this.segmentStream = new ChunkedReadStream(innerStream);
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData)
: this(innerStream, getData, noHeader: false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
/// <param name="noHeader">
/// When <see langword="true"/>, the payload is treated as raw DEFLATE with no zlib header.
/// </param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData, bool noHeader)
{
this.innerStream = innerStream;
this.getData = getData;
this.noHeader = noHeader;
}
/// <inheritdoc/>
public override bool CanRead => this.innerStream.CanRead;
/// <inheritdoc/>
public override bool CanSeek => false;
/// <inheritdoc/>
public override bool CanWrite => throw new NotSupportedException();
/// <inheritdoc/>
public override long Length => throw new NotSupportedException();
/// <inheritdoc/>
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
=> this.segmentStream = new ChunkedReadStream(innerStream, getData);
/// <summary>
/// Gets the compressed stream over the deframed inner stream.
@ -114,15 +35,16 @@ internal sealed class ZlibInflateStream : Stream
public DeflateStream? CompressedStream { get; private set; }
/// <summary>
/// Adds new bytes from a frame found in the original stream.
/// Sets the length of the next segment of compressed input and, on first
/// call, parses the zlib header.
/// </summary>
/// <param name="bytes">The current remaining data according to the chunk length.</param>
/// <param name="isCriticalChunk">Whether the chunk to be inflated is a critical chunk.</param>
/// <param name="bytes">The remaining data length for the current segment.</param>
/// <param name="isCriticalChunk">Whether to throw on a malformed zlib header.</param>
/// <returns>The <see cref="bool"/>.</returns>
[MemberNotNullWhen(true, nameof(CompressedStream))]
public bool AllocateNewBytes(int bytes, bool isCriticalChunk)
{
this.currentDataRemaining = bytes;
this.segmentStream.SetCurrentSegmentLength(bytes);
if (this.CompressedStream is null)
{
return this.InitializeInflateStream(isCriticalChunk);
@ -131,114 +53,15 @@ internal sealed class ZlibInflateStream : Stream
return true;
}
/// <inheritdoc/>
public override void Flush() => throw new NotSupportedException();
/// <inheritdoc/>
public override int ReadByte()
public void Dispose()
{
this.currentDataRemaining--;
return this.innerStream.ReadByte();
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
if (this.currentDataRemaining is 0)
{
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return 0;
}
}
int bytesToRead = Math.Min(count, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
int totalBytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
long innerStreamLength = this.innerStream.Length;
// Keep reading data until we've reached the end of the stream or filled the buffer.
int bytesRead = 0;
offset += totalBytesRead;
while (this.currentDataRemaining is 0 && totalBytesRead < count)
{
this.currentDataRemaining = this.getData();
if (this.currentDataRemaining is 0)
{
return totalBytesRead;
}
offset += bytesRead;
if (offset >= innerStreamLength || offset >= count)
{
return totalBytesRead;
}
bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
bytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
if (bytesRead == 0)
{
return totalBytesRead;
}
totalBytesRead += bytesRead;
}
return totalBytesRead;
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc/>
public override void SetLength(long value) => throw new NotSupportedException();
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
// Dispose managed resources.
if (this.CompressedStream != null)
{
this.CompressedStream.Dispose();
this.CompressedStream = null;
}
}
base.Dispose(disposing);
// Call the appropriate methods to clean up
// unmanaged resources here.
// Note disposing is done.
this.isDisposed = true;
this.CompressedStream?.Dispose();
this.segmentStream?.Dispose();
}
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Apple CgBI IDATs omit the zlib CMF/FLG header and the Adler-32 trailer,
// wrapping a raw DEFLATE payload directly. Skip the header parsing in that mode.
if (this.noHeader)
{
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
return true;
}
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a
@ -250,9 +73,8 @@ internal sealed class ZlibInflateStream : Stream
// +---+---+
// |CMF|FLG|
// +---+---+
int cmf = this.innerStream.ReadByte();
int flag = this.innerStream.ReadByte();
this.currentDataRemaining -= 2;
int cmf = this.segmentStream.ReadByte();
int flag = this.segmentStream.ReadByte();
if (cmf == -1 || flag == -1)
{
return false;
@ -290,16 +112,13 @@ internal sealed class ZlibInflateStream : Stream
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4)
if (this.segmentStream.Read(ChecksumBuffer, 0, 4) != 4)
{
return false;
}
this.currentDataRemaining -= 4;
}
// Initialize the deflate BufferedReadStream.
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true);
return true;
}

319
src/ImageSharp/Formats/Png/PngCgbiProcessor.cs

@ -0,0 +1,319 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.SimdUtils;
namespace SixLabors.ImageSharp.Formats.Png;
/// <summary>
/// Reverses the pixel mangling applied by Apple's CgBI PNG variant. CgBI files
/// (emitted by <c>pngcrush -iphone</c>) swap channel order from RGB(A) to BGR(A)
/// and premultiply RGB samples by alpha. This converts a defiltered scanline back
/// to standard PNG semantics in place so the existing scanline processors can
/// consume it unchanged. CgBI is only emitted for 8-bit truecolor (with or
/// without alpha); other color types are left alone.
/// </summary>
/// <remarks>
/// See https://theapplewiki.com/wiki/PNG_CgBI_Format
/// </remarks>
internal static class PngCgbiProcessor
{
// Per-pixel byte indices that swap CgBI's BGRA layout to Rgba32's RGBA.
// MMShuffle3012 expands to [2, 1, 0, 3] per 4-byte pixel; the same 64-byte
// sequence seeds all three shuffle masks (Vector128/256 take a leading slice).
private static readonly byte[] BgraToRgbaShuffleBytes = BuildShuffleBytes();
private static readonly Vector128<byte> BgraToRgbaShuffle128 = Vector128.Create(new ReadOnlySpan<byte>(BgraToRgbaShuffleBytes, 0, Vector128<byte>.Count));
private static readonly Vector256<byte> BgraToRgbaShuffle256 = Vector256.Create(new ReadOnlySpan<byte>(BgraToRgbaShuffleBytes, 0, Vector256<byte>.Count));
private static readonly Vector512<byte> BgraToRgbaShuffle512 = Vector512.Create(BgraToRgbaShuffleBytes);
/// <summary>
/// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline in place.
/// </summary>
/// <param name="configuration">The configuration used by the Rgb24 R/B swap.</param>
/// <param name="scanline">The defiltered pixel bytes (without the leading filter byte).</param>
/// <param name="colorType">The PNG color type from IHDR.</param>
public static void ApplyTransform(Configuration configuration, Span<byte> scanline, PngColorType colorType)
{
if (colorType == PngColorType.RgbWithAlpha)
{
Span<Rgba32> pixels = MemoryMarshal.Cast<byte, Rgba32>(scanline);
int i = 0;
if (Vector512.IsHardwareAccelerated && pixels.Length >= Vector512<int>.Count)
{
i = ApplyTransformVector512(scanline, pixels.Length);
}
if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= Vector256<int>.Count)
{
i = ApplyTransformVector256(scanline, i, pixels.Length);
}
if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= Vector128<int>.Count)
{
i = ApplyTransformVector128(scanline, i, pixels.Length);
}
for (; i < pixels.Length; i++)
{
ref Rgba32 pixel = ref pixels[i];
pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
UndoPremultiplicationScalar(ref pixel);
}
}
else if (colorType == PngColorType.Rgb)
{
// No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations.
Span<Rgb24> target = MemoryMarshal.Cast<byte, Rgb24>(scanline);
PixelOperations<Rgb24>.Instance.FromBgr24Bytes(configuration, scanline, target, target.Length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UndoPremultiplicationScalar(ref Rgba32 pixel)
{
byte a = pixel.A;
if (a is 0 or byte.MaxValue)
{
return;
}
// Reverse: c' = c * a / 255 => c = round(c' * 255 / a)
int half = a >> 1;
byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
pixel = new Rgba32(r, g, b, a);
}
private static int ApplyTransformVector512(Span<byte> scanline, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = 0;
// Indices stay within their own 4-byte pixel, so the per-pixel pattern
// is also valid under the per-128-bit-lane vpshufb that ShuffleNative
// selects on AVX-512BW hosts.
Vector512<byte> shuffleMask = BgraToRgbaShuffle512;
Vector512<int> zero = Vector512<int>.Zero;
Vector512<int> one = Vector512<int>.One;
Vector512<int> byteMax = Vector512.Create((int)byte.MaxValue);
for (; i <= pixelCount - Vector512<int>.Count; i += Vector512<int>.Count)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector512<byte> bgra = Unsafe.ReadUnaligned<Vector512<byte>>(ref blockRef);
Vector512<byte> rgba = Vector512_.ShuffleNative(bgra, shuffleMask);
Vector512<int> packed = rgba.AsInt32();
Vector512<int> alpha = Vector512.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector512<int> partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, byteMax));
Vector512<int> r = packed & byteMax;
Vector512<int> g = Vector512.ShiftRightLogical(packed, 8) & byteMax;
Vector512<int> b = Vector512.ShiftRightLogical(packed, 16) & byteMax;
Vector512<int> safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one);
Vector512<int> halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1);
Vector512<float> safeAlphaF = Vector512.ConvertToSingle(safeAlpha);
// ConvertToInt32 truncates toward zero (cvttps2dq / fcvtzs); since
// every quotient here is non-negative, that matches the scalar
// ((c * 255) + (a >> 1)) / a integer-division floor.
Vector512<int> unpremultipliedR = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF));
Vector512<int> unpremultipliedG = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF));
Vector512<int> unpremultipliedB = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector512<int> finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector512<int> finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector512<int> finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector512<int> result =
finalR |
Vector512.ShiftLeft(finalG, 8) |
Vector512.ShiftLeft(finalB, 16) |
Vector512.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static int ApplyTransformVector256(Span<byte> scanline, int startPixel, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = startPixel;
// vpshufb is 128-bit lane-local and uses only the low 4 bits of each
// index, so the same per-pixel [2,1,0,3] pattern in both lanes keeps
// every byte inside its own lane.
Vector256<byte> shuffleMask = BgraToRgbaShuffle256;
Vector256<int> zero = Vector256<int>.Zero;
Vector256<int> one = Vector256<int>.One;
Vector256<int> byteMax = Vector256.Create((int)byte.MaxValue);
for (; i <= pixelCount - Vector256<int>.Count; i += Vector256<int>.Count)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector256<byte> bgra = Unsafe.ReadUnaligned<Vector256<byte>>(ref blockRef);
Vector256<byte> rgba = Vector256_.ShufflePerLane(bgra, shuffleMask);
Vector256<int> packed = rgba.AsInt32();
Vector256<int> alpha = Vector256.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector256<int> partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, byteMax));
Vector256<int> r = packed & byteMax;
Vector256<int> g = Vector256.ShiftRightLogical(packed, 8) & byteMax;
Vector256<int> b = Vector256.ShiftRightLogical(packed, 16) & byteMax;
Vector256<int> safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one);
Vector256<int> halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1);
Vector256<float> safeAlphaF = Vector256.ConvertToSingle(safeAlpha);
// ConvertToInt32 truncates toward zero (cvttps2dq / fcvtzs); since
// every quotient here is non-negative, that matches the scalar
// ((c * 255) + (a >> 1)) / a integer-division floor.
Vector256<int> unpremultipliedR = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF));
Vector256<int> unpremultipliedG = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF));
Vector256<int> unpremultipliedB = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector256<int> finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector256<int> finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector256<int> finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector256<int> result =
finalR |
Vector256.ShiftLeft(finalG, 8) |
Vector256.ShiftLeft(finalB, 16) |
Vector256.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static int ApplyTransformVector128(Span<byte> scanline, int startPixel, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = startPixel;
Vector128<byte> shuffleMask = BgraToRgbaShuffle128;
Vector128<int> zero = Vector128<int>.Zero;
Vector128<int> one = Vector128<int>.One;
Vector128<int> byteMax = Vector128.Create((int)byte.MaxValue);
for (; i <= pixelCount - Vector128<int>.Count; i += Vector128<int>.Count)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector128<byte> bgra = Unsafe.ReadUnaligned<Vector128<byte>>(ref blockRef);
Vector128<byte> rgba = Vector128_.ShuffleNative(bgra, shuffleMask);
Vector128<int> packed = rgba.AsInt32();
Vector128<int> alpha = Vector128.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector128<int> partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, byteMax));
Vector128<int> r = packed & byteMax;
Vector128<int> g = Vector128.ShiftRightLogical(packed, 8) & byteMax;
Vector128<int> b = Vector128.ShiftRightLogical(packed, 16) & byteMax;
Vector128<int> safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one);
Vector128<int> halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1);
Vector128<float> safeAlphaF = Vector128.ConvertToSingle(safeAlpha);
// ConvertToInt32 truncates toward zero (cvttps2dq / fcvtzs); since
// every quotient here is non-negative, that matches the scalar
// ((c * 255) + (a >> 1)) / a integer-division floor.
Vector128<int> unpremultipliedR = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF));
Vector128<int> unpremultipliedG = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF));
Vector128<int> unpremultipliedB = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector128<int> finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector128<int> finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector128<int> finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector128<int> result =
finalR |
Vector128.ShiftLeft(finalG, 8) |
Vector128.ShiftLeft(finalB, 16) |
Vector128.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static byte[] BuildShuffleBytes()
{
byte[] bytes = new byte[Vector512<byte>.Count];
Span<byte> span = bytes;
Shuffle.MMShuffleSpan(ref span, Shuffle.MMShuffle3012);
return bytes;
}
}

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

@ -9,8 +9,6 @@ using System.IO.Compression;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
@ -767,7 +765,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// <param name="chunkLength">The length of the chunk that containing the compressed scanline data.</param>
/// <param name="image"> The pixel data.</param>
/// <param name="pngMetadata">The png metadata</param>
/// <param name="getData">A delegate to get more data from the inner stream for <see cref="ZlibInflateStream"/>.</param>
/// <param name="getData">A delegate to get more data from the inner stream when chunk boundaries are crossed.</param>
/// <param name="frameControl">The frame control</param>
/// <param name="cancellationToken">The cancellation token.</param>
private void ReadScanlines<TPixel>(
@ -779,14 +777,34 @@ internal sealed class PngDecoderCore : ImageDecoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using ZlibInflateStream inflateStream = new(this.currentStream, getData, noHeader: this.isCgbi);
// CgBI IDATs wrap a raw DEFLATE payload directly (no zlib CMF/FLG header
// and no Adler-32 trailer); skip the zlib header parser entirely.
if (this.isCgbi)
{
using ChunkedReadStream segmentStream = new(this.currentStream, getData);
segmentStream.SetCurrentSegmentLength(chunkLength);
using DeflateStream cgbiDataStream = new(segmentStream, CompressionMode.Decompress, leaveOpen: true);
this.DecodeFromDeflate(cgbiDataStream, image, pngMetadata, frameControl, cancellationToken);
return;
}
using ZlibInflateStream inflateStream = new(this.currentStream, getData);
if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData))
{
return;
}
DeflateStream dataStream = inflateStream.CompressedStream!;
this.DecodeFromDeflate(inflateStream.CompressedStream!, image, pngMetadata, frameControl, cancellationToken);
}
private void DecodeFromDeflate<TPixel>(
DeflateStream dataStream,
ImageFrame<TPixel> image,
PngMetadata pngMetadata,
in FrameControl frameControl,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.header.InterlaceMethod is PngInterlaceMode.Adam7)
{
this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken);
@ -902,7 +920,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
if (this.isCgbi)
{
this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
PngCgbiProcessor.ApplyTransform(this.configuration, scanSpan[1..], this.pngColorType);
}
this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer);
@ -1037,7 +1055,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
if (this.isCgbi)
{
this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
PngCgbiProcessor.ApplyTransform(this.configuration, scanSpan[1..], this.pngColorType);
}
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
@ -1431,6 +1449,22 @@ internal sealed class PngDecoderCore : ImageDecoderCore
this.pngColorType = this.header.ColorType;
this.Dimensions = new Size(this.header.Width, this.header.Height);
// Apple's pngcrush emits the CgBI chunk before IHDR, so the header
// compatibility check is deferred until both chunks have been seen.
if (this.isCgbi)
{
ThrowIfInvalidCgbiContent(this.header);
}
}
private static void ThrowIfInvalidCgbiContent(in PngHeader header)
{
if (header.BitDepth != 8 || (header.ColorType is not PngColorType.Rgb and not PngColorType.RgbWithAlpha))
{
PngThrowHelper.ThrowInvalidImageContentException(
$"CgBI is only supported for 8-bit truecolor images. Was bit depth '{header.BitDepth}', color type '{header.ColorType}'.");
}
}
/// <summary>
@ -2493,303 +2527,4 @@ internal sealed class PngDecoderCore : ImageDecoderCore
private void SwapScanlineBuffers()
=> (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline);
/// <summary>
/// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline.
/// CgBI PNGs are emitted by <c>pngcrush -iphone</c> with channel order swapped
/// from RGB(A) to BGR(A) and RGB samples premultiplied by alpha. This converts
/// the bytes back to standard PNG semantics in place so the existing scanline
/// processors can consume them unchanged. CgBI is only emitted for 8-bit
/// truecolor (with or without alpha); other color types are left alone.
/// </summary>
/// <remarks>
/// See https://theapplewiki.com/wiki/PNG_CgBI_Format
/// </remarks>
/// <param name="scanline">The defiltered pixel bytes (without the leading filter byte).</param>
/// <param name="colorType">The PNG color type from IHDR.</param>
private void ApplyCgbiTransform(Span<byte> scanline, PngColorType colorType)
{
if (colorType == PngColorType.RgbWithAlpha)
{
Span<Rgba32> pixels = MemoryMarshal.Cast<byte, Rgba32>(scanline);
int i = 0;
if (Vector512.IsHardwareAccelerated && pixels.Length >= 16)
{
i = ApplyCgbiTransformVector512(scanline, pixels.Length);
}
if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= 8)
{
i = ApplyCgbiTransformVector256(scanline, i, pixels.Length);
}
if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= 4)
{
i = ApplyCgbiTransformVector128(scanline, i, pixels.Length);
}
for (; i < pixels.Length; i++)
{
ref Rgba32 pixel = ref pixels[i];
pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
UndoCgbiPremultiplicationScalar(ref pixel);
}
}
else if (colorType == PngColorType.Rgb)
{
// No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations.
Span<Rgb24> target = MemoryMarshal.Cast<byte, Rgb24>(scanline);
PixelOperations<Rgb24>.Instance.FromBgr24Bytes(this.configuration, scanline, target, target.Length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UndoCgbiPremultiplicationScalar(ref Rgba32 pixel)
{
byte a = pixel.A;
if (a is 0 or byte.MaxValue)
{
return;
}
// Reverse: c' = c * a / 255 => c = round(c' * 255 / a)
int half = a >> 1;
byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
pixel = new Rgba32(r, g, b, a);
}
private static int ApplyCgbiTransformVector512(Span<byte> scanline, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = 0;
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
// MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
// CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
// The generated mask only swaps bytes inside each pixel, so it remains
// correct for the optimized 512-bit byte shuffle helper.
Vector512<byte> shuffleMask = Unsafe.As<byte, Vector512<byte>>(ref MemoryMarshal.GetReference(temp));
Vector512<int> zero = Vector512<int>.Zero;
Vector512<int> one = Vector512<int>.One;
Vector512<int> byteMask = Vector512.Create(0xFF);
Vector512<int> opaque = Vector512.Create(0xFF);
Vector512<int> byteMax = Vector512.Create((int)byte.MaxValue);
for (; i <= pixelCount - 16; i += 16)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector512<byte> bgra = Unsafe.ReadUnaligned<Vector512<byte>>(ref blockRef);
Vector512<byte> rgba = Vector512_.ShuffleNative(bgra, shuffleMask);
Vector512<int> packed = rgba.AsInt32();
Vector512<int> alpha = Vector512.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector512<int> partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, opaque));
Vector512<int> r = packed & byteMask;
Vector512<int> g = Vector512.ShiftRightLogical(packed, 8) & byteMask;
Vector512<int> b = Vector512.ShiftRightLogical(packed, 16) & byteMask;
Vector512<int> safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one);
Vector512<int> halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1);
Vector512<float> safeAlphaF = Vector512.ConvertToSingle(safeAlpha);
// The scalar path computes ((c * 255) + (a >> 1)) / a with integer
// division. Floor the positive quotient before converting so SIMD does
// not use the default round-to-nearest conversion and drift by one.
Vector512<int> unpremultipliedR = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
Vector512<int> unpremultipliedG = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
Vector512<int> unpremultipliedB = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector512<int> finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector512<int> finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector512<int> finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector512<int> result =
finalR |
Vector512.ShiftLeft(finalG, 8) |
Vector512.ShiftLeft(finalB, 16) |
Vector512.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static int ApplyCgbiTransformVector256(Span<byte> scanline, int startPixel, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = startPixel;
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
// MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
// CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
// Avx2.Shuffle is 128-bit lane-local, and the generated mask repeats inside
// each lane, so no byte ever needs to cross the lane boundary.
Vector256<byte> shuffleMask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(temp));
Vector256<int> zero = Vector256<int>.Zero;
Vector256<int> one = Vector256<int>.One;
Vector256<int> byteMask = Vector256.Create(0xFF);
Vector256<int> opaque = Vector256.Create(0xFF);
Vector256<int> byteMax = Vector256.Create((int)byte.MaxValue);
for (; i <= pixelCount - 8; i += 8)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector256<byte> bgra = Unsafe.ReadUnaligned<Vector256<byte>>(ref blockRef);
Vector256<byte> rgba = Vector256_.ShufflePerLane(bgra, shuffleMask);
Vector256<int> packed = rgba.AsInt32();
Vector256<int> alpha = Vector256.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector256<int> partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, opaque));
Vector256<int> r = packed & byteMask;
Vector256<int> g = Vector256.ShiftRightLogical(packed, 8) & byteMask;
Vector256<int> b = Vector256.ShiftRightLogical(packed, 16) & byteMask;
Vector256<int> safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one);
Vector256<int> halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1);
Vector256<float> safeAlphaF = Vector256.ConvertToSingle(safeAlpha);
// The scalar path computes ((c * 255) + (a >> 1)) / a with integer
// division. Floor the positive quotient before converting so SIMD does
// not use the default round-to-nearest conversion and drift by one.
Vector256<int> unpremultipliedR = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
Vector256<int> unpremultipliedG = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
Vector256<int> unpremultipliedB = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector256<int> finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector256<int> finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector256<int> finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector256<int> result =
finalR |
Vector256.ShiftLeft(finalG, 8) |
Vector256.ShiftLeft(finalB, 16) |
Vector256.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static int ApplyCgbiTransformVector128(Span<byte> scanline, int startPixel, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = startPixel;
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
// MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
// CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
Vector128<byte> shuffleMask = Unsafe.As<byte, Vector128<byte>>(ref MemoryMarshal.GetReference(temp));
Vector128<int> zero = Vector128<int>.Zero;
Vector128<int> one = Vector128<int>.One;
Vector128<int> byteMask = Vector128.Create(0xFF);
Vector128<int> opaque = Vector128.Create(0xFF);
Vector128<int> byteMax = Vector128.Create((int)byte.MaxValue);
for (; i <= pixelCount - 4; i += 4)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector128<byte> bgra = Unsafe.ReadUnaligned<Vector128<byte>>(ref blockRef);
Vector128<byte> rgba = Vector128_.ShuffleNative(bgra, shuffleMask);
Vector128<int> packed = rgba.AsInt32();
Vector128<int> alpha = Vector128.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector128<int> partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, opaque));
Vector128<int> r = packed & byteMask;
Vector128<int> g = Vector128.ShiftRightLogical(packed, 8) & byteMask;
Vector128<int> b = Vector128.ShiftRightLogical(packed, 16) & byteMask;
Vector128<int> safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one);
Vector128<int> halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1);
Vector128<float> safeAlphaF = Vector128.ConvertToSingle(safeAlpha);
// The scalar path computes ((c * 255) + (a >> 1)) / a with integer
// division. Floor the positive quotient before converting so SIMD does
// not use the default round-to-nearest conversion and drift by one.
Vector128<int> unpremultipliedR = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
Vector128<int> unpremultipliedG = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
Vector128<int> unpremultipliedB = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector128<int> finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector128<int> finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector128<int> finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector128<int> result =
finalR |
Vector128.ShiftLeft(finalG, 8) |
Vector128.ShiftLeft(finalB, 16) |
Vector128.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
}

105
tests/ImageSharp.Tests/Formats/Png/PngCgbiProcessorTests.cs

@ -0,0 +1,105 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats.Png;
[Trait("Format", "Png")]
public class PngCgbiProcessorTests
{
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(3)]
[InlineData(4)]
[InlineData(7)]
[InlineData(8)]
[InlineData(15)]
[InlineData(16)]
[InlineData(17)]
[InlineData(31)]
[InlineData(32)]
[InlineData(33)]
[InlineData(64)]
public void ApplyTransform_RgbWithAlpha_MatchesScalar(int pixelCount) =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
serialized => AssertApplyTransformMatchesScalar(FeatureTestRunner.Deserialize<int>(serialized)),
pixelCount,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
private static void AssertApplyTransformMatchesScalar(int pixelCount)
{
byte[] input = CreateBgraScanline(pixelCount);
byte[] processorOutput = (byte[])input.Clone();
byte[] scalarOutput = (byte[])input.Clone();
PngCgbiProcessor.ApplyTransform(Configuration.Default, processorOutput, PngColorType.RgbWithAlpha);
ApplyCgbiTransformScalarReference(scalarOutput);
Assert.Equal(scalarOutput, processorOutput);
}
/// <summary>
/// Builds synthetic input for tests. Produces inputs that exercise all three alpha cases.
/// </summary>
/// <returns>Channel values laid out as a defiltered CgBI scanline in premultiplied BGRA order</returns>
private static byte[] CreateBgraScanline(int pixelCount)
{
// The distinct strides keep the channels from being equal to each other or constant across pixels
// So a bug that e.g. swaps two channels or reuses one channel's value doesn't accidentally pass
const int alphaCaseCount = 7;
const int redStride = 13;
const int greenStride = 29;
const int blueStride = 53;
byte[] bytes = new byte[pixelCount * 4];
for (int p = 0; p < pixelCount; p++)
{
// Cycling alpha through [0..255], and an odd partial value every pixel rotation
// ensures all three branches get covered within any scanline of 3 or more pixels
byte a = (p % alphaCaseCount) switch
{
0 => byte.MinValue,
1 => byte.MaxValue,
_ => (byte)(((p * 37) + 23) | 1) // Produce a spread of alpha values and make sure to never get 0
};
int offset = p * 4;
bytes[offset + 0] = Premultiply((byte)(p * blueStride), a);
bytes[offset + 1] = Premultiply((byte)(p * greenStride), a);
bytes[offset + 2] = Premultiply((byte)(p * redStride), a);
bytes[offset + 3] = a;
}
return bytes;
// CgBI stores channels premultiplied by alpha
static byte Premultiply(byte channel, byte alpha) => (byte)(channel * alpha / byte.MaxValue);
}
private static void ApplyCgbiTransformScalarReference(Span<byte> scanline)
{
Span<Rgba32> pixels = MemoryMarshal.Cast<byte, Rgba32>(scanline);
for (int i = 0; i < pixels.Length; i++)
{
ref Rgba32 pixel = ref pixels[i];
pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
byte a = pixel.A;
if (a is 0 or byte.MaxValue)
{
continue;
}
int half = a >> 1;
byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
pixel = new Rgba32(r, g, b, a);
}
}
}

22
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -767,6 +767,28 @@ public partial class PngDecoderTests
Assert.Equal(expectedHeight, imageInfo.Height);
}
[Theory]
[InlineData(TestImages.Png.Cgbi.BitDepth16)]
[InlineData(TestImages.Png.Cgbi.Palette)]
public void Identify_CgBI_IncompatibleHeader_ThrowsInvalidImageContentException(string imagePath)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
InvalidImageContentException ex = Assert.Throws<InvalidImageContentException>(() => Image.Identify(stream));
Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message);
}
[Theory]
[WithFile(TestImages.Png.Cgbi.BitDepth16, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Palette, PixelTypes.Rgba32)]
public void Decode_CgBI_IncompatibleHeader_ThrowsInvalidImageContentException<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
InvalidImageContentException ex = Assert.Throws<InvalidImageContentException>(
() => { using Image<TPixel> image = provider.GetImage(PngDecoder.Instance); });
Assert.Contains("CgBI is only supported for 8-bit truecolor images", ex.Message);
}
[Theory]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)]

4
tests/ImageSharp.Tests/TestImages.cs

@ -189,6 +189,10 @@ public static class TestImages
// Issue 410: https://github.com/SixLabors/ImageSharp/issues/410
public const string Issue410 = "Png/issues/Issue_410.png";
// Synthetic fixtures derived from colors.png to exercise CgBI validation.
public const string BitDepth16 = "Png/cgbi/colors-cgbi-bitdepth16.png";
public const string Palette = "Png/cgbi/colors-cgbi-palette.png";
}
public static class Bad

3
tests/Images/Input/Png/cgbi/colors-cgbi-bitdepth16.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59610bc03f6ca867e5f71c574b3a0d1942c9e3a230c8a32bf3007cb82f286866
size 12853

3
tests/Images/Input/Png/cgbi/colors-cgbi-palette.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3a2f20c69ae423523a8f41887e3f37257a338f2220c2ea44d35c87daf8c3aa3
size 12853
Loading…
Cancel
Save