diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 7d7a92777..29a47001f 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/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. diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index 916888af3..f1925913c 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/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; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs index e96d935e3..3d7798ec2 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -13,20 +13,9 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1; internal sealed class Av1CodecConfiguration { /// - /// The optional open bitstream units following the fixed four-byte configuration record. + /// The optional sequence-header payload retained from the configuration open bitstream units. /// - private readonly byte[] configObus; - - /// - /// The byte offset of the optional sequence-header payload within , or -1 when - /// the configuration contains no sequence header. - /// - private readonly int configSequenceHeaderOffset; - - /// - /// The byte length of the optional sequence-header payload within . - /// - private readonly int configSequenceHeaderLength; + private readonly byte[] configSequenceHeader; /// /// The sequence-header OBU extension byte, or -1 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 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(configSequenceHeaderLength); + configObus.Slice(configSequenceHeaderOffset, configSequenceHeaderLength).CopyTo(this.configSequenceHeader); + } } /// @@ -163,11 +164,6 @@ internal sealed class Av1CodecConfiguration /// public byte ChromaSamplePosition { get; } - /// - /// Gets the optional configuration open bitstream units following the fixed record. - /// - public ReadOnlyMemory ConfigObus => this.configObus; - /// /// Validates the AV1 image item OBU layout and metadata against its item properties and configuration record. /// @@ -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 configSequenceHeader = this.configObus.AsSpan( - this.configSequenceHeaderOffset, - this.configSequenceHeaderLength); - ReadOnlySpan 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."); diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs index 97513743d..f462cd660 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/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); diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs index b181bd27d..68a38f8a9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs +++ b/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); diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs index fe90e274e..0036a83ff 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs @@ -11,35 +11,79 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; internal class Av1BlockModeInfo { /// - /// Stores the palette size for luma and for the shared chroma mode. + /// The palette size for the luma plane. /// - private int[] paletteSize; + private int lumaPaletteSize; /// - /// Stores the decoded palette colors for the Y, U, and V planes. + /// The palette size shared by both chroma planes. /// - private readonly ushort[][] paletteColors = [[], [], []]; + private int chromaPaletteSize; /// - /// Stores the luma and shared chroma palette color-index maps. + /// Stores the decoded luma palette colors. /// - private readonly byte[][] paletteColorIndexMaps = [[], []]; + private InlineArray8 lumaPaletteColors; + + /// + /// Stores the decoded blue-difference chroma palette colors. + /// + private InlineArray8 chromaBluePaletteColors; + + /// + /// Stores the decoded red-difference chroma palette colors. + /// + private InlineArray8 chromaRedPaletteColors; + + /// + /// Stores the luma palette color-index map. + /// + private byte[] lumaPaletteColorIndexMap = []; + + /// + /// Stores the shared chroma palette color-index map. + /// + private byte[] chromaPaletteColorIndexMap = []; + + /// + /// The directional prediction angle adjustment for luma. + /// + private int lumaAngleDelta; + + /// + /// The directional prediction angle adjustment shared by both chroma planes. + /// + private int chromaAngleDelta; + + /// + /// The plane-relative index of the first luma transform. + /// + private int firstLumaTransformLocation; + + /// + /// The plane-relative index of the first chroma transform. + /// + private int firstChromaTransformLocation; + + /// + /// The number of luma transform units. + /// + private int lumaTransformUnitCount; + + /// + /// The number of transform units for one chroma plane. + /// + private int chromaTransformUnitCount; /// /// Initializes a new instance of the class. /// - /// The number of color planes in the decoded frame. /// The decoded block size. /// The block origin relative to its superblock in 4x4 mode-information units. - 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]; } /// @@ -93,9 +137,9 @@ internal class Av1BlockModeInfo public int ChromaFromLumaAlphaSign { get; set; } /// - /// 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. /// - public int[] AngleDelta { get; set; } + public bool UseFilterIntra { get; set; } /// /// Gets the position relative to the superblock in 4x4 mode-information units. @@ -103,47 +147,141 @@ internal class Av1BlockModeInfo public Point PositionInSuperblock { get; } /// - /// Gets or sets the filter-intra syntax for the block. + /// Gets or sets the filter-intra mode selected for the block. /// - public Av1IntraFilterModeInfo FilterIntraModeInfo { get; internal set; } + public Av1FilterIntraMode FilterIntraMode { get; set; } /// - /// Gets the plane-relative index of the first for this block. + /// Gets the directional prediction angle adjustment for a color plane. /// - public int[] FirstTransformLocation { get; } + /// The color plane. + /// The luma adjustment or the adjustment shared by both chroma planes. + public int GetAngleDelta(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaAngleDelta : this.chromaAngleDelta; /// - /// 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. /// - public int[] TransformUnitsCount { get; internal set; } + /// The luma or chroma plane class. + /// The directional prediction angle adjustment. + public void SetAngleDelta(Av1PlaneType planeType, int value) + { + if (planeType == Av1PlaneType.Y) + { + this.lumaAngleDelta = value; + } + else + { + this.chromaAngleDelta = value; + } + } + + /// + /// Gets the plane-relative index of the first transform for a color plane. + /// + /// The color plane. + /// The first transform index for luma or the selected chroma plane. + public int GetFirstTransformLocation(Av1Plane plane) + => plane == Av1Plane.Y ? this.firstLumaTransformLocation : this.firstChromaTransformLocation; + + /// + /// Gets the plane-relative index of the first transform for a plane class. + /// + /// The luma or chroma plane class. + /// The first transform index for the plane class. + public int GetFirstTransformLocation(Av1PlaneType planeType) + => planeType == Av1PlaneType.Y ? this.firstLumaTransformLocation : this.firstChromaTransformLocation; + + /// + /// Sets the plane-relative index of the first transform for a plane class. + /// + /// The luma or chroma plane class. + /// The first transform index. + public void SetFirstTransformLocation(Av1PlaneType planeType, int value) + { + if (planeType == Av1PlaneType.Y) + { + this.firstLumaTransformLocation = value; + } + else + { + this.firstChromaTransformLocation = value; + } + } + + /// + /// Gets the number of transform units for a color plane. + /// + /// The color plane. + /// The luma count or the count for one chroma plane. + public int GetTransformUnitCount(Av1Plane plane) + => plane == Av1Plane.Y ? this.lumaTransformUnitCount : this.chromaTransformUnitCount; + + /// + /// Gets the number of transform units for a plane class. + /// + /// The luma or chroma plane class. + /// The transform-unit count for the plane class. + public int GetTransformUnitCount(Av1PlaneType planeType) + => planeType == Av1PlaneType.Y ? this.lumaTransformUnitCount : this.chromaTransformUnitCount; + + /// + /// Sets the number of transform units for a plane class. + /// + /// The luma or chroma plane class. + /// The transform-unit count. + public void SetTransformUnitCount(Av1PlaneType planeType, int value) + { + if (planeType == Av1PlaneType.Y) + { + this.lumaTransformUnitCount = value; + } + else + { + this.chromaTransformUnitCount = value; + } + } /// /// Gets the palette size for the specified color plane. /// /// The color plane. /// The palette size for the plane. - public int GetPaletteSize(Av1Plane plane) => this.paletteSize[Math.Min(1, (int)plane)]; + public int GetPaletteSize(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaPaletteSize : this.chromaPaletteSize; /// /// Gets the palette size for the specified plane class. /// /// The luma or chroma plane class. /// The palette size for the plane class. - public int GetPaletteSize(Av1PlaneType planeType) => this.paletteSize[(int)planeType]; + public int GetPaletteSize(Av1PlaneType planeType) => planeType == Av1PlaneType.Y ? this.lumaPaletteSize : this.chromaPaletteSize; /// /// Sets the luma and shared chroma palette sizes. /// /// The luma palette size. /// The palette size shared by the chroma planes. - public void SetPaletteSizes(int ySize, int uvSize) => this.paletteSize = [ySize, uvSize]; + public void SetPaletteSizes(int ySize, int uvSize) + { + this.lumaPaletteSize = ySize; + this.chromaPaletteSize = uvSize; + } /// /// Gets the decoded palette colors for a color plane. /// /// The color plane. /// The palette colors in prediction-index order. - public ReadOnlySpan GetPaletteColors(Av1Plane plane) => this.paletteColors[(int)plane]; + public ReadOnlySpan GetPaletteColors(Av1Plane plane) + { + if (plane == Av1Plane.Y) + { + return this.lumaPaletteColors[..this.lumaPaletteSize]; + } + + return plane == Av1Plane.U + ? this.chromaBluePaletteColors[..this.chromaPaletteSize] + : this.chromaRedPaletteColors[..this.chromaPaletteSize]; + } /// /// Stores the decoded palette colors for a color plane. @@ -151,7 +289,20 @@ internal class Av1BlockModeInfo /// The color plane. /// The palette colors in prediction-index order. public void SetPaletteColors(Av1Plane plane, ReadOnlySpan 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); + } + } /// /// Gets the palette color-index map for a color plane. @@ -159,7 +310,7 @@ internal class Av1BlockModeInfo /// The color plane. /// The luma map for or the shared chroma map for either chroma plane. public ReadOnlySpan GetPaletteColorIndexMap(Av1Plane plane) - => this.paletteColorIndexMaps[Math.Min(1, (int)plane)]; + => plane == Av1Plane.Y ? this.lumaPaletteColorIndexMap : this.chromaPaletteColorIndexMap; /// /// Stores the palette color-index map for a plane class. @@ -167,5 +318,14 @@ internal class Av1BlockModeInfo /// The luma or shared chroma plane class. /// The row-major color-index map including coded-block edge padding. public void SetPaletteColorIndexMap(Av1PlaneType planeType, byte[] colorIndexMap) - => this.paletteColorIndexMaps[(int)planeType] = colorIndexMap; + { + if (planeType == Av1PlaneType.Y) + { + this.lumaPaletteColorIndexMap = colorIndexMap; + } + else + { + this.chromaPaletteColorIndexMap = colorIndexMap; + } + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1IntraFilterModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1IntraFilterModeInfo.cs deleted file mode 100644 index 52db23dd4..000000000 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1IntraFilterModeInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; - -/// -/// Stores whether filter-intra prediction is active and which filter-intra mode is selected. -/// -internal class Av1IntraFilterModeInfo -{ - /// - /// Gets or sets a value indicating whether filter-intra prediction is enabled for the block. - /// - public bool UseFilterIntra { get; set; } - - /// - /// Gets or sets the filter-intra mode selected for the block. - /// - public Av1FilterIntraMode Mode { get; set; } -} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 6e8b3fb9e..2e72a21c5 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/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 /// The selected luma transform size. 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 lumaTransformInfo = superblockInfo.GetTransformInfoY(); Span 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 /// The current coding block. 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; } } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs index 708217894..4c443331f 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs +++ b/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 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."); diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs index dedc2e87c..c3902c1d3 100644 --- a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs +++ b/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(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 itemData = itemMemory.GetSpan(); + + // XmpProfile retains its input array after the assembled item buffer is returned to its pool. + byte[] ownedData = GC.AllocateUninitializedArray(itemData.Length); + itemData.CopyTo(ownedData); + metadata.XmpProfile = new XmpProfile(ownedData); + }); } } } @@ -2297,7 +2306,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore /// /// The image metadata receiving the Exif profile. /// The complete HEIF Exif item including its four-byte offset field. - private static void ApplyExifProfile(ImageMetadata metadata, byte[] itemData) + private static void ApplyExifProfile(ImageMetadata metadata, ReadOnlySpan itemData) { if (itemData.Length < 8) { @@ -2305,7 +2314,7 @@ internal sealed class HeifDecoderCore : ImageDecoderCore } uint declaredTiffHeaderOffset = BinaryPrimitives.ReadUInt32BigEndian(itemData); - Span exifData = itemData.AsSpan(4); + ReadOnlySpan 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 tiffData = exifData[actualTiffHeaderOffset..]; + + // ExifProfile retains its input array after the assembled item buffers are disposed at the end of decode. + byte[] ownedData = GC.AllocateUninitializedArray(tiffData.Length); + tiffData.CopyTo(ownedData); + metadata.ExifProfile = new ExifProfile(ownedData); } /// diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs index 8a28213fe..79ace6407 100644 --- a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs +++ b/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 items = new(); List 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 /// /// The source pixel format. /// The source image. - /// The encoded primary-item payload. + /// The encoded primary-item payload length. /// The destination item collection. - private static void GenerateItems(Image image, byte[] pixels, List items) + private static void GenerateItems(Image image, long pixelDataLength, List items) where TPixel : unmanaged, IPixel { 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 /// /// Writes the encoded primary-item bytes in a media-data box. /// - /// The encoded item payload. + /// The encoded item payload stream. /// The destination stream. - private void WriteMediaDataBox(Span data, Stream stream) + private void WriteMediaDataBox(ChunkedMemoryStream data, Stream stream) { Span 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); } /// @@ -438,8 +439,8 @@ internal sealed class HeifEncoderCore /// The source pixel format. /// The source image. /// The token used to cancel payload encoding. - /// The encoded JPEG item bytes. - private byte[] CompressPixels(Image image, CancellationToken cancellationToken) + /// The pooled stream containing the encoded JPEG item bytes. + private ChunkedMemoryStream CompressPixels(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { 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; + } } } diff --git a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs index 96464c56b..164fb633e 100644 --- a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs @@ -36,14 +36,18 @@ internal static class HeifPropertyParser /// /// The complete ICC profile bytes. /// The validated ICC profile. - public static IccProfile ParseIccProfile(byte[] data) + public static IccProfile ParseIccProfile(ReadOnlySpan 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(data.Length); + data.CopyTo(ownedData); + IccProfile profile = new(ownedData); if (!profile.CheckIsValid()) { throw new InvalidIccProfileException("Invalid HEIF ICC profile."); diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs index 0c9e58db5..d8d2a51fc 100644 --- a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -1173,8 +1173,7 @@ internal sealed class HeifSequenceParser stream.Position -= 4; using IMemoryOwner 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)) { diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs index f9fe8a661..5119e2144 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcNalUnit.cs +++ b/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; /// @@ -24,7 +26,7 @@ internal sealed class HevcNalUnit int rbspLength = HevcRbspDecoder.Decode( encodedPayload, rbspBuffer, - out int[] emulationPreventionBytePositions); + out ReadOnlyMemory emulationPreventionBytePositions); this.EncodedPayloadLength = encodedPayload.Length; this.Rbsp = rbspBuffer.AsMemory(0, rbspLength); @@ -49,7 +51,7 @@ internal sealed class HevcNalUnit /// /// Gets the zero-based encoded-payload positions of removed emulation-prevention bytes. /// - public IReadOnlyList EmulationPreventionBytePositions { get; } + public ReadOnlyMemory EmulationPreventionBytePositions { get; } } /// @@ -72,47 +74,75 @@ internal static class HevcRbspDecoder public static int Decode( ReadOnlySpan encodedPayload, Span destination, - out int[] emulationPreventionBytePositions) + out ReadOnlyMemory emulationPreventionBytePositions) { DebugGuard.MustBeGreaterThanOrEqualTo(destination.Length, encodedPayload.Length, nameof(destination)); int destinationOffset = 0; + int preventionByteCount = 0; int consecutiveZeroBytes = 0; - List? preventionBytePositions = null; - for (int sourceOffset = 0; sourceOffset < encodedPayload.Length; sourceOffset++) + int[]? rentedPositions = null; + Span 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.Shared.Rent(preventionBytePositions.IsEmpty ? 16 : preventionBytePositions.Length * 2); + preventionBytePositions.CopyTo(expandedPositions); + if (rentedPositions is not null) + { + ArrayPool.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(preventionByteCount); - emulationPreventionBytePositions = preventionBytePositions?.ToArray() ?? Array.Empty(); - return destinationOffset; + preventionBytePositions[..preventionByteCount].CopyTo(retainedPositions); + emulationPreventionBytePositions = retainedPositions; + return destinationOffset; + } + finally + { + if (rentedPositions is not null) + { + ArrayPool.Shared.Return(rentedPositions); + } + } } } diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs index ee8005a30..93d53befe 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs +++ b/src/ImageSharp/Formats/Heif/Hevc/HevcShortTermReferencePictureSet.cs @@ -8,31 +8,41 @@ namespace SixLabors.ImageSharp.Formats.Heif.Hevc; /// internal sealed class HevcShortTermReferencePictureSet { + /// + /// Stores the bounded signed picture-order differences in HEVC reference order. + /// + private InlineArray16 deltaPictureOrders; + + /// + /// Stores the bounded current-picture usage flags corresponding to . + /// + private InlineArray16 usedByCurrentPicture; + /// /// Initializes a new instance of the class. /// - /// The signed picture-order differences in HEVC reference order. - /// The corresponding current-picture usage flags. - private HevcShortTermReferencePictureSet(int[] deltaPictureOrders, bool[] usedByCurrentPicture) + private HevcShortTermReferencePictureSet() { - this.DeltaPictureOrders = deltaPictureOrders; - this.UsedByCurrentPicture = usedByCurrentPicture; } /// - /// Gets the signed picture-order differences in HEVC reference order. + /// Gets the number of pictures declared by the reference-picture set. /// - public IReadOnlyList DeltaPictureOrders { get; } + public int Count { get; private set; } /// - /// Gets the flags indicating which reference pictures are used by the current picture. + /// Gets a signed picture-order difference in HEVC reference order. /// - public IReadOnlyList UsedByCurrentPicture { get; } + /// The zero-based reference-picture index. + /// The signed picture-order difference. + public int GetDeltaPictureOrder(int index) => this.deltaPictureOrders[index]; /// - /// Gets the number of pictures declared by the reference-picture set. + /// Gets whether a reference picture is used by the current picture. /// - public int Count => this.DeltaPictureOrders.Count; + /// The zero-based reference-picture index. + /// when the reference is used by the current picture. + public bool IsUsedByCurrentPicture(int index) => this.usedByCurrentPicture[index]; /// /// Reads one SPS short-term reference-picture set. @@ -47,8 +57,9 @@ internal sealed class HevcShortTermReferencePictureSet IReadOnlyList previousSets, int index) { - Span deltaPictureOrders = stackalloc int[16]; - Span usedByCurrentPicture = stackalloc bool[16]; + HevcShortTermReferencePictureSet result = new(); + Span deltaPictureOrders = result.deltaPictureOrders; + Span 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; } } diff --git a/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs b/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs index 02a37b1a2..085e900ae 100644 --- a/src/ImageSharp/Formats/Heif/Hevc/HevcSliceSegmentHeader.cs +++ b/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 /// The encoded byte-sequence payload offset at the same syntax boundary. private static int GetEncodedPayloadOffset( int rbspOffset, - IReadOnlyList emulationPreventionBytePositions) + ReadOnlySpan emulationPreventionBytePositions) { int encodedOffset = rbspOffset; foreach (int preventionBytePosition in emulationPreventionBytePositions) diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs index bb9c31d6d..e98cbde83 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs +++ b/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()]; diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs index d2111811a..049cc71b6 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs +++ b/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; diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs index 541961647..c04908719 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs +++ b/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); } + /// + /// Verifies that AVIF grid composition retains and converts the presented image's non-sRGB ICC profile. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] + public void DecodeAvifGridConvertsEmbeddedNonSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); + using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + using Image expected = Image.Load(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); + } + + /// + /// Verifies that AVIF sequence ICC conversion is applied to every presented frame. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] + public void DecodeAvifSequenceConvertsEveryFrameWithEmbeddedNonSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); + using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + using Image expected = Image.Load(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); + } + } + + /// + /// Verifies that non-sRGB ICC conversion follows auxiliary-alpha composition and preserves the composed alpha values. + /// + [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 preserved = Image.Load(preserveOptions, encoded); + using Image converted = Image.Load(convertOptions, encoded); + using Image 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 preservedRow = preserved.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + Span 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); + } + + /// + /// Verifies that compact profile handling retains non-sRGB ICC profiles and leaves their pixels unconverted. + /// + [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(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image 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)); + } + /// /// Verifies that compact profile handling removes a canonical sRGB ICC profile without changing pixels. /// @@ -129,6 +251,9 @@ public class HeifDecoderTests /// [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(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index a20ca719b..f93782729 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/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"; diff --git a/tests/Images/Input/Heif/ducky_romm_icc_alpha.avif b/tests/Images/Input/Heif/ducky_romm_icc_alpha.avif new file mode 100644 index 000000000..21b44e40d --- /dev/null +++ b/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 diff --git a/tests/Images/Input/Heif/perceptual_icc_grid.avif b/tests/Images/Input/Heif/perceptual_icc_grid.avif new file mode 100644 index 000000000..9d183f198 --- /dev/null +++ b/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 diff --git a/tests/Images/Input/Heif/perceptual_icc_sequence.avif b/tests/Images/Input/Heif/perceptual_icc_sequence.avif new file mode 100644 index 000000000..ae7154dd3 --- /dev/null +++ b/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