Browse Source

Complete AVIF ICC handling and remove HEIF materializations

pull/2633/head
James Jackson-South 1 week ago
parent
commit
70fae55378
  1. 12
      HEIF_IMPLEMENTATION_PLAN.md
  2. 2
      src/ImageSharp/Formats/DecoderOptions.cs
  3. 48
      src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs
  4. 8
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs
  5. 6
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  6. 216
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs
  7. 20
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1IntraFilterModeInfo.cs
  8. 72
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs
  9. 10
      src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs
  10. 32
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  11. 44
      src/ImageSharp/Formats/Heif/HeifEncoderCore.cs
  12. 8
      src/ImageSharp/Formats/Heif/HeifPropertyParser.cs
  13. 3
      src/ImageSharp/Formats/Heif/HeifSequenceParser.cs
  14. 86
      src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs
  15. 44
      src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs
  16. 4
      src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs
  17. 6
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  18. 4
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs
  19. 125
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs
  20. 6
      tests/ImageSharp.Tests/TestImages.cs
  21. 3
      tests/Images/Input/Heif/ducky_romm_icc_alpha.avif
  22. 3
      tests/Images/Input/Heif/perceptual_icc_grid.avif
  23. 3
      tests/Images/Input/Heif/perceptual_icc_sequence.avif

12
HEIF_IMPLEMENTATION_PLAN.md

@ -35,9 +35,17 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [x] Prove that a genuine non-sRGB AVIF profile changes decoded pixels and matches the independently converted source image within the documented AV1 tolerance.
- [x] Verify canonical-sRGB compaction and metadata skipping independently from ICC preservation and color conversion.
- [ ] Complete the SIMD YUV/CICP paths for every supported AV1 bit depth, chroma format, range, matrix, transfer function, color primary, and chroma position, with scalar fallback only when hardware vectorization is unavailable or the operation is inherently non-vectorizable.
- [ ] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation.
- [x] Apply ICC conversion only after the SIMD YUV/CICP stage, alpha composition, grid assembly, and presentation transforms have produced the presented RGB image; retain ImageSharp's shared ICC converter and optimize reusable bulk kernels rather than creating a HEIF-specific color-management implementation.
- [ ] Verify ICC preservation, conversion, compaction, and metadata skipping for grids, alpha-composited images, every presented sequence frame, and the completed HEVC path.
- [x] Independently encode AVIF primary, grid, alpha-auxiliary, and two-frame sequence vectors with libavif 1.4.2/libaom 3.14.1; verify exact profile preservation, non-sRGB conversion, canonical-sRGB compaction, non-sRGB compact preservation, metadata skipping, every presented frame, and unchanged composed alpha.
- [ ] Repeat the same matrix for HEVC primary, grid, alpha-auxiliary, and sequence paths after HEVC reconstruction is complete.
- [ ] Benchmark representative 8/10/12-bit end-to-end AVIF and HEIC color pipelines with and without ICC conversion, recording absolute timings and allocations before marking Phase 5 complete.
- [x] **Completed:** remove production `ToArray()` materializations from the HEIF implementation and make every retained copy an explicit ownership decision.
- [x] Store AV1 palette colors and HEVC short-term reference-picture state inline at their normative fixed bounds.
- [x] Retain only the AV1 configuration sequence-header payload required for item/sample equivalence instead of every configuration OBU.
- [x] Allocate HEVC emulation-prevention positions once at the exact validated count without a growing list plus array copy.
- [x] Use ImageSharp's pooled `ChunkedMemoryStream` and direct chunk writes for the legacy JPEG item payload.
- [x] Keep exact owned arrays only where ICC, Exif, XMP, or retained property state must outlive pooled parser buffers.
- [x] **Completed:** eliminate per-call scratch allocation across every AV1 forward and inverse 1-D transform, replace shared mutable transform scratch with operation-owned workspace, and implement libaom-shaped bulk transform paths with scalar parity.
- [x] Inventory the existing hot-path scratch use: 15 forward/inverse transformer types contain 30 `stackalloc int[...]` sites.
- [x] Refactor every affected forward transform to reuse caller-owned workspace and its output buffer for staged ping-pong storage.
@ -252,7 +260,7 @@ This assessment is based on the current source after the upstream ImageSharp mer
- Alpha auxiliary decoding now recognizes `auxC`, `auxl`, and `prem`, supports a direct auxiliary image/grid and the per-color-grid-tile form, preserves normalized alpha through `L16`/`Rgba64`, box-resamples differing plane sizes, and reports alpha presence. HEVC alpha remains blocked on the HEVC image-item decoder, while independent AVIF alpha fixtures are still required against the incomplete AV1 reconstruction pipeline.
- Clean aperture, image rotation, and image mirror properties now validate their registered payloads, exact integer crop geometry, and essential associations; affect Identify dimensions; and reuse ImageSharp's optimized crop/rotate/flip processors after auxiliary alpha composition in the MIAF-defined order. Independent transform vectors must still verify every crop/rotation/mirror/alpha combination.
- Decode now resolves `cdsc`-associated Exif and `application/rdf+xml` XMP items for the primary still image, validates the declared Exif TIFF-header offset, and attaches the payloads through ImageSharp's existing profile types before presentation transforms. Independent AVIF, HEIC, and HIF metadata fixtures and Identify-time profile reporting remain required.
- ICC and `nclx` CICP color properties are now associated with the presented color item instead of global parser state, validated, and exposed on Decode and Identify through the existing ImageSharp profiles. AV1 decode requires specified sequence-header fields and the range flag to match `nclx`, uses the property only for fields the bitstream leaves unspecified, and exposes the effective sequence-header CICP description when no property is present. Independent libavif fixtures now verify exact ICC byte preservation, observable non-sRGB conversion against the paired PNG, canonical-sRGB compaction, and metadata skipping for a primary AVIF item. Identify-time bitstream fallback, grid/alpha/sequence ICC coverage, independent CICP vectors, and HEVC integration remain required.
- ICC and `nclx` CICP color properties are now associated with the presented color item instead of global parser state, validated, and exposed on Decode and Identify through the existing ImageSharp profiles. AV1 decode requires specified sequence-header fields and the range flag to match `nclx`, uses the property only for fields the bitstream leaves unspecified, and exposes the effective sequence-header CICP description when no property is present. Independent libavif fixtures verify exact ICC byte preservation, observable non-sRGB conversion, canonical-sRGB compaction, non-sRGB compact preservation, and metadata skipping for primary, grid, alpha-composited, and every presented sequence frame; the alpha vector also verifies that ICC conversion does not alter composed alpha. Identify-time bitstream fallback, independent CICP vectors, and HEVC integration remain required.
- Pixel aspect ratio now preserves the complete unsigned spacing pair, affects Decode and Identify through ImageSharp's existing unitless resolution metadata, and follows quarter-turn presentation rotation. Independent grid, rotation, and maximum-spacing fixtures remain required.
- AV1 codec configuration is now retained per `av01` image item rather than in decoder-global state. Decode requires the property, validates its fixed record against the item's sequence header and any associated `pixi` channel depths, and requires matching configurations across grid tiles. Identify now reports the configuration's 8/10/12-bit precision and monochrome shape. The optional `configOBUs` sequence is bounded and validated, including its mandatory size fields, first-position/at-most-one sequence-header rule, the image item's exactly-one sequence-header rule, and exact comparison of a repeated configuration header with the item header. Content light-level and mastering-display color-volume information are decoded from the bounded `clli` and `mdcv` image properties and matching AV1 HDR metadata OBUs, with the representations' distinct fixed-point precision, grid/property precedence, and `SkipMetadata` behavior preserved. The still-image `cclv`, `amve`, `reve`, and `ndwt` properties preserve content-volume, ambient-viewing, mastering-environment, and diffuse-white descriptions without importing their video-SEI or sample-entry state models. Independent fixtures and encoder preservation remain required. HEVC `hvcC` is now associated with each `hvc1` image item, structurally validated through its VPS/SPS/PPS arrays, and used to report encoded precision and monochrome shape; item-payload validation is implemented for the bounded decoder path, while pixel reconstruction remains incomplete.
- Several image-item properties and relationships remain missing or parsed without fully affecting output.

2
src/ImageSharp/Formats/DecoderOptions.cs

