diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index d4d055823..39fcef9c4 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; @@ -212,6 +213,7 @@ public sealed class Configuration /// . /// . /// . + /// . /// /// The default configuration of . internal static Configuration CreateDefaultInstance() => new( @@ -222,5 +224,6 @@ public sealed class Configuration new PbmConfigurationModule(), new TgaConfigurationModule(), new TiffConfigurationModule(), - new WebpConfigurationModule()); + new WebpConfigurationModule(), + new QoiConfigurationModule()); } diff --git a/src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs b/src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs new file mode 100644 index 000000000..ab0d0aa2c --- /dev/null +++ b/src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Qoi; + +/// +/// Registers the image encoders, decoders and mime type detectors for the qoi format. +/// +public sealed class QoiConfigurationModule : IImageFormatConfigurationModule +{ + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(QoiFormat.Instance, QoiDecoder.Instance); + //configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder()); + configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector()); + } +} diff --git a/src/ImageSharp/Formats/Qoi/QoiConstants.cs b/src/ImageSharp/Formats/Qoi/QoiConstants.cs index acebb13a7..62508d3be 100644 --- a/src/ImageSharp/Formats/Qoi/QoiConstants.cs +++ b/src/ImageSharp/Formats/Qoi/QoiConstants.cs @@ -7,19 +7,21 @@ namespace SixLabors.ImageSharp.Formats.Qoi; internal static class QoiConstants { + private static readonly byte[] SMagic = Encoding.UTF8.GetBytes("qoif"); + /// /// Gets the bytes that indicates the image is QOI /// - public static ReadOnlySpan Magic => Encoding.UTF8.GetBytes("qoif"); + public static ReadOnlySpan Magic => SMagic; /// /// The list of mimetypes that equate to a QOI. /// See https://github.com/phoboslab/qoi/issues/167 /// - public static readonly IEnumerable MimeTypes = new[] { "image/qoi", "image/x-qoi", "image/vnd.qoi" }; + public static readonly string[] MimeTypes = { "image/qoi", "image/x-qoi", "image/vnd.qoi" }; /// /// The list of file extensions that equate to a QOI. /// - public static readonly IEnumerable FileExtensions = new[] { "qoi" }; + public static readonly string[] FileExtensions = { "qoi" }; } diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs index 1594165ba..5cec6c244 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -48,47 +50,67 @@ internal class QoiDecoderCore : IImageDecoderInternals public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { ImageMetadata metadata = new(); + QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); - byte[] magicBytes = new byte[4], widthBytes = new byte[4], heightBytes = new byte[4]; + Span magicBytes = stackalloc byte[4]; + Span widthBytes = stackalloc byte[4]; + Span heightBytes = stackalloc byte[4]; // Read magic bytes int read = stream.Read(magicBytes); if (read != 4 || !magicBytes.SequenceEqual(QoiConstants.Magic.ToArray())) { - throw new InvalidImageContentException("The image is not a QOI image"); + ThrowInvalidImageContentException(); } // If it's a qoi image, read the rest of properties read = stream.Read(widthBytes); if (read != 4) { - throw new InvalidImageContentException("The image is not a QOI image"); + ThrowInvalidImageContentException(); } read = stream.Read(heightBytes); if (read != 4) { - throw new InvalidImageContentException("The image is not a QOI image"); + ThrowInvalidImageContentException(); } - widthBytes = widthBytes.Reverse().ToArray(); - heightBytes = heightBytes.Reverse().ToArray(); - Size size = new((int)BitConverter.ToUInt32(widthBytes), (int)BitConverter.ToUInt32(heightBytes)); + // These numbers are in Big Endian so we have to reverse them to get the real number + uint width = BinaryPrimitives.ReadUInt32BigEndian(widthBytes), + height = BinaryPrimitives.ReadUInt32BigEndian(heightBytes); + if (width == 0 || height == 0) + { + throw new InvalidImageContentException( + $"The image has an invalid size: width = {width}, height = {height}"); + } + + qoiMetadata.Width = width; + qoiMetadata.Height = height; + + Size size = new((int)width, (int)height); int channels = stream.ReadByte(); - if (channels == -1) + if (channels is -1 or (not 3 and not 4)) { - throw new InvalidImageContentException("The image is not a QOI image"); + ThrowInvalidImageContentException(); } PixelTypeInfo pixelType = new(8 * channels); + qoiMetadata.Channels = (QoiChannels)channels; int colorSpace = stream.ReadByte(); - if (colorSpace == -1) + if (colorSpace is -1 or (not 0 and not 1)) { - throw new InvalidImageContentException("The image is not a QOI image"); + ThrowInvalidImageContentException(); } + qoiMetadata.ColorSpace = (QoiColorSpace)colorSpace; + return new ImageInfo(pixelType, size, metadata); } + + [DoesNotReturn] + private static void ThrowInvalidImageContentException() + => throw new InvalidImageContentException("The image is not a valid QOI image."); } diff --git a/src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs b/src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs new file mode 100644 index 000000000..720e308b3 --- /dev/null +++ b/src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Png; + +namespace SixLabors.ImageSharp.Formats.Qoi; + +/// +/// Detects qoi file headers +/// +public class QoiImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => 14; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null; + return format != null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + => header.Length == this.HeaderSize && header[..4] == QoiConstants.Magic; +} diff --git a/tests/ImageSharp.Benchmarks/Program.cs b/tests/ImageSharp.Benchmarks/Program.cs index 55cff86d5..75e5a8233 100644 --- a/tests/ImageSharp.Benchmarks/Program.cs +++ b/tests/ImageSharp.Benchmarks/Program.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; namespace SixLabors.ImageSharp.Benchmarks; @@ -16,5 +15,5 @@ public class Program /// public static void Main(string[] args) => BenchmarkSwitcher .FromAssembly(typeof(Program).Assembly) - .Run(args, new DebugInProcessConfig()); + .Run(args); } diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs new file mode 100644 index 000000000..9a5e0bfaf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Formats.Qoi; + +[Trait("Format", "Qoi")] +[ValidateDisposedMemoryAllocations] +public class QoiDecoderTests +{ + [Theory] + [InlineData(TestImages.Qoi.Dice)] + [InlineData(TestImages.Qoi.EdgeCase)] + [InlineData(TestImages.Qoi.Kodim10)] + [InlineData(TestImages.Qoi.Kodim23)] + [InlineData(TestImages.Qoi.QoiLogo)] + [InlineData(TestImages.Qoi.TestCard)] + [InlineData(TestImages.Qoi.TestCardRGBA)] + [InlineData(TestImages.Qoi.Wikipedia008)] + public void Identify(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + } +} diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index a6197b600..48daa5ead 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -73,5 +73,9 @@ + + + +