mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
12 changed files with 816 additions and 617 deletions
@ -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(); |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.IO.Compression; |
||||
|
using SixLabors.ImageSharp.IO; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Compression.Zlib; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 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 ZlibInflateReader : IDisposable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Used to read the Adler-32 and Crc-32 checksums.
|
||||
|
/// We don't actually use this for anything so it doesn't
|
||||
|
/// have to be threadsafe.
|
||||
|
/// </summary>
|
||||
|
private static readonly byte[] ChecksumBuffer = new byte[4]; |
||||
|
|
||||
|
private readonly ChunkedReadStream segmentStream; |
||||
|
|
||||
|
public ZlibInflateReader(BufferedReadStream innerStream) |
||||
|
=> this.segmentStream = new ChunkedReadStream(innerStream); |
||||
|
|
||||
|
public ZlibInflateReader(BufferedReadStream innerStream, Func<int> getData) |
||||
|
=> this.segmentStream = new ChunkedReadStream(innerStream, getData); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the compressed stream over the deframed inner stream.
|
||||
|
/// </summary>
|
||||
|
public DeflateStream? CompressedStream { get; private set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the length of the next segment of compressed input and, on first
|
||||
|
/// call, parses the zlib header.
|
||||
|
/// </summary>
|
||||
|
/// <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.segmentStream.SetCurrentSegmentLength(bytes); |
||||
|
if (this.CompressedStream is null) |
||||
|
{ |
||||
|
return this.InitializeInflateStream(isCriticalChunk); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
this.CompressedStream?.Dispose(); |
||||
|
this.segmentStream?.Dispose(); |
||||
|
} |
||||
|
|
||||
|
[MemberNotNullWhen(true, nameof(CompressedStream))] |
||||
|
private bool InitializeInflateStream(bool isCriticalChunk) |
||||
|
{ |
||||
|
// Read the zlib header : http://tools.ietf.org/html/rfc1950
|
||||
|
// CMF(Compression Method and flags)
|
||||
|
// This byte is divided into a 4 - bit compression method and a
|
||||
|
// 4-bit information field depending on the compression method.
|
||||
|
// bits 0 to 3 CM Compression method
|
||||
|
// bits 4 to 7 CINFO Compression info
|
||||
|
//
|
||||
|
// 0 1
|
||||
|
// +---+---+
|
||||
|
// |CMF|FLG|
|
||||
|
// +---+---+
|
||||
|
int cmf = this.segmentStream.ReadByte(); |
||||
|
int flag = this.segmentStream.ReadByte(); |
||||
|
if (cmf == -1 || flag == -1) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if ((cmf & 0x0F) == 8) |
||||
|
{ |
||||
|
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
|
||||
|
int cinfo = (cmf & 0xF0) >> 4; |
||||
|
|
||||
|
if (cinfo > 7) |
||||
|
{ |
||||
|
if (isCriticalChunk) |
||||
|
{ |
||||
|
// Values of CINFO above 7 are not allowed in RFC1950.
|
||||
|
// CINFO is not defined in this specification for CM not equal to 8.
|
||||
|
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
else if (isCriticalChunk) |
||||
|
{ |
||||
|
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// The preset dictionary.
|
||||
|
bool fdict = (flag & 32) != 0; |
||||
|
if (fdict) |
||||
|
{ |
||||
|
// 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.segmentStream.Read(ChecksumBuffer, 0, 4) != 4) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
@ -1,306 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Six Labors Split License.
|
|
||||
|
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.IO.Compression; |
|
||||
using SixLabors.ImageSharp.IO; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Provides methods and properties for deframing streams from PNGs.
|
|
||||
/// </summary>
|
|
||||
internal sealed class ZlibInflateStream : Stream |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Used to read the Adler-32 and Crc-32 checksums.
|
|
||||
/// We don't actually use this for anything so it doesn't
|
|
||||
/// have to be threadsafe.
|
|
||||
/// </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; |
|
||||
|
|
||||
/// <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) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <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(); } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the compressed stream over the deframed inner stream.
|
|
||||
/// </summary>
|
|
||||
public DeflateStream? CompressedStream { get; private set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Adds new bytes from a frame found in the original stream.
|
|
||||
/// </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>
|
|
||||
/// <returns>The <see cref="bool"/>.</returns>
|
|
||||
[MemberNotNullWhen(true, nameof(CompressedStream))] |
|
||||
public bool AllocateNewBytes(int bytes, bool isCriticalChunk) |
|
||||
{ |
|
||||
this.currentDataRemaining = bytes; |
|
||||
if (this.CompressedStream is null) |
|
||||
{ |
|
||||
return this.InitializeInflateStream(isCriticalChunk); |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override void Flush() => throw new NotSupportedException(); |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public override int ReadByte() |
|
||||
{ |
|
||||
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; |
|
||||
} |
|
||||
|
|
||||
[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
|
|
||||
// 4-bit information field depending on the compression method.
|
|
||||
// bits 0 to 3 CM Compression method
|
|
||||
// bits 4 to 7 CINFO Compression info
|
|
||||
//
|
|
||||
// 0 1
|
|
||||
// +---+---+
|
|
||||
// |CMF|FLG|
|
|
||||
// +---+---+
|
|
||||
int cmf = this.innerStream.ReadByte(); |
|
||||
int flag = this.innerStream.ReadByte(); |
|
||||
this.currentDataRemaining -= 2; |
|
||||
if (cmf == -1 || flag == -1) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
if ((cmf & 0x0F) == 8) |
|
||||
{ |
|
||||
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
|
|
||||
int cinfo = (cmf & 0xF0) >> 4; |
|
||||
|
|
||||
if (cinfo > 7) |
|
||||
{ |
|
||||
if (isCriticalChunk) |
|
||||
{ |
|
||||
// Values of CINFO above 7 are not allowed in RFC1950.
|
|
||||
// CINFO is not defined in this specification for CM not equal to 8.
|
|
||||
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
else if (isCriticalChunk) |
|
||||
{ |
|
||||
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
// The preset dictionary.
|
|
||||
bool fdict = (flag & 32) != 0; |
|
||||
if (fdict) |
|
||||
{ |
|
||||
// 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) |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
this.currentDataRemaining -= 4; |
|
||||
} |
|
||||
|
|
||||
// Initialize the deflate BufferedReadStream.
|
|
||||
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true); |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:59610bc03f6ca867e5f71c574b3a0d1942c9e3a230c8a32bf3007cb82f286866 |
||||
|
size 12853 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:a3a2f20c69ae423523a8f41887e3f37257a338f2220c2ea44d35c87daf8c3aa3 |
||||
|
size 12853 |
||||
Loading…
Reference in new issue