Skip to content

Commit 07e6597

Browse files
committed
Finishing qoi encoder
-Also adding Decode method without TPixel value -Adding stream end check to decoder (we must discuss if it's necesarry or not) -formating general code
1 parent a87f781 commit 07e6597

File tree

5 files changed

+282
-4
lines changed

5 files changed

+282
-4
lines changed

src/ImageSharp/Formats/Qoi/QoiDecoder.cs

Lines changed: 5 additions & 1 deletion
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.Formats.Qoi;
57
internal class QoiDecoder : ImageDecoder
68
{
@@ -10,6 +12,7 @@ private QoiDecoder()
1012

1113
public static QoiDecoder Instance { get; } = new();
1214

15+
/// <inheritdoc />
1316
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
1417
{
1518
Guard.NotNull(options, nameof(options));
@@ -23,11 +26,12 @@ protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream s
2326
return image;
2427
}
2528

29+
/// <inheritdoc />
2630
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
2731
{
2832
Guard.NotNull(options, nameof(options));
2933
Guard.NotNull(stream, nameof(stream));
30-
throw new NotImplementedException();
34+
return this.Decode<Rgba32>(options, stream, cancellationToken);
3135
}
3236

3337
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ private void ProcessHeader(Stream stream)
141141
private static void ThrowInvalidImageContentException()
142142
=> throw new InvalidImageContentException("The image is not a valid QOI image.");
143143

144-
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
144+
private void ProcessPixels<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
145145
where TPixel : unmanaged, IPixel<TPixel>
146146
{
147147
Rgba32[] previouslySeenPixels = new Rgba32[64];
148-
Rgba32 previousPixel = new (0,0,0,255);
148+
Rgba32 previousPixel = new(0,0,0,255);
149149

150150
// We save the pixel to avoid loosing the fully opaque black pixel
151151
// See https://github.com/phoboslab/qoi/issues/258
@@ -258,12 +258,28 @@ private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> p
258258
ThrowInvalidImageContentException();
259259
return;
260260
}
261+
261262
break;
262263
}
264+
263265
pixels[j,i] = pixel;
264266
previousPixel = readPixel;
265267
}
266268
}
269+
270+
// Check stream end
271+
for (int i = 0; i < 7; i++)
272+
{
273+
if (stream.ReadByte() != 0)
274+
{
275+
ThrowInvalidImageContentException();
276+
}
277+
}
278+
279+
if (stream.ReadByte() != 1)
280+
{
281+
ThrowInvalidImageContentException();
282+
}
267283
}
268284

269285
private int GetArrayPosition(Rgba32 pixel) => ((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)) % 64;
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

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

8+
/// <summary>
9+
/// Image encoder for writing an image to a stream as a QOI image
10+
/// </summary>
611
public class QoiEncoder : ImageEncoder
712
{
813
/// <inheritdoc />
914
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
1015
{
11-
throw new NotImplementedException();
16+
QoiEncoderCore encoder = new(image.GetConfiguration(), this);
17+
encoder.Encode(image, stream, cancellationToken);
1218
}
1319
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Buffers.Binary;
5+
using System.Runtime.InteropServices.ComTypes;
6+
using SixLabors.ImageSharp.Memory;
7+
using SixLabors.ImageSharp.PixelFormats;
8+
9+
namespace SixLabors.ImageSharp.Formats.Qoi;
10+
11+
/// <summary>
12+
/// Image encoder for writing an image to a stream as a QOi image
13+
/// </summary>
14+
public class QoiEncoderCore : IImageEncoderInternals
15+
{
16+
/// <summary>
17+
/// The global configuration.
18+
/// </summary>
19+
private Configuration configuration;
20+
21+
/// <summary>
22+
/// The encoder with options.
23+
/// </summary>
24+
private readonly QoiEncoder encoder;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="QoiEncoderCore"/> class.
28+
/// </summary>
29+
/// <param name="configuration">The configuration.</param>
30+
/// <param name="encoder">The encoder with options.</param>
31+
public QoiEncoderCore(Configuration configuration, QoiEncoder encoder)
32+
{
33+
this.configuration = configuration;
34+
this.encoder = encoder;
35+
}
36+
37+
/// <inheritdoc />
38+
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
39+
where TPixel : unmanaged, IPixel<TPixel>
40+
{
41+
Guard.NotNull(image, nameof(image));
42+
Guard.NotNull(stream, nameof(stream));
43+
44+
WriteHeader(image, stream);
45+
WritePixels(image, stream);
46+
WriteEndOfStream(stream);
47+
stream.Flush();
48+
}
49+
50+
private static void WriteHeader(Image image, Stream stream)
51+
{
52+
// Get metadata
53+
Span<byte> width = stackalloc byte[4];
54+
Span<byte> height = stackalloc byte[4];
55+
BinaryPrimitives.WriteUInt32BigEndian(width, (uint)image.Width);
56+
BinaryPrimitives.WriteUInt32BigEndian(height, (uint)image.Height);
57+
QoiChannels qoiChannels = image.PixelType.BitsPerPixel == 24 ? QoiChannels.Rgb : QoiChannels.Rgba;
58+
59+
// I need to check this, how do I check it with the pixel type or metadata of the original image?
60+
const QoiColorSpace qoiColorSpace = QoiColorSpace.SrgbWithLinearAlpha;
61+
62+
// Write header to the stream
63+
stream.Write(QoiConstants.Magic);
64+
stream.Write(width);
65+
stream.Write(height);
66+
stream.WriteByte((byte)qoiChannels);
67+
stream.WriteByte((byte)qoiColorSpace);
68+
}
69+
70+
private static void WritePixels<TPixel>(Image<TPixel> image, Stream stream) where TPixel : unmanaged, IPixel<TPixel>
71+
{
72+
// Start image encoding
73+
Rgba32[] previouslySeenPixels = new Rgba32[64];
74+
Rgba32 previousPixel = new(0, 0, 0, 255);
75+
int pixelArrayPosition = GetArrayPosition(previousPixel);
76+
previouslySeenPixels[pixelArrayPosition] = previousPixel;
77+
78+
Buffer2D<TPixel> pixels = image.Frames[0].PixelBuffer;
79+
Rgba32 currentRgba32 = new();
80+
for (int i = 0; i < pixels.Height; i++)
81+
{
82+
for (int j = 0; j < pixels.Width && i < pixels.Height; j++)
83+
{
84+
// We get the RGBA value from pixels
85+
TPixel currentPixel = pixels[j, i];
86+
currentPixel.ToRgba32(ref currentRgba32);
87+
88+
// First, we check if the current pixel is equal to the previous one
89+
// If so, we do a QOI_OP_RUN
90+
if (currentRgba32.Equals(previousPixel))
91+
{
92+
/* It looks like this isn't an error, but this makes possible that
93+
* files start with a QOI_OP_RUN if their first pixel is a fully opaque
94+
* black. However, the decoder of this project takes that into consideration
95+
*
96+
* To further details, see https://github.com/phoboslab/qoi/issues/258,
97+
* and we should discuss what to do about this approach and
98+
* if it's correct
99+
*/
100+
byte repetitions = 0;
101+
do
102+
{
103+
repetitions++;
104+
j++;
105+
if (j == pixels.Width)
106+
{
107+
j = 0;
108+
i++;
109+
}
110+
111+
if (i == pixels.Height)
112+
{
113+
break;
114+
}
115+
116+
currentPixel = pixels[j, i];
117+
currentPixel.ToRgba32(ref currentRgba32);
118+
} while (currentRgba32.Equals(previousPixel) && repetitions < 62);
119+
120+
j--;
121+
stream.WriteByte((byte)((byte)QoiChunkEnum.QOI_OP_RUN | (repetitions - 1)));
122+
123+
/* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
124+
* it will be taken and compared on the next iteration
125+
*/
126+
continue;
127+
}
128+
129+
// else, we check if it exists in the previously seen pixels
130+
// If so, we do a QOI_OP_INDEX
131+
pixelArrayPosition = GetArrayPosition(currentRgba32);
132+
if (previouslySeenPixels[pixelArrayPosition].Equals(currentPixel))
133+
{
134+
stream.WriteByte((byte)pixelArrayPosition);
135+
}
136+
else
137+
{
138+
// else, we check if the difference is less than -2..1
139+
// Since it wasn't found on the previously seen pixels, we save it
140+
previouslySeenPixels[pixelArrayPosition] = currentRgba32;
141+
142+
sbyte diffRed = (sbyte)(currentRgba32.R - previousPixel.R),
143+
diffGreen = (sbyte)(currentRgba32.G - previousPixel.G),
144+
diffBlue = (sbyte)(currentRgba32.B - previousPixel.B);
145+
146+
// If so, we do a QOI_OP_DIFF
147+
if (diffRed is > -3 and < 2 &&
148+
diffGreen is > -3 and < 2 &&
149+
diffBlue is > -3 and < 2 &&
150+
currentRgba32.A == previousPixel.A)
151+
{
152+
// Bottom limit is -2, so we add 2 to make it equal to 0
153+
byte dr = (byte)(diffRed + 2),
154+
dg = (byte)(diffGreen + 2),
155+
db = (byte)(diffBlue + 2),
156+
valueToWrite = (byte)((byte)QoiChunkEnum.QOI_OP_DIFF | (dr << 4) | (dg << 2) | db);
157+
stream.WriteByte(valueToWrite);
158+
}
159+
else
160+
{
161+
// else, we check if the green difference is less than -32..31 and the rest -8..7
162+
// If so, we do a QOI_OP_LUMA
163+
sbyte diffRedGreen = (sbyte)(diffRed - diffGreen),
164+
diffBlueGreen = (sbyte)(diffBlue - diffGreen);
165+
if (diffGreen is > -33 and < 8 &&
166+
diffRedGreen is > -9 and < 8 &&
167+
diffBlueGreen is > -9 and < 8 &&
168+
currentRgba32.A == previousPixel.A)
169+
{
170+
byte dr_dg = (byte)(diffRedGreen + 8),
171+
db_dg = (byte)(diffBlueGreen + 8),
172+
byteToWrite1 = (byte)((byte)QoiChunkEnum.QOI_OP_LUMA | (diffGreen + 32)),
173+
byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
174+
stream.WriteByte(byteToWrite1);
175+
stream.WriteByte(byteToWrite2);
176+
}
177+
else
178+
{
179+
// else, we check if the alpha is equal to the previous pixel
180+
// If so, we do a QOI_OP_RGB
181+
if (currentRgba32.A == previousPixel.A)
182+
{
183+
stream.WriteByte((byte)QoiChunkEnum.QOI_OP_RGB);
184+
stream.WriteByte(currentRgba32.R);
185+
stream.WriteByte(currentRgba32.G);
186+
stream.WriteByte(currentRgba32.B);
187+
}
188+
else
189+
{
190+
// else, we do a QOI_OP_RGBA
191+
stream.WriteByte((byte)QoiChunkEnum.QOI_OP_RGBA);
192+
stream.WriteByte(currentRgba32.R);
193+
stream.WriteByte(currentRgba32.G);
194+
stream.WriteByte(currentRgba32.B);
195+
stream.WriteByte(currentRgba32.A);
196+
}
197+
}
198+
}
199+
}
200+
201+
previousPixel = currentRgba32;
202+
}
203+
}
204+
}
205+
206+
private static void WriteEndOfStream(Stream stream)
207+
{
208+
// Write bytes to end stream
209+
for (int i = 0; i < 7; i++)
210+
{
211+
stream.WriteByte(0);
212+
}
213+
214+
stream.WriteByte(1);
215+
}
216+
217+
private static int GetArrayPosition(Rgba32 pixel)
218+
=> ((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)) % 64;
219+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Formats.Qoi;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
7+
8+
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
9+
10+
[Trait("Format", "Qoi")]
11+
[ValidateDisposedMemoryAllocations]
12+
public class QoiEncoderTests
13+
{
14+
[Theory]
15+
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32)]
16+
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32)]
17+
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32)]
18+
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32)]
19+
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32)]
20+
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32)]
21+
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32)]
22+
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32)]
23+
private static void Encode<TPixel>(TestImageProvider<TPixel> provider) where TPixel : unmanaged, IPixel<TPixel>
24+
{
25+
using Image<TPixel> image = provider.GetImage();
26+
using MemoryStream stream = new();
27+
QoiEncoder encoder = new();
28+
image.Save(stream, encoder);
29+
stream.Position = 0;
30+
using Image<TPixel> encodedImage = (Image<TPixel>)Image.Load(stream);
31+
ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage);
32+
}
33+
}

0 commit comments

Comments
 (0)