mirror of https://github.com/SixLabors/ImageSharp
6 changed files with 393 additions and 12 deletions
@ -0,0 +1,180 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
|
|||
/// <summary>
|
|||
/// Reads fixed-width and Exp-Golomb HEVC syntax from a most-significant-bit-first byte span.
|
|||
/// </summary>
|
|||
internal ref struct HevcBitReader |
|||
{ |
|||
/// <summary>
|
|||
/// The complete raw byte sequence buffer.
|
|||
/// </summary>
|
|||
private readonly ReadOnlySpan<byte> data; |
|||
|
|||
/// <summary>
|
|||
/// The zero-based position of the next bit to read.
|
|||
/// </summary>
|
|||
private int bitPosition; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HevcBitReader"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="data">The bounded HEVC syntax bytes.</param>
|
|||
public HevcBitReader(ReadOnlySpan<byte> data) |
|||
{ |
|||
this.data = data; |
|||
this.bitPosition = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the zero-based position of the next bit to read.
|
|||
/// </summary>
|
|||
public readonly int BitPosition => this.bitPosition; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of unread bits in the bounded byte span.
|
|||
/// </summary>
|
|||
public readonly int BitsRemaining => (this.data.Length * 8) - this.bitPosition; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the next bit begins a byte.
|
|||
/// </summary>
|
|||
public readonly bool IsByteAligned => (this.bitPosition & 7) == 0; |
|||
|
|||
/// <summary>
|
|||
/// Reads an unsigned fixed-width value in most-significant-bit-first order.
|
|||
/// </summary>
|
|||
/// <param name="bitCount">The number of bits to read.</param>
|
|||
/// <returns>The decoded unsigned value.</returns>
|
|||
/// <exception cref="InvalidImageContentException">
|
|||
/// The requested value extends beyond the bounded HEVC syntax.
|
|||
/// </exception>
|
|||
public uint ReadBits(int bitCount) |
|||
{ |
|||
DebugGuard.MustBeBetweenOrEqualTo(bitCount, 0, 32, nameof(bitCount)); |
|||
if (bitCount > this.BitsRemaining) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC bitstream is truncated."); |
|||
} |
|||
|
|||
uint value = 0; |
|||
int remaining = bitCount; |
|||
while (remaining > 0) |
|||
{ |
|||
// HEVC fixed-width syntax is MSB-first. Reading only the available portion of each byte keeps the
|
|||
// same operation valid for both aligned parameter fields and fields that straddle byte boundaries.
|
|||
int byteOffset = this.bitPosition >> 3; |
|||
int bitOffset = this.bitPosition & 7; |
|||
int bitsFromByte = Math.Min(remaining, 8 - bitOffset); |
|||
int shift = 8 - bitOffset - bitsFromByte; |
|||
uint mask = (1U << bitsFromByte) - 1; |
|||
|
|||
value = (value << bitsFromByte) | ((uint)(this.data[byteOffset] >> shift) & mask); |
|||
this.bitPosition += bitsFromByte; |
|||
remaining -= bitsFromByte; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a one-bit HEVC flag.
|
|||
/// </summary>
|
|||
/// <returns><see langword="true"/> when the coded flag is one; otherwise, <see langword="false"/>.</returns>
|
|||
/// <exception cref="InvalidImageContentException">The flag extends beyond the bounded HEVC syntax.</exception>
|
|||
public bool ReadFlag() => this.ReadBits(1) != 0; |
|||
|
|||
/// <summary>
|
|||
/// Reads an unsigned exponential-Golomb value.
|
|||
/// </summary>
|
|||
/// <returns>The decoded unsigned value.</returns>
|
|||
/// <exception cref="InvalidImageContentException">
|
|||
/// The code is truncated or exceeds the range of a 32-bit unsigned integer.
|
|||
/// </exception>
|
|||
public uint ReadUnsignedExpGolomb() |
|||
{ |
|||
int leadingZeroBits = 0; |
|||
while (!this.ReadFlag()) |
|||
{ |
|||
leadingZeroBits++; |
|||
if (leadingZeroBits > 32) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC unsigned Exp-Golomb value exceeds 32 bits."); |
|||
} |
|||
} |
|||
|
|||
// In ue(v), the zero-prefix length selects an all-one basis and the equally wide suffix selects the
|
|||
// offset from that basis. Keeping those parts separate makes the 32-bit overflow boundary explicit.
|
|||
uint suffix = this.ReadBits(leadingZeroBits); |
|||
if (leadingZeroBits == 32) |
|||
{ |
|||
// Only an all-zero suffix fits after the 32-bit all-one basis.
|
|||
if (suffix != 0) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC unsigned Exp-Golomb value exceeds 32 bits."); |
|||
} |
|||
|
|||
return uint.MaxValue; |
|||
} |
|||
|
|||
return ((1U << leadingZeroBits) - 1) + suffix; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a signed exponential-Golomb value.
|
|||
/// </summary>
|
|||
/// <returns>The decoded signed value.</returns>
|
|||
/// <exception cref="InvalidImageContentException">
|
|||
/// The code is truncated or exceeds the range of a 32-bit signed integer.
|
|||
/// </exception>
|
|||
public int ReadSignedExpGolomb() |
|||
{ |
|||
uint codeNumber = this.ReadUnsignedExpGolomb(); |
|||
|
|||
// HEVC's se(v) mapping alternates positive and negative magnitudes: 0, 1, -1, 2, -2, and so on.
|
|||
if ((codeNumber & 1) == 0) |
|||
{ |
|||
return -(int)(codeNumber >> 1); |
|||
} |
|||
|
|||
ulong magnitude = ((ulong)codeNumber + 1) >> 1; |
|||
if (magnitude > int.MaxValue) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC signed Exp-Golomb value exceeds 32 bits."); |
|||
} |
|||
|
|||
return (int)magnitude; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads and validates the stop bit and zero alignment bits that terminate an HEVC raw byte sequence payload.
|
|||
/// </summary>
|
|||
/// <exception cref="InvalidImageContentException">
|
|||
/// The trailing-bit pattern is truncated, malformed, or followed by additional data.
|
|||
/// </exception>
|
|||
public void ReadRbspTrailingBits() |
|||
{ |
|||
// An RBSP ends with one stop bit followed only by zero bits up to the next byte boundary.
|
|||
if (!this.ReadFlag()) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC RBSP stop bit is not set."); |
|||
} |
|||
|
|||
while (!this.IsByteAligned) |
|||
{ |
|||
if (this.ReadFlag()) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC RBSP has a nonzero alignment bit."); |
|||
} |
|||
} |
|||
|
|||
// Each reader is bounded to one RBSP, so reaching alignment before the buffer end means the caller left
|
|||
// syntax unread or the NAL unit contains bytes beyond its normative terminator.
|
|||
if (this.BitsRemaining != 0) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC RBSP contains unexpected trailing data."); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
|
|||
/// <summary>
|
|||
/// Contains one decoded HEVC network abstraction layer unit.
|
|||
/// </summary>
|
|||
internal sealed class HevcNalUnit |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HevcNalUnit"/> class.
|
|||
/// </summary>
|
|||
/// <param name="data">The complete NAL unit, including its two-byte header.</param>
|
|||
/// <exception cref="InvalidImageContentException">The NAL header or encoded payload is malformed.</exception>
|
|||
public HevcNalUnit(ReadOnlySpan<byte> data) |
|||
{ |
|||
this.Header = HevcNalUnitHeader.Parse(data); |
|||
|
|||
// Container and configuration NAL units carry EBSP bytes. Decode them once at the boundary so every
|
|||
// parameter-set and slice parser observes the same validated RBSP representation.
|
|||
ReadOnlySpan<byte> encodedPayload = data[2..]; |
|||
byte[] rbspBuffer = new byte[encodedPayload.Length]; |
|||
int rbspLength = HevcRbspDecoder.Decode(encodedPayload, rbspBuffer); |
|||
this.Rbsp = rbspBuffer.AsMemory(0, rbspLength); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the decoded two-byte NAL-unit header.
|
|||
/// </summary>
|
|||
public HevcNalUnitHeader Header { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the raw byte sequence payload after removal of emulation-prevention bytes.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<byte> Rbsp { get; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Removes HEVC emulation-prevention bytes from an encoded raw byte sequence payload.
|
|||
/// </summary>
|
|||
internal static class HevcRbspDecoder |
|||
{ |
|||
/// <summary>
|
|||
/// Decodes an encoded byte sequence payload into a raw byte sequence payload.
|
|||
/// </summary>
|
|||
/// <param name="encodedPayload">The NAL payload following the two-byte header.</param>
|
|||
/// <param name="destination">A buffer at least as long as <paramref name="encodedPayload"/>.</param>
|
|||
/// <returns>The number of decoded bytes written to <paramref name="destination"/>.</returns>
|
|||
/// <exception cref="InvalidImageContentException">
|
|||
/// The payload contains a forbidden start-code-like byte sequence or an invalid emulation-prevention byte.
|
|||
/// </exception>
|
|||
public static int Decode(ReadOnlySpan<byte> encodedPayload, Span<byte> destination) |
|||
{ |
|||
DebugGuard.MustBeGreaterThanOrEqualTo(destination.Length, encodedPayload.Length, nameof(destination)); |
|||
|
|||
int destinationOffset = 0; |
|||
int consecutiveZeroBytes = 0; |
|||
for (int sourceOffset = 0; sourceOffset < encodedPayload.Length; sourceOffset++) |
|||
{ |
|||
byte value = encodedPayload[sourceOffset]; |
|||
|
|||
// HEVC section 7.3.1.1 forbids 00 00 00 through 00 00 02 in EBSP form. A 03 after two zeros is an
|
|||
// emulation-prevention byte only when another byte in the range 00 through 03 follows it.
|
|||
if (consecutiveZeroBytes == 2) |
|||
{ |
|||
if (value < 3) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC NAL unit contains a forbidden start-code-like byte sequence."); |
|||
} |
|||
|
|||
if (value == 3) |
|||
{ |
|||
sourceOffset++; |
|||
if (sourceOffset == encodedPayload.Length || encodedPayload[sourceOffset] > 3) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC NAL unit contains an invalid emulation-prevention byte."); |
|||
} |
|||
|
|||
// Removal depends on the preceding two decoded bytes, so this deliberately remains a single
|
|||
// scalar pass rather than introducing a second SIMD behavior model for a non-hot syntax path.
|
|||
value = encodedPayload[sourceOffset]; |
|||
consecutiveZeroBytes = 0; |
|||
} |
|||
} |
|||
|
|||
destination[destinationOffset++] = value; |
|||
consecutiveZeroBytes = value == 0 ? consecutiveZeroBytes + 1 : 0; |
|||
} |
|||
|
|||
return destinationOffset; |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Hevc; |
|||
|
|||
/// <summary>
|
|||
/// Contains the type, layer, and temporal identifier encoded by an HEVC NAL-unit header.
|
|||
/// </summary>
|
|||
internal readonly struct HevcNalUnitHeader |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="HevcNalUnitHeader"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="nalUnitType">The six-bit NAL-unit type.</param>
|
|||
/// <param name="layerId">The six-bit layer identifier.</param>
|
|||
/// <param name="temporalId">The zero-based temporal identifier.</param>
|
|||
private HevcNalUnitHeader(byte nalUnitType, byte layerId, byte temporalId) |
|||
{ |
|||
this.NalUnitType = nalUnitType; |
|||
this.LayerId = layerId; |
|||
this.TemporalId = temporalId; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the six-bit NAL-unit type.
|
|||
/// </summary>
|
|||
public byte NalUnitType { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the six-bit layer identifier.
|
|||
/// </summary>
|
|||
public byte LayerId { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the zero-based temporal identifier.
|
|||
/// </summary>
|
|||
public byte TemporalId { get; } |
|||
|
|||
/// <summary>
|
|||
/// Reads and validates an HEVC NAL-unit header.
|
|||
/// </summary>
|
|||
/// <param name="data">The complete NAL unit beginning with its two-byte header.</param>
|
|||
/// <returns>The decoded header.</returns>
|
|||
/// <exception cref="InvalidImageContentException">
|
|||
/// The header is truncated, its forbidden bit is set, or its temporal identifier is reserved.
|
|||
/// </exception>
|
|||
public static HevcNalUnitHeader Parse(ReadOnlySpan<byte> data) |
|||
{ |
|||
if (data.Length < 2) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC NAL-unit header is truncated."); |
|||
} |
|||
|
|||
// Use the same bounded MSB-first reader as the RBSP parsers so header truncation and field ordering have
|
|||
// one behavior model instead of a second set of shifts and masks.
|
|||
HevcBitReader reader = new(data[..2]); |
|||
bool forbiddenZeroBit = reader.ReadFlag(); |
|||
byte nalUnitType = (byte)reader.ReadBits(6); |
|||
byte layerId = (byte)reader.ReadBits(6); |
|||
byte temporalIdPlusOne = (byte)reader.ReadBits(3); |
|||
if (forbiddenZeroBit || temporalIdPlusOne == 0) |
|||
{ |
|||
throw new InvalidImageContentException("The HEVC NAL-unit header is invalid."); |
|||
} |
|||
|
|||
return new HevcNalUnitHeader(nalUnitType, layerId, (byte)(temporalIdPlusOne - 1)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue