Skip to content

Commit bca998d

Browse files
committed
Implementing qoi decoder
I need to check phoboslab/qoi#258 because there's a bug with the decoder
1 parent a1342bf commit bca998d

16 files changed

+234
-18
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Qoi;
5+
6+
public enum QoiChunkEnum
7+
{
8+
QOI_OP_RGB = 0b11111110,
9+
QOI_OP_RGBA = 0b11111111,
10+
QOI_OP_INDEX = 0b00000000,
11+
QOI_OP_DIFF = 0b01000000,
12+
QOI_OP_LUMA = 0b10000000,
13+
QOI_OP_RUN = 0b11000000
14+
}

src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public sealed class QoiConfigurationModule : IImageFormatConfigurationModule
1212
public void Configure(Configuration configuration)
1313
{
1414
configuration.ImageFormatsManager.SetDecoder(QoiFormat.Instance, QoiDecoder.Instance);
15-
//configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder());
15+
configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder());
1616
configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector());
1717
}
1818
}

src/ImageSharp/Formats/Qoi/QoiDecoder.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using SixLabors.ImageSharp.Formats.Png;
5-
64
namespace SixLabors.ImageSharp.Formats.Qoi;
75
internal class QoiDecoder : ImageDecoder
86
{
@@ -16,7 +14,13 @@ protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream s
1614
{
1715
Guard.NotNull(options, nameof(options));
1816
Guard.NotNull(stream, nameof(stream));
19-
throw new NotImplementedException();
17+
18+
QoiDecoderCore decoder = new(options);
19+
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
20+
21+
ScaleToTargetSize(options, image);
22+
23+
return image;
2024
}
2125

2226
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs

Lines changed: 152 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,49 @@ public QoiDecoderCore(DecoderOptions options)
4444

4545
public Size Dimensions { get; }
4646

47+
/// <inheritdoc />
4748
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
48-
where TPixel : unmanaged, IPixel<TPixel> => throw new NotImplementedException();
49+
where TPixel : unmanaged, IPixel<TPixel>
50+
{
51+
// Process the header to get metadata
52+
this.ProcessHeader(stream);
53+
54+
// Create Image object
55+
ImageMetadata metadata = new()
56+
{
57+
DecodedImageFormat = QoiFormat.Instance,
58+
HorizontalResolution = this.header.Width,
59+
VerticalResolution = this.header.Height,
60+
ResolutionUnits = PixelResolutionUnit.AspectRatio
61+
};
62+
Image<TPixel> image = new(this.configuration, (int)this.header.Width, (int)this.header.Height, metadata);
63+
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
64+
65+
this.ProcessPixels(stream, pixels);
66+
67+
return image;
68+
}
4969

70+
/// <inheritdoc />
5071
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
5172
{
5273
ImageMetadata metadata = new();
53-
QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
5474

75+
this.ProcessHeader(stream);
76+
PixelTypeInfo pixelType = new(8 * (int)this.header.Channels);
77+
Size size = new((int)this.header.Width, (int)this.header.Height);
78+
79+
return new ImageInfo(pixelType, size, metadata);
80+
}
81+
82+
/// <summary>
83+
/// Processes the 14-byte header to validate the image and save the metadata
84+
/// in <see cref="header"/>
85+
/// </summary>
86+
/// <param name="stream">The stream where the bytes are being read</param>
87+
/// <exception cref="InvalidImageContentException">If the stream doesn't store a qoi image</exception>
88+
private void ProcessHeader(Stream stream)
89+
{
5590
Span<byte> magicBytes = stackalloc byte[4];
5691
Span<byte> widthBytes = stackalloc byte[4];
5792
Span<byte> heightBytes = stackalloc byte[4];
@@ -85,32 +120,138 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
85120
$"The image has an invalid size: width = {width}, height = {height}");
86121
}
87122

88-
qoiMetadata.Width = width;
89-
qoiMetadata.Height = height;
90-
91-
Size size = new((int)width, (int)height);
92-
93123
int channels = stream.ReadByte();
94124
if (channels is -1 or (not 3 and not 4))
95125
{
96126
ThrowInvalidImageContentException();
97127
}
98128

99129
PixelTypeInfo pixelType = new(8 * channels);
100-
qoiMetadata.Channels = (QoiChannels)channels;
101130

102131
int colorSpace = stream.ReadByte();
103132
if (colorSpace is -1 or (not 0 and not 1))
104133
{
105134
ThrowInvalidImageContentException();
106135
}
107136

108-
qoiMetadata.ColorSpace = (QoiColorSpace)colorSpace;
109-
110-
return new ImageInfo(pixelType, size, metadata);
137+
this.header = new QoiHeader(width, height, (QoiChannels)channels, (QoiColorSpace)colorSpace);
111138
}
112139

113140
[DoesNotReturn]
114141
private static void ThrowInvalidImageContentException()
115142
=> throw new InvalidImageContentException("The image is not a valid QOI image.");
143+
144+
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
145+
where TPixel : unmanaged, IPixel<TPixel>
146+
{
147+
Rgba32[] previouslySeenPixels = new Rgba32[64];
148+
Rgba32 previousPixel = new (0,0,0,255);
149+
for (int i = 0; i < this.header.Height; i++)
150+
{
151+
for (int j = 0; j < this.header.Width; j++)
152+
{
153+
byte operationByte = (byte)stream.ReadByte();
154+
byte[] pixelBytes;
155+
Rgba32 readPixel;
156+
TPixel pixel = new();
157+
int pixelArrayPosition;
158+
switch ((QoiChunkEnum)operationByte)
159+
{
160+
case QoiChunkEnum.QOI_OP_RGB:
161+
pixelBytes = new byte[3];
162+
if (stream.Read(pixelBytes) < 3)
163+
{
164+
ThrowInvalidImageContentException();
165+
}
166+
167+
readPixel = previousPixel with { R = pixelBytes[0], G = pixelBytes[1], B = pixelBytes[2] };
168+
pixel.FromRgba32(readPixel);
169+
pixelArrayPosition = this.GetArrayPosition(readPixel);
170+
previouslySeenPixels[pixelArrayPosition] = readPixel;
171+
break;
172+
173+
case QoiChunkEnum.QOI_OP_RGBA:
174+
pixelBytes = new byte[4];
175+
if (stream.Read(pixelBytes) < 4)
176+
{
177+
ThrowInvalidImageContentException();
178+
}
179+
180+
readPixel = new Rgba32(pixelBytes[0], pixelBytes[1], pixelBytes[2], pixelBytes[3]);
181+
pixel.FromRgba32(readPixel);
182+
pixelArrayPosition = this.GetArrayPosition(readPixel);
183+
previouslySeenPixels[pixelArrayPosition] = readPixel;
184+
break;
185+
186+
default:
187+
switch ((QoiChunkEnum)(operationByte & 0b11000000))
188+
{
189+
case QoiChunkEnum.QOI_OP_INDEX:
190+
readPixel = previouslySeenPixels[operationByte];
191+
pixel.FromRgba32(readPixel);
192+
break;
193+
case QoiChunkEnum.QOI_OP_DIFF:
194+
// Get each value
195+
byte redDifference = (byte)((operationByte & 0b00110000) >> 4),
196+
greenDifference = (byte)((operationByte & 0b00001100) >> 2),
197+
blueDifference = (byte)(operationByte & 0b00000011);
198+
readPixel = previousPixel with
199+
{
200+
R = (byte)((previousPixel.R + (redDifference - 2)) % 256),
201+
G = (byte)((previousPixel.G + (greenDifference - 2)) % 256),
202+
B = (byte)((previousPixel.B + (blueDifference - 2)) % 256)
203+
};
204+
pixel.FromRgba32(readPixel);
205+
pixelArrayPosition = this.GetArrayPosition(readPixel);
206+
previouslySeenPixels[pixelArrayPosition] = readPixel;
207+
break;
208+
case QoiChunkEnum.QOI_OP_LUMA:
209+
// Get difference green channel
210+
byte diffGreen = (byte)(operationByte & 0b00111111),
211+
currentGreen = (byte)((previousPixel.G + (diffGreen - 32)) % 256),
212+
nextByte = (byte)stream.ReadByte(),
213+
diffRedDG = (byte)(nextByte >> 4),
214+
diffBlueDG = (byte)(nextByte & 0b00001111),
215+
currentRed = (byte)((diffRedDG-8 + (diffGreen - 32) + previousPixel.R)%256),
216+
currentBlue = (byte)((diffBlueDG-8 + (diffGreen - 32) + previousPixel.B)%256);
217+
readPixel = previousPixel with { R = currentRed, B = currentBlue, G = currentGreen };
218+
pixel.FromRgba32(readPixel);
219+
pixelArrayPosition = this.GetArrayPosition(readPixel);
220+
previouslySeenPixels[pixelArrayPosition] = readPixel;
221+
break;
222+
case QoiChunkEnum.QOI_OP_RUN:
223+
byte repetitions = (byte)(operationByte & 0b00111111);
224+
if(repetitions is 62 or 63)
225+
{
226+
ThrowInvalidImageContentException();
227+
}
228+
229+
readPixel = previousPixel;
230+
pixel.FromRgba32(readPixel);
231+
for (int k = -1; k < repetitions; k++, j++)
232+
{
233+
if (j == this.header.Width)
234+
{
235+
j = 0;
236+
i++;
237+
}
238+
pixels[j,i] = pixel;
239+
}
240+
241+
j--;
242+
continue;
243+
244+
default:
245+
ThrowInvalidImageContentException();
246+
return;
247+
}
248+
break;
249+
}
250+
pixels[j,i] = pixel;
251+
previousPixel = readPixel;
252+
}
253+
}
254+
}
255+
256+
private int GetArrayPosition(Rgba32 pixel) => ((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)) % 64;
116257
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Qoi;
5+
6+
public class QoiEncoder : ImageEncoder
7+
{
8+
/// <inheritdoc />
9+
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
10+
{
11+
throw new NotImplementedException();
12+
}
13+
}

src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Buffers.Binary;
54
using System.Diagnostics.CodeAnalysis;
6-
using SixLabors.ImageSharp.Formats.Png;
75

86
namespace SixLabors.ImageSharp.Formats.Qoi;
97

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using SixLabors.ImageSharp.PixelFormats;
5+
46
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
57

68
[Trait("Format", "Qoi")]
@@ -26,4 +28,22 @@ public void Identify(string imagePath)
2628
Assert.NotNull(imageInfo);
2729
Assert.Equal(imageInfo.Metadata.DecodedImageFormat, ImageSharp.Formats.Qoi.QoiFormat.Instance);
2830
}
31+
32+
[Theory]
33+
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32)]
34+
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32)]
35+
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32)]
36+
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32)]
37+
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32)]
38+
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32)]
39+
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32)]
40+
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32)]
41+
public void Decode<TPixel>(TestImageProvider<TPixel> provider)
42+
where TPixel : unmanaged, IPixel<TPixel>
43+
{
44+
using Image<TPixel> image = provider.GetImage();
45+
image.DebugSave(provider);
46+
47+
image.CompareToReferenceOutput(provider);
48+
}
2949
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using SixLabors.ImageSharp.Formats.Jpeg;
99
using SixLabors.ImageSharp.Formats.Pbm;
1010
using SixLabors.ImageSharp.Formats.Png;
11+
using SixLabors.ImageSharp.Formats.Qoi;
1112
using SixLabors.ImageSharp.Formats.Tga;
1213
using SixLabors.ImageSharp.Formats.Tiff;
1314
using SixLabors.ImageSharp.Formats.Webp;
@@ -62,7 +63,8 @@ private static Configuration CreateDefaultConfiguration()
6263
new PbmConfigurationModule(),
6364
new TgaConfigurationModule(),
6465
new WebpConfigurationModule(),
65-
new TiffConfigurationModule());
66+
new TiffConfigurationModule(),
67+
new QoiConfigurationModule());
6668

6769
IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration();
6870
IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)