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