@ -78,7 +78,7 @@ public sealed class DecoderOptions
return false;
}
if (this.ColorProfileHandling == ColorProfileHandling.Preserve)
if (this.ColorProfileHandling != ColorProfileHandling.Convert)
{
return false;
}

48
src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs

@ -13,20 +13,9 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1;
internal sealed class Av1CodecConfiguration
{
/// <summary>
/// The optional open bitstream units following the fixed four-byte configuration record.
/// The optional sequence-header payload retained from the configuration open bitstream units.
/// </summary>
private readonly byte[] configObus;
/// <summary>
/// The byte offset of the optional sequence-header payload within <see cref="configObus"/>, or <c>-1</c> when
/// the configuration contains no sequence header.
/// </summary>
private readonly int configSequenceHeaderOffset;
/// <summary>
/// The byte length of the optional sequence-header payload within <see cref="configObus"/>.
/// </summary>
private readonly int configSequenceHeaderLength;
private readonly byte[] configSequenceHeader;
/// <summary>
/// The sequence-header OBU extension byte, or <c>-1</c> when its header has no extension.
@ -94,15 +83,15 @@ internal sealed class Av1CodecConfiguration
// The delay syntax is consumed to validate the fixed record, but it describes sample presentation and has
// no meaning for the independently presented image item supported by this bounded container implementation.
this.configObus = boxBuffer[4..].ToArray();
ReadOnlySpan<byte> configObus = boxBuffer[4..];
int sequenceHeaderCount = ScanObus(
this.configObus,
configObus,
true,
true,
"AV1 codec configuration",
options,
out this.configSequenceHeaderOffset,
out this.configSequenceHeaderLength,
out int configSequenceHeaderOffset,
out int configSequenceHeaderLength,
out this.configSequenceHeaderExtension,
out this.configContentLightLevel,
out this.configMasteringDisplayColorVolume);
@ -111,6 +100,18 @@ internal sealed class Av1CodecConfiguration
{
throw new InvalidImageContentException("The AV1 codec configuration contains more than one sequence header OBU.");
}
if (configSequenceHeaderOffset < 0)
{
this.configSequenceHeader = [];
}
else
{
// The property-reader span is pooled and reused. Retain only the sequence-header bytes required for
// item/sample equivalence instead of materializing every optional configuration OBU.
this.configSequenceHeader = GC.AllocateUninitializedArray<byte>(configSequenceHeaderLength);
configObus.Slice(configSequenceHeaderOffset, configSequenceHeaderLength).CopyTo(this.configSequenceHeader);
}
}
/// <summary>
@ -163,11 +164,6 @@ internal sealed class Av1CodecConfiguration
/// </summary>
public byte ChromaSamplePosition { get; }
/// <summary>
/// Gets the optional configuration open bitstream units following the fixed record.
/// </summary>
public ReadOnlyMemory<byte> ConfigObus => this.configObus;
/// <summary>
/// Validates the AV1 image item OBU layout and metadata against its item properties and configuration record.
/// </summary>
@ -277,12 +273,8 @@ internal sealed class Av1CodecConfiguration
throw new InvalidImageContentException($"The {sourceName} contains {sequenceHeaderCount} sequence header OBUs instead of {requirement}.");
}
if (this.configSequenceHeaderOffset >= 0 && dataSequenceHeaderOffset >= 0)
if (this.configSequenceHeader.Length != 0 && dataSequenceHeaderOffset >= 0)
{
ReadOnlySpan<byte> configSequenceHeader = this.configObus.AsSpan(
this.configSequenceHeaderOffset,
this.configSequenceHeaderLength);
ReadOnlySpan<byte> dataSequenceHeader = data.Slice(
dataSequenceHeaderOffset,
dataSequenceHeaderLength);
@ -291,7 +283,7 @@ internal sealed class Av1CodecConfiguration
// size field while a payload's final OBU may omit one, and different legal LEB128 widths do not alter
// the Sequence Header OBU being repeated.
if (this.configSequenceHeaderExtension != dataSequenceHeaderExtension
|| !configSequenceHeader.SequenceEqual(dataSequenceHeader))
|| !this.configSequenceHeader.AsSpan().SequenceEqual(dataSequenceHeader))
{
throw new InvalidImageContentException(
$"The AV1 codec configuration sequence header does not match the {sourceName} sequence header.");

8
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs

@ -819,7 +819,13 @@ internal ref struct Av1SymbolDecoder
if (plane == (int)Av1Plane.Y)
{
transformInfo.Type = this.ReadTransformType(transformSize, useReducedTransformSet, modeInfo.FilterIntraModeInfo.UseFilterIntra, this.baseQIndex, modeInfo.FilterIntraModeInfo.Mode, modeInfo.YMode);
transformInfo.Type = this.ReadTransformType(
transformSize,
useReducedTransformSet,
modeInfo.UseFilterIntra,
this.baseQIndex,
modeInfo.FilterIntraMode,
modeInfo.YMode);
}
transformInfo.Type = ComputeTransformType(planeType, modeInfo, isLossless, transformSize, transformInfo, useReducedTransformSet);

6
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs

@ -397,10 +397,10 @@ internal class Av1PredictionDecoder
return;
}
Av1FilterIntraMode filterIntraMode = (plane == Av1Plane.Y && modeInfo.FilterIntraModeInfo.UseFilterIntra)
? modeInfo.FilterIntraModeInfo.Mode : Av1FilterIntraMode.AllFilterIntraModes;
Av1FilterIntraMode filterIntraMode = (plane == Av1Plane.Y && modeInfo.UseFilterIntra)
? modeInfo.FilterIntraMode : Av1FilterIntraMode.AllFilterIntraModes;
int angleDelta = modeInfo.AngleDelta[Math.Min(1, (int)plane)];
int angleDelta = modeInfo.GetAngleDelta(plane);
Av1BlockSize blockSize = modeInfo.BlockSize;
bool haveTop = blockModeInfoRowOffset > 0 || (subY > 0 ? partitionInfo.AvailableAboveForChroma : partitionInfo.AvailableAbove);

216
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs

@ -11,35 +11,79 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
internal class Av1BlockModeInfo
{
/// <summary>
/// Stores the palette size for luma and for the shared chroma mode.
/// The palette size for the luma plane.
/// </summary>
private int[] paletteSize;
private int lumaPaletteSize;
/// <summary>
/// Stores the decoded palette colors for the Y, U, and V planes.
/// The palette size shared by both chroma planes.
/// </summary>
private readonly ushort[][] paletteColors = [[], [], []];
private int chromaPaletteSize;
/// <summary>
/// Stores the luma and shared chroma palette color-index maps.
/// Stores the decoded luma palette colors.
/// </summary>
private readonly byte[][] paletteColorIndexMaps = [[], []];
private InlineArray8<ushort> lumaPaletteColors;
/// <summary>
/// Stores the decoded blue-difference chroma palette colors.
/// </summary>
private InlineArray8<ushort> chromaBluePaletteColors;
/// <summary>
/// Stores the decoded red-difference chroma palette colors.
/// </summary>
private InlineArray8<ushort> chromaRedPaletteColors;
/// <summary>
/// Stores the luma palette color-index map.
/// </summary>
private byte[] lumaPaletteColorIndexMap = [];
/// <summary>
/// Stores the shared chroma palette color-index map.
/// </summary>
private byte[] chromaPaletteColorIndexMap = [];
/// <summary>
/// The directional prediction angle adjustment for luma.
/// </summary>
private int lumaAngleDelta;
/// <summary>
/// The directional prediction angle adjustment shared by both chroma planes.
/// </summary>
private int chromaAngleDelta;
/// <summary>
/// The plane-relative index of the first luma transform.
/// </summary>
private int firstLumaTransformLocation;
/// <summary>
/// The plane-relative index of the first chroma transform.
/// </summary>
private int firstChromaTransformLocation;
/// <summary>
/// The number of luma transform units.
/// </summary>
private int lumaTransformUnitCount;
/// <summary>
/// The number of transform units for one chroma plane.
/// </summary>
private int chromaTransformUnitCount;
/// <summary>
/// Initializes a new instance of the <see cref="Av1BlockModeInfo"/> class.
/// </summary>
/// <param name="numPlanes">The number of color planes in the decoded frame.</param>
/// <param name="blockSize">The decoded block size.</param>
/// <param name="positionInSuperblock">The block origin relative to its superblock in 4x4 mode-information units.</param>
public Av1BlockModeInfo(int numPlanes, Av1BlockSize blockSize, Point positionInSuperblock)
public Av1BlockModeInfo(Av1BlockSize blockSize, Point positionInSuperblock)
{
this.BlockSize = blockSize;
this.PositionInSuperblock = positionInSuperblock;
this.AngleDelta = new int[numPlanes - 1];
this.paletteSize = new int[numPlanes - 1];
this.FilterIntraModeInfo = new();
this.FirstTransformLocation = new int[numPlanes - 1];
this.TransformUnitsCount = new int[numPlanes - 1];
}
/// <summary>
@ -93,9 +137,9 @@ internal class Av1BlockModeInfo
public int ChromaFromLumaAlphaSign { get; set; }
/// <summary>
/// Gets or sets the directional prediction angle adjustments for the chroma planes.
/// Gets or sets a value indicating whether filter-intra prediction is enabled for the block.
/// </summary>
public int[] AngleDelta { get; set; }
public bool UseFilterIntra { get; set; }
/// <summary>
/// Gets the position relative to the superblock in 4x4 mode-information units.
@ -103,47 +147,141 @@ internal class Av1BlockModeInfo
public Point PositionInSuperblock { get; }
/// <summary>
/// Gets or sets the filter-intra syntax for the block.
/// Gets or sets the filter-intra mode selected for the block.
/// </summary>
public Av1IntraFilterModeInfo FilterIntraModeInfo { get; internal set; }
public Av1FilterIntraMode FilterIntraMode { get; set; }
/// <summary>
/// Gets the plane-relative index of the first <see cref="Av1TransformInfo"/> for this block.
/// Gets the directional prediction angle adjustment for a color plane.
/// </summary>
public int[] FirstTransformLocation { get; }
/// <param name="plane">The color plane.</param>
/// <returns>The luma adjustment or the adjustment shared by both chroma planes.</returns>
public int GetAngleDelta(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaAngleDelta : this.chromaAngleDelta;
/// <summary>
/// Gets or sets the number of transform units for luma and for each chroma plane.
/// Sets the directional prediction angle adjustment for a plane class.
/// </summary>
public int[] TransformUnitsCount { get; internal set; }
/// <param name="planeType">The luma or chroma plane class.</param>
/// <param name="value">The directional prediction angle adjustment.</param>
public void SetAngleDelta(Av1PlaneType planeType, int value)
{
if (planeType == Av1PlaneType.Y)
{
this.lumaAngleDelta = value;
}
else
{
this.chromaAngleDelta = value;
}
}
/// <summary>
/// Gets the plane-relative index of the first transform for a color plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <returns>The first transform index for luma or the selected chroma plane.</returns>
public int GetFirstTransformLocation(Av1Plane plane)
=> plane == Av1Plane.Y ? this.firstLumaTransformLocation : this.firstChromaTransformLocation;
/// <summary>
/// Gets the plane-relative index of the first transform for a plane class.
/// </summary>
/// <param name="planeType">The luma or chroma plane class.</param>
/// <returns>The first transform index for the plane class.</returns>
public int GetFirstTransformLocation(Av1PlaneType planeType)
=> planeType == Av1PlaneType.Y ? this.firstLumaTransformLocation : this.firstChromaTransformLocation;
/// <summary>
/// Sets the plane-relative index of the first transform for a plane class.
/// </summary>
/// <param name="planeType">The luma or chroma plane class.</param>
/// <param name="value">The first transform index.</param>
public void SetFirstTransformLocation(Av1PlaneType planeType, int value)
{
if (planeType == Av1PlaneType.Y)
{
this.firstLumaTransformLocation = value;
}
else
{
this.firstChromaTransformLocation = value;
}
}
/// <summary>
/// Gets the number of transform units for a color plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <returns>The luma count or the count for one chroma plane.</returns>
public int GetTransformUnitCount(Av1Plane plane)
=> plane == Av1Plane.Y ? this.lumaTransformUnitCount : this.chromaTransformUnitCount;
/// <summary>
/// Gets the number of transform units for a plane class.
/// </summary>
/// <param name="planeType">The luma or chroma plane class.</param>
/// <returns>The transform-unit count for the plane class.</returns>
public int GetTransformUnitCount(Av1PlaneType planeType)
=> planeType == Av1PlaneType.Y ? this.lumaTransformUnitCount : this.chromaTransformUnitCount;
/// <summary>
/// Sets the number of transform units for a plane class.
/// </summary>
/// <param name="planeType">The luma or chroma plane class.</param>
/// <param name="value">The transform-unit count.</param>
public void SetTransformUnitCount(Av1PlaneType planeType, int value)
{
if (planeType == Av1PlaneType.Y)
{
this.lumaTransformUnitCount = value;
}
else
{
this.chromaTransformUnitCount = value;
}
}
/// <summary>
/// Gets the palette size for the specified color plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <returns>The palette size for the plane.</returns>
public int GetPaletteSize(Av1Plane plane) => this.paletteSize[Math.Min(1, (int)plane)];
public int GetPaletteSize(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaPaletteSize : this.chromaPaletteSize;
/// <summary>
/// Gets the palette size for the specified plane class.
/// </summary>
/// <param name="planeType">The luma or chroma plane class.</param>
/// <returns>The palette size for the plane class.</returns>
public int GetPaletteSize(Av1PlaneType planeType) => this.paletteSize[(int)planeType];
public int GetPaletteSize(Av1PlaneType planeType) => planeType == Av1PlaneType.Y ? this.lumaPaletteSize : this.chromaPaletteSize;
/// <summary>
/// Sets the luma and shared chroma palette sizes.
/// </summary>
/// <param name="ySize">The luma palette size.</param>
/// <param name="uvSize">The palette size shared by the chroma planes.</param>
public void SetPaletteSizes(int ySize, int uvSize) => this.paletteSize = [ySize, uvSize];
public void SetPaletteSizes(int ySize, int uvSize)
{
this.lumaPaletteSize = ySize;
this.chromaPaletteSize = uvSize;
}
/// <summary>
/// Gets the decoded palette colors for a color plane.
/// </summary>
/// <param name="plane">The color plane.</param>
/// <returns>The palette colors in prediction-index order.</returns>
public ReadOnlySpan<ushort> GetPaletteColors(Av1Plane plane) => this.paletteColors[(int)plane];
public ReadOnlySpan<ushort> GetPaletteColors(Av1Plane plane)
{
if (plane == Av1Plane.Y)
{
return this.lumaPaletteColors[..this.lumaPaletteSize];
}
return plane == Av1Plane.U
? this.chromaBluePaletteColors[..this.chromaPaletteSize]
: this.chromaRedPaletteColors[..this.chromaPaletteSize];
}
/// <summary>
/// Stores the decoded palette colors for a color plane.
@ -151,7 +289,20 @@ internal class Av1BlockModeInfo
/// <param name="plane">The color plane.</param>
/// <param name="colors">The palette colors in prediction-index order.</param>
public void SetPaletteColors(Av1Plane plane, ReadOnlySpan<ushort> colors)
=> this.paletteColors[(int)plane] = colors.ToArray();
{
if (plane == Av1Plane.Y)
{
colors.CopyTo(this.lumaPaletteColors);
}
else if (plane == Av1Plane.U)
{
colors.CopyTo(this.chromaBluePaletteColors);
}
else
{
colors.CopyTo(this.chromaRedPaletteColors);
}
}
/// <summary>
/// Gets the palette color-index map for a color plane.
@ -159,7 +310,7 @@ internal class Av1BlockModeInfo
/// <param name="plane">The color plane.</param>
/// <returns>The luma map for <see cref="Av1Plane.Y"/> or the shared chroma map for either chroma plane.</returns>
public ReadOnlySpan<byte> GetPaletteColorIndexMap(Av1Plane plane)
=> this.paletteColorIndexMaps[Math.Min(1, (int)plane)];
=> plane == Av1Plane.Y ? this.lumaPaletteColorIndexMap : this.chromaPaletteColorIndexMap;
/// <summary>
/// Stores the palette color-index map for a plane class.
@ -167,5 +318,14 @@ internal class Av1BlockModeInfo
/// <param name="planeType">The luma or shared chroma plane class.</param>
/// <param name="colorIndexMap">The row-major color-index map including coded-block edge padding.</param>
public void SetPaletteColorIndexMap(Av1PlaneType planeType, byte[] colorIndexMap)
=> this.paletteColorIndexMaps[(int)planeType] = colorIndexMap;
{
if (planeType == Av1PlaneType.Y)
{
this.lumaPaletteColorIndexMap = colorIndexMap;
}
else
{
this.chromaPaletteColorIndexMap = colorIndexMap;
}
}
}

20
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1IntraFilterModeInfo.cs

@ -1,20 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
/// <summary>
/// Stores whether filter-intra prediction is active and which filter-intra mode is selected.
/// </summary>
internal class Av1IntraFilterModeInfo
{
/// <summary>
/// Gets or sets a value indicating whether filter-intra prediction is enabled for the block.
/// </summary>
public bool UseFilterIntra { get; set; }
/// <summary>
/// Gets or sets the filter-intra mode selected for the block.
/// </summary>
public Av1FilterIntraMode Mode { get; set; }
}

72
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -649,13 +649,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
int columnIndex = modeInfoLocation.X;
int block4x4Width = blockSize.Get4x4WideCount();
int block4x4Height = blockSize.Get4x4HighCount();
int planesCount = this.SequenceHeader.ColorConfig.PlaneCount;
Point superblockLocation = superblockInfo.Position * this.SequenceHeader.SuperblockModeInfoSize;
Point locationInSuperblock = new Point(modeInfoLocation.X - superblockLocation.X, modeInfoLocation.Y - superblockLocation.Y);
Av1BlockModeInfo blockModeInfo = new(planesCount, blockSize, locationInSuperblock);
Av1BlockModeInfo blockModeInfo = new(blockSize, locationInSuperblock);
blockModeInfo.PartitionType = partitionType;
blockModeInfo.FirstTransformLocation[0] = this.firstTransformOffset[0];
blockModeInfo.FirstTransformLocation[1] = this.firstTransformOffset[1];
blockModeInfo.SetFirstTransformLocation(Av1PlaneType.Y, this.firstTransformOffset[0]);
blockModeInfo.SetFirstTransformLocation(Av1PlaneType.Uv, this.firstTransformOffset[1]);
bool hasChroma = HasChroma(this.SequenceHeader, modeInfoLocation, blockSize);
Av1PartitionInfo partitionInfo = new(blockModeInfo, superblockInfo, hasChroma, partitionType);
partitionInfo.ColumnIndex = columnIndex;
@ -737,12 +736,13 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
bool isLossless = this.FrameHeader.LosslessArray[partitionInfo.ModeInfo.SegmentId];
bool isLosslessBlock = isLossless && (blockSize >= Av1BlockSize.Block64x64) && (blockSize <= Av1BlockSize.Block128x128);
int subSampling = (this.SequenceHeader.ColorConfig.SubSamplingX ? 1 : 0) + (this.SequenceHeader.ColorConfig.SubSamplingY ? 1 : 0);
int chromaTransformUnitCount = isLosslessBlock ? ((maxBlocksWide * maxBlocksHigh) >> subSampling) : partitionInfo.ModeInfo.TransformUnitsCount[(int)Av1PlaneType.Uv];
int chromaTransformUnitCount = isLosslessBlock
? (maxBlocksWide * maxBlocksHigh) >> subSampling
: partitionInfo.ModeInfo.GetTransformUnitCount(Av1PlaneType.Uv);
int[] transformInfoIndices = new int[3];
transformInfoIndices[0] = superblockInfo.TransformInfoIndexY + partitionInfo.ModeInfo.FirstTransformLocation[(int)Av1PlaneType.Y];
transformInfoIndices[1] = superblockInfo.TransformInfoIndexUv + partitionInfo.ModeInfo.FirstTransformLocation[(int)Av1PlaneType.Uv];
transformInfoIndices[2] = transformInfoIndices[1] + chromaTransformUnitCount;
int lumaTransformInfoIndex = superblockInfo.TransformInfoIndexY + partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y);
int chromaBlueTransformInfoIndex = superblockInfo.TransformInfoIndexUv + partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv);
int chromaRedTransformInfoIndex = chromaBlueTransformInfoIndex + chromaTransformUnitCount;
int forceSplitCount = 0;
// AV1 forces residual traversal into at most 64x64 regions even when the coding block is larger.
@ -757,6 +757,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
int transformUnitCount;
int subX = (plane > 0 && this.SequenceHeader.ColorConfig.SubSamplingX) ? 1 : 0;
int subY = (plane > 0 && this.SequenceHeader.ColorConfig.SubSamplingY) ? 1 : 0;
int transformInfoIndex = plane switch
{
0 => lumaTransformInfoIndex,
1 => chromaBlueTransformInfoIndex,
_ => chromaRedTransformInfoIndex,
};
if (plane != 0 && !partitionInfo.IsChroma)
{
@ -770,12 +776,12 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
// directly after applying the plane's chroma subsampling.
int unitHeight = Av1Math.RoundPowerOf2(Math.Min(modeUnitBlocksHigh + row, maxBlocksHigh), 0);
int unitWidth = Av1Math.RoundPowerOf2(Math.Min(modeUnitBlocksWide + column, maxBlocksWide), 0);
DebugGuard.IsTrue(transformInfoSpan[transformInfoIndices[plane]].Size == Av1TransformSize.Size4x4, "Lossless frame shall have transform units of size 4x4.");
DebugGuard.IsTrue(transformInfoSpan[transformInfoIndex].Size == Av1TransformSize.Size4x4, "Lossless frame shall have transform units of size 4x4.");
transformUnitCount = ((unitWidth - column) * (unitHeight - row)) >> (subX + subY);
}
else
{
totalTransformUnitCount = partitionInfo.ModeInfo.TransformUnitsCount[Math.Min(1, plane)];
totalTransformUnitCount = partitionInfo.ModeInfo.GetTransformUnitCount((Av1Plane)plane);
transformUnitCount = this.transformUnitCount[plane][forceSplitCount];
DebugGuard.IsFalse(totalTransformUnitCount == 0, nameof(totalTransformUnitCount), string.Empty);
@ -790,7 +796,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
DebugGuard.IsFalse(transformUnitCount == 0, nameof(transformUnitCount), string.Empty);
for (int tu = 0; tu < transformUnitCount; tu++)
{
Av1TransformInfo transformInfo = transformInfoSpan[transformInfoIndices[plane]];
Av1TransformInfo transformInfo = transformInfoSpan[transformInfoIndex];
DebugGuard.MustBeLessThanOrEqualTo(transformInfo.OffsetX, maxBlocksWide, nameof(transformInfo));
DebugGuard.MustBeLessThanOrEqualTo(transformInfo.OffsetY, maxBlocksHigh, nameof(transformInfo));
@ -838,7 +844,22 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
transformInfo.CodeBlockFlag = false;
}
transformInfoIndices[plane]++;
transformInfoIndex++;
}
// Each plane advances independently because its transform descriptors occupy a separate
// contiguous range. Scalar cursors avoid allocating a three-element array for every block.
switch (plane)
{
case 0:
lumaTransformInfoIndex = transformInfoIndex;
break;
case 1:
chromaBlueTransformInfoIndex = transformInfoIndex;
break;
default:
chromaRedTransformInfoIndex = transformInfoIndex;
break;
}
}
@ -1210,8 +1231,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="transformSize">The selected luma transform size.</param>
private unsafe void UpdateTransformInfo(Av1PartitionInfo partitionInfo, Av1SuperblockInfo superblockInfo, Av1BlockSize blockSize, Av1TransformSize transformSize)
{
int transformInfoYIndex = partitionInfo.ModeInfo.FirstTransformLocation[(int)Av1PlaneType.Y];
int transformInfoUvIndex = partitionInfo.ModeInfo.FirstTransformLocation[(int)Av1PlaneType.Uv];
int transformInfoYIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y);
int transformInfoUvIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv);
Span<Av1TransformInfo> lumaTransformInfo = superblockInfo.GetTransformInfoY();
Span<Av1TransformInfo> chromaTransformInfo = superblockInfo.GetTransformInfoUv();
int totalLumaTransformUnitCount = 0;
@ -1291,7 +1312,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
DebugGuard.IsTrue(
(transformInfoUvIndex - totalChromaTransformUnitCount) ==
partitionInfo.ModeInfo.FirstTransformLocation[(int)Av1PlaneType.Uv],
partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv),
nameof(totalChromaTransformUnitCount));
int originalIndex = transformInfoUvIndex - totalChromaTransformUnitCount;
ref Av1TransformInfo originalInfo = ref chromaTransformInfo[originalIndex];
@ -1305,8 +1326,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
}
}
partitionInfo.ModeInfo.TransformUnitsCount[(int)Av1PlaneType.Y] = totalLumaTransformUnitCount;
partitionInfo.ModeInfo.TransformUnitsCount[(int)Av1PlaneType.Uv] = totalChromaTransformUnitCount;
partitionInfo.ModeInfo.SetTransformUnitCount(Av1PlaneType.Y, totalLumaTransformUnitCount);
partitionInfo.ModeInfo.SetTransformUnitCount(Av1PlaneType.Uv, totalChromaTransformUnitCount);
this.firstTransformOffset[(int)Av1PlaneType.Y] += totalLumaTransformUnitCount;
this.firstTransformOffset[(int)Av1PlaneType.Uv] += totalChromaTransformUnitCount << 1;
@ -1426,7 +1447,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
{
partitionInfo.ModeInfo.YMode = reader.ReadYMode(partitionInfo.AboveModeInfo, partitionInfo.LeftModeInfo);
partitionInfo.ModeInfo.AngleDelta[(int)Av1PlaneType.Y] = IntraAngleInfo(ref reader, partitionInfo.ModeInfo.YMode, partitionInfo.ModeInfo.BlockSize);
partitionInfo.ModeInfo.SetAngleDelta(
Av1PlaneType.Y,
IntraAngleInfo(ref reader, partitionInfo.ModeInfo.YMode, partitionInfo.ModeInfo.BlockSize));
if (partitionInfo.IsChroma && !this.SequenceHeader.ColorConfig.IsMonochrome)
{
partitionInfo.ModeInfo.UvMode = reader.ReadIntraModeUv(partitionInfo.ModeInfo.YMode, this.IsChromaForLumaAllowed(partitionInfo));
@ -1435,7 +1459,9 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
ReadChromaFromLumaAlphas(ref reader, partitionInfo.ModeInfo);
}
partitionInfo.ModeInfo.AngleDelta[(int)Av1PlaneType.Uv] = IntraAngleInfo(ref reader, partitionInfo.ModeInfo.UvMode, partitionInfo.ModeInfo.BlockSize);
partitionInfo.ModeInfo.SetAngleDelta(
Av1PlaneType.Uv,
IntraAngleInfo(ref reader, partitionInfo.ModeInfo.UvMode, partitionInfo.ModeInfo.BlockSize));
}
else
{
@ -1491,7 +1517,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
/// <param name="partitionInfo">The current coding block.</param>
private void FilterIntraModeInfo(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
{
partitionInfo.ModeInfo.FilterIntraModeInfo.UseFilterIntra = false;
partitionInfo.ModeInfo.UseFilterIntra = false;
if (this.SequenceHeader.EnableFilterIntra &&
partitionInfo.ModeInfo.YMode == Av1PredictionMode.DC &&
partitionInfo.ModeInfo.GetPaletteSize(Av1PlaneType.Y) == 0 &&
@ -1500,8 +1526,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable
Av1FilterIntraMode filterIntraMode = reader.ReadFilterUltraMode(partitionInfo.ModeInfo.BlockSize);
if (filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes)
{
partitionInfo.ModeInfo.FilterIntraModeInfo.UseFilterIntra = true;
partitionInfo.ModeInfo.FilterIntraModeInfo.Mode = filterIntraMode;
partitionInfo.ModeInfo.UseFilterIntra = true;
partitionInfo.ModeInfo.FilterIntraMode = filterIntraMode;
}
}
}

10
src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs

@ -181,7 +181,7 @@ internal sealed class Av1BlockDecoder : IDisposable
bool isLosslessBlock = isLossless && ((blockSize >= Av1BlockSize.Block64x64) && (blockSize <= Av1BlockSize.Block128x128));
int chromaTransformUnitCount = isLosslessBlock
? (maxBlocksWide * maxBlocksHigh) >> ((colorConfig.SubSamplingX ? 1 : 0) + (colorConfig.SubSamplingY ? 1 : 0))
: modeInfo.TransformUnitsCount[(int)Av1Plane.U];
: modeInfo.GetTransformUnitCount(Av1Plane.U);
bool highBitDepth = this.frameBuffer.BytesPerSample == 2;
Av1PredictionDecoder predictionDecoder = new(this.sequenceHeader, this.frameHeader);
@ -199,9 +199,9 @@ internal sealed class Av1BlockDecoder : IDisposable
// following the U descriptors for this block, so the V base includes the complete U transform-unit count.
int transformInfoIndex = plane switch
{
2 => superblockInfo.TransformInfoIndexUv + modeInfo.FirstTransformLocation[plane - 1] + chromaTransformUnitCount,
1 => superblockInfo.TransformInfoIndexUv + modeInfo.FirstTransformLocation[plane],
0 => superblockInfo.TransformInfoIndexY + modeInfo.FirstTransformLocation[plane],
2 => superblockInfo.TransformInfoIndexUv + modeInfo.GetFirstTransformLocation(Av1Plane.V) + chromaTransformUnitCount,
1 => superblockInfo.TransformInfoIndexUv + modeInfo.GetFirstTransformLocation(Av1Plane.U),
0 => superblockInfo.TransformInfoIndexY + modeInfo.GetFirstTransformLocation(Av1Plane.Y),
_ => throw new InvalidImageContentException("Maximum of 3 color planes")
};
Span<Av1TransformInfo> transformInfo = superblockInfo.GetTransformInfo(plane)[transformInfoIndex..];
@ -214,7 +214,7 @@ internal sealed class Av1BlockDecoder : IDisposable
}
else
{
transformUnitCount = modeInfo.TransformUnitsCount[Math.Min(1, plane)];
transformUnitCount = modeInfo.GetTransformUnitCount((Av1Plane)plane);
}
Guard.IsFalse(transformUnitCount == 0, nameof(transformUnitCount), "Must have at least a single transform unit to decode.");

32
src/ImageSharp/Formats/Heif/HeifDecoderCore.cs

@ -1205,7 +1205,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
throw new InvalidImageContentException("The pixel information property contains unexpected trailing data.");
}
byte[] channelBitDepths = boxBuffer.Slice(offset, channelCount).ToArray();
// Property associations are resolved after the pooled box buffer is reused, so retain the
// exact channel vector once at this ownership boundary.
byte[] channelBitDepths = GC.AllocateUninitializedArray<byte>(channelCount);
boxBuffer.Slice(offset, channelCount).CopyTo(channelBitDepths);
for (int i = 0; i < channelBitDepths.Length; i++)
{
if (channelBitDepths[i] == 0)
@ -1238,11 +1241,10 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
if (!this.Options.SkipMetadata)
{
EnsureBufferRemaining(boxBuffer, 4, 1, "ICC color information");
byte[] iccData = boxBuffer[4..].ToArray();
IccProfile? iccProfile = null;
try
{
iccProfile = HeifPropertyParser.ParseIccProfile(iccData);
iccProfile = HeifPropertyParser.ParseIccProfile(boxBuffer[4..]);
}
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex))
{
@ -2279,15 +2281,22 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
continue;
}
byte[] itemData = itemMemory.GetSpan().ToArray();
if (metadataItem.Type == Heif4CharCode.Exif)
{
this.ExecuteAncillarySegmentAction(() => ApplyExifProfile(metadata, itemData));
this.ExecuteAncillarySegmentAction(() => ApplyExifProfile(metadata, itemMemory.GetSpan()));
}
else if (metadataItem.Type == Heif4CharCode.Mime &&
string.Equals(metadataItem.ContentType, "application/rdf+xml", StringComparison.Ordinal))
{
this.ExecuteAncillarySegmentAction(() => metadata.XmpProfile = new XmpProfile(itemData));
this.ExecuteAncillarySegmentAction(() =>
{
Span<byte> itemData = itemMemory.GetSpan();
// XmpProfile retains its input array after the assembled item buffer is returned to its pool.
byte[] ownedData = GC.AllocateUninitializedArray<byte>(itemData.Length);
itemData.CopyTo(ownedData);
metadata.XmpProfile = new XmpProfile(ownedData);
});
}
}
}
@ -2297,7 +2306,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
/// </summary>
/// <param name="metadata">The image metadata receiving the Exif profile.</param>
/// <param name="itemData">The complete HEIF Exif item including its four-byte offset field.</param>
private static void ApplyExifProfile(ImageMetadata metadata, byte[] itemData)
private static void ApplyExifProfile(ImageMetadata metadata, ReadOnlySpan<byte> itemData)
{
if (itemData.Length < 8)
{
@ -2305,7 +2314,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
}
uint declaredTiffHeaderOffset = BinaryPrimitives.ReadUInt32BigEndian(itemData);
Span<byte> exifData = itemData.AsSpan(4);
ReadOnlySpan<byte> exifData = itemData[4..];
int actualTiffHeaderOffset = -1;
// Annex A stores the offset to the first TIFF byte-order marker. Match libavif by finding the first valid
@ -2334,7 +2343,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
throw new InvalidImageContentException("The HEIF Exif item has an invalid TIFF-header offset.");
}
metadata.ExifProfile = new ExifProfile(exifData[actualTiffHeaderOffset..].ToArray());
ReadOnlySpan<byte> tiffData = exifData[actualTiffHeaderOffset..];
// ExifProfile retains its input array after the assembled item buffers are disposed at the end of decode.
byte[] ownedData = GC.AllocateUninitializedArray<byte>(tiffData.Length);
tiffData.CopyTo(ownedData);
metadata.ExifProfile = new ExifProfile(ownedData);
}
/// <summary>

44
src/ImageSharp/Formats/Heif/HeifEncoderCore.cs

@ -3,6 +3,7 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -47,7 +48,7 @@ internal sealed class HeifEncoderCore
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
byte[] pixels = this.encoder.CompressionMethod switch
using ChunkedMemoryStream compressedPixels = this.encoder.CompressionMethod switch
{
HeifCompressionMethod.LegacyJpeg => this.CompressPixels(image, cancellationToken),
HeifCompressionMethod.Av1 => throw new NotSupportedException("AV1 encoding is not implemented."),
@ -57,12 +58,12 @@ internal sealed class HeifEncoderCore
List<HeifItem> items = new();
List<HeifItemLink> links = new();
GenerateItems(image, pixels, items);
GenerateItems(image, compressedPixels.Length, items);
// Write out the generated header and pixels.
this.WriteFileTypeBox(stream);
this.WriteMetadataBox(items, links, stream);
this.WriteMediaDataBox(pixels, stream);
this.WriteMediaDataBox(compressedPixels, stream);
stream.Flush();
HeifMetadata meta = image.Metadata.GetHeifMetadata();
@ -74,13 +75,13 @@ internal sealed class HeifEncoderCore
/// </summary>
/// <typeparam name="TPixel">The source pixel format.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="pixels">The encoded primary-item payload.</param>
/// <param name="pixelDataLength">The encoded primary-item payload length.</param>
/// <param name="items">The destination item collection.</param>
private static void GenerateItems<TPixel>(Image<TPixel> image, byte[] pixels, List<HeifItem> items)
private static void GenerateItems<TPixel>(Image<TPixel> image, long pixelDataLength, List<HeifItem> items)
where TPixel : unmanaged, IPixel<TPixel>
{
HeifItem primaryItem = new(Heif4CharCode.Jpeg, 1u);
primaryItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, 0L, pixels.LongLength));
primaryItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, 0L, pixelDataLength));
primaryItem.BitsPerPixel = 24;
primaryItem.ChannelCount = 3;
primaryItem.SetExtent(image.Size);
@ -420,16 +421,16 @@ internal sealed class HeifEncoderCore
/// <summary>
/// Writes the encoded primary-item bytes in a media-data box.
/// </summary>
/// <param name="data">The encoded item payload.</param>
/// <param name="data">The encoded item payload stream.</param>
/// <param name="stream">The destination stream.</param>
private void WriteMediaDataBox(Span<byte> data, Stream stream)
private void WriteMediaDataBox(ChunkedMemoryStream data, Stream stream)
{
Span<byte> buf = stackalloc byte[12];
int bytesWritten = WriteBoxHeader(buf, Heif4CharCode.Mdat);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)(data.Length + bytesWritten));
BinaryPrimitives.WriteUInt32BigEndian(buf, checked((uint)(data.Length + bytesWritten)));
stream.Write(buf[..bytesWritten]);
stream.Write(data);
data.WriteTo(stream);
}
/// <summary>
@ -438,8 +439,8 @@ internal sealed class HeifEncoderCore
/// <typeparam name="TPixel">The source pixel format.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="cancellationToken">The token used to cancel payload encoding.</param>
/// <returns>The encoded JPEG item bytes.</returns>
private byte[] CompressPixels<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
/// <returns>The pooled stream containing the encoded JPEG item bytes.</returns>
private ChunkedMemoryStream CompressPixels<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
if (this.encoder.Lossless)
@ -468,16 +469,25 @@ internal sealed class HeifEncoderCore
_ => throw new NotSupportedException($"HEIF chroma sampling '{this.encoder.ChromaSubsampling}' is not supported.")
};
using MemoryStream stream = new();
ChunkedMemoryStream stream = new(this.configuration.MemoryAllocator);
JpegEncoder encoder = new()
{
Quality = this.encoder.Quality,
ColorType = colorType
};
// ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation
// so HEIF encoding cannot return while its temporary item payload is still being produced.
image.SaveAsJpegAsync(stream, encoder, cancellationToken).GetAwaiter().GetResult();
return stream.ToArray();
try
{
// ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation so HEIF encoding
// cannot return while its pooled item payload is still being produced.
image.SaveAsJpegAsync(stream, encoder, cancellationToken).GetAwaiter().GetResult();
return stream;
}
catch
{
// Ownership transfers to the caller only after encoding succeeds.
stream.Dispose();
throw;
}
}
}

8
src/ImageSharp/Formats/Heif/HeifPropertyParser.cs

@ -36,14 +36,18 @@ internal static class HeifPropertyParser
/// </summary>
/// <param name="data">The complete ICC profile bytes.</param>
/// <returns>The validated ICC profile.</returns>
public static IccProfile ParseIccProfile(byte[] data)
public static IccProfile ParseIccProfile(ReadOnlySpan<byte> data)
{
if (data.Length == 0)
{
throw new InvalidImageContentException("The HEIF ICC color property contains an empty profile.");
}
IccProfile profile = new(data);
// IccProfile retains its input array while the HEIF box-reader buffer is pooled and reused. Perform the one
// required ownership transfer here so item and sequence parsing cannot introduce additional materializations.
byte[] ownedData = GC.AllocateUninitializedArray<byte>(data.Length);
data.CopyTo(ownedData);
IccProfile profile = new(ownedData);
if (!profile.CheckIsValid())
{
throw new InvalidIccProfileException("Invalid HEIF ICC profile.");

3
src/ImageSharp/Formats/Heif/HeifSequenceParser.cs

@ -1173,8 +1173,7 @@ internal sealed class HeifSequenceParser
stream.Position -= 4;
using IMemoryOwner<byte> payload = this.boxReader.ReadPayload(stream, boxLength);
byte[] profileData = payload.GetSpan()[4..].ToArray();
track.IccProfile = HeifPropertyParser.ParseIccProfile(profileData);
track.IccProfile = HeifPropertyParser.ParseIccProfile(payload.GetSpan()[4..]);
}
catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex))
{

86
src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// <summary>
@ -24,7 +26,7 @@ internal sealed class HevcNalUnit
int rbspLength = HevcRbspDecoder.Decode(
encodedPayload,
rbspBuffer,
out int[] emulationPreventionBytePositions);
out ReadOnlyMemory<int> emulationPreventionBytePositions);
this.EncodedPayloadLength = encodedPayload.Length;
this.Rbsp = rbspBuffer.AsMemory(0, rbspLength);
@ -49,7 +51,7 @@ internal sealed class HevcNalUnit
/// <summary>
/// Gets the zero-based encoded-payload positions of removed emulation-prevention bytes.
/// </summary>
public IReadOnlyList<int> EmulationPreventionBytePositions { get; }
public ReadOnlyMemory<int> EmulationPreventionBytePositions { get; }
}
/// <summary>
@ -72,47 +74,75 @@ internal static class HevcRbspDecoder
public static int Decode(
ReadOnlySpan<byte> encodedPayload,
Span<byte> destination,
out int[] emulationPreventionBytePositions)
out ReadOnlyMemory<int> emulationPreventionBytePositions)
{
DebugGuard.MustBeGreaterThanOrEqualTo(destination.Length, encodedPayload.Length, nameof(destination));
int destinationOffset = 0;
int preventionByteCount = 0;
int consecutiveZeroBytes = 0;
List<int>? preventionBytePositions = null;
for (int sourceOffset = 0; sourceOffset < encodedPayload.Length; sourceOffset++)
int[]? rentedPositions = null;
Span<int> preventionBytePositions = [];
try
{
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)
for (int sourceOffset = 0; sourceOffset < encodedPayload.Length; sourceOffset++)
{
if (value < 3)
{
throw new InvalidImageContentException("The HEVC NAL unit contains a forbidden start-code-like byte sequence.");
}
byte value = encodedPayload[sourceOffset];
if (value == 3)
// 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)
{
(preventionBytePositions ??= []).Add(sourceOffset);
sourceOffset++;
if (sourceOffset == encodedPayload.Length || encodedPayload[sourceOffset] > 3)
if (value < 3)
{
throw new InvalidImageContentException("The HEVC NAL unit contains an invalid emulation-prevention byte.");
throw new InvalidImageContentException("The HEVC NAL unit contains a forbidden start-code-like byte sequence.");
}
// 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;
if (value == 3)
{
sourceOffset++;
if (sourceOffset == encodedPayload.Length || encodedPayload[sourceOffset] > 3)
{
throw new InvalidImageContentException("The HEVC NAL unit contains an invalid emulation-prevention byte.");
}
if (preventionByteCount == preventionBytePositions.Length)
{
int[] expandedPositions = ArrayPool<int>.Shared.Rent(preventionBytePositions.IsEmpty ? 16 : preventionBytePositions.Length * 2);
preventionBytePositions.CopyTo(expandedPositions);
if (rentedPositions is not null)
{
ArrayPool<int>.Shared.Return(rentedPositions);
}
rentedPositions = expandedPositions;
preventionBytePositions = rentedPositions;
}
preventionBytePositions[preventionByteCount++] = sourceOffset - 1;
value = encodedPayload[sourceOffset];
consecutiveZeroBytes = 0;
}
}
destination[destinationOffset++] = value;
consecutiveZeroBytes = value == 0 ? consecutiveZeroBytes + 1 : 0;
}
destination[destinationOffset++] = value;
consecutiveZeroBytes = value == 0 ? consecutiveZeroBytes + 1 : 0;
}
int[] retainedPositions = preventionByteCount == 0
? []
: GC.AllocateUninitializedArray<int>(preventionByteCount);
emulationPreventionBytePositions = preventionBytePositions?.ToArray() ?? Array.Empty<int>();
return destinationOffset;
preventionBytePositions[..preventionByteCount].CopyTo(retainedPositions);
emulationPreventionBytePositions = retainedPositions;
return destinationOffset;
}
finally
{
if (rentedPositions is not null)
{
ArrayPool<int>.Shared.Return(rentedPositions);
}
}
}
}

44
src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs

@ -8,31 +8,41 @@ namespace SixLabors.ImageSharp.Formats.Heif.Hevc;
/// </summary>
internal sealed class HevcShortTermReferencePictureSet
{
/// <summary>
/// Stores the bounded signed picture-order differences in HEVC reference order.
/// </summary>
private InlineArray16<int> deltaPictureOrders;
/// <summary>
/// Stores the bounded current-picture usage flags corresponding to <see cref="deltaPictureOrders"/>.
/// </summary>
private InlineArray16<bool> usedByCurrentPicture;
/// <summary>
/// Initializes a new instance of the <see cref="HevcShortTermReferencePictureSet"/> class.
/// </summary>
/// <param name="deltaPictureOrders">The signed picture-order differences in HEVC reference order.</param>
/// <param name="usedByCurrentPicture">The corresponding current-picture usage flags.</param>
private HevcShortTermReferencePictureSet(int[] deltaPictureOrders, bool[] usedByCurrentPicture)
private HevcShortTermReferencePictureSet()
{
this.DeltaPictureOrders = deltaPictureOrders;
this.UsedByCurrentPicture = usedByCurrentPicture;
}
/// <summary>
/// Gets the signed picture-order differences in HEVC reference order.
/// Gets the number of pictures declared by the reference-picture set.
/// </summary>
public IReadOnlyList<int> DeltaPictureOrders { get; }
public int Count { get; private set; }
/// <summary>
/// Gets the flags indicating which reference pictures are used by the current picture.
/// Gets a signed picture-order difference in HEVC reference order.
/// </summary>
public IReadOnlyList<bool> UsedByCurrentPicture { get; }
/// <param name="index">The zero-based reference-picture index.</param>
/// <returns>The signed picture-order difference.</returns>
public int GetDeltaPictureOrder(int index) => this.deltaPictureOrders[index];
/// <summary>
/// Gets the number of pictures declared by the reference-picture set.
/// Gets whether a reference picture is used by the current picture.
/// </summary>
public int Count => this.DeltaPictureOrders.Count;
/// <param name="index">The zero-based reference-picture index.</param>
/// <returns><see langword="true"/> when the reference is used by the current picture.</returns>
public bool IsUsedByCurrentPicture(int index) => this.usedByCurrentPicture[index];
/// <summary>
/// Reads one SPS short-term reference-picture set.
@ -47,8 +57,9 @@ internal sealed class HevcShortTermReferencePictureSet
IReadOnlyList<HevcShortTermReferencePictureSet> previousSets,
int index)
{
Span<int> deltaPictureOrders = stackalloc int[16];
Span<bool> usedByCurrentPicture = stackalloc bool[16];
HevcShortTermReferencePictureSet result = new();
Span<int> deltaPictureOrders = result.deltaPictureOrders;
Span<bool> usedByCurrentPicture = result.usedByCurrentPicture;
int pictureCount = 0;
bool interSetPrediction = index > 0 && reader.ReadFlag();
if (interSetPrediction)
@ -79,7 +90,7 @@ internal sealed class HevcShortTermReferencePictureSet
}
int referenceDelta = referenceIndex < referenceSet.Count
? referenceSet.DeltaPictureOrders[referenceIndex]
? referenceSet.GetDeltaPictureOrder(referenceIndex)
: 0;
long deltaPictureOrder = (long)deltaReferencePictureSet + referenceDelta;
@ -171,8 +182,7 @@ internal sealed class HevcShortTermReferencePictureSet
}
}
return new HevcShortTermReferencePictureSet(
deltaPictureOrders[..pictureCount].ToArray(),
usedByCurrentPicture[..pictureCount].ToArray());
result.Count = pictureCount;
return result;
}
}

4
src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs

@ -115,7 +115,7 @@ internal sealed class HevcSliceSegmentHeader
int encodedHeaderLength = GetEncodedPayloadOffset(
this.HeaderLength,
nalUnit.EmulationPreventionBytePositions);
nalUnit.EmulationPreventionBytePositions.Span);
int availableEncodedData = nalUnit.EncodedPayloadLength - encodedHeaderLength;
int cumulativeEntryPointOffset = 0;
@ -409,7 +409,7 @@ internal sealed class HevcSliceSegmentHeader
/// <returns>The encoded byte-sequence payload offset at the same syntax boundary.</returns>
private static int GetEncodedPayloadOffset(
int rbspOffset,
IReadOnlyList<int> emulationPreventionBytePositions)
ReadOnlySpan<int> emulationPreventionBytePositions)
{
int encodedOffset = rbspOffset;
foreach (int preventionBytePosition in emulationPreventionBytePositions)

6
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -27,7 +27,7 @@ public class Av1CoefficientsEntropyTests
Av1ComponentType componentType = Av1ComponentType.Luminance;
Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC;
ushort endOfBlock = 0;
Av1BlockModeInfo modeInfo = new(Av1Constants.MaxPlanes, blockSize, new Point(0, 0));
Av1BlockModeInfo modeInfo = new(blockSize, new Point(0, 0));
Av1TransformInfo transformInfo = new(transformSize, 0, 0);
int[] aboveContexts = new int[1];
int[] leftContexts = new int[1];
@ -77,7 +77,7 @@ public class Av1CoefficientsEntropyTests
const Av1PredictionMode intraDirection = Av1PredictionMode.DC;
const Av1ComponentType componentType = Av1ComponentType.Luminance;
const Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC;
Av1BlockModeInfo modeInfo = new(Av1Constants.MaxPlanes, blockSize, new Point(0, 0));
Av1BlockModeInfo modeInfo = new(blockSize, new Point(0, 0));
Av1TransformInfo transformInfo = new(transformSize, 0, 0);
int[] aboveContexts = new int[1];
int[] leftContexts = new int[1];
@ -132,7 +132,7 @@ public class Av1CoefficientsEntropyTests
private static void RoundTripCoefficientsCore(ushort endOfBlock, Av1ComponentType componentType, Av1BlockSize blockSize, Av1TransformSize transformSize, Av1TransformType transformType, Av1PredictionMode intraDirection, Av1FilterIntraMode filterIntraMode)
{
Av1BlockModeInfo modeInfo = new(Av1Constants.MaxPlanes, blockSize, new Point(0, 0));
Av1BlockModeInfo modeInfo = new(blockSize, new Point(0, 0));
Av1TransformInfo transformInfo = new(transformSize, 0, 0);
int[] aboveContexts = new int[transformSize.Get4x4WideCount()];
int[] leftContexts = new int[transformSize.Get4x4HighCount()];

4
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs

@ -255,8 +255,8 @@ public class Av1TilingTests
continue;
}
int transformUnitCount = modeInfo.TransformUnitsCount[Math.Min(plane, 1)];
int transformInfoIndex = modeInfo.FirstTransformLocation[Math.Min(plane, 1)];
int transformUnitCount = modeInfo.GetTransformUnitCount((Av1Plane)plane);
int transformInfoIndex = modeInfo.GetFirstTransformLocation((Av1Plane)plane);
if (plane == (int)Av1Plane.V)
{
transformInfoIndex += transformUnitCount;

125
tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

@ -2,11 +2,14 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Heif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.ColorProfiles.Icc;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
namespace SixLabors.ImageSharp.Tests.Formats.Heif;
@ -105,6 +108,125 @@ public class HeifDecoderTests
ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted);
}
/// <summary>
/// Verifies that AVIF grid composition retains and converts the presented image's non-sRGB ICC profile.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
public void DecodeAvifGridConvertsEmbeddedNonSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert };
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<TPixel> expectedPreserved = Image.Load<TPixel>(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
using Image<TPixel> expected = Image.Load<TPixel>(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(converted.Metadata.IccProfile);
Assert.Equal(expectedPreserved.Metadata.IccProfile!.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray());
Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted));
ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted);
}
/// <summary>
/// Verifies that AVIF sequence ICC conversion is applied to every presented frame.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
public void DecodeAvifSequenceConvertsEveryFrameWithEmbeddedNonSrgbIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert };
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> converted = provider.GetImage(HeifDecoder.Instance, convertOptions);
using Image<TPixel> expectedPreserved = Image.Load<TPixel>(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
using Image<TPixel> expected = Image.Load<TPixel>(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes);
Assert.Equal(2, preserved.Frames.Count);
Assert.Equal(preserved.Frames.Count, converted.Frames.Count);
Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(converted.Metadata.IccProfile);
Assert.Equal(expectedPreserved.Metadata.IccProfile!.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray());
for (int i = 0; i < converted.Frames.Count; i++)
{
Assert.False(ImageComparer.Exact.CompareImagesOrFrames(i, preserved.Frames[i], converted.Frames[i]).IsEmpty);
Assert.True(ImageComparer.TolerantPercentage(1F, 20).CompareImagesOrFrames(i, expected.Frames.RootFrame, converted.Frames[i]).IsEmpty);
}
}
/// <summary>
/// Verifies that non-sRGB ICC conversion follows auxiliary-alpha composition and preserves the composed alpha values.
/// </summary>
[Fact]
public void DecodeAvifAlphaImageConvertsEmbeddedIccProfileWithoutChangingAlpha()
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert };
byte[] encoded = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes;
using Image<Rgba32> preserved = Image.Load<Rgba32>(preserveOptions, encoded);
using Image<Rgba32> converted = Image.Load<Rgba32>(convertOptions, encoded);
using Image<Rgba32> expected = preserved.Clone();
ColorProfileConverter converter = new(new ColorConversionOptions
{
SourceIccProfile = expected.Metadata.IccProfile,
TargetIccProfile = CompactSrgbV4Profile.Profile,
MemoryAllocator = expected.Configuration.MemoryAllocator,
});
// Build the oracle from the fully composed preserved decode so that only ICC ordering and alpha retention
// are under test; the independently encoded AV1 color and alpha payloads remain identical in both paths.
converter.Convert(expected);
Assert.NotNull(preserved.Metadata.IccProfile);
Assert.Null(converted.Metadata.IccProfile);
Assert.Equal(TestIccProfiles.GetProfile(TestIccProfiles.RommRgb).ToByteArray(), preserved.Metadata.IccProfile.ToByteArray());
Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted));
for (int y = 0; y < converted.Height; y++)
{
Span<Rgba32> preservedRow = preserved.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
Span<Rgba32> convertedRow = converted.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < convertedRow.Length; x++)
{
Assert.Equal(preservedRow[x].A, convertedRow[x].A);
}
}
ImageComparer.Exact.VerifySimilarity(expected, converted);
}
/// <summary>
/// Verifies that compact profile handling retains non-sRGB ICC profiles and leaves their pixels unconverted.
/// </summary>
[Theory]
[WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)]
public void DecodeAvifRetainsNonSrgbIccProfileWhenCompacting<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve };
DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact };
using Image<TPixel> preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions);
using Image<TPixel> compact = provider.GetImage(HeifDecoder.Instance, compactOptions);
Assert.NotNull(preserved.Metadata.IccProfile);
Assert.NotNull(compact.Metadata.IccProfile);
Assert.Equal(preserved.Metadata.IccProfile.ToByteArray(), compact.Metadata.IccProfile.ToByteArray());
Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact));
}
/// <summary>
/// Verifies that compact profile handling removes a canonical sRGB ICC profile without changing pixels.
/// </summary>
@ -129,6 +251,9 @@ public class HeifDecoderTests
/// </summary>
[Theory]
[WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)]
[WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)]
public void DecodeAvifSkipsEmbeddedIccProfileWithMetadata<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{

6
tests/ImageSharp.Tests/TestImages.cs

@ -1304,6 +1304,12 @@ public static class TestImages
// Independently encoded from Png.Icc.Perceptual with libavif 1.4.2 and libaom 3.14.1. Unlike the
// canonical-sRGB Paris profile, this perceptual profile requires an observable color conversion.
public const string PerceptualIccAvif = "Heif/perceptual_icc.avif";
public const string PerceptualIccGridAvif = "Heif/perceptual_icc_grid.avif";
public const string PerceptualIccSequenceAvif = "Heif/perceptual_icc_sequence.avif";
// Independently encoded from Png.Ducky with libavif 1.4.2 and libaom 3.14.1. The ROMM RGB profile is
// deliberately non-sRGB so that color conversion is observable while the auxiliary alpha item is composed.
public const string DuckyRommIccAlphaAvif = "Heif/ducky_romm_icc_alpha.avif";
// Sourced from libavif/tests/data under libavif's BSD-2-Clause license.
public const string Animated8Bit = "Heif/colors-animated-8bpc.avif";

3
tests/Images/Input/Heif/ducky_romm_icc_alpha.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9419a5e9893bf7236aed6ee1b0bd50578ff59d8e63a1e61aea0761142afa2b20
size 5344

3
tests/Images/Input/Heif/perceptual_icc_grid.avif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:337e2cbd72043eda399b65189b563ce4935086a05f671414d1c1667abfd0a18f
size 205897

3
tests/Images/Input/Heif/perceptual_icc_sequence.avif

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