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.
411 lines
17 KiB
411 lines
17 KiB
// Copyright (c) Six Labors.
|
|
// Licensed under the Six Labors Split License.
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using SixLabors.ImageSharp.Formats;
|
|
using SixLabors.ImageSharp.Formats.Webp;
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
|
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Tests.TestUtilities;
|
|
|
|
// ReSharper disable InconsistentNaming
|
|
namespace SixLabors.ImageSharp.Tests.Formats.Webp;
|
|
|
|
[Trait("Format", "Webp")]
|
|
public class WebpMetaDataTests
|
|
{
|
|
public static IEnumerable<object[]> IccMetadataOptions()
|
|
{
|
|
foreach (SegmentIntegrityHandling integrity in new[] { SegmentIntegrityHandling.Strict, SegmentIntegrityHandling.IgnoreAncillary, SegmentIntegrityHandling.IgnoreImageData })
|
|
{
|
|
foreach (bool skipMetadata in new[] { false, true })
|
|
{
|
|
foreach (bool animated in new[] { false, true })
|
|
{
|
|
yield return new object[] { integrity, skipMetadata, animated };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static IEnumerable<object[]> TruncatedMetadataOptions()
|
|
{
|
|
foreach (string chunkType in new[] { "EXIF", "XMP " })
|
|
{
|
|
foreach (uint length in new[] { 0x40000000U, 0xFFFFFFFEU, uint.MaxValue })
|
|
{
|
|
foreach (SegmentIntegrityHandling integrity in new[] { SegmentIntegrityHandling.Strict, SegmentIntegrityHandling.IgnoreAncillary, SegmentIntegrityHandling.IgnoreImageData })
|
|
{
|
|
yield return new object[] { chunkType, length, integrity, false };
|
|
yield return new object[] { chunkType, length, integrity, true };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)]
|
|
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)]
|
|
public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLossyImage<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
DecoderOptions options = new() { SkipMetadata = ignoreMetadata };
|
|
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance, options);
|
|
if (ignoreMetadata)
|
|
{
|
|
Assert.Null(image.Metadata.ExifProfile);
|
|
}
|
|
else
|
|
{
|
|
ExifProfile exifProfile = image.Metadata.ExifProfile;
|
|
Assert.NotNull(exifProfile);
|
|
Assert.NotEmpty(exifProfile.Values);
|
|
Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Software) && m.GetValue().Equals("GIMP 2.10.2"));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, false)]
|
|
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32, true)]
|
|
public void IgnoreMetadata_ControlsWhetherExifIsParsed_WithLosslessImage<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
DecoderOptions options = new() { SkipMetadata = ignoreMetadata };
|
|
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance, options);
|
|
if (ignoreMetadata)
|
|
{
|
|
Assert.Null(image.Metadata.ExifProfile);
|
|
}
|
|
else
|
|
{
|
|
ExifProfile exifProfile = image.Metadata.ExifProfile;
|
|
Assert.NotNull(exifProfile);
|
|
Assert.NotEmpty(exifProfile.Values);
|
|
Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Make) && m.GetValue().Equals("Canon"));
|
|
Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Model) && m.GetValue().Equals("Canon PowerShot S40"));
|
|
Assert.Contains(exifProfile.Values, m => m.Tag.Equals(ExifTag.Software) && m.GetValue().Equals("GIMP 2.10.2"));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, false)]
|
|
[WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, true)]
|
|
[WithFile(TestImages.Webp.Lossless.WithIccp, PixelTypes.Rgba32, false)]
|
|
[WithFile(TestImages.Webp.Lossless.WithIccp, PixelTypes.Rgba32, true)]
|
|
public void IgnoreMetadata_ControlsWhetherIccpIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
DecoderOptions options = new() { SkipMetadata = ignoreMetadata };
|
|
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance, options);
|
|
if (ignoreMetadata)
|
|
{
|
|
Assert.Null(image.Metadata.IccProfile);
|
|
}
|
|
else
|
|
{
|
|
Assert.NotNull(image.Metadata.IccProfile);
|
|
Assert.NotEmpty(image.Metadata.IccProfile.Entries);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, false)]
|
|
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, true)]
|
|
public async Task IgnoreMetadata_ControlsWhetherXmpIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
DecoderOptions options = new() { SkipMetadata = ignoreMetadata };
|
|
using Image<TPixel> image = await provider.GetImageAsync(WebpDecoder.Instance, options);
|
|
if (ignoreMetadata)
|
|
{
|
|
Assert.Null(image.Metadata.XmpProfile);
|
|
}
|
|
else
|
|
{
|
|
Assert.NotNull(image.Metadata.XmpProfile);
|
|
Assert.NotEmpty(image.Metadata.XmpProfile.Data);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(WebpFileFormatType.Lossy)]
|
|
[InlineData(WebpFileFormatType.Lossless)]
|
|
public void Encode_WritesExifWithPadding(WebpFileFormatType fileFormatType)
|
|
{
|
|
// arrange
|
|
using Image<Rgba32> input = new(25, 25);
|
|
using MemoryStream memoryStream = new();
|
|
ExifProfile expectedExif = new();
|
|
string expectedSoftware = "ImageSharp";
|
|
expectedExif.SetValue(ExifTag.Software, expectedSoftware);
|
|
input.Metadata.ExifProfile = expectedExif;
|
|
|
|
// act
|
|
input.Save(memoryStream, new WebpEncoder() { FileFormat = fileFormatType });
|
|
memoryStream.Position = 0;
|
|
|
|
// assert
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(memoryStream);
|
|
ExifProfile actualExif = image.Metadata.ExifProfile;
|
|
Assert.NotNull(actualExif);
|
|
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count);
|
|
Assert.Equal(expectedSoftware, actualExif.GetValue(ExifTag.Software).Value);
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32)]
|
|
public void EncodeLossyWebp_PreservesExif<TPixel>(TestImageProvider<TPixel> provider)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
// arrange
|
|
using Image<TPixel> input = provider.GetImage(WebpDecoder.Instance);
|
|
using MemoryStream memoryStream = new();
|
|
ExifProfile expectedExif = input.Metadata.ExifProfile;
|
|
|
|
// act
|
|
input.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy });
|
|
memoryStream.Position = 0;
|
|
|
|
// assert
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(memoryStream);
|
|
ExifProfile actualExif = image.Metadata.ExifProfile;
|
|
Assert.NotNull(actualExif);
|
|
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossless.WithExif, PixelTypes.Rgba32)]
|
|
public void EncodeLosslessWebp_PreservesExif<TPixel>(TestImageProvider<TPixel> provider)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
// arrange
|
|
using Image<TPixel> input = provider.GetImage(WebpDecoder.Instance);
|
|
using MemoryStream memoryStream = new();
|
|
ExifProfile expectedExif = input.Metadata.ExifProfile;
|
|
|
|
// act
|
|
input.Save(memoryStream, new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless });
|
|
memoryStream.Position = 0;
|
|
|
|
// assert
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(memoryStream);
|
|
ExifProfile actualExif = image.Metadata.ExifProfile;
|
|
Assert.NotNull(actualExif);
|
|
Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count);
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]
|
|
[WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossy)]
|
|
public void Encode_PreservesColorProfile<TPixel>(TestImageProvider<TPixel> provider, WebpFileFormatType fileFormat)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
using Image<TPixel> input = provider.GetImage(WebpDecoder.Instance);
|
|
ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile;
|
|
byte[] expectedProfileBytes = expectedProfile.ToByteArray();
|
|
|
|
using MemoryStream memStream = new();
|
|
input.Save(memStream, new WebpEncoder()
|
|
{
|
|
FileFormat = fileFormat
|
|
});
|
|
|
|
memStream.Position = 0;
|
|
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
|
|
ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile;
|
|
byte[] actualProfileBytes = actualProfile.ToByteArray();
|
|
|
|
Assert.NotNull(actualProfile);
|
|
Assert.Equal(expectedProfileBytes, actualProfileBytes);
|
|
}
|
|
|
|
[Theory]
|
|
[WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)]
|
|
public void WebpDecoder_IgnoresInvalidExifChunk<TPixel>(TestImageProvider<TPixel> provider)
|
|
where TPixel : unmanaged, IPixel<TPixel>
|
|
{
|
|
Exception ex = Record.Exception(() =>
|
|
{
|
|
using Image<TPixel> image = provider.GetImage();
|
|
});
|
|
Assert.Null(ex);
|
|
}
|
|
|
|
[Fact]
|
|
public void Identify_InvalidExifChunk_IgnoresNonCriticalErrorsByDefault()
|
|
{
|
|
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
|
|
ImageInfo info = Image.Identify(stream);
|
|
Assert.True(info.Width > 0);
|
|
Assert.True(info.Height > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Identify_InvalidExifChunk_ThrowsWithStrict()
|
|
{
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, stream));
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_InvalidExifChunk_ThrowsWithStrict()
|
|
{
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
using MemoryStream stream = new(TestFile.Create(TestImages.Webp.Lossy.WithExifNotEnoughData).Bytes, false);
|
|
Assert.Throws<InvalidImageContentException>(() =>
|
|
{
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, stream);
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("ICCP", 0xFFFFFFFEU)]
|
|
[InlineData("EXIF", 0xFFFFFFFEU)]
|
|
[InlineData("XMP ", 0xFFFFFFFEU)]
|
|
[InlineData("ICCP", uint.MaxValue)]
|
|
[InlineData("EXIF", uint.MaxValue)]
|
|
[InlineData("XMP ", uint.MaxValue)]
|
|
public void Decode_WithOversizedMetadataChunk_ThrowsInvalidImageContentException(string chunkType, uint length)
|
|
{
|
|
byte[] payload = Convert.FromHexString(
|
|
"524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304");
|
|
Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(34), length);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("ICCP")]
|
|
[InlineData("EXIF")]
|
|
[InlineData("XMP ")]
|
|
public void Decode_WithMetadataChunkLargerThanRemainingData_ThrowsInStrictMode(string chunkType)
|
|
{
|
|
byte[] payload = Convert.FromHexString(
|
|
"524946460000000057454250565038580A00000020000000010000010000494343500000004000000000");
|
|
Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4));
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(TruncatedMetadataOptions))]
|
|
public void Decode_TruncatedTrailingMetadata_RespectsOptions(string chunkType, uint length, SegmentIntegrityHandling integrity, bool skipMetadata)
|
|
{
|
|
byte[] payload = CreateWebpWithMetadata(chunkType, length, false, false);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata };
|
|
|
|
if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata)
|
|
{
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
|
|
}
|
|
else
|
|
{
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, payload);
|
|
Assert.Equal(new Size(2, 2), image.Size);
|
|
for (int y = 0; y < image.Height; y++)
|
|
{
|
|
for (int x = 0; x < image.Width; x++)
|
|
{
|
|
Assert.Equal(new Rgba32(17, 34, 51), image[x, y]);
|
|
}
|
|
}
|
|
|
|
Assert.Null(image.Metadata.ExifProfile);
|
|
Assert.Null(image.Metadata.XmpProfile);
|
|
|
|
ImageInfo info = Image.Identify(options, payload);
|
|
Assert.Equal(image.Size, info.Size);
|
|
Assert.Null(info.Metadata.ExifProfile);
|
|
Assert.Null(info.Metadata.XmpProfile);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(IccMetadataOptions))]
|
|
public void Decode_InvalidIccPayload_RespectsOptionsAndReadsFollowingImage(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated)
|
|
{
|
|
byte[] payload = CreateWebpWithMetadata("ICCP", 4, true, animated);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata };
|
|
|
|
if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata)
|
|
{
|
|
Assert.Throws<InvalidIccProfileException>(() => Image.Load(options, payload));
|
|
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, payload));
|
|
}
|
|
else
|
|
{
|
|
using Image<Rgba32> image = Image.Load<Rgba32>(options, payload);
|
|
Assert.Equal(new Size(2, 2), image.Size);
|
|
Assert.Equal(animated ? 2 : 1, image.Frames.Count);
|
|
Assert.Equal(new Rgba32(17, 34, 51), image[0, 0]);
|
|
Assert.Null(image.Metadata.IccProfile);
|
|
|
|
ImageInfo info = Image.Identify(options, payload);
|
|
Assert.Equal(image.Size, info.Size);
|
|
Assert.Null(info.Metadata.IccProfile);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(IccMetadataOptions))]
|
|
public void Decode_TruncatedIccFraming_RemainsFatal(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated)
|
|
{
|
|
byte[] payload = CreateWebpWithMetadata("ICCP", 0x40000000, true, animated);
|
|
DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata };
|
|
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Load(options, payload));
|
|
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Places a metadata declaration around a complete lossless image to test recovery independently of pixel truncation.
|
|
/// </summary>
|
|
private static byte[] CreateWebpWithMetadata(string chunkType, uint length, bool beforeImage, bool animated)
|
|
{
|
|
byte[] header = Convert.FromHexString(
|
|
"524946460000000057454250565038580A00000020000000010000010000494343500000004000000000");
|
|
header[20] = chunkType switch { "ICCP" => 0x20, "EXIF" => 0x08, _ => 0x04 };
|
|
Encoding.ASCII.GetBytes(chunkType, header.AsSpan(30, 4));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(34), length);
|
|
|
|
using Image<Rgba32> source = new(2, 2, new Rgba32(17, 34, 51));
|
|
if (animated)
|
|
{
|
|
header[20] |= 0x02;
|
|
using Image<Rgba32> secondFrame = new(2, 2, new Rgba32(51, 34, 17));
|
|
source.Frames.AddFrame(secondFrame.Frames.RootFrame);
|
|
}
|
|
|
|
using MemoryStream encoded = new();
|
|
source.Save(encoded, new WebpEncoder { FileFormat = WebpFileFormatType.Lossless });
|
|
byte[] imageData = encoded.ToArray();
|
|
using MemoryStream combined = new();
|
|
combined.Write(header.AsSpan(0, 30));
|
|
if (beforeImage)
|
|
{
|
|
combined.Write(header.AsSpan(30));
|
|
}
|
|
|
|
// Replace the encoder's extended header when present, keeping its complete
|
|
// image or animation chunks and the deliberately chosen metadata declaration.
|
|
int imageChunkOffset = imageData.AsSpan(12, 4).SequenceEqual("VP8X"u8) ? 30 : 12;
|
|
combined.Write(imageData.AsSpan(imageChunkOffset));
|
|
if (!beforeImage)
|
|
{
|
|
combined.Write(header.AsSpan(30));
|
|
}
|
|
|
|
byte[] payload = combined.ToArray();
|
|
BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)payload.Length - 8);
|
|
return payload;
|
|
}
|
|
}
|
|
|