mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
14 KiB
315 lines
14 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Color;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
|
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
|
|
using SixLabors.ImageSharp.Metadata;
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.PixelFormats.Utils;
|
|
|
|
namespace SixLabors.ImageSharp.Formats.Heif.Av1;
|
|
|
|
/// <summary>
|
|
/// Decodes one AV1 still-image elementary stream into an ImageSharp image.
|
|
/// </summary>
|
|
internal sealed class Av1Decoder : IAv1TileReader, IDisposable
|
|
{
|
|
/// <summary>
|
|
/// The open-bitstream-unit parser for the current image item.
|
|
/// </summary>
|
|
private readonly ObuReader obuReader;
|
|
|
|
/// <summary>
|
|
/// The configuration used for decoded image and scratch-memory allocation.
|
|
/// </summary>
|
|
private readonly Configuration configuration;
|
|
|
|
/// <summary>
|
|
/// The tile parser shared by all tile groups in the current frame.
|
|
/// </summary>
|
|
private Av1TileReader? tileReader;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Av1Decoder"/> class.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration used for image and scratch-memory allocation.</param>
|
|
public Av1Decoder(Configuration configuration)
|
|
{
|
|
this.configuration = configuration;
|
|
this.obuReader = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the decoded frame header, or <see langword="null"/> before the stream provides one.
|
|
/// </summary>
|
|
public ObuFrameHeader? FrameHeader { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the decoded sequence header, or <see langword="null"/> before the stream provides one.
|
|
/// </summary>
|
|
public ObuSequenceHeader? SequenceHeader { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the tile and superblock state for the decoded frame, or <see langword="null"/> before tile parsing completes.
|
|
/// </summary>
|
|
public Av1FrameInfo? FrameInfo { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Decodes an AV1 still-image elementary stream.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
|
|
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
|
|
/// <param name="containerColorProfile">
|
|
/// The container color description that supplies unspecified sequence-header color information.
|
|
/// </param>
|
|
/// <param name="codecConfiguration">
|
|
/// The item-associated AV1 codec configuration validated against the coded sequence header.
|
|
/// </param>
|
|
/// <returns>The decoded image.</returns>
|
|
public Image<TPixel> Decode<TPixel>(
|
|
Span<byte> buffer,
|
|
CicpProfile? containerColorProfile = null,
|
|
Av1CodecConfiguration? codecConfiguration = null)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
ImageFrame<TPixel> frame = this.DecodeFrame<TPixel>(buffer, containerColorProfile, codecConfiguration, out CicpProfile effectiveColorProfile);
|
|
ImageMetadata metadata = new()
|
|
{
|
|
CicpProfile = effectiveColorProfile
|
|
};
|
|
|
|
try
|
|
{
|
|
return new Image<TPixel>(this.configuration, metadata, [frame]);
|
|
}
|
|
catch
|
|
{
|
|
// Ownership transfers only after the image constructor accepts the decoded frame.
|
|
frame.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decodes an AV1 elementary-stream payload into one independently owned ImageSharp frame.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
|
|
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
|
|
/// <param name="containerColorProfile">
|
|
/// The container color description that supplies unspecified sequence-header color information.
|
|
/// </param>
|
|
/// <param name="codecConfiguration">
|
|
/// The AV1 codec configuration validated against the coded sequence header.
|
|
/// </param>
|
|
/// <param name="effectiveColorProfile">Receives the effective CICP description used for conversion.</param>
|
|
/// <returns>The decoded frame. Ownership transfers to the caller.</returns>
|
|
public ImageFrame<TPixel> DecodeFrame<TPixel>(
|
|
Span<byte> buffer,
|
|
CicpProfile? containerColorProfile,
|
|
Av1CodecConfiguration? codecConfiguration,
|
|
out CicpProfile effectiveColorProfile)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
using Av1FrameBuffer<byte> frameBuffer = this.DecodeFrameBuffer(
|
|
buffer,
|
|
containerColorProfile,
|
|
codecConfiguration,
|
|
out effectiveColorProfile);
|
|
|
|
ImageFrame<TPixel>? resultFrame = null;
|
|
try
|
|
{
|
|
resultFrame = new ImageFrame<TPixel>(
|
|
this.configuration,
|
|
this.FrameHeader!.FrameSize.SuperResolutionUpscaledWidth,
|
|
this.FrameHeader.FrameSize.FrameHeight);
|
|
|
|
Av1YuvConverter.ConvertToRgb(this.configuration, frameBuffer, resultFrame);
|
|
resultFrame.Metadata.CicpProfile = effectiveColorProfile.DeepClone();
|
|
return resultFrame;
|
|
}
|
|
catch
|
|
{
|
|
resultFrame?.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decodes an AV1 elementary-stream payload and composes its luma plane directly into a packed color frame.
|
|
/// </summary>
|
|
/// <typeparam name="TPixel">The destination color pixel type.</typeparam>
|
|
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
|
|
/// <param name="containerColorProfile">
|
|
/// The container color description that supplies unspecified sequence-header color information.
|
|
/// </param>
|
|
/// <param name="codecConfiguration">The AV1 codec configuration validated against the coded sequence header.</param>
|
|
/// <param name="expectedCodedSize">The required coded dimensions, or an empty size when the item extent may differ.</param>
|
|
/// <param name="destination">The packed color frame receiving alpha values.</param>
|
|
/// <param name="outputSize">The complete presented size of the auxiliary image or grid tile.</param>
|
|
/// <param name="destinationRectangle">The destination region receiving the top-left portion of the presented alpha image.</param>
|
|
/// <param name="premultiplied">Whether stored color samples must be converted to unassociated alpha.</param>
|
|
public void DecodeAlpha<TPixel>(
|
|
Span<byte> buffer,
|
|
CicpProfile? containerColorProfile,
|
|
Av1CodecConfiguration? codecConfiguration,
|
|
Size expectedCodedSize,
|
|
ImageFrame<TPixel> destination,
|
|
Size outputSize,
|
|
Rectangle destinationRectangle,
|
|
bool premultiplied)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
using Av1FrameBuffer<byte> frameBuffer = this.DecodeFrameBuffer(buffer, containerColorProfile, codecConfiguration, out _);
|
|
if (expectedCodedSize != default && (frameBuffer.Width != expectedCodedSize.Width || frameBuffer.Height != expectedCodedSize.Height))
|
|
{
|
|
throw new InvalidImageContentException("The decoded alpha sample dimensions do not match its visual sample entry.");
|
|
}
|
|
|
|
if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv400)
|
|
{
|
|
// AVIF auxiliary alpha is the luma plane of an AV1 monochrome image. Accepting chroma-bearing payloads
|
|
// would silently reinterpret a color image and contradict the Sequence Header mono_chrome requirement.
|
|
throw new InvalidImageContentException("An AV1 auxiliary alpha image must be encoded as monochrome.");
|
|
}
|
|
|
|
Av1YuvConverter.ComposeAlpha(
|
|
this.configuration,
|
|
frameBuffer,
|
|
destination,
|
|
outputSize,
|
|
destinationRectangle,
|
|
premultiplied);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses and reconstructs one AV1 frame while retaining its native component planes for the caller.
|
|
/// </summary>
|
|
/// <param name="buffer">The complete AV1 elementary-stream payload.</param>
|
|
/// <param name="containerColorProfile">
|
|
/// The container color description that supplies unspecified sequence-header color information.
|
|
/// </param>
|
|
/// <param name="codecConfiguration">The AV1 codec configuration validated against the coded sequence header.</param>
|
|
/// <param name="effectiveColorProfile">Receives the effective CICP description associated with the native planes.</param>
|
|
/// <returns>The reconstructed native frame buffer. Ownership transfers to the caller.</returns>
|
|
private Av1FrameBuffer<byte> DecodeFrameBuffer(
|
|
Span<byte> buffer,
|
|
CicpProfile? containerColorProfile,
|
|
Av1CodecConfiguration? codecConfiguration,
|
|
out CicpProfile effectiveColorProfile)
|
|
{
|
|
Av1BitStreamReader reader = new(buffer);
|
|
this.obuReader.ReadAll(ref reader, buffer.Length, () => this, false);
|
|
Guard.NotNull(this.tileReader, nameof(this.tileReader));
|
|
Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader));
|
|
Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader));
|
|
codecConfiguration?.Validate(this.SequenceHeader);
|
|
|
|
if (containerColorProfile is not null)
|
|
{
|
|
ObuColorConfig colorConfig = this.SequenceHeader.ColorConfig;
|
|
ObuColorPrimaries containerColorPrimaries = (ObuColorPrimaries)containerColorProfile.ColorPrimaries;
|
|
ObuTransferCharacteristics containerTransferCharacteristics =
|
|
(ObuTransferCharacteristics)containerColorProfile.TransferCharacteristics;
|
|
|
|
ObuMatrixCoefficients containerMatrixCoefficients =
|
|
(ObuMatrixCoefficients)containerColorProfile.MatrixCoefficients;
|
|
|
|
// AV1-ISOBMFF permits nclx to supply only bitstream fields explicitly coded as unspecified. A
|
|
// different specified value is a conformance error rather than a container-level color override.
|
|
if (colorConfig.ColorPrimaries == ObuColorPrimaries.Unspecified)
|
|
{
|
|
colorConfig.ColorPrimaries = containerColorPrimaries;
|
|
}
|
|
else if (colorConfig.ColorPrimaries != containerColorPrimaries)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF CICP color primaries do not match the AV1 sequence header.");
|
|
}
|
|
|
|
if (colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Unspecified)
|
|
{
|
|
colorConfig.TransferCharacteristics = containerTransferCharacteristics;
|
|
}
|
|
else if (colorConfig.TransferCharacteristics != containerTransferCharacteristics)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF CICP transfer characteristics do not match the AV1 sequence header.");
|
|
}
|
|
|
|
if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Unspecified)
|
|
{
|
|
colorConfig.MatrixCoefficients = containerMatrixCoefficients;
|
|
}
|
|
else if (colorConfig.MatrixCoefficients != containerMatrixCoefficients)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header.");
|
|
}
|
|
|
|
if (colorConfig.ColorRange != containerColorProfile.FullRange)
|
|
{
|
|
throw new InvalidImageContentException("The HEIF CICP color range does not match the AV1 sequence header.");
|
|
}
|
|
}
|
|
|
|
this.FrameInfo = this.tileReader.FrameInfo;
|
|
Av1FrameBuffer<byte> frameBuffer = new(
|
|
this.configuration,
|
|
this.SequenceHeader,
|
|
this.SequenceHeader.ColorConfig.GetColorFormat(),
|
|
false);
|
|
|
|
try
|
|
{
|
|
using Av1FrameDecoder frameDecoder = new(this.SequenceHeader, this.FrameHeader, this.FrameInfo, frameBuffer);
|
|
frameDecoder.DecodeFrame();
|
|
|
|
// Preserve the effective CICP description used for conversion, including container values that legally
|
|
// supplied unspecified bitstream fields. This also exposes bitstream-only color metadata to callers.
|
|
ObuColorConfig effectiveColorConfig = this.SequenceHeader.ColorConfig;
|
|
effectiveColorProfile = new CicpProfile(
|
|
(byte)effectiveColorConfig.ColorPrimaries,
|
|
(byte)effectiveColorConfig.TransferCharacteristics,
|
|
(byte)effectiveColorConfig.MatrixCoefficients,
|
|
effectiveColorConfig.ColorRange);
|
|
|
|
return frameBuffer;
|
|
}
|
|
catch
|
|
{
|
|
frameBuffer.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses one entropy-coded tile payload into the current frame state.
|
|
/// </summary>
|
|
/// <param name="tileData">The entropy-coded tile payload.</param>
|
|
/// <param name="tileNum">The raster-order tile index.</param>
|
|
public void ReadTile(Span<byte> tileData, int tileNum)
|
|
{
|
|
if (this.tileReader is null)
|
|
{
|
|
this.SequenceHeader = this.obuReader.SequenceHeader;
|
|
this.FrameHeader = this.obuReader.FrameHeader;
|
|
Guard.NotNull(this.SequenceHeader, nameof(this.SequenceHeader));
|
|
Guard.NotNull(this.FrameHeader, nameof(this.FrameHeader));
|
|
|
|
// Every tile group in a frame contributes to the same mode-info and coefficient state.
|
|
this.tileReader = new Av1TileReader(this.configuration, this.SequenceHeader, this.FrameHeader);
|
|
}
|
|
|
|
this.tileReader.ReadTile(tileData, tileNum);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Releases the tile reader and its frame-scoped parsing storage.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
this.tileReader?.Dispose();
|
|
this.tileReader = null;
|
|
}
|
|
}
|
|
|