Browse Source

Merge branch 'main' into js/gif-fixes

pull/2500/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
4b15595c6b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitattributes
  2. 13
      ImageSharp.sln
  3. 2
      shared-infrastructure
  4. 24
      src/ImageSharp/Common/Helpers/Numerics.cs
  5. 5
      src/ImageSharp/Configuration.cs
  6. 205
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  7. 3
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  8. 4
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
  9. 8
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  10. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  11. 20
      src/ImageSharp/Formats/Qoi/MetadataExtensions.cs
  12. 20
      src/ImageSharp/Formats/Qoi/QoiChannels.cs
  13. 56
      src/ImageSharp/Formats/Qoi/QoiChunk.cs
  14. 22
      src/ImageSharp/Formats/Qoi/QoiColorSpace.cs
  15. 18
      src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs
  16. 27
      src/ImageSharp/Formats/Qoi/QoiConstants.cs
  17. 43
      src/ImageSharp/Formats/Qoi/QoiDecoder.cs
  18. 291
      src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
  19. 33
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  20. 231
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  21. 34
      src/ImageSharp/Formats/Qoi/QoiFormat.cs
  22. 45
      src/ImageSharp/Formats/Qoi/QoiHeader.cs
  23. 25
      src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs
  24. 40
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  25. BIN
      src/ImageSharp/Formats/Qoi/qoi-specification.pdf
  26. 7
      src/ImageSharp/Image{TPixel}.cs
  27. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  28. 11
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  29. 135
      tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs
  30. 56
      tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs
  31. 44
      tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs
  32. 4
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  33. 13
      tests/ImageSharp.Tests/TestImages.cs
  34. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  35. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_dice.png
  36. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_edgecase.png
  37. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim10.png
  38. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim23.png
  39. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_qoi_logo.png
  40. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard.png
  41. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard_rgba.png
  42. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_wikipedia_008.png
  43. 3
      tests/Images/Input/Jpg/issues/issue-2478-jfxx.jpg
  44. 3
      tests/Images/Input/Qoi/dice.qoi
  45. 3
      tests/Images/Input/Qoi/edgecase.qoi
  46. 3
      tests/Images/Input/Qoi/kodim10.qoi
  47. 3
      tests/Images/Input/Qoi/kodim23.qoi
  48. 3
      tests/Images/Input/Qoi/qoi_logo.qoi
  49. 3
      tests/Images/Input/Qoi/testcard.qoi
  50. 3
      tests/Images/Input/Qoi/testcard_rgba.qoi
  51. 3
      tests/Images/Input/Qoi/wikipedia_008.qoi

1
.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

13
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}

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 9a6cf00d9a3d482bb08211dd8309f4724a2735cb
Subproject commit 353b9afe32a8000410312d17263407cd7bb82d19

24
src/ImageSharp/Common/Helpers/Numerics.cs

@ -73,6 +73,30 @@ internal static class Numerics
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo8(nint x) => x & 7;
/// <summary>
/// Calculates <paramref name="x"/> % 64
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Modulo64(int x) => x & 63;
/// <summary>
/// Calculates <paramref name="x"/> % 64
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo64(nint x) => x & 63;
/// <summary>
/// Calculates <paramref name="x"/> % 256
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Modulo256(int x) => x & 255;
/// <summary>
/// Calculates <paramref name="x"/> % 256
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo256(nint x) => x & 255;
/// <summary>
/// Fast (x mod m) calculator, with the restriction that
/// <paramref name="m"/> should be power of 2.

5
src/ImageSharp/Configuration.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
@ -212,6 +213,7 @@ public sealed class Configuration
/// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>.
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance() => new(
@ -222,5 +224,6 @@ public sealed class Configuration
new PbmConfigurationModule(),
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule());
new WebpConfigurationModule(),
new QoiConfigurationModule());
}

205
src/ImageSharp/Formats/ImageExtensions.Save.cs

@ -9,9 +9,10 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
namespace SixLabors.ImageSharp;
@ -531,47 +532,47 @@ public static partial class ImageExtensions
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
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);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
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));
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
@ -579,46 +580,46 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
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);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
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));
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
@ -626,54 +627,54 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
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));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
@ -681,46 +682,46 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
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));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
@ -728,10 +729,10 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
@ -836,4 +837,106 @@ public static partial class ImageExtensions
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsWebpAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsWebp(this Image source, Stream stream)
=> SaveAsWebp(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsWebpAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
=> source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
}

3
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)

4
src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs

@ -71,7 +71,9 @@ internal readonly struct JFifMarker : IEquatable<JFifMarker>
/// <param name="marker">The marker to return.</param>
public static bool TryParse(ReadOnlySpan<byte> bytes, out JFifMarker marker)
{
if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker))
// Some images incorrectly use JFXX as the App0 marker (Issue 2478)
if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker)
|| ProfileResolver.IsProfile(bytes, ProfileResolver.JFxxMarker))
{
byte majorVersion = bytes[5];
byte minorVersion = bytes[6];

8
src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs

@ -16,6 +16,14 @@ internal static class ProfileResolver
(byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0'
};
/// <summary>
/// Gets the JFXX specific markers.
/// </summary>
public static ReadOnlySpan<byte> JFxxMarker => new[]
{
(byte)'J', (byte)'F', (byte)'X', (byte)'X', (byte)'\0'
};
/// <summary>
/// Gets the ICC specific markers.
/// </summary>

2
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -34,7 +34,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The configuration instance for the decoding operation.
/// The configuration instance for the encoding operation.
/// </summary>
private readonly Configuration configuration;

20
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;
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static partial class MetadataExtensions
{
/// <summary>
/// Gets the qoi format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="QoiMetadata"/>.</returns>
public static QoiMetadata GetQoiMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(QoiFormat.Instance);
}

20
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;
/// <summary>
/// Provides enumeration of available QOI color channels.
/// </summary>
public enum QoiChannels
{
/// <summary>
/// Each pixel is an R,G,B triple.
/// </summary>
Rgb = 3,
/// <summary>
/// Each pixel is an R,G,B triple, followed by an alpha sample.
/// </summary>
Rgba = 4
}

56
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;
/// <summary>
/// Enum that contains the operations that encoder and decoder must process, written
/// in binary to be easier to compare them in the reference
/// </summary>
internal enum QoiChunk
{
/// <summary>
/// Indicates that the operation is QOI_OP_RGB where the RGB values are written
/// in one byte each one after this marker
/// </summary>
QoiOpRgb = 0b11111110,
/// <summary>
/// Indicates that the operation is QOI_OP_RGBA where the RGBA values are written
/// in one byte each one after this marker
/// </summary>
QoiOpRgba = 0b11111111,
/// <summary>
/// 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
/// </summary>
QoiOpIndex = 0b00000000,
/// <summary>
/// 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)
/// </summary>
QoiOpDiff = 0b01000000,
/// <summary>
/// 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)
/// </summary>
QoiOpLuma = 0b10000000,
/// <summary>
/// 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
/// </summary>
QoiOpRun = 0b11000000
}

22
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;
/// <summary>
/// Enum for the different QOI color spaces.
/// </summary>
public enum QoiColorSpace
{
/// <summary>
/// sRGB color space with linear alpha value
/// </summary>
SrgbWithLinearAlpha,
/// <summary>
/// All the values in the color space are linear
/// </summary>
AllChannelsLinear
}

18
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the qoi format.
/// </summary>
public sealed class QoiConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetDecoder(QoiFormat.Instance, QoiDecoder.Instance);
configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector());
}
}

27
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");
/// <summary>
/// Gets the bytes that indicates the image is QOI
/// </summary>
public static ReadOnlySpan<byte> Magic => SMagic;
/// <summary>
/// Gets the list of mimetypes that equate to a QOI.
/// See https://github.com/phoboslab/qoi/issues/167
/// </summary>
public static string[] MimeTypes { get; } = { "image/qoi", "image/x-qoi", "image/vnd.qoi" };
/// <summary>
/// Gets the list of file extensions that equate to a QOI.
/// </summary>
public static string[] FileExtensions { get; } = { "qoi" };
}

43
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();
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
QoiDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc />
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
return this.Decode<Rgba32>(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);
}
}

291
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
{
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The QOI header.
/// </summary>
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; }
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<TPixel> image = new(this.configuration, (int)this.header.Width, (int)this.header.Height, metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
this.ProcessPixels(stream, pixels);
return image;
}
/// <inheritdoc />
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);
}
/// <summary>
/// Processes the 14-byte header to validate the image and save the metadata
/// in <see cref="header"/>
/// </summary>
/// <param name="stream">The stream where the bytes are being read</param>
/// <exception cref="InvalidImageContentException">If the stream doesn't store a qoi image</exception>
private void ProcessHeader(BufferedReadStream stream)
{
Span<byte> magicBytes = stackalloc byte[4];
Span<byte> widthBytes = stackalloc byte[4];
Span<byte> 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<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Rgba32> previouslySeenPixelsBuffer = this.memoryAllocator.Allocate<Rgba32>(64, AllocationOptions.Clean);
Span<Rgba32> 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<byte> pixelBytes = MemoryMarshal.CreateSpan(ref Unsafe.As<Rgba32, byte>(ref readPixel), 4);
TPixel pixel = default;
for (int i = 0; i < this.header.Height; i++)
{
Span<TPixel> 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));
}

33
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;
/// <summary>
/// Image encoder for writing an image to a stream as a QOI image
/// </summary>
public class QoiEncoder : ImageEncoder
{
/// <summary>
/// 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.
/// </summary>
public QoiChannels? Channels { get; init; }
/// <summary>
/// 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.
/// </summary>
public QoiColorSpace? ColorSpace { get; init; }
/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration());
encoder.Encode(image, stream, cancellationToken);
}
}

231
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;
/// <summary>
/// Image encoder for writing an image to a stream as a QOi image
/// </summary>
internal class QoiEncoderCore : IImageEncoderInternals
{
/// <summary>
/// The encoder with options
/// </summary>
private readonly QoiEncoder encoder;
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The configuration instance for the encoding operation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Initializes a new instance of the <see cref="QoiEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration of the Encoder.</param>
public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration)
{
this.encoder = encoder;
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
}
/// <inheritdoc />
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<byte> width = stackalloc byte[4];
Span<byte> 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<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// Start image encoding
using IMemoryOwner<Rgba32> previouslySeenPixelsBuffer = this.memoryAllocator.Allocate<Rgba32>(64, AllocationOptions.Clean);
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
Rgba32 previousPixel = new(0, 0, 0, 255);
Rgba32 currentRgba32 = default;
Buffer2D<TPixel> pixels = image.Frames[0].PixelBuffer;
using IMemoryOwner<Rgba32> rgbaRowBuffer = this.memoryAllocator.Allocate<Rgba32>(pixels.Width);
Span<Rgba32> rgbaRow = rgbaRowBuffer.GetSpan();
for (int i = 0; i < pixels.Height; i++)
{
Span<TPixel> row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.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<TPixel>.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));
}

34
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the qoi format.
/// </summary>
public sealed class QoiFormat : IImageFormat<QoiMetadata>
{
private QoiFormat()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static QoiFormat Instance { get; } = new QoiFormat();
/// <inheritdoc/>
public string DefaultMimeType => "image/qoi";
/// <inheritdoc/>
public string Name => "QOI";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => QoiConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => QoiConstants.FileExtensions;
/// <inheritdoc/>
public QoiMetadata CreateDefaultFormatMetadata() => new();
}

45
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;
/// <summary>
/// Represents the qoi header chunk.
/// </summary>
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;
}
/// <summary>
/// Gets the magic bytes "qoif"
/// </summary>
public byte[] Magic { get; } = Encoding.UTF8.GetBytes("qoif");
/// <summary>
/// Gets the image width in pixels (Big Endian)
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets the image height in pixels (Big Endian)
/// </summary>
public uint Height { get; }
/// <summary>
/// Gets the color channels of the image. 3 = RGB, 4 = RGBA.
/// </summary>
public QoiChannels Channels { get; }
/// <summary>
/// Gets the color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
/// </summary>
public QoiColorSpace ColorSpace { get; }
}

25
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;
/// <summary>
/// Detects qoi file headers
/// </summary>
public class QoiImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 14;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null;
return format != null;
}
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= this.HeaderSize && QoiConstants.Magic.SequenceEqual(header[..4]);
}

40
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;
/// <summary>
/// Provides Qoi specific metadata information for the image.
/// </summary>
public class QoiMetadata : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
/// </summary>
public QoiMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private QoiMetadata(QoiMetadata other)
{
this.Channels = other.Channels;
this.ColorSpace = other.ColorSpace;
}
/// <summary>
/// Gets or sets color channels of the image. 3 = RGB, 4 = RGBA.
/// </summary>
public QoiChannels Channels { get; set; }
/// <summary>
/// Gets or sets color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
/// </summary>
public QoiColorSpace ColorSpace { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new QoiMetadata(this);
}

BIN
src/ImageSharp/Formats/Qoi/qoi-specification.pdf

Binary file not shown.

7
src/ImageSharp/Image{TPixel}.cs

@ -418,12 +418,7 @@ public sealed class Image<TPixel> : Image
{
Guard.NotNull(frames, nameof(frames));
ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault();
if (rootFrame == null)
{
throw new ArgumentException("Must not be empty.", nameof(frames));
}
ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames));
Size rootSize = rootFrame.Size();

2
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()
{

11
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -314,4 +314,15 @@ public partial class JpegDecoderTests
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
// https://github.com/SixLabors/ImageSharp/issues/2478
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2478_JFXX, PixelTypes.Rgba32)]
public void Issue2478_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
}

135
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<L8> 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<L8> 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<L8> 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<L8> 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<L8> 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<L8> 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<L8> 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<L8> image = new(10, 10))
{
await image.SaveAsQoiAsync(memoryStream, new QoiEncoder());
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is QoiFormat);
}
}

56
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<TPixel>(TestImageProvider<TPixel> provider, QoiChannels channels, QoiColorSpace colorSpace)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
QoiMetadata qoiMetadata = image.Metadata.GetQoiMetadata();
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
Assert.Equal(qoiMetadata.Channels, channels);
Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
}
}

44
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<TPixel>(TestImageProvider<TPixel> provider, QoiChannels channels, QoiColorSpace colorSpace)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<TPixel> encodedImage = (Image<TPixel>)Image.Load(stream);
QoiMetadata qoiMetadata = encodedImage.Metadata.GetQoiMetadata();
ImageComparer.Exact.CompareImages(image, encodedImage);
Assert.Equal(qoiMetadata.Channels, channels);
Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
}
}

4
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -76,5 +76,9 @@
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Folder Include="Formats\Qoi\" />
</ItemGroup>
</Project>

13
tests/ImageSharp.Tests/TestImages.cs

@ -290,6 +290,7 @@ public static class TestImages
public const string Issue2315_NotEnoughBytes = "Jpg/issues/issue-2315.jpg";
public const string Issue2334_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.jpg";
public const string Issue2478_JFXX = "Jpg/issues/issue-2478-jfxx.jpg";
public static class Fuzz
{
@ -1043,4 +1044,16 @@ public static class TestImages
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";
}
}

4
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
@ -62,7 +63,8 @@ public static partial class TestEnvironment
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();

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
tests/Images/Input/Jpg/issues/issue-2478-jfxx.jpg

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6bd5d14cbbead348404511801d7a2bacab19174e9f4063b5d2cec96f28fd578e
size 300170

3
tests/Images/Input/Qoi/dice.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b05a622813eff15ce64f33ab76eee3f9d144f5cf24386e13ddf17c27f6310a01
size 519653

3
tests/Images/Input/Qoi/edgecase.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3cae50b533fbc796171a0763c29a576eaac475d04b6a95fe46b02d440f609e11
size 2114

3
tests/Images/Input/Qoi/kodim10.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e330cc81299a2641386f32bdf4b7070b8d5f8f2f76d899ced389b5a1469e65b0
size 652383

3
tests/Images/Input/Qoi/kodim23.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d225e987dc07262be2acee5dee164b5f48d3a49dd0e03f426b3111b52f265548
size 675251

3
tests/Images/Input/Qoi/qoi_logo.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6519746939c2b6bc6776a65ce87b1dbd769069c2d2c11295453e9f35160ba57
size 16488

3
tests/Images/Input/Qoi/testcard.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de309646439d2e49c51d9921eb1faff9af4cb33f0019a24ccb57dce1ef00dbab
size 21857

3
tests/Images/Input/Qoi/testcard_rgba.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b284ed810a892bca34e89a956b7f8bf21afae4826197a8f3eaef90e470e2149e
size 24167

3
tests/Images/Input/Qoi/wikipedia_008.qoi

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a289c12cd96cc3ff65fcafa1a6d55c5cace0095a45bc570ca1a4d8b79a20b4df
size 1521134
Loading…
Cancel
Save