diff --git a/.gitattributes b/.gitattributes
index ff4ec94087..3647a7063d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -118,6 +118,7 @@
*.bmp filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
+*.qoi filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
diff --git a/ImageSharp.sln b/ImageSharp.sln
index 6eab8da752..3cd03b31bc 100644
--- a/ImageSharp.sln
+++ b/ImageSharp.sln
@@ -646,6 +646,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tga", "Tga", "{5DFC394F-136
tests\Images\Input\Tga\targa_8bit_rle.tga = tests\Images\Input\Tga\targa_8bit_rle.tga
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-4935-41CD-BA85-CF11BFF55A45}"
+ ProjectSection(SolutionItems) = preProject
+ tests\Images\Input\Qoi\dice.qoi = tests\Images\Input\Qoi\dice.qoi
+ tests\Images\Input\Qoi\edgecase.qoi = tests\Images\Input\Qoi\edgecase.qoi
+ tests\Images\Input\Qoi\kodim10.qoi = tests\Images\Input\Qoi\kodim10.qoi
+ tests\Images\Input\Qoi\kodim23.qoi = tests\Images\Input\Qoi\kodim23.qoi
+ tests\Images\Input\Qoi\qoi_logo.qoi = tests\Images\Input\Qoi\qoi_logo.qoi
+ tests\Images\Input\Qoi\testcard.qoi = tests\Images\Input\Qoi\testcard.qoi
+ tests\Images\Input\Qoi\testcard_rgba.qoi = tests\Images\Input\Qoi\testcard_rgba.qoi
+ tests\Images\Input\Qoi\wikipedia_008.qoi = tests\Images\Input\Qoi\wikipedia_008.qoi
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -698,6 +710,7 @@ Global
{FC527290-2F22-432C-B77B-6E815726B02C} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
+ {E801B508-4935-41CD-BA85-CF11BFF55A45} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}
diff --git a/shared-infrastructure b/shared-infrastructure
index 9a6cf00d9a..353b9afe32 160000
--- a/shared-infrastructure
+++ b/shared-infrastructure
@@ -1 +1 @@
-Subproject commit 9a6cf00d9a3d482bb08211dd8309f4724a2735cb
+Subproject commit 353b9afe32a8000410312d17263407cd7bb82d19
diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs
index 5af5db3cda..aba3c0abdc 100644
--- a/src/ImageSharp/Common/Helpers/Numerics.cs
+++ b/src/ImageSharp/Common/Helpers/Numerics.cs
@@ -73,6 +73,30 @@ public static int LeastCommonMultiple(int a, int b)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo8(nint x) => x & 7;
+ ///
+ /// Calculates % 64
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int Modulo64(int x) => x & 63;
+
+ ///
+ /// Calculates % 64
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static nint Modulo64(nint x) => x & 63;
+
+ ///
+ /// Calculates % 256
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int Modulo256(int x) => x & 255;
+
+ ///
+ /// Calculates % 256
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static nint Modulo256(nint x) => x & 255;
+
///
/// Fast (x mod m) calculator, with the restriction that
/// should be power of 2.
diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs
index d4d0558238..39fcef9c40 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -8,6 +8,7 @@
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 void Configure(IImageFormatConfigurationModule configuration)
/// .
/// .
/// .
+ /// .
///
/// The default configuration of .
internal static Configuration CreateDefaultInstance() => new(
@@ -222,5 +224,6 @@ public void Configure(IImageFormatConfigurationModule configuration)
new PbmConfigurationModule(),
new TgaConfigurationModule(),
new TiffConfigurationModule(),
- new WebpConfigurationModule());
+ new WebpConfigurationModule(),
+ new QoiConfigurationModule());
}
diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs
index 71458333f0..30f576e5c4 100644
--- a/src/ImageSharp/Formats/ImageExtensions.Save.cs
+++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs
@@ -9,9 +9,10 @@
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.Webp;
using SixLabors.ImageSharp.Formats.Tiff;
+using SixLabors.ImageSharp.Formats.Webp;
namespace SixLabors.ImageSharp;
@@ -531,47 +532,47 @@ public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder e
cancellationToken);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The file path to save the image to.
/// Thrown if the path is null.
- public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, default);
+ public static void SaveAsQoi(this Image source, string path) => SaveAsQoi(source, path, default);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The file path to save the image to.
/// Thrown if the path is null.
/// A representing the asynchronous operation.
- public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, default);
+ public static Task SaveAsQoiAsync(this Image source, string path) => SaveAsQoiAsync(source, path, default);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The file path to save the image to.
/// The token to monitor for cancellation requests.
/// Thrown if the path is null.
/// A representing the asynchronous operation.
- public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken)
- => SaveAsTgaAsync(source, path, default, cancellationToken);
+ public static Task SaveAsQoiAsync(this Image source, string path, CancellationToken cancellationToken)
+ => SaveAsQoiAsync(source, path, default, cancellationToken);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The file path to save the image to.
/// The encoder to save the image with.
/// Thrown if the path is null.
- public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) =>
+ public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) =>
source.Save(
path,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance));
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The file path to save the image to.
@@ -579,46 +580,46 @@ public static void SaveAsTga(this Image source, string path, TgaEncoder encoder)
/// The token to monitor for cancellation requests.
/// Thrown if the path is null.
/// A representing the asynchronous operation.
- public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default)
+ public static Task SaveAsQoiAsync(this Image source, string path, QoiEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
path,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance),
cancellationToken);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The stream to save the image to.
/// Thrown if the stream is null.
- public static void SaveAsTga(this Image source, Stream stream)
- => SaveAsTga(source, stream, default);
+ public static void SaveAsQoi(this Image source, Stream stream)
+ => SaveAsQoi(source, stream, default);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The stream to save the image to.
/// The token to monitor for cancellation requests.
/// Thrown if the stream is null.
/// A representing the asynchronous operation.
- public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
- => SaveAsTgaAsync(source, stream, default, cancellationToken);
+ public static Task SaveAsQoiAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
+ => SaveAsQoiAsync(source, stream, default, cancellationToken);
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The stream to save the image to.
/// The encoder to save the image with.
/// Thrown if the stream is null.
- public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
+ public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder)
=> source.Save(
stream,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance));
///
- /// Saves the image to the given stream with the Tga format.
+ /// Saves the image to the given stream with the Qoi format.
///
/// The image this method extends.
/// The stream to save the image to.
@@ -626,54 +627,54 @@ public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encode
/// The token to monitor for cancellation requests.
/// Thrown if the stream is null.
/// A representing the asynchronous operation.
- public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default)
+ public static Task SaveAsQoiAsync(this Image source, Stream stream, QoiEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
stream,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance),
cancellationToken);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The file path to save the image to.
/// Thrown if the path is null.
- public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, default);
+ public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, default);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The file path to save the image to.
/// Thrown if the path is null.
/// A representing the asynchronous operation.
- public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, default);
+ public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, default);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The file path to save the image to.
/// The token to monitor for cancellation requests.
/// Thrown if the path is null.
/// A representing the asynchronous operation.
- public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken)
- => SaveAsWebpAsync(source, path, default, cancellationToken);
+ public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken)
+ => SaveAsTgaAsync(source, path, default, cancellationToken);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The file path to save the image to.
/// The encoder to save the image with.
/// Thrown if the path is null.
- public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) =>
+ public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) =>
source.Save(
path,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The file path to save the image to.
@@ -681,46 +682,46 @@ public static void SaveAsWebp(this Image source, string path, WebpEncoder encode
/// The token to monitor for cancellation requests.
/// Thrown if the path is null.
/// A representing the asynchronous operation.
- public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default)
+ public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
path,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
cancellationToken);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The stream to save the image to.
/// Thrown if the stream is null.
- public static void SaveAsWebp(this Image source, Stream stream)
- => SaveAsWebp(source, stream, default);
+ public static void SaveAsTga(this Image source, Stream stream)
+ => SaveAsTga(source, stream, default);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The stream to save the image to.
/// The token to monitor for cancellation requests.
/// Thrown if the stream is null.
/// A representing the asynchronous operation.
- public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
- => SaveAsWebpAsync(source, stream, default, cancellationToken);
+ public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
+ => SaveAsTgaAsync(source, stream, default, cancellationToken);
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The stream to save the image to.
/// The encoder to save the image with.
/// Thrown if the stream is null.
- public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
+ public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
=> source.Save(
stream,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
///
- /// Saves the image to the given stream with the Webp format.
+ /// Saves the image to the given stream with the Tga format.
///
/// The image this method extends.
/// The stream to save the image to.
@@ -728,10 +729,10 @@ public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder enco
/// The token to monitor for cancellation requests.
/// Thrown if the stream is null.
/// A representing the asynchronous operation.
- public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default)
+ public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
stream,
- encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
cancellationToken);
///
@@ -836,4 +837,106 @@ public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance),
cancellationToken);
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// Thrown if the path is null.
+ public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, default);
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// Thrown if the path is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, default);
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the path is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken)
+ => SaveAsWebpAsync(source, path, default, cancellationToken);
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// The encoder to save the image with.
+ /// Thrown if the path is null.
+ public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) =>
+ source.Save(
+ path,
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The file path to save the image to.
+ /// The encoder to save the image with.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the path is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default)
+ => source.SaveAsync(
+ path,
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
+ cancellationToken);
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// Thrown if the stream is null.
+ public static void SaveAsWebp(this Image source, Stream stream)
+ => SaveAsWebp(source, stream, default);
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the stream is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
+ => SaveAsWebpAsync(source, stream, default, cancellationToken);
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// The encoder to save the image with.
+ /// Thrown if the stream is null.
+ public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
+ => source.Save(
+ stream,
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
+
+ ///
+ /// Saves the image to the given stream with the Webp format.
+ ///
+ /// The image this method extends.
+ /// The stream to save the image to.
+ /// The encoder to save the image with.
+ /// The token to monitor for cancellation requests.
+ /// Thrown if the stream is null.
+ /// A representing the asynchronous operation.
+ public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default)
+ => source.SaveAsync(
+ stream,
+ encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
+ cancellationToken);
+
}
diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt
index 64f3bde9cc..538f62d041 100644
--- a/src/ImageSharp/Formats/ImageExtensions.Save.tt
+++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt
@@ -14,9 +14,10 @@ using SixLabors.ImageSharp.Advanced;
"Jpeg",
"Pbm",
"Png",
+ "Qoi",
"Tga",
- "Webp",
"Tiff",
+ "Webp",
};
foreach (string fmt in formats)
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index fb1d33277a..175a9f777d 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -34,7 +34,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private readonly MemoryAllocator memoryAllocator;
///
- /// The configuration instance for the decoding operation.
+ /// The configuration instance for the encoding operation.
///
private readonly Configuration configuration;
diff --git a/src/ImageSharp/Formats/Qoi/MetadataExtensions.cs b/src/ImageSharp/Formats/Qoi/MetadataExtensions.cs
new file mode 100644
index 0000000000..1e0fa88997
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/MetadataExtensions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Qoi;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp;
+
+///
+/// Extension methods for the type.
+///
+public static partial class MetadataExtensions
+{
+ ///
+ /// Gets the qoi format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static QoiMetadata GetQoiMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(QoiFormat.Instance);
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiChannels.cs b/src/ImageSharp/Formats/Qoi/QoiChannels.cs
new file mode 100644
index 0000000000..a76aeef28d
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiChannels.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Provides enumeration of available QOI color channels.
+///
+public enum QoiChannels
+{
+ ///
+ /// Each pixel is an R,G,B triple.
+ ///
+ Rgb = 3,
+
+ ///
+ /// Each pixel is an R,G,B triple, followed by an alpha sample.
+ ///
+ Rgba = 4
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiChunk.cs b/src/ImageSharp/Formats/Qoi/QoiChunk.cs
new file mode 100644
index 0000000000..06886b9691
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiChunk.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Enum that contains the operations that encoder and decoder must process, written
+/// in binary to be easier to compare them in the reference
+///
+internal enum QoiChunk
+{
+ ///
+ /// Indicates that the operation is QOI_OP_RGB where the RGB values are written
+ /// in one byte each one after this marker
+ ///
+ QoiOpRgb = 0b11111110,
+
+ ///
+ /// Indicates that the operation is QOI_OP_RGBA where the RGBA values are written
+ /// in one byte each one after this marker
+ ///
+ QoiOpRgba = 0b11111111,
+
+ ///
+ /// Indicates that the operation is QOI_OP_INDEX where one byte contains a 2-bit
+ /// marker (0b00) followed by an index on the previously seen pixels array 0..63
+ ///
+ QoiOpIndex = 0b00000000,
+
+ ///
+ /// Indicates that the operation is QOI_OP_DIFF where one byte contains a 2-bit
+ /// marker (0b01) followed by 2-bit differences in red, green and blue channel
+ /// with the previous pixel with a bias of 2 (-2..1)
+ ///
+ QoiOpDiff = 0b01000000,
+
+ ///
+ /// Indicates that the operation is QOI_OP_LUMA where one byte contains a 2-bit
+ /// marker (0b01) followed by a 6-bits number that indicates the difference of
+ /// the green channel with the previous pixel. Then another byte that contains
+ /// a 4-bit number that indicates the difference of the red channel minus the
+ /// previous difference, and another 4-bit number that indicates the difference
+ /// of the blue channel minus the green difference
+ /// Example: 0b10[6-bits diff green] 0b[6-bits dr-dg][6-bits db-dg]
+ /// dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g)
+ /// db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g)
+ ///
+ QoiOpLuma = 0b10000000,
+
+ ///
+ /// Indicates that the operation is QOI_OP_RUN where one byte contains a 2-bit
+ /// marker (0b11) followed by a 6-bits number that indicates the times that the
+ /// previous pixel is repeated
+ ///
+ QoiOpRun = 0b11000000
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiColorSpace.cs b/src/ImageSharp/Formats/Qoi/QoiColorSpace.cs
new file mode 100644
index 0000000000..9133f88b91
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiColorSpace.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+// ReSharper disable InconsistentNaming
+// ReSharper disable IdentifierTypo
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Enum for the different QOI color spaces.
+///
+public enum QoiColorSpace
+{
+ ///
+ /// sRGB color space with linear alpha value
+ ///
+ SrgbWithLinearAlpha,
+
+ ///
+ /// All the values in the color space are linear
+ ///
+ AllChannelsLinear
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs b/src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs
new file mode 100644
index 0000000000..ff40f7e17d
--- /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
new file mode 100644
index 0000000000..9643ccef03
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiConstants.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Text;
+
+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 => SMagic;
+
+ ///
+ /// Gets the list of mimetypes that equate to a QOI.
+ /// See https://github.com/phoboslab/qoi/issues/167
+ ///
+ public static string[] MimeTypes { get; } = { "image/qoi", "image/x-qoi", "image/vnd.qoi" };
+
+ ///
+ /// Gets the list of file extensions that equate to a QOI.
+ ///
+ public static string[] FileExtensions { get; } = { "qoi" };
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoder.cs b/src/ImageSharp/Formats/Qoi/QoiDecoder.cs
new file mode 100644
index 0000000000..a54095dfc6
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiDecoder.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+internal class QoiDecoder : ImageDecoder
+{
+ private QoiDecoder()
+ {
+ }
+
+ public static QoiDecoder Instance { get; } = new();
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ QoiDecoderCore decoder = new(options);
+ Image image = decoder.Decode(options.Configuration, stream, cancellationToken);
+
+ ScaleToTargetSize(options, image);
+
+ return image;
+ }
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+ return this.Decode(options, stream, cancellationToken);
+ }
+
+ protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+ return new QoiDecoderCore(options).Identify(options.Configuration, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
new file mode 100644
index 0000000000..deb0a37f05
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
@@ -0,0 +1,291 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+internal class QoiDecoderCore : IImageDecoderInternals
+{
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// Used the manage memory allocations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The QOI header.
+ ///
+ private QoiHeader header;
+
+ public QoiDecoderCore(DecoderOptions options)
+ {
+ this.Options = options;
+ this.configuration = options.Configuration;
+ this.memoryAllocator = this.configuration.MemoryAllocator;
+ }
+
+ public DecoderOptions Options { get; }
+
+ public Size Dimensions { get; }
+
+ ///
+ public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ // Process the header to get metadata
+ this.ProcessHeader(stream);
+
+ // Create Image object
+ ImageMetadata metadata = new()
+ {
+ DecodedImageFormat = QoiFormat.Instance,
+ HorizontalResolution = this.header.Width,
+ VerticalResolution = this.header.Height,
+ ResolutionUnits = PixelResolutionUnit.AspectRatio
+ };
+ QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
+ qoiMetadata.Channels = this.header.Channels;
+ qoiMetadata.ColorSpace = this.header.ColorSpace;
+ Image image = new(this.configuration, (int)this.header.Width, (int)this.header.Height, metadata);
+ Buffer2D pixels = image.GetRootFramePixelBuffer();
+
+ this.ProcessPixels(stream, pixels);
+
+ return image;
+ }
+
+ ///
+ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
+ {
+ this.ProcessHeader(stream);
+ PixelTypeInfo pixelType = new(8 * (int)this.header.Channels);
+ Size size = new((int)this.header.Width, (int)this.header.Height);
+
+ ImageMetadata metadata = new();
+ QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
+ qoiMetadata.Channels = this.header.Channels;
+ qoiMetadata.ColorSpace = this.header.ColorSpace;
+
+ return new ImageInfo(pixelType, size, metadata);
+ }
+
+ ///
+ /// Processes the 14-byte header to validate the image and save the metadata
+ /// in
+ ///
+ /// The stream where the bytes are being read
+ /// If the stream doesn't store a qoi image
+ private void ProcessHeader(BufferedReadStream stream)
+ {
+ 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()))
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ // If it's a qoi image, read the rest of properties
+ read = stream.Read(widthBytes);
+ if (read != 4)
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ read = stream.Read(heightBytes);
+ if (read != 4)
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ // These numbers are in Big Endian so we have to reverse them to get the real number
+ uint width = BinaryPrimitives.ReadUInt32BigEndian(widthBytes);
+ uint height = BinaryPrimitives.ReadUInt32BigEndian(heightBytes);
+ if (width == 0 || height == 0)
+ {
+ throw new InvalidImageContentException(
+ $"The image has an invalid size: width = {width}, height = {height}");
+ }
+
+ int channels = stream.ReadByte();
+ if (channels is -1 or (not 3 and not 4))
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ int colorSpace = stream.ReadByte();
+ if (colorSpace is -1 or (not 0 and not 1))
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ this.header = new QoiHeader(width, height, (QoiChannels)channels, (QoiColorSpace)colorSpace);
+ }
+
+ [DoesNotReturn]
+ private static void ThrowInvalidImageContentException()
+ => throw new InvalidImageContentException("The image is not a valid QOI image.");
+
+ private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels)
+ where TPixel : unmanaged, IPixel
+ {
+ using IMemoryOwner previouslySeenPixelsBuffer = this.memoryAllocator.Allocate(64, AllocationOptions.Clean);
+ Span previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
+ Rgba32 previousPixel = new(0, 0, 0, 255);
+
+ // We save the pixel to avoid loosing the fully opaque black pixel
+ // See https://github.com/phoboslab/qoi/issues/258
+ int pixelArrayPosition = GetArrayPosition(previousPixel);
+ previouslySeenPixels[pixelArrayPosition] = previousPixel;
+ byte operationByte;
+ Rgba32 readPixel = default;
+ Span pixelBytes = MemoryMarshal.CreateSpan(ref Unsafe.As(ref readPixel), 4);
+ TPixel pixel = default;
+
+ for (int i = 0; i < this.header.Height; i++)
+ {
+ Span row = pixels.DangerousGetRowSpan(i);
+ for (int j = 0; j < row.Length; j++)
+ {
+ operationByte = (byte)stream.ReadByte();
+ switch ((QoiChunk)operationByte)
+ {
+ // Reading one pixel with previous alpha intact
+ case QoiChunk.QoiOpRgb:
+ if (stream.Read(pixelBytes[..3]) < 3)
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ readPixel.A = previousPixel.A;
+ pixel.FromRgba32(readPixel);
+ pixelArrayPosition = GetArrayPosition(readPixel);
+ previouslySeenPixels[pixelArrayPosition] = readPixel;
+ break;
+
+ // Reading one pixel with new alpha
+ case QoiChunk.QoiOpRgba:
+ if (stream.Read(pixelBytes) < 4)
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ pixel.FromRgba32(readPixel);
+ pixelArrayPosition = GetArrayPosition(readPixel);
+ previouslySeenPixels[pixelArrayPosition] = readPixel;
+ break;
+
+ default:
+ switch ((QoiChunk)(operationByte & 0b11000000))
+ {
+ // Getting one pixel from previously seen pixels
+ case QoiChunk.QoiOpIndex:
+ readPixel = previouslySeenPixels[operationByte];
+ pixel.FromRgba32(readPixel);
+ break;
+
+ // Get one pixel from the difference (-2..1) of the previous pixel
+ case QoiChunk.QoiOpDiff:
+ int redDifference = (operationByte & 0b00110000) >> 4;
+ int greenDifference = (operationByte & 0b00001100) >> 2;
+ int blueDifference = operationByte & 0b00000011;
+ readPixel = previousPixel with
+ {
+ R = (byte)Numerics.Modulo256(previousPixel.R + (redDifference - 2)),
+ G = (byte)Numerics.Modulo256(previousPixel.G + (greenDifference - 2)),
+ B = (byte)Numerics.Modulo256(previousPixel.B + (blueDifference - 2))
+ };
+ pixel.FromRgba32(readPixel);
+ pixelArrayPosition = GetArrayPosition(readPixel);
+ previouslySeenPixels[pixelArrayPosition] = readPixel;
+ break;
+
+ // Get green difference in 6 bits and red and blue differences
+ // depending on the green one
+ case QoiChunk.QoiOpLuma:
+ int diffGreen = operationByte & 0b00111111;
+ int currentGreen = Numerics.Modulo256(previousPixel.G + (diffGreen - 32));
+ int nextByte = stream.ReadByte();
+ int diffRedDG = nextByte >> 4;
+ int diffBlueDG = nextByte & 0b00001111;
+ int currentRed = Numerics.Modulo256(diffRedDG - 8 + (diffGreen - 32) + previousPixel.R);
+ int currentBlue = Numerics.Modulo256(diffBlueDG - 8 + (diffGreen - 32) + previousPixel.B);
+ readPixel = previousPixel with { R = (byte)currentRed, B = (byte)currentBlue, G = (byte)currentGreen };
+ pixel.FromRgba32(readPixel);
+ pixelArrayPosition = GetArrayPosition(readPixel);
+ previouslySeenPixels[pixelArrayPosition] = readPixel;
+ break;
+
+ // Repeating the previous pixel 1..63 times
+ case QoiChunk.QoiOpRun:
+ int repetitions = operationByte & 0b00111111;
+ if (repetitions is 62 or 63)
+ {
+ ThrowInvalidImageContentException();
+ }
+
+ readPixel = previousPixel;
+ pixel.FromRgba32(readPixel);
+ for (int k = -1; k < repetitions; k++, j++)
+ {
+ if (j == row.Length)
+ {
+ j = 0;
+ i++;
+ row = pixels.DangerousGetRowSpan(i);
+ }
+
+ row[j] = pixel;
+ }
+
+ j--;
+ continue;
+
+ default:
+ ThrowInvalidImageContentException();
+ return;
+ }
+
+ break;
+ }
+
+ row[j] = pixel;
+ previousPixel = readPixel;
+ }
+ }
+
+ // Check stream end
+ for (int i = 0; i < 7; i++)
+ {
+ if (stream.ReadByte() != 0)
+ {
+ ThrowInvalidImageContentException();
+ }
+ }
+
+ if (stream.ReadByte() != 1)
+ {
+ ThrowInvalidImageContentException();
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetArrayPosition(Rgba32 pixel)
+ => Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11));
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs
new file mode 100644
index 0000000000..b3769d45cb
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Advanced;
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Image encoder for writing an image to a stream as a QOI image
+///
+public class QoiEncoder : ImageEncoder
+{
+ ///
+ /// Gets the color channels on the image that can be
+ /// RGB or RGBA. This is purely informative. It doesn't
+ /// change the way data chunks are encoded.
+ ///
+ public QoiChannels? Channels { get; init; }
+
+ ///
+ /// Gets the color space of the image that can be sRGB with
+ /// linear alpha or all channels linear. This is purely
+ /// informative. It doesn't change the way data chunks are encoded.
+ ///
+ public QoiColorSpace? ColorSpace { get; init; }
+
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration());
+ encoder.Encode(image, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
new file mode 100644
index 0000000000..40b246faf2
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
@@ -0,0 +1,231 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Image encoder for writing an image to a stream as a QOi image
+///
+internal class QoiEncoderCore : IImageEncoderInternals
+{
+ ///
+ /// The encoder with options
+ ///
+ private readonly QoiEncoder encoder;
+
+ ///
+ /// Used the manage memory allocations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The configuration instance for the encoding operation.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The encoder with options.
+ /// The to use for buffer allocations.
+ /// The configuration of the Encoder.
+ public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration)
+ {
+ this.encoder = encoder;
+ this.memoryAllocator = memoryAllocator;
+ this.configuration = configuration;
+ }
+
+ ///
+ public void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ Guard.NotNull(image, nameof(image));
+ Guard.NotNull(stream, nameof(stream));
+
+ this.WriteHeader(image, stream);
+ this.WritePixels(image, stream);
+ WriteEndOfStream(stream);
+ stream.Flush();
+ }
+
+ private void WriteHeader(Image image, Stream stream)
+ {
+ // Get metadata
+ Span width = stackalloc byte[4];
+ Span height = stackalloc byte[4];
+ BinaryPrimitives.WriteUInt32BigEndian(width, (uint)image.Width);
+ BinaryPrimitives.WriteUInt32BigEndian(height, (uint)image.Height);
+ QoiChannels qoiChannels = this.encoder.Channels ?? QoiChannels.Rgba;
+ QoiColorSpace qoiColorSpace = this.encoder.ColorSpace ?? QoiColorSpace.SrgbWithLinearAlpha;
+
+ // Write header to the stream
+ stream.Write(QoiConstants.Magic);
+ stream.Write(width);
+ stream.Write(height);
+ stream.WriteByte((byte)qoiChannels);
+ stream.WriteByte((byte)qoiColorSpace);
+ }
+
+ private void WritePixels(Image image, Stream stream)
+ where TPixel : unmanaged, IPixel
+ {
+ // Start image encoding
+ using IMemoryOwner previouslySeenPixelsBuffer = this.memoryAllocator.Allocate(64, AllocationOptions.Clean);
+ Span previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
+ Rgba32 previousPixel = new(0, 0, 0, 255);
+ Rgba32 currentRgba32 = default;
+ Buffer2D pixels = image.Frames[0].PixelBuffer;
+ using IMemoryOwner rgbaRowBuffer = this.memoryAllocator.Allocate(pixels.Width);
+ Span rgbaRow = rgbaRowBuffer.GetSpan();
+
+ for (int i = 0; i < pixels.Height; i++)
+ {
+ Span row = pixels.DangerousGetRowSpan(i);
+ PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow);
+ for (int j = 0; j < row.Length && i < pixels.Height; j++)
+ {
+ // We get the RGBA value from pixels
+ currentRgba32 = rgbaRow[j];
+
+ // First, we check if the current pixel is equal to the previous one
+ // If so, we do a QOI_OP_RUN
+ if (currentRgba32.Equals(previousPixel))
+ {
+ /* It looks like this isn't an error, but this makes possible that
+ * files start with a QOI_OP_RUN if their first pixel is a fully opaque
+ * black. However, the decoder of this project takes that into consideration
+ *
+ * To further details, see https://github.com/phoboslab/qoi/issues/258,
+ * and we should discuss what to do about this approach and
+ * if it's correct
+ */
+ int repetitions = 0;
+ do
+ {
+ repetitions++;
+ j++;
+ if (j == row.Length)
+ {
+ j = 0;
+ i++;
+ if (i == pixels.Height)
+ {
+ break;
+ }
+
+ row = pixels.DangerousGetRowSpan(i);
+ PixelOperations.Instance.ToRgba32(this.configuration, row, rgbaRow);
+ }
+
+ currentRgba32 = rgbaRow[j];
+ }
+ while (currentRgba32.Equals(previousPixel) && repetitions < 62);
+
+ j--;
+ stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
+
+ /* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
+ * it will be taken and compared on the next iteration
+ */
+ continue;
+ }
+
+ // else, we check if it exists in the previously seen pixels
+ // If so, we do a QOI_OP_INDEX
+ int pixelArrayPosition = GetArrayPosition(currentRgba32);
+ if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32))
+ {
+ stream.WriteByte((byte)pixelArrayPosition);
+ }
+ else
+ {
+ // else, we check if the difference is less than -2..1
+ // Since it wasn't found on the previously seen pixels, we save it
+ previouslySeenPixels[pixelArrayPosition] = currentRgba32;
+
+ int diffRed = currentRgba32.R - previousPixel.R;
+ int diffGreen = currentRgba32.G - previousPixel.G;
+ int diffBlue = currentRgba32.B - previousPixel.B;
+
+ // If so, we do a QOI_OP_DIFF
+ if (diffRed is >= -2 and <= 1 &&
+ diffGreen is >= -2 and <= 1 &&
+ diffBlue is >= -2 and <= 1 &&
+ currentRgba32.A == previousPixel.A)
+ {
+ // Bottom limit is -2, so we add 2 to make it equal to 0
+ int dr = diffRed + 2;
+ int dg = diffGreen + 2;
+ int db = diffBlue + 2;
+ byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
+ stream.WriteByte(valueToWrite);
+ }
+ else
+ {
+ // else, we check if the green difference is less than -32..31 and the rest -8..7
+ // If so, we do a QOI_OP_LUMA
+ int diffRedGreen = diffRed - diffGreen;
+ int diffBlueGreen = diffBlue - diffGreen;
+ if (diffGreen is >= -32 and <= 31 &&
+ diffRedGreen is >= -8 and <= 7 &&
+ diffBlueGreen is >= -8 and <= 7 &&
+ currentRgba32.A == previousPixel.A)
+ {
+ int dr_dg = diffRedGreen + 8;
+ int db_dg = diffBlueGreen + 8;
+ byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
+ byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
+ stream.WriteByte(byteToWrite1);
+ stream.WriteByte(byteToWrite2);
+ }
+ else
+ {
+ // else, we check if the alpha is equal to the previous pixel
+ // If so, we do a QOI_OP_RGB
+ if (currentRgba32.A == previousPixel.A)
+ {
+ stream.WriteByte((byte)QoiChunk.QoiOpRgb);
+ stream.WriteByte(currentRgba32.R);
+ stream.WriteByte(currentRgba32.G);
+ stream.WriteByte(currentRgba32.B);
+ }
+ else
+ {
+ // else, we do a QOI_OP_RGBA
+ stream.WriteByte((byte)QoiChunk.QoiOpRgba);
+ stream.WriteByte(currentRgba32.R);
+ stream.WriteByte(currentRgba32.G);
+ stream.WriteByte(currentRgba32.B);
+ stream.WriteByte(currentRgba32.A);
+ }
+ }
+ }
+ }
+
+ previousPixel = currentRgba32;
+ }
+ }
+ }
+
+ private static void WriteEndOfStream(Stream stream)
+ {
+ // Write bytes to end stream
+ for (int i = 0; i < 7; i++)
+ {
+ stream.WriteByte(0);
+ }
+
+ stream.WriteByte(1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int GetArrayPosition(Rgba32 pixel)
+ => Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11));
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiFormat.cs b/src/ImageSharp/Formats/Qoi/QoiFormat.cs
new file mode 100644
index 0000000000..ca2d7ae452
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiFormat.cs
@@ -0,0 +1,34 @@
+// 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 QoiFormat : IImageFormat
+{
+ private QoiFormat()
+ {
+ }
+
+ ///
+ /// Gets the shared instance.
+ ///
+ public static QoiFormat Instance { get; } = new QoiFormat();
+
+ ///
+ public string DefaultMimeType => "image/qoi";
+
+ ///
+ public string Name => "QOI";
+
+ ///
+ public IEnumerable MimeTypes => QoiConstants.MimeTypes;
+
+ ///
+ public IEnumerable FileExtensions => QoiConstants.FileExtensions;
+
+ ///
+ public QoiMetadata CreateDefaultFormatMetadata() => new();
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiHeader.cs b/src/ImageSharp/Formats/Qoi/QoiHeader.cs
new file mode 100644
index 0000000000..951d6701b9
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiHeader.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Text;
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Represents the qoi header chunk.
+///
+internal readonly struct QoiHeader
+{
+ public QoiHeader(uint width, uint height, QoiChannels channels, QoiColorSpace colorSpace)
+ {
+ this.Width = width;
+ this.Height = height;
+ this.Channels = channels;
+ this.ColorSpace = colorSpace;
+ }
+
+ ///
+ /// Gets the magic bytes "qoif"
+ ///
+ public byte[] Magic { get; } = Encoding.UTF8.GetBytes("qoif");
+
+ ///
+ /// Gets the image width in pixels (Big Endian)
+ ///
+ public uint Width { get; }
+
+ ///
+ /// Gets the image height in pixels (Big Endian)
+ ///
+ public uint Height { get; }
+
+ ///
+ /// Gets the color channels of the image. 3 = RGB, 4 = RGBA.
+ ///
+ public QoiChannels Channels { get; }
+
+ ///
+ /// Gets the color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
+ ///
+ public QoiColorSpace ColorSpace { get; }
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs b/src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs
new file mode 100644
index 0000000000..d264ec5bc3
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+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 && QoiConstants.Magic.SequenceEqual(header[..4]);
+}
diff --git a/src/ImageSharp/Formats/Qoi/QoiMetadata.cs b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
new file mode 100644
index 0000000000..610c6c15b8
--- /dev/null
+++ b/src/ImageSharp/Formats/Qoi/QoiMetadata.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Qoi;
+
+///
+/// Provides Qoi specific metadata information for the image.
+///
+public class QoiMetadata : IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public QoiMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The metadata to create an instance from.
+ private QoiMetadata(QoiMetadata other)
+ {
+ this.Channels = other.Channels;
+ this.ColorSpace = other.ColorSpace;
+ }
+
+ ///
+ /// Gets or sets color channels of the image. 3 = RGB, 4 = RGBA.
+ ///
+ public QoiChannels Channels { get; set; }
+
+ ///
+ /// Gets or sets color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
+ ///
+ public QoiColorSpace ColorSpace { get; set; }
+
+ ///
+ public IDeepCloneable DeepClone() => new QoiMetadata(this);
+}
diff --git a/src/ImageSharp/Formats/Qoi/qoi-specification.pdf b/src/ImageSharp/Formats/Qoi/qoi-specification.pdf
new file mode 100644
index 0000000000..3ffa4bd615
Binary files /dev/null and b/src/ImageSharp/Formats/Qoi/qoi-specification.pdf differ
diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs
index ed0db9b85b..69654329c4 100644
--- a/src/ImageSharp/Image{TPixel}.cs
+++ b/src/ImageSharp/Image{TPixel}.cs
@@ -418,12 +418,7 @@ private static Size ValidateFramesAndGetSize(IEnumerable> fra
{
Guard.NotNull(frames, nameof(frames));
- ImageFrame? rootFrame = frames.FirstOrDefault();
-
- if (rootFrame == null)
- {
- throw new ArgumentException("Must not be empty.", nameof(frames));
- }
+ ImageFrame? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames));
Size rootSize = rootFrame.Size();
diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs
index 3853c445f7..c5d61726c8 100644
--- a/tests/ImageSharp.Tests/ConfigurationTests.cs
+++ b/tests/ImageSharp.Tests/ConfigurationTests.cs
@@ -20,7 +20,7 @@ public class ConfigurationTests
public Configuration DefaultConfiguration { get; }
- private readonly int expectedDefaultConfigurationCount = 8;
+ private readonly int expectedDefaultConfigurationCount = 9;
public ConfigurationTests()
{
diff --git a/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs
new file mode 100644
index 0000000000..31ec27da0c
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs
@@ -0,0 +1,135 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Qoi;
+using SixLabors.ImageSharp.Formats;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
+
+public class ImageExtensionsTest
+{
+ [Fact]
+ public void SaveAsQoi_Path()
+ {
+ string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest));
+ string file = Path.Combine(dir, "SaveAsQoi_Path.qoi");
+
+ using (Image image = new(10, 10))
+ {
+ image.SaveAsQoi(file);
+ }
+
+ IImageFormat format = Image.DetectFormat(file);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public async Task SaveAsQoiAsync_Path()
+ {
+ string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest));
+ string file = Path.Combine(dir, "SaveAsQoiAsync_Path.qoi");
+
+ using (Image image = new(10, 10))
+ {
+ await image.SaveAsQoiAsync(file);
+ }
+
+ IImageFormat format = Image.DetectFormat(file);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public void SaveAsQoi_Path_Encoder()
+ {
+ string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions));
+ string file = Path.Combine(dir, "SaveAsQoi_Path_Encoder.qoi");
+
+ using (Image image = new(10, 10))
+ {
+ image.SaveAsQoi(file, new QoiEncoder());
+ }
+
+ IImageFormat format = Image.DetectFormat(file);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public async Task SaveAsQoiAsync_Path_Encoder()
+ {
+ string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions));
+ string file = Path.Combine(dir, "SaveAsQoiAsync_Path_Encoder.qoi");
+
+ using (Image image = new(10, 10))
+ {
+ await image.SaveAsQoiAsync(file, new QoiEncoder());
+ }
+
+ IImageFormat format = Image.DetectFormat(file);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public void SaveAsQoi_Stream()
+ {
+ using MemoryStream memoryStream = new();
+
+ using (Image image = new(10, 10))
+ {
+ image.SaveAsQoi(memoryStream);
+ }
+
+ memoryStream.Position = 0;
+
+ IImageFormat format = Image.DetectFormat(memoryStream);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public async Task SaveAsQoiAsync_StreamAsync()
+ {
+ using MemoryStream memoryStream = new();
+
+ using (Image image = new(10, 10))
+ {
+ await image.SaveAsQoiAsync(memoryStream);
+ }
+
+ memoryStream.Position = 0;
+
+ IImageFormat format = Image.DetectFormat(memoryStream);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public void SaveAsQoi_Stream_Encoder()
+ {
+ using MemoryStream memoryStream = new();
+
+ using (Image image = new(10, 10))
+ {
+ image.SaveAsQoi(memoryStream, new QoiEncoder());
+ }
+
+ memoryStream.Position = 0;
+
+ IImageFormat format = Image.DetectFormat(memoryStream);
+ Assert.True(format is QoiFormat);
+ }
+
+ [Fact]
+ public async Task SaveAsQoiAsync_Stream_Encoder()
+ {
+ using MemoryStream memoryStream = new();
+
+ using (Image image = new(10, 10))
+ {
+ await image.SaveAsQoiAsync(memoryStream, new QoiEncoder());
+ }
+
+ memoryStream.Position = 0;
+
+ IImageFormat format = Image.DetectFormat(memoryStream);
+ Assert.True(format is QoiFormat);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs
new file mode 100644
index 0000000000..387980e464
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Qoi;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
+
+[Trait("Format", "Qoi")]
+[ValidateDisposedMemoryAllocations]
+public class QoiDecoderTests
+{
+ [Theory]
+ [InlineData(TestImages.Qoi.Dice, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.EdgeCase, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.Kodim10, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.Kodim23, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.QoiLogo, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.TestCard, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.TestCardRGBA, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [InlineData(TestImages.Qoi.Wikipedia008, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ public void Identify(string imagePath, QoiChannels channels, QoiColorSpace colorSpace)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using MemoryStream stream = new(testFile.Bytes, false);
+
+ ImageInfo imageInfo = Image.Identify(stream);
+ QoiMetadata qoiMetadata = imageInfo.Metadata.GetQoiMetadata();
+
+ Assert.NotNull(imageInfo);
+ Assert.Equal(imageInfo.Metadata.DecodedImageFormat, QoiFormat.Instance);
+ Assert.Equal(qoiMetadata.Channels, channels);
+ Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ public void Decode(TestImageProvider provider, QoiChannels channels, QoiColorSpace colorSpace)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ QoiMetadata qoiMetadata = image.Metadata.GetQoiMetadata();
+ image.DebugSave(provider);
+
+ image.CompareToReferenceOutput(provider);
+ Assert.Equal(qoiMetadata.Channels, channels);
+ Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs
new file mode 100644
index 0000000000..d57b597b06
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Qoi;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
+
+[Trait("Format", "Qoi")]
+[ValidateDisposedMemoryAllocations]
+public class QoiEncoderTests
+{
+ [Theory]
+ [WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
+ [WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
+ public static void Encode(TestImageProvider provider, QoiChannels channels, QoiColorSpace colorSpace)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(new MagickReferenceDecoder());
+ using MemoryStream stream = new();
+ QoiEncoder encoder = new()
+ {
+ Channels = channels,
+ ColorSpace = colorSpace
+ };
+ image.Save(stream, encoder);
+ stream.Position = 0;
+
+ using Image encodedImage = (Image)Image.Load(stream);
+ QoiMetadata qoiMetadata = encodedImage.Metadata.GetQoiMetadata();
+
+ ImageComparer.Exact.CompareImages(image, encodedImage);
+ Assert.Equal(qoiMetadata.Channels, channels);
+ Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
+ }
+}
diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj
index d17cffc4ff..dc081e0bea 100644
--- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj
+++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj
@@ -76,5 +76,9 @@
+
+
+
+
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 95c9ac6732..a25424b6d9 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1041,4 +1041,16 @@ public static class Pbm
public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm";
public const string Issue2477 = "Pbm/issue2477.pbm";
}
+
+ public static class Qoi
+ {
+ public const string Dice = "Qoi/dice.qoi";
+ public const string EdgeCase = "Qoi/edgecase.qoi";
+ public const string Kodim10 = "Qoi/kodim10.qoi";
+ public const string Kodim23 = "Qoi/kodim23.qoi";
+ public const string QoiLogo = "Qoi/qoi_logo.qoi";
+ public const string TestCard = "Qoi/testcard.qoi";
+ public const string TestCardRGBA = "Qoi/testcard_rgba.qoi";
+ public const string Wikipedia008 = "Qoi/wikipedia_008.qoi";
+ }
}
diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
index b91b5631bd..6c6f300d02 100644
--- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
@@ -8,6 +8,7 @@
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;
@@ -62,7 +63,8 @@ private static Configuration CreateDefaultConfiguration()
new PbmConfigurationModule(),
new TgaConfigurationModule(),
new WebpConfigurationModule(),
- new TiffConfigurationModule());
+ new TiffConfigurationModule(),
+ new QoiConfigurationModule());
IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration();
IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_dice.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_dice.png
new file mode 100644
index 0000000000..44b2fa1187
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_dice.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8e4a5cf4e80ed1e1106eceb3e873aecf7b8e0022dfe39aa4c0c64ffc41091f09
+size 243458
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_edgecase.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_edgecase.png
new file mode 100644
index 0000000000..d499d74c31
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_edgecase.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12c966382b318c58578e3823ac066c597ce1e16ce7c2315b0f9d66451803a082
+size 1245
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim10.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim10.png
new file mode 100644
index 0000000000..c038bcdcf2
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim10.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ca18bd41b7d6db902e86c7a1be32ceb0989aaec0bf9fa94ca599887970b83e63
+size 598510
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim23.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim23.png
new file mode 100644
index 0000000000..1742ec80f3
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim23.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f6c7a229a652bfcaba998e713e169072475bea9bba35374be9219eb19c6ab42b
+size 562295
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_qoi_logo.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_qoi_logo.png
new file mode 100644
index 0000000000..28e4bca77d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_qoi_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:593549012cf9573c457c4de9161c347f1ae81d80c057ea70b89fbb197bdd028f
+size 16953
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard.png
new file mode 100644
index 0000000000..3ba2c266ca
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ad1df5a4549a4860e00fbb53328208d4458e1961ae2fac290278c612432d1e7
+size 12299
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard_rgba.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard_rgba.png
new file mode 100644
index 0000000000..4b2c4c0c0d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard_rgba.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed62e82f1fed2bf16569298a61f792706a1b61e99026acefcbf8aeb0da6f6e08
+size 16075
diff --git a/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_wikipedia_008.png b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_wikipedia_008.png
new file mode 100644
index 0000000000..56b87a98f6
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_wikipedia_008.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed7705c6ccb440f6bff77b0b9ac8275576d3f1c1fa4ecaa83ff80a72359e6f2f
+size 1376202
diff --git a/tests/Images/Input/Qoi/dice.qoi b/tests/Images/Input/Qoi/dice.qoi
new file mode 100644
index 0000000000..0b1399a25b
--- /dev/null
+++ b/tests/Images/Input/Qoi/dice.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b05a622813eff15ce64f33ab76eee3f9d144f5cf24386e13ddf17c27f6310a01
+size 519653
diff --git a/tests/Images/Input/Qoi/edgecase.qoi b/tests/Images/Input/Qoi/edgecase.qoi
new file mode 100644
index 0000000000..8ce4eb1fdc
--- /dev/null
+++ b/tests/Images/Input/Qoi/edgecase.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3cae50b533fbc796171a0763c29a576eaac475d04b6a95fe46b02d440f609e11
+size 2114
diff --git a/tests/Images/Input/Qoi/kodim10.qoi b/tests/Images/Input/Qoi/kodim10.qoi
new file mode 100644
index 0000000000..c0e3dab4ca
--- /dev/null
+++ b/tests/Images/Input/Qoi/kodim10.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e330cc81299a2641386f32bdf4b7070b8d5f8f2f76d899ced389b5a1469e65b0
+size 652383
diff --git a/tests/Images/Input/Qoi/kodim23.qoi b/tests/Images/Input/Qoi/kodim23.qoi
new file mode 100644
index 0000000000..d1c3fb59c1
--- /dev/null
+++ b/tests/Images/Input/Qoi/kodim23.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d225e987dc07262be2acee5dee164b5f48d3a49dd0e03f426b3111b52f265548
+size 675251
diff --git a/tests/Images/Input/Qoi/qoi_logo.qoi b/tests/Images/Input/Qoi/qoi_logo.qoi
new file mode 100644
index 0000000000..74624947ed
--- /dev/null
+++ b/tests/Images/Input/Qoi/qoi_logo.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e6519746939c2b6bc6776a65ce87b1dbd769069c2d2c11295453e9f35160ba57
+size 16488
diff --git a/tests/Images/Input/Qoi/testcard.qoi b/tests/Images/Input/Qoi/testcard.qoi
new file mode 100644
index 0000000000..4e283b24ee
--- /dev/null
+++ b/tests/Images/Input/Qoi/testcard.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:de309646439d2e49c51d9921eb1faff9af4cb33f0019a24ccb57dce1ef00dbab
+size 21857
diff --git a/tests/Images/Input/Qoi/testcard_rgba.qoi b/tests/Images/Input/Qoi/testcard_rgba.qoi
new file mode 100644
index 0000000000..7f0de939e5
--- /dev/null
+++ b/tests/Images/Input/Qoi/testcard_rgba.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b284ed810a892bca34e89a956b7f8bf21afae4826197a8f3eaef90e470e2149e
+size 24167
diff --git a/tests/Images/Input/Qoi/wikipedia_008.qoi b/tests/Images/Input/Qoi/wikipedia_008.qoi
new file mode 100644
index 0000000000..2d84a0ad1e
--- /dev/null
+++ b/tests/Images/Input/Qoi/wikipedia_008.qoi
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a289c12cd96cc3ff65fcafa1a6d55c5cace0095a45bc570ca1a4d8b79a20b4df
+size 1521134