// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests;
///
/// A test image file.
///
public class TestFormat : IImageFormatConfigurationModule, IImageFormat
{
private readonly Dictionary sampleImages = new();
// We should not change Configuration.Default in individual tests!
// Create new configuration instances with new Configuration(TestFormat.GlobalTestFormat) instead!
public static TestFormat GlobalTestFormat { get; } = new();
public TestFormat()
{
this.Encoder = new TestEncoder(this);
this.Decoder = new TestDecoder(this);
}
public List DecodeCalls { get; } = new();
public TestEncoder Encoder { get; }
public TestDecoder Decoder { get; }
private readonly byte[] header = Guid.NewGuid().ToByteArray();
public MemoryStream CreateStream(byte[] marker = null)
{
var ms = new MemoryStream();
byte[] data = this.header;
ms.Write(data, 0, data.Length);
if (marker != null)
{
ms.Write(marker, 0, marker.Length);
}
ms.Position = 0;
return ms;
}
public Stream CreateAsyncSemaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512)
{
byte[] buffer = new byte[size];
this.header.CopyTo(buffer, 0);
var semaphoreStream = new SemaphoreReadMemoryStream(buffer, waitAfterPosition, notifyWaitPositionReachedSemaphore, continueSemaphore);
return seeakable ? semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false);
}
public void VerifySpecificDecodeCall(byte[] marker, Configuration config)
where TPixel : unmanaged, IPixel
{
DecodeOperation[] discovered = this.DecodeCalls.Where(x => x.IsMatch(marker, config, typeof(TPixel))).ToArray();
Assert.True(discovered.Length > 0, "No calls to decode on this format with the provided options happened");
foreach (DecodeOperation d in discovered)
{
this.DecodeCalls.Remove(d);
}
}
public void VerifyAgnosticDecodeCall(byte[] marker, Configuration config)
{
DecodeOperation[] discovered = this.DecodeCalls.Where(x => x.IsMatch(marker, config, typeof(TestPixelForAgnosticDecode))).ToArray();
Assert.True(discovered.Length > 0, "No calls to decode on this format with the provided options happened");
foreach (DecodeOperation d in discovered)
{
this.DecodeCalls.Remove(d);
}
}
public Image Sample()
where TPixel : unmanaged, IPixel
{
lock (this.sampleImages)
{
if (!this.sampleImages.ContainsKey(typeof(TPixel)))
{
this.sampleImages.Add(typeof(TPixel), new Image(1, 1));
}
return (Image)this.sampleImages[typeof(TPixel)];
}
}
public Image SampleAgnostic() => this.Sample();
public string MimeType => "img/test";
public string Extension => "test_ext";
public IEnumerable SupportedExtensions => new[] { "test_ext" };
public int HeaderSize => this.header.Length;
public string Name => this.Extension;
public string DefaultMimeType => this.MimeType;
public IEnumerable MimeTypes => new[] { this.MimeType };
public IEnumerable FileExtensions => this.SupportedExtensions;
public bool IsSupportedFileFormat(ReadOnlySpan fileHeader)
{
if (fileHeader.Length < this.header.Length)
{
return false;
}
for (int i = 0; i < this.header.Length; i++)
{
if (fileHeader[i] != this.header[i])
{
return false;
}
}
return true;
}
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.AddImageFormatDetector(new TestHeader(this));
configuration.ImageFormatsManager.SetEncoder(this, new TestEncoder(this));
configuration.ImageFormatsManager.SetDecoder(this, new TestDecoder(this));
}
public struct DecodeOperation
{
public byte[] Marker;
internal Configuration Config;
public Type PixelType;
public bool IsMatch(byte[] testMarker, Configuration config, Type pixelType)
{
if (this.Config != config || this.PixelType != pixelType)
{
return false;
}
if (testMarker.Length != this.Marker.Length)
{
return false;
}
for (int i = 0; i < this.Marker.Length; i++)
{
if (testMarker[i] != this.Marker[i])
{
return false;
}
}
return true;
}
}
public class TestHeader : IImageFormatDetector
{
private readonly TestFormat testFormat;
public int HeaderSize => this.testFormat.HeaderSize;
public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.testFormat.IsSupportedFileFormat(header) ? this.testFormat : null;
return format != null;
}
public TestHeader(TestFormat testFormat) => this.testFormat = testFormat;
}
public class TestDecoder : SpecializedImageDecoder
{
private readonly TestFormat testFormat;
public TestDecoder(TestFormat testFormat) => this.testFormat = testFormat;
public IEnumerable MimeTypes => new[] { this.testFormat.MimeType };
public IEnumerable FileExtensions => this.testFormat.SupportedExtensions;
public int HeaderSize => this.testFormat.HeaderSize;
public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header);
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Image image =
this.Decode(this.CreateDefaultSpecializedOptions(options), stream, cancellationToken);
ImageFrameCollection m = image.Frames;
return new(image.PixelType, image.Size, image.Metadata, new List(image.Frames.Select(x => x.Metadata)));
}
protected override TestDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options)
=> new() { GeneralOptions = options };
protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Configuration configuration = options.GeneralOptions.Configuration;
using MemoryStream ms = new();
stream.CopyTo(ms, configuration.StreamProcessingBufferSize);
byte[] marker = ms.ToArray().Skip(this.testFormat.header.Length).ToArray();
this.testFormat.DecodeCalls.Add(new DecodeOperation
{
Marker = marker,
Config = configuration,
PixelType = typeof(TPixel)
});
// TODO record this happened so we can verify it.
return this.testFormat.Sample();
}
protected override Image Decode(TestDecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode(options, stream, cancellationToken);
}
public class TestDecoderOptions : ISpecializedDecoderOptions
{
public DecoderOptions GeneralOptions { get; init; } = DecoderOptions.Default;
}
public class TestEncoder : IImageEncoder
{
private readonly TestFormat testFormat;
public TestEncoder(TestFormat testFormat) => this.testFormat = testFormat;
public IEnumerable MimeTypes => new[] { this.testFormat.MimeType };
public IEnumerable FileExtensions => this.testFormat.SupportedExtensions;
public bool SkipMetadata { get; init; }
public void Encode(Image image, Stream stream)
where TPixel : unmanaged, IPixel
{
// TODO record this happened so we can verify it.
}
public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
=> Task.CompletedTask; // TODO record this happened so we can verify it.
}
public struct TestPixelForAgnosticDecode : IPixel
{
public readonly Rgba32 ToRgba32() => default;
public readonly Vector4 ToScaledVector4() => default;
public readonly Vector4 ToVector4() => default;
public static PixelTypeInfo GetPixelTypeInfo()
=> PixelTypeInfo.Create(
PixelComponentInfo.Create(2, 8, 8),
PixelColorType.Red | PixelColorType.Green,
PixelAlphaRepresentation.None);
public static PixelOperations CreatePixelOperations() => new();
public static TestPixelForAgnosticDecode FromScaledVector4(Vector4 vector) => default;
public static TestPixelForAgnosticDecode FromVector4(Vector4 vector) => default;
public static TestPixelForAgnosticDecode FromAbgr32(Abgr32 source) => default;
public static TestPixelForAgnosticDecode FromArgb32(Argb32 source) => default;
public static TestPixelForAgnosticDecode FromBgra5551(Bgra5551 source) => default;
public static TestPixelForAgnosticDecode FromBgr24(Bgr24 source) => default;
public static TestPixelForAgnosticDecode FromBgra32(Bgra32 source) => default;
public static TestPixelForAgnosticDecode FromL8(L8 source) => default;
public static TestPixelForAgnosticDecode FromL16(L16 source) => default;
public static TestPixelForAgnosticDecode FromLa16(La16 source) => default;
public static TestPixelForAgnosticDecode FromLa32(La32 source) => default;
public static TestPixelForAgnosticDecode FromRgb24(Rgb24 source) => default;
public static TestPixelForAgnosticDecode FromRgba32(Rgba32 source) => default;
public static TestPixelForAgnosticDecode FromRgb48(Rgb48 source) => default;
public static TestPixelForAgnosticDecode FromRgba64(Rgba64 source) => default;
public bool Equals(TestPixelForAgnosticDecode other) => false;
}
}