Browse Source

Complete HEIF decoder option recovery

pull/2633/head
James Jackson-South 1 week ago
parent
commit
deb6575ccb
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 120
      src/ImageSharp/Formats/Heif/HeifDecoderCore.cs
  3. 302
      tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

@ -93,6 +93,8 @@ Checkboxes may be marked complete only when the implementation and the verificat
- [x] Require unity movie and track matrices so image presentation remains on the optimized `clap`/`irot`/`imir` path without a movie compositor.
- [x] Apply the shared `DecoderOptions` contract consistently to still items, nested payload codecs, grids, metadata properties, and sequence samples.
- `Strict` rejects recoverable ancillary and image-data errors, `IgnoreAncillary` suppresses only ancillary failures, and `IgnoreImageData` additionally permits failed image properties or samples to be omitted. `SkipMetadata` avoids optional property and item-payload validation, while cancellation and the caller configuration flow into nested JPEG and AV1 decoders. Target scaling and ICC conversion remain presentation-level operations after item or grid composition. The focused Release matrix passes all 19 new still-image policy cases, all 3 new sequence-sample cases, and the complete 37-test sequence-parser suite; the Release test-project build completes with zero errors.
- [x] Apply the same recovery boundary to still-image item relationships, coded payloads, primary-item thumbnail fallback, and optional alpha composition.
- Unknown item-reference types are skipped within their validated child boundaries. `cdsc` failures follow ancillary policy and are not parsed when metadata is skipped. `dimg`, `auxl`, `prem`, and `thmb` failures follow image-data policy. `IgnoreImageData` can omit an unreadable alpha plane or recover from a failed primary payload through a valid registered thumbnail, but decoding still fails when no color presentation remains. Real AVIF fixtures cover corrupt alpha payloads, corrupt alpha relationships, malformed Exif relationships, and strict, ancillary-only, image-data, and metadata-skipping behavior.
- [ ] Complete reference-dependent AV1 and HEVC sample reconstruction and independent sequence vectors.
- [ ] Write the same bounded movie, track, sample-description, location, dependency, timing, repetition, alpha, and metadata syntax from ImageSharp frames.
- [ ] Decode frame dependencies, durations, repetition, frame-local auxiliary images, and frame-local metadata into the existing ImageSharp multi-frame model.
@ -393,9 +395,11 @@ Tasks:
- [ ] Define distinct HEIC and AVIF public format types over the shared internal HEIF container and register the correct brands, MIME types, and extensions.
- [ ] Add generated `SaveAsHeic` and `SaveAsAvif` APIs and format metadata integration through the same mechanisms as established codecs. Define generic `SaveAsHeif` only if its options require an explicit supported payload codec.
- [ ] Define decoder options using existing `DecoderOptions` behavior, including target pixel type, metadata handling, cancellation, and image-size limits.
- [x] Generic decode selects the caller's pixel type and the non-generic entry point defaults to `Rgba32`. `TargetSize` and `Sampler` are applied once after HEIF presentation composition. `MaxFrames` bounds retained sequence samples. `SkipMetadata`, `SegmentIntegrityHandling`, `ColorProfileHandling`, `Configuration`, and cancellation flow through the container, item, sequence, and nested-codec boundaries.
- [ ] Complete adversarial dimension and allocation-limit coverage for still items, grids, auxiliary images, and sequence tracks before closing this contract item. Large payload and image buffers already use the configured allocator, but the complete cross-product has not been verified.
- [ ] Define codec-specific encoder options with observable semantics for quality, speed/effort, lossless mode, chroma subsampling, bit depth, alpha quality, and metadata handling. Avoid exposing internal HEVC or AV1 tuning knobs without a stable user-facing meaning.
- [ ] Make `Rgba32` the default 8-bit decode output so alpha is not silently lost.
- [ ] Remove unsupported JPEG 2000, JPEG-XR, JPEG-XS, and AVC capability claims unless those payload codecs are added to the completion matrix. Retain legacy JPEG as an explicit supported HEIF image-item codec.
- [x] Make `Rgba32` the default 8-bit decode output so alpha is not silently lost.
- [x] Remove unsupported JPEG 2000, JPEG-XR, JPEG-XS, and AVC capability claims unless those payload codecs are added to the completion matrix. Retain legacy JPEG as an explicit supported HEIF image-item codec.
Exit gate:

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

@ -1002,34 +1002,69 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
int referenceEnd = checked(bytesRead + referenceHeaderLength + (int)referenceLength);
Span<byte> referenceBuffer = boxBuffer[..referenceEnd];
bytesRead += referenceHeaderLength;
uint sourceId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead);
if (this.FindItemById(sourceId) is null)
if (linkType is not Heif4CharCode.Dimg
and not Heif4CharCode.Auxl
and not Heif4CharCode.Prem
and not Heif4CharCode.Thmb
and not Heif4CharCode.Cdsc)
{
throw new InvalidImageContentException($"The item reference box references unknown source item ID {sourceId}.");
// Unknown reference types do not participate in the bounded image model. Their child-box boundary
// was validated above, so skip the payload without imposing semantics from a general ISOBMFF reader.
bytesRead = referenceEnd;
continue;
}
HeifItemLink link = new(linkType, sourceId);
if (this.Options.SkipMetadata && linkType == Heif4CharCode.Cdsc)
{
// Descriptive metadata links have no effect when their payloads are not requested. Avoid validating
// their optional item graph while preserving the surrounding image relationships.
bytesRead = referenceEnd;
continue;
}
EnsureBufferRemaining(referenceBuffer, bytesRead, 2, "item reference");
int count = BinaryPrimitives.ReadUInt16BigEndian(referenceBuffer[bytesRead..]);
bytesRead += 2;
for (uint i = 0; i < count; i++)
try
{
uint destId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead);
if (this.FindItemById(destId) is null)
uint sourceId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead);
if (this.FindItemById(sourceId) is null)
{
throw new InvalidImageContentException($"The item reference box references unknown destination item ID {destId}.");
throw new InvalidImageContentException($"The item reference box references unknown source item ID {sourceId}.");
}
link.DestinationIds.Add(destId);
}
HeifItemLink link = new(linkType, sourceId);
EnsureBufferRemaining(referenceBuffer, bytesRead, 2, "item reference");
int count = BinaryPrimitives.ReadUInt16BigEndian(referenceBuffer[bytesRead..]);
bytesRead += 2;
for (uint i = 0; i < count; i++)
{
uint destId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead);
if (this.FindItemById(destId) is null)
{
throw new InvalidImageContentException($"The item reference box references unknown destination item ID {destId}.");
}
link.DestinationIds.Add(destId);
}
if (bytesRead != referenceEnd)
if (bytesRead != referenceEnd)
{
throw new InvalidImageContentException($"The '{linkType}' item reference length does not match its entry count.");
}
this.itemLinks.Add(link);
}
catch (Exception ex) when (linkType == Heif4CharCode.Cdsc && ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex))
{
throw new InvalidImageContentException($"The '{linkType}' item reference length does not match its entry count.");
// A malformed descriptive link cannot change reconstructed pixels, so non-strict modes omit it.
bytesRead = referenceEnd;
}
catch (Exception ex) when (linkType != Heif4CharCode.Cdsc && ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.Options, ex))
{
// IgnoreImageData permits a malformed optional image relationship to be omitted while retaining
// independently reconstructable items and thumbnail fallbacks.
bytesRead = referenceEnd;
}
this.itemLinks.Add(link);
}
}
@ -2034,6 +2069,12 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
// A failed optional metadata extent is discarded without weakening image-item extent validation.
extentMemory?.Dispose();
}
catch (Exception ex) when (!isMetadataItem && ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.Options, ex))
{
// Keep the item declaration but omit its unreadable payload. The presentation can still use a valid
// thumbnail, omit an auxiliary plane, or reject the file later when no decodable color item remains.
extentMemory?.Dispose();
}
catch
{
// The dictionary takes ownership only after every declared extent has been assembled successfully.
@ -2048,32 +2089,57 @@ internal sealed class HeifDecoderCore : ImageDecoderCore
throw new ImageFormatException("No primary HEIF item defined.");
}
Image<TPixel>? image = null;
HeifItem itemToDecode = rootItem;
IHeifItemDecoder<TPixel>? itemDecoder = this.GetItemDecoder<TPixel>(rootItem, buffers);
bool supportedItemFound = itemDecoder is not null;
if (itemDecoder is not null)
{
this.ExecuteImageDataSegmentAction(
() => image = this.DecodeImageItem(rootItem, itemDecoder, buffers, cancellationToken));
}
HeifItem itemToDecode = rootItem;
if (itemDecoder is null)
if (image is null)
{
// Unable to decode the primary image, decode the thumbnail instead.
// An unsupported primary item always permits its registered thumbnail fallback. IgnoreImageData also
// reaches this branch after a recoverable primary payload failure, matching other multi-image decoders.
HeifItem? thumbnailItem = this.FindDecodableThumbnail<TPixel>(rootItem);
if (thumbnailItem is not null)
{
itemDecoder = HeifCompressionFactory.GetDecoder<TPixel>(thumbnailItem.Type);
itemToDecode = thumbnailItem;
supportedItemFound |= itemDecoder is not null;
if (itemDecoder is not null)
{
itemToDecode = thumbnailItem;
this.ExecuteImageDataSegmentAction(
() => image = this.DecodeImageItem(thumbnailItem, itemDecoder, buffers, cancellationToken));
}
}
}
if (itemDecoder is null)
if (image is null || itemDecoder is null)
{
throw new ImageFormatException("No decodable item found inside this HEIF container.");
if (!supportedItemFound)
{
throw new ImageFormatException("No supported image item was found inside this HEIF container.");
}
throw new InvalidImageContentException("The HEIF container does not contain a decodable image item.");
}
Image<TPixel> image = this.DecodeImageItem(itemToDecode, itemDecoder, buffers, cancellationToken);
try
{
using Image<L16>? alphaImage = this.DecodeAlphaPlane(itemToDecode, buffers, cancellationToken, out bool alphaPremultiplied);
if (alphaImage is not null)
Image<L16>? alphaImage = null;
bool alphaPremultiplied = false;
this.ExecuteImageDataSegmentAction(
() => alphaImage = this.DecodeAlphaPlane(itemToDecode, buffers, cancellationToken, out alphaPremultiplied));
using (alphaImage)
{
this.ApplyAlpha(image, alphaImage, alphaPremultiplied);
if (alphaImage is not null)
{
this.ApplyAlpha(image, alphaImage, alphaPremultiplied);
}
}
if (!this.Options.SkipMetadata)

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

@ -363,6 +363,137 @@ public class HeifDecoderTests
Assert.Same(configuration, image.Configuration);
}
/// <summary>
/// Verifies that invalid optional alpha payloads remain fatal when image-data errors cannot be ignored.
/// </summary>
[Theory]
[InlineData(SegmentIntegrityHandling.Strict)]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
public void DecodeRejectsInvalidAlphaPayloadUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling)
{
byte[] data = [.. TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes];
uint alphaItemId = FindFirstItemReferenceSourceId(data, Heif4CharCode.Auxl);
ClearItemPayload(data, alphaItemId);
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
Assert.ThrowsAny<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
/// <summary>
/// Verifies that <see cref="SegmentIntegrityHandling.IgnoreImageData"/> omits a corrupt optional alpha item while
/// retaining the independently decodable color item.
/// </summary>
[Fact]
public void DecodeOmitsInvalidAlphaPayloadWhenImageDataErrorsAreIgnored()
{
byte[] source = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes;
byte[] data = [.. source];
uint alphaItemId = FindFirstItemReferenceSourceId(data, Heif4CharCode.Auxl);
ClearItemPayload(data, alphaItemId);
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
using Image<Rgba32> expected = Image.Load<Rgba32>(source);
using Image<Rgba32> actual = Image.Load<Rgba32>(options, data);
AssertOpaqueRgbMatches(expected, actual);
}
/// <summary>
/// Verifies that a malformed alpha relationship remains fatal when image-data errors cannot be ignored.
/// </summary>
[Theory]
[InlineData(SegmentIntegrityHandling.Strict)]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
public void DecodeRejectsMalformedAlphaReferenceUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling)
{
byte[] data = [.. TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes];
InvalidateFirstItemReferenceSource(data, Heif4CharCode.Auxl);
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
/// <summary>
/// Verifies that <see cref="SegmentIntegrityHandling.IgnoreImageData"/> omits a malformed optional alpha
/// relationship while retaining the independently decodable color item.
/// </summary>
[Fact]
public void DecodeOmitsMalformedAlphaReferenceWhenImageDataErrorsAreIgnored()
{
byte[] source = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes;
byte[] data = [.. source];
InvalidateFirstItemReferenceSource(data, Heif4CharCode.Auxl);
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
using Image<Rgba32> expected = Image.Load<Rgba32>(source);
using Image<Rgba32> actual = Image.Load<Rgba32>(options, data);
AssertOpaqueRgbMatches(expected, actual);
}
/// <summary>
/// Verifies that strict validation rejects a malformed descriptive metadata relationship.
/// </summary>
[Fact]
public void DecodeRejectsMalformedMetadataReferenceInStrictMode()
{
byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes];
InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc);
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
});
}
/// <summary>
/// Verifies that non-strict validation omits a malformed descriptive relationship without weakening image-data
/// validation.
/// </summary>
[Theory]
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
[InlineData(SegmentIntegrityHandling.IgnoreImageData)]
public void DecodeOmitsMalformedMetadataReferenceWhenAncillaryErrorsAreIgnored(SegmentIntegrityHandling handling)
{
byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes];
InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc);
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
Assert.Null(image.Metadata.ExifProfile);
Assert.NotNull(image.Metadata.XmpProfile);
Assert.NotNull(image.Metadata.IccProfile);
}
/// <summary>
/// Verifies that skipped metadata is neither retained nor validated through its optional descriptive links.
/// </summary>
[Fact]
public void DecodeDoesNotValidateSkippedMetadataReference()
{
byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes];
InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc);
DecoderOptions options = new()
{
SkipMetadata = true,
SegmentIntegrityHandling = SegmentIntegrityHandling.Strict
};
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
Assert.Null(image.Metadata.ExifProfile);
Assert.Null(image.Metadata.XmpProfile);
Assert.Null(image.Metadata.IccProfile);
}
[Fact]
public void IdentifyIgnoresUnknownMetadataBox()
{
@ -883,6 +1014,177 @@ public class HeifDecoderTests
return -1;
}
/// <summary>
/// Reads the source item identifier from the first registered relationship of the requested type.
/// </summary>
/// <param name="data">The complete HEIF container.</param>
/// <param name="referenceType">The item-reference child type.</param>
/// <returns>The source item identifier.</returns>
private static uint FindFirstItemReferenceSourceId(ReadOnlySpan<byte> data, Heif4CharCode referenceType)
{
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
Assert.True(metaOffset >= 0);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]);
int itemReferenceOffset = FindBoxOffset(data, Heif4CharCode.Iref, metaOffset + 12, metaSize - 12);
Assert.True(itemReferenceOffset >= 0);
int itemReferenceSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[itemReferenceOffset..]);
int relationshipOffset = FindBoxOffset(data, referenceType, itemReferenceOffset + 12, itemReferenceSize - 12);
Assert.True(relationshipOffset >= 0);
byte version = data[itemReferenceOffset + 8];
Assert.InRange(version, (byte)0, (byte)1);
return version == 0
? BinaryPrimitives.ReadUInt16BigEndian(data[(relationshipOffset + 8)..])
: BinaryPrimitives.ReadUInt32BigEndian(data[(relationshipOffset + 8)..]);
}
/// <summary>
/// Replaces the source item identifier of the first requested relationship with an undeclared value.
/// </summary>
/// <param name="data">The complete mutable HEIF container.</param>
/// <param name="referenceType">The item-reference child type.</param>
private static void InvalidateFirstItemReferenceSource(Span<byte> data, Heif4CharCode referenceType)
{
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
Assert.True(metaOffset >= 0);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]);
int itemReferenceOffset = FindBoxOffset(data, Heif4CharCode.Iref, metaOffset + 12, metaSize - 12);
Assert.True(itemReferenceOffset >= 0);
int itemReferenceSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[itemReferenceOffset..]);
int relationshipOffset = FindBoxOffset(data, referenceType, itemReferenceOffset + 12, itemReferenceSize - 12);
Assert.True(relationshipOffset >= 0);
byte version = data[itemReferenceOffset + 8];
Assert.InRange(version, (byte)0, (byte)1);
if (version == 0)
{
BinaryPrimitives.WriteUInt16BigEndian(data[(relationshipOffset + 8)..], ushort.MaxValue);
}
else
{
BinaryPrimitives.WriteUInt32BigEndian(data[(relationshipOffset + 8)..], uint.MaxValue);
}
}
/// <summary>
/// Clears every file-relative extent belonging to the requested item while retaining the container structure.
/// </summary>
/// <param name="data">The complete mutable HEIF container.</param>
/// <param name="itemId">The item whose coded payload is cleared.</param>
private static void ClearItemPayload(Span<byte> data, uint itemId)
{
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
Assert.True(metaOffset >= 0);
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]);
int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12);
Assert.True(itemLocationOffset >= 0);
int offset = itemLocationOffset + 8;
byte version = data[offset];
offset += 4;
int extentOffsetSize = data[offset] >> 4;
int extentLengthSize = data[offset] & 0x0F;
offset++;
int baseOffsetSize = data[offset] >> 4;
int extentIndexSize = version is 1 or 2 ? data[offset] & 0x0F : 0;
offset++;
uint itemCount = version == 2
? BinaryPrimitives.ReadUInt32BigEndian(data[offset..])
: BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += version == 2 ? 4 : 2;
bool found = false;
for (uint itemIndex = 0; itemIndex < itemCount; itemIndex++)
{
uint currentItemId = version == 2
? BinaryPrimitives.ReadUInt32BigEndian(data[offset..])
: BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += version == 2 ? 4 : 2;
if (version is 1 or 2)
{
ushort constructionMethod = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
Assert.Equal(0, constructionMethod & 0x0F);
offset += 2;
}
// The data-reference index is zero for the self-contained image items used by the fixture.
Assert.Equal(0, BinaryPrimitives.ReadUInt16BigEndian(data[offset..]));
offset += 2;
ulong baseOffset = ReadVariableUnsigned(data, baseOffsetSize, ref offset);
int extentCount = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]);
offset += 2;
for (int extentIndex = 0; extentIndex < extentCount; extentIndex++)
{
_ = ReadVariableUnsigned(data, extentIndexSize, ref offset);
ulong extentOffset = ReadVariableUnsigned(data, extentOffsetSize, ref offset);
ulong extentLength = ReadVariableUnsigned(data, extentLengthSize, ref offset);
if (currentItemId == itemId)
{
data.Slice(checked((int)(baseOffset + extentOffset)), checked((int)extentLength)).Clear();
found = true;
}
}
}
Assert.True(found);
}
/// <summary>
/// Reads one zero-width, 32-bit, or 64-bit unsigned item-location field.
/// </summary>
/// <param name="data">The complete HEIF container.</param>
/// <param name="size">The field width in bytes.</param>
/// <param name="offset">The current read offset, advanced past the field.</param>
/// <returns>The decoded field value.</returns>
private static ulong ReadVariableUnsigned(ReadOnlySpan<byte> data, int size, ref int offset)
{
ulong value = size switch
{
0 => 0,
4 => BinaryPrimitives.ReadUInt32BigEndian(data[offset..]),
8 => BinaryPrimitives.ReadUInt64BigEndian(data[offset..]),
_ => throw new InvalidOperationException($"Unexpected item-location field width {size} in the test fixture.")
};
offset += size;
return value;
}
/// <summary>
/// Verifies that omitting an invalid alpha item preserves color channels and produces opaque output.
/// </summary>
/// <param name="expected">The image decoded with its valid alpha item.</param>
/// <param name="actual">The image decoded after the alpha item or relationship was invalidated.</param>
private static void AssertOpaqueRgbMatches(Image<Rgba32> expected, Image<Rgba32> actual)
{
Assert.False(actual.Metadata.GetHeifMetadata().HasAlpha);
Assert.Equal(expected.Size, actual.Size);
for (int y = 0; y < actual.Height; y++)
{
Span<Rgba32> expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
Span<Rgba32> actualRow = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
for (int x = 0; x < actualRow.Length; x++)
{
Assert.Equal(expectedRow[x].R, actualRow[x].R);
Assert.Equal(expectedRow[x].G, actualRow[x].G);
Assert.Equal(expectedRow[x].B, actualRow[x].B);
Assert.Equal(byte.MaxValue, actualRow[x].A);
}
}
}
private static byte[] InsertBytes(byte[] data, int offset, ReadOnlySpan<byte> inserted)
{
byte[] result = new byte[data.Length + inserted.Length];

Loading…
Cancel
Save