Browse Source

Implementing qoi decoder

I need to check https://github.com/phoboslab/qoi/issues/258 because there's a bug with the decoder
qoi
LuisAlfredo92 3 years ago
parent
commit
bca998d91d
No known key found for this signature in database GPG Key ID: 13A8436905993B8F
  1. 14
      src/ImageSharp/Formats/Qoi/QoiChunkEnum.cs
  2. 2
      src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs
  3. 10
      src/ImageSharp/Formats/Qoi/QoiDecoder.cs
  4. 163
      src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
  5. 13
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  6. 2
      src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs
  7. 20
      tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs
  8. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  9. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_dice.png
  10. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_edgecase.png
  11. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim10.png
  12. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_kodim23.png
  13. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_qoi_logo.png
  14. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard.png
  15. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_testcard_rgba.png
  16. 3
      tests/Images/External/ReferenceOutput/QoiDecoderTests/Decode_Rgba32_wikipedia_008.png

14
src/ImageSharp/Formats/Qoi/QoiChunkEnum.cs

@ -0,0 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
public enum QoiChunkEnum
{
QOI_OP_RGB = 0b11111110,
QOI_OP_RGBA = 0b11111111,
QOI_OP_INDEX = 0b00000000,
QOI_OP_DIFF = 0b01000000,
QOI_OP_LUMA = 0b10000000,
QOI_OP_RUN = 0b11000000
}

2
src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs

@ -12,7 +12,7 @@ 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.SetEncoder(QoiFormat.Instance, new QoiEncoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector());
}
}

10
src/ImageSharp/Formats/Qoi/QoiDecoder.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Png;
namespace SixLabors.ImageSharp.Formats.Qoi;
internal class QoiDecoder : ImageDecoder
{
@ -16,7 +14,13 @@ internal class QoiDecoder : ImageDecoder
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
throw new NotImplementedException();
QoiDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

163
src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs

@ -44,14 +44,49 @@ internal class QoiDecoderCore : IImageDecoderInternals
public Size Dimensions { get; }
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> => throw new NotImplementedException();
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
};
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)
{
ImageMetadata metadata = new();
QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
this.ProcessHeader(stream);
PixelTypeInfo pixelType = new(8 * (int)this.header.Channels);
Size size = new((int)this.header.Width, (int)this.header.Height);
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(Stream stream)
{
Span<byte> magicBytes = stackalloc byte[4];
Span<byte> widthBytes = stackalloc byte[4];
Span<byte> heightBytes = stackalloc byte[4];
@ -85,11 +120,6 @@ internal class QoiDecoderCore : IImageDecoderInternals
$"The image has an invalid size: width = {width}, height = {height}");
}
qoiMetadata.Width = width;
qoiMetadata.Height = height;
Size size = new((int)width, (int)height);
int channels = stream.ReadByte();
if (channels is -1 or (not 3 and not 4))
{
@ -97,7 +127,6 @@ internal class QoiDecoderCore : IImageDecoderInternals
}
PixelTypeInfo pixelType = new(8 * channels);
qoiMetadata.Channels = (QoiChannels)channels;
int colorSpace = stream.ReadByte();
if (colorSpace is -1 or (not 0 and not 1))
@ -105,12 +134,124 @@ internal class QoiDecoderCore : IImageDecoderInternals
ThrowInvalidImageContentException();
}
qoiMetadata.ColorSpace = (QoiColorSpace)colorSpace;
return new ImageInfo(pixelType, size, metadata);
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>
{
Rgba32[] previouslySeenPixels = new Rgba32[64];
Rgba32 previousPixel = new (0,0,0,255);
for (int i = 0; i < this.header.Height; i++)
{
for (int j = 0; j < this.header.Width; j++)
{
byte operationByte = (byte)stream.ReadByte();
byte[] pixelBytes;
Rgba32 readPixel;
TPixel pixel = new();
int pixelArrayPosition;
switch ((QoiChunkEnum)operationByte)
{
case QoiChunkEnum.QOI_OP_RGB:
pixelBytes = new byte[3];
if (stream.Read(pixelBytes) < 3)
{
ThrowInvalidImageContentException();
}
readPixel = previousPixel with { R = pixelBytes[0], G = pixelBytes[1], B = pixelBytes[2] };
pixel.FromRgba32(readPixel);
pixelArrayPosition = this.GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
case QoiChunkEnum.QOI_OP_RGBA:
pixelBytes = new byte[4];
if (stream.Read(pixelBytes) < 4)
{
ThrowInvalidImageContentException();
}
readPixel = new Rgba32(pixelBytes[0], pixelBytes[1], pixelBytes[2], pixelBytes[3]);
pixel.FromRgba32(readPixel);
pixelArrayPosition = this.GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
default:
switch ((QoiChunkEnum)(operationByte & 0b11000000))
{
case QoiChunkEnum.QOI_OP_INDEX:
readPixel = previouslySeenPixels[operationByte];
pixel.FromRgba32(readPixel);
break;
case QoiChunkEnum.QOI_OP_DIFF:
// Get each value
byte redDifference = (byte)((operationByte & 0b00110000) >> 4),
greenDifference = (byte)((operationByte & 0b00001100) >> 2),
blueDifference = (byte)(operationByte & 0b00000011);
readPixel = previousPixel with
{
R = (byte)((previousPixel.R + (redDifference - 2)) % 256),
G = (byte)((previousPixel.G + (greenDifference - 2)) % 256),
B = (byte)((previousPixel.B + (blueDifference - 2)) % 256)
};
pixel.FromRgba32(readPixel);
pixelArrayPosition = this.GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
case QoiChunkEnum.QOI_OP_LUMA:
// Get difference green channel
byte diffGreen = (byte)(operationByte & 0b00111111),
currentGreen = (byte)((previousPixel.G + (diffGreen - 32)) % 256),
nextByte = (byte)stream.ReadByte(),
diffRedDG = (byte)(nextByte >> 4),
diffBlueDG = (byte)(nextByte & 0b00001111),
currentRed = (byte)((diffRedDG-8 + (diffGreen - 32) + previousPixel.R)%256),
currentBlue = (byte)((diffBlueDG-8 + (diffGreen - 32) + previousPixel.B)%256);
readPixel = previousPixel with { R = currentRed, B = currentBlue, G = currentGreen };
pixel.FromRgba32(readPixel);
pixelArrayPosition = this.GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
case QoiChunkEnum.QOI_OP_RUN:
byte repetitions = (byte)(operationByte & 0b00111111);
if(repetitions is 62 or 63)
{
ThrowInvalidImageContentException();
}
readPixel = previousPixel;
pixel.FromRgba32(readPixel);
for (int k = -1; k < repetitions; k++, j++)
{
if (j == this.header.Width)
{
j = 0;
i++;
}
pixels[j,i] = pixel;
}
j--;
continue;
default:
ThrowInvalidImageContentException();
return;
}
break;
}
pixels[j,i] = pixel;
previousPixel = readPixel;
}
}
}
private int GetArrayPosition(Rgba32 pixel) => ((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)) % 64;
}

13
src/ImageSharp/Formats/Qoi/QoiEncoder.cs

@ -0,0 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
public class QoiEncoder : ImageEncoder
{
/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}

2
src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs

@ -1,9 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Png;
namespace SixLabors.ImageSharp.Formats.Qoi;

20
tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
[Trait("Format", "Qoi")]
@ -26,4 +28,22 @@ public class QoiDecoderTests
Assert.NotNull(imageInfo);
Assert.Equal(imageInfo.Metadata.DecodedImageFormat, ImageSharp.Formats.Qoi.QoiFormat.Instance);
}
[Theory]
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32)]
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32)]
public void Decode<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}
}

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:1b39ab87da8d615f1ebb1348789a767b6421771dfcf11cd0b7cac618a0b8d6f1
size 918

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
Loading…
Cancel
Save