mirror of https://github.com/SixLabors/ImageSharp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
702 lines
29 KiB
702 lines
29 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System.Buffers.Binary;
|
|
using SixLabors.ImageSharp.Formats;
|
|
using SixLabors.ImageSharp.Formats.Heif;
|
|
using SixLabors.ImageSharp.Metadata;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Processing;
|
|
|
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif;
|
|
|
|
[Trait("Format", "Heif")]
|
|
[ValidateDisposedMemoryAllocations]
|
|
public class HeifDecoderTests
|
|
{
|
|
private const uint UnknownBoxType = 0x74657374U;
|
|
|
|
private static ReadOnlySpan<byte> MalformedJpegApp13 =>
|
|
[
|
|
0xFF, 0xED,
|
|
0x00, 0x1D,
|
|
(byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.',
|
|
(byte)'0', 0x00,
|
|
(byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!',
|
|
(byte)'!'
|
|
];
|
|
|
|
[Theory]
|
|
[InlineData(TestImages.Heif.Image1, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 3992, 2992)]
|
|
[InlineData(TestImages.Heif.Sample640x427, HeifCompressionMethod.Hevc, HeifBitDepth.Bit8, 640, 428)]
|
|
[InlineData(TestImages.Heif.FujiFilmHif, HeifCompressionMethod.LegacyJpeg, HeifBitDepth.Bit8, 7728, 5152)]
|
|
[InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, HeifBitDepth.Bit8, 480, 640)]
|
|
public void Identify(string imagePath, HeifCompressionMethod compressionMethod, HeifBitDepth bitDepth, int width, int height)
|
|
{
|
|
TestFile testFile = TestFile.Create(imagePath);
|
|
using MemoryStream stream = new(testFile.Bytes, false);
|
|
|
|
ImageInfo imageInfo = Image.Identify(stream);
|
|
HeifMetadata heicMetadata = imageInfo.Metadata.GetHeifMetadata();
|
|
|
|
Assert.NotNull(imageInfo);
|
|
Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat);
|
|
Assert.Equal(compressionMethod, heicMetadata.CompressionMethod);
|
|
Assert.Equal(bitDepth, heicMetadata.BitDepth);
|
|
Assert.Equal(width, imageInfo.Width);
|
|
Assert.Equal(height, imageInfo.Height);
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Heif.FujiFilmHif, PixelTypes.Rgba32)]
|
|
public void Decode<TPixel>(TestImageProvider<TPixel> provider)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
using Image<TPixel> image = provider.GetImage();
|
|
HeifMetadata heicMetadata = image.Metadata.GetHeifMetadata();
|
|
image.DebugSave(provider);
|
|
|
|
image.CompareToReferenceOutput(provider);
|
|
Assert.Equal(HeifCompressionMethod.LegacyJpeg, heicMetadata.CompressionMethod);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeIgnoresUnknownTopLevelBox()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
data = InsertBytes(data, data.Length, CreateUnknownBox());
|
|
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(data);
|
|
|
|
Assert.Equal(new Size(2, 3), image.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeAppliesTargetSizeOnceToThePresentedHeifImage()
|
|
{
|
|
using Image<Rgba32> source = new(64, 48);
|
|
for (int y = 0; y < source.Height; y++)
|
|
{
|
|
Span<Rgba32> row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y);
|
|
for (int x = 0; x < row.Length; x++)
|
|
{
|
|
row[x] = new Rgba32((byte)(x * 3), (byte)(y * 5), (byte)((x * 7) + (y * 11)));
|
|
}
|
|
}
|
|
|
|
using MemoryStream stream = new();
|
|
source.Save(stream, new HeifEncoder());
|
|
byte[] data = stream.ToArray();
|
|
Size targetSize = new(17, 17);
|
|
DecoderOptions options = new() { TargetSize = targetSize };
|
|
|
|
using Image<Rgba32> expected = Image.Load<Rgba32>(data);
|
|
expected.Mutate(context => context.Resize(new ResizeOptions { Size = targetSize, Mode = ResizeMode.Max, Sampler = options.Sampler }));
|
|
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
|
|
|
|
Assert.Equal(expected.Size, image.Size);
|
|
for (int y = 0; y < image.Height; y++)
|
|
{
|
|
Assert.True(image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y).SequenceEqual(
|
|
expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y)));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodePropagatesStrictValidationToLegacyJpegItems()
|
|
{
|
|
byte[] data = CreateContainerWithMalformedJpegMetadata();
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() =>
|
|
{
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreImageData)]
|
|
public void DecodePropagatesRecoverableMetadataValidationToLegacyJpegItems(SegmentIntegrityHandling handling)
|
|
{
|
|
byte[] data = CreateContainerWithMalformedJpegMetadata();
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
|
|
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
|
|
|
|
Assert.Equal(new Size(2, 3), image.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodePropagatesSkipMetadataToLegacyJpegItems()
|
|
{
|
|
byte[] data = CreateContainerWithMalformedJpegMetadata();
|
|
DecoderOptions options = new()
|
|
{
|
|
SkipMetadata = true,
|
|
SegmentIntegrityHandling = SegmentIntegrityHandling.Strict
|
|
};
|
|
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
|
|
|
|
Assert.Equal(new Size(2, 3), image.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodePropagatesConfigurationToLegacyJpegItems()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
Configuration configuration = Configuration.CreateDefaultInstance();
|
|
DecoderOptions options = new() { Configuration = configuration };
|
|
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, data);
|
|
|
|
Assert.Same(configuration, image.Configuration);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyIgnoresUnknownMetadataBox()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox());
|
|
IncrementBoxSize(data, metaOffset, 8);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyIgnoresUnknownNonEssentialProperty()
|
|
{
|
|
byte[] data = CreateContainerWithUnknownProperty(false);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsUnknownEssentialProperty()
|
|
{
|
|
byte[] data = CreateContainerWithUnknownProperty(true);
|
|
|
|
InvalidImageContentException exception = Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
|
|
Assert.Contains("essential", exception.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsMalformedAncillaryPropertyInStrictMode()
|
|
{
|
|
byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, data));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreImageData)]
|
|
public void IdentifyIgnoresMalformedAncillaryPropertyWhenPermitted(SegmentIntegrityHandling handling)
|
|
{
|
|
byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
|
|
|
|
ImageInfo imageInfo = Image.Identify(options, data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
Assert.Equal(PixelResolutionUnit.PixelsPerInch, imageInfo.Metadata.ResolutionUnits);
|
|
Assert.Equal(96D, imageInfo.Metadata.HorizontalResolution);
|
|
Assert.Equal(96D, imageInfo.Metadata.VerticalResolution);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyDoesNotValidateSkippedAncillaryPropertyMetadata()
|
|
{
|
|
byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false);
|
|
DecoderOptions options = new()
|
|
{
|
|
SkipMetadata = true,
|
|
SegmentIntegrityHandling = SegmentIntegrityHandling.Strict
|
|
};
|
|
|
|
ImageInfo imageInfo = Image.Identify(options, data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SegmentIntegrityHandling.Strict)]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
|
|
public void IdentifyRejectsMalformedImagePropertyUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling)
|
|
{
|
|
byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyIgnoresMalformedImagePropertyWhenImageDataErrorsAreIgnored()
|
|
{
|
|
byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
|
|
|
|
ImageInfo imageInfo = Image.Identify(options, data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsDuplicateAncillaryPropertyAssociationInStrictMode()
|
|
{
|
|
byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, data));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreImageData)]
|
|
public void IdentifyIgnoresDuplicateAncillaryPropertyAssociationWhenPermitted(SegmentIntegrityHandling handling)
|
|
{
|
|
byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
|
|
|
|
ImageInfo imageInfo = Image.Identify(options, data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
Assert.Equal(PixelResolutionUnit.AspectRatio, imageInfo.Metadata.ResolutionUnits);
|
|
Assert.Equal(1D, imageInfo.Metadata.HorizontalResolution);
|
|
Assert.Equal(2D, imageInfo.Metadata.VerticalResolution);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(SegmentIntegrityHandling.Strict)]
|
|
[InlineData(SegmentIntegrityHandling.IgnoreAncillary)]
|
|
public void IdentifyRejectsDuplicateImagePropertyAssociationUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling)
|
|
{
|
|
byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = handling };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyIgnoresDuplicateImagePropertyAssociationWhenImageDataErrorsAreIgnored()
|
|
{
|
|
byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
|
|
|
|
ImageInfo imageInfo = Image.Identify(options, data);
|
|
|
|
Assert.Equal(new Size(3, 2), imageInfo.Size);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Heif4CharCode.Heic)]
|
|
[InlineData(Heif4CharCode.Heix)]
|
|
[InlineData(Heif4CharCode.Mif1)]
|
|
[InlineData(Heif4CharCode.Avif)]
|
|
[InlineData(Heif4CharCode.Jpeg)]
|
|
public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand)
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
|
|
HeifImageFormatDetector detector = new();
|
|
|
|
bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format);
|
|
|
|
Assert.True(detected);
|
|
Assert.Same(HeifFormat.Instance, format);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsSupportedCompatibleBrand()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Heif4CharCode.Hevc)]
|
|
[InlineData(Heif4CharCode.Hevx)]
|
|
[InlineData(Heif4CharCode.Avis)]
|
|
public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand)
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
|
|
HeifImageFormatDetector detector = new();
|
|
|
|
bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format);
|
|
|
|
Assert.True(detected);
|
|
Assert.Same(HeifFormat.Instance, format);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Heif4CharCode.Hevm)]
|
|
[InlineData(Heif4CharCode.Hevs)]
|
|
[InlineData(Heif4CharCode.Jpgs)]
|
|
public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand)
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand);
|
|
HeifImageFormatDetector detector = new();
|
|
|
|
Assert.False(detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsUnsupportedBrands()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType);
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType);
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType);
|
|
using MemoryStream stream = new(data, false);
|
|
|
|
Assert.Throws<ImageFormatException>(() => HeifDecoder.Instance.Identify(DecoderOptions.Default, stream));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsExtendedSizeTopLevelBox()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
byte[] box = new byte[16];
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 1);
|
|
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
|
|
BinaryPrimitives.WriteUInt64BigEndian(box.AsSpan(8), (ulong)box.Length);
|
|
data = InsertBytes(data, data.Length, box);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsUuidTopLevelBox()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
byte[] box = new byte[24];
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
|
|
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
|
|
data = InsertBytes(data, data.Length, box);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsSizeZeroTopLevelBox()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
byte[] box = CreateUnknownBox();
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 0);
|
|
data = InsertBytes(data, data.Length, box);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsExtendedSizeItemInfoEntry()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
|
|
int infeOffset = iinfOffset + 14;
|
|
uint infeSize = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(infeOffset));
|
|
data = InsertBytes(data, infeOffset + 8, new byte[8]);
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(infeOffset), 1);
|
|
BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan(infeOffset + 8), infeSize + 8);
|
|
IncrementBoxSize(data, metaOffset, 8);
|
|
IncrementBoxSize(data, iinfOffset, 8);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsSizeZeroMetadataChild()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
byte[] box = CreateUnknownBox();
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 0);
|
|
data = InsertBytes(data, metaOffset + metaSize, box);
|
|
IncrementBoxSize(data, metaOffset, box.Length);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsMetadataChildBeyondParent()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
byte[] box = CreateUnknownBox();
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 16);
|
|
data = InsertBytes(data, metaOffset + metaSize, box);
|
|
IncrementBoxSize(data, metaOffset, box.Length);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsItemInfoEntryBeyondParent()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
|
|
uint iinfSize = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iinfOffset));
|
|
int infeOffset = iinfOffset + 14;
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(infeOffset), iinfSize);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsBoxSmallerThanHeader()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
byte[] box = CreateUnknownBox();
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 4);
|
|
data = InsertBytes(data, data.Length, box);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsTruncatedExtendedSizeHeader()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
byte[] box = new byte[12];
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 1);
|
|
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType);
|
|
data = InsertBytes(data, data.Length, box);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsTruncatedUuidHeader()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
byte[] box = new byte[16];
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, 24);
|
|
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid);
|
|
data = InsertBytes(data, data.Length, box);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsItemPropertiesBeforeItemInfo()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
|
|
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
|
|
int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset));
|
|
data = MoveBoxBefore(data, iprpOffset, iprpSize, iinfOffset);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyAcceptsItemLocationBeforeItemInfo()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12);
|
|
int ilocOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12);
|
|
int ilocSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ilocOffset));
|
|
data = MoveBoxBefore(data, ilocOffset, ilocSize, iinfOffset);
|
|
|
|
ImageInfo imageInfo = Image.Identify(data);
|
|
|
|
Assert.Equal(new Size(2, 3), imageInfo.Size);
|
|
}
|
|
|
|
[Fact]
|
|
public void IdentifyRejectsDuplicateUniqueMetadataBox()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12);
|
|
int pitmSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(pitmOffset));
|
|
data = InsertBytes(data, metaOffset + metaSize, data.AsSpan(pitmOffset, pitmSize));
|
|
IncrementBoxSize(data, metaOffset, pitmSize);
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(data));
|
|
}
|
|
|
|
private static byte[] CreateEncodedContainer()
|
|
{
|
|
using Image<Rgba32> image = new(2, 3);
|
|
using MemoryStream stream = new();
|
|
image.Save(stream, new HeifEncoder());
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static byte[] CreateContainerWithUnknownProperty(bool essential)
|
|
=> CreateContainerWithProperty(CreateUnknownBox(), essential);
|
|
|
|
private static byte[] CreateContainerWithProperty(ReadOnlySpan<byte> property, bool essential)
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
|
|
int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset));
|
|
int ipcoOffset = FindBoxOffset(data, Heif4CharCode.Ipco, iprpOffset + 8, iprpSize - 8);
|
|
int ipcoSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipcoOffset));
|
|
int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8);
|
|
|
|
// Insert the property before ipma so its one-based index is 2 and all parent box sizes remain explicit.
|
|
data = InsertBytes(data, ipcoOffset + ipcoSize, property);
|
|
IncrementBoxSize(data, metaOffset, property.Length);
|
|
IncrementBoxSize(data, iprpOffset, property.Length);
|
|
IncrementBoxSize(data, ipcoOffset, property.Length);
|
|
ipmaOffset += property.Length;
|
|
|
|
// The generated container has one item with one property association; append the inserted property to that entry.
|
|
int associationCountOffset = ipmaOffset + 18;
|
|
data[associationCountOffset]++;
|
|
int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset));
|
|
byte association = (byte)(2 | (essential ? 0x80 : 0));
|
|
data = InsertBytes(data, associationOffset, new byte[] { association });
|
|
IncrementBoxSize(data, metaOffset, 1);
|
|
IncrementBoxSize(data, iprpOffset, 1);
|
|
IncrementBoxSize(data, ipmaOffset, 1);
|
|
return data;
|
|
}
|
|
|
|
private static byte[] CreateContainerWithMalformedJpegMetadata()
|
|
{
|
|
byte[] data = CreateEncodedContainer();
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12);
|
|
int mediaDataOffset = FindBoxOffset(data, Heif4CharCode.Mdat, 0, data.Length);
|
|
|
|
// The generated item uses one file-relative extent. Insert the malformed JPEG application segment after its
|
|
// start-of-image marker, then update the enclosing media-data size and the exact declared extent length.
|
|
data = InsertBytes(data, mediaDataOffset + 10, MalformedJpegApp13);
|
|
IncrementBoxSize(data, mediaDataOffset, MalformedJpegApp13.Length);
|
|
uint extentLength = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(itemLocationOffset + 32));
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(itemLocationOffset + 32), extentLength + (uint)MalformedJpegApp13.Length);
|
|
return data;
|
|
}
|
|
|
|
private static byte[] CreateContainerWithDuplicatePropertyAssociation(ReadOnlySpan<byte> property, bool essential)
|
|
{
|
|
byte[] data = CreateContainerWithProperty(property, essential);
|
|
int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length);
|
|
int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset));
|
|
int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12);
|
|
int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset));
|
|
int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8);
|
|
int associationCountOffset = ipmaOffset + 18;
|
|
|
|
// Repeat the inserted property's one-based index in the existing item entry without changing box structure.
|
|
data[associationCountOffset]++;
|
|
int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset));
|
|
byte association = (byte)(2 | (essential ? 0x80 : 0));
|
|
data = InsertBytes(data, associationOffset, new byte[] { association });
|
|
IncrementBoxSize(data, metaOffset, 1);
|
|
IncrementBoxSize(data, iprpOffset, 1);
|
|
IncrementBoxSize(data, ipmaOffset, 1);
|
|
return data;
|
|
}
|
|
|
|
private static byte[] CreateUnknownBox()
|
|
=> CreateEmptyBox((Heif4CharCode)UnknownBoxType);
|
|
|
|
private static byte[] CreateEmptyBox(Heif4CharCode type)
|
|
=> CreateBox(type, []);
|
|
|
|
private static byte[] CreatePixelAspectRatioBox()
|
|
{
|
|
byte[] payload = new byte[8];
|
|
BinaryPrimitives.WriteUInt32BigEndian(payload, 2);
|
|
BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(4), 1);
|
|
return CreateBox(Heif4CharCode.Pasp, payload);
|
|
}
|
|
|
|
private static byte[] CreateRotationBox() => CreateBox(Heif4CharCode.Irot, [1]);
|
|
|
|
private static byte[] CreateBox(Heif4CharCode type, ReadOnlySpan<byte> payload)
|
|
{
|
|
byte[] box = new byte[8 + payload.Length];
|
|
BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length);
|
|
BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)type);
|
|
payload.CopyTo(box.AsSpan(8));
|
|
return box;
|
|
}
|
|
|
|
private static int FindBoxOffset(ReadOnlySpan<byte> data, Heif4CharCode type, int offset, int length)
|
|
{
|
|
int endOffset = offset + length;
|
|
while (offset < endOffset)
|
|
{
|
|
int boxSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
|
|
Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(data[(offset + 4)..]);
|
|
if (boxType == type)
|
|
{
|
|
return offset;
|
|
}
|
|
|
|
offset += boxSize;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static byte[] InsertBytes(byte[] data, int offset, ReadOnlySpan<byte> inserted)
|
|
{
|
|
byte[] result = new byte[data.Length + inserted.Length];
|
|
data.AsSpan(0, offset).CopyTo(result);
|
|
inserted.CopyTo(result.AsSpan(offset));
|
|
data.AsSpan(offset).CopyTo(result.AsSpan(offset + inserted.Length));
|
|
return result;
|
|
}
|
|
|
|
private static byte[] MoveBoxBefore(byte[] data, int boxOffset, int boxSize, int beforeOffset)
|
|
{
|
|
byte[] result = new byte[data.Length];
|
|
data.AsSpan(0, beforeOffset).CopyTo(result);
|
|
data.AsSpan(boxOffset, boxSize).CopyTo(result.AsSpan(beforeOffset));
|
|
data.AsSpan(beforeOffset, boxOffset - beforeOffset).CopyTo(result.AsSpan(beforeOffset + boxSize));
|
|
data.AsSpan(boxOffset + boxSize).CopyTo(result.AsSpan(boxOffset + boxSize));
|
|
return result;
|
|
}
|
|
|
|
private static void IncrementBoxSize(byte[] data, int offset, int increment)
|
|
{
|
|
uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset));
|
|
BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), size + (uint)increment);
|
|
}
|
|
}
|
|
|