// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; namespace SixLabors.ImageSharp.Tests { /// /// A test image file. /// public class TestFormat : IConfigurationModule, IImageFormat { private readonly Dictionary sampleImages = new Dictionary(); // 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 TestFormat(); public TestFormat() { this.Encoder = new TestEncoder(this); this.Decoder = new TestDecoder(this); } public List DecodeCalls { get; } = new List(); public TestEncoder Encoder { get; } public TestDecoder Decoder { get; } private 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 CreateAsyncSamaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512) { var buffer = new byte[size]; this.header.CopyTo(buffer, 0); var semaphoreStream = new SemaphoreReadMemoryStream(buffer, waitAfterPosition, notifyWaitPositionReachedSemaphore, continueSemaphore); return seeakable ? (Stream)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.Any(), "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.Any(), "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 header) { if (header.Length < this.header.Length) { return false; } for (int i = 0; i < this.header.Length; i++) { if (header[i] != this.header[i]) { return false; } } return true; } public void Configure(Configuration host) { host.ImageFormatsManager.AddImageFormatDetector(new TestHeader(this)); host.ImageFormatsManager.SetEncoder(this, new TestEncoder(this)); host.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 TestFormat testFormat; public int HeaderSize => this.testFormat.HeaderSize; public IImageFormat DetectFormat(ReadOnlySpan header) { if (this.testFormat.IsSupportedFileFormat(header)) { return this.testFormat; } return null; } public TestHeader(TestFormat testFormat) { this.testFormat = testFormat; } } public class TestDecoder : IImageDecoder, IImageInfoDetector { private 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 Image Decode(Configuration config, Stream stream) where TPixel : unmanaged, IPixel => this.DecodeImpl(config, stream, default).GetAwaiter().GetResult(); public Task> DecodeAsync(Configuration config, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel => this.DecodeImpl(config, stream, cancellationToken); private async Task> DecodeImpl(Configuration config, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var ms = new MemoryStream(); await stream.CopyToAsync(ms, config.StreamProcessingBufferSize, cancellationToken); var marker = ms.ToArray().Skip(this.testFormat.header.Length).ToArray(); this.testFormat.DecodeCalls.Add(new DecodeOperation { Marker = marker, Config = config, PixelType = typeof(TPixel) }); // TODO record this happened so we can verify it. return this.testFormat.Sample(); } public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header); public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) => await this.DecodeAsync(configuration, stream, cancellationToken); public IImageInfo Identify(Configuration configuration, Stream stream) => this.IdentifyAsync(configuration, stream, default).GetAwaiter().GetResult(); public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) => await this.DecodeImpl(configuration, stream, cancellationToken); } public class TestEncoder : ImageSharp.Formats.IImageEncoder { private TestFormat testFormat; public TestEncoder(TestFormat testFormat) { this.testFormat = testFormat; } public IEnumerable MimeTypes => new[] { this.testFormat.MimeType }; public IEnumerable FileExtensions => this.testFormat.SupportedExtensions; 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 { // TODO record this happened so we can verify it. return Task.CompletedTask; } } public struct TestPixelForAgnosticDecode : IPixel { public PixelOperations CreatePixelOperations() => new PixelOperations(); public void FromScaledVector4(Vector4 vector) { } public Vector4 ToScaledVector4() => default; public void FromVector4(Vector4 vector) { } public Vector4 ToVector4() => default; public void FromArgb32(Argb32 source) { } public void FromBgra5551(Bgra5551 source) { } public void FromBgr24(Bgr24 source) { } public void FromBgra32(Bgra32 source) { } public void FromL8(L8 source) { } public void FromL16(L16 source) { } public void FromLa16(La16 source) { } public void FromLa32(La32 source) { } public void FromRgb24(Rgb24 source) { } public void FromRgba32(Rgba32 source) { } public void ToRgba32(ref Rgba32 dest) { } public void FromRgb48(Rgb48 source) { } public void FromRgba64(Rgba64 source) { } public bool Equals(TestPixelForAgnosticDecode other) => false; } } }