Browse Source

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
qoi
LuisAlfredo92 3 years ago
parent
commit
07e65973a3
No known key found for this signature in database GPG Key ID: 13A8436905993B8F
  1. 6
      src/ImageSharp/Formats/Qoi/QoiDecoder.cs
  2. 20
      src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
  3. 8
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  4. 219
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  5. 33
      tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs

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

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi;
internal class QoiDecoder : ImageDecoder
{
@ -10,6 +12,7 @@ internal class QoiDecoder : ImageDecoder
public static QoiDecoder Instance { get; } = new();
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
@ -23,11 +26,12 @@ internal class QoiDecoder : ImageDecoder
return image;
}
/// <inheritdoc />
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
throw new NotImplementedException();
return this.Decode<Rgba32>(options, stream, cancellationToken);
}
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

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

@ -141,11 +141,11 @@ internal class QoiDecoderCore : IImageDecoderInternals
private static void ThrowInvalidImageContentException()
=> throw new InvalidImageContentException("The image is not a valid QOI image.");
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
private void ProcessPixels<TPixel>(Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32[] previouslySeenPixels = new Rgba32[64];
Rgba32 previousPixel = new (0,0,0,255);
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
@ -258,12 +258,28 @@ internal class QoiDecoderCore : IImageDecoderInternals
ThrowInvalidImageContentException();
return;
}
break;
}
pixels[j,i] = pixel;
previousPixel = readPixel;
}
}
// Check stream end
for (int i = 0; i < 7; i++)
{
if (stream.ReadByte() != 0)
{
ThrowInvalidImageContentException();
}
}
if (stream.ReadByte() != 1)
{
ThrowInvalidImageContentException();
}
}
private int GetArrayPosition(Rgba32 pixel) => ((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)) % 64;

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

@ -1,13 +1,19 @@
// 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
{
/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
throw new NotImplementedException();
QoiEncoderCore encoder = new(image.GetConfiguration(), this);
encoder.Encode(image, stream, cancellationToken);
}
}

219
src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs

@ -0,0 +1,219 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Runtime.InteropServices.ComTypes;
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>
public class QoiEncoderCore : IImageEncoderInternals
{
/// <summary>
/// The global configuration.
/// </summary>
private Configuration configuration;
/// <summary>
/// The encoder with options.
/// </summary>
private readonly QoiEncoder encoder;
/// <summary>
/// Initializes a new instance of the <see cref="QoiEncoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="encoder">The encoder with options.</param>
public QoiEncoderCore(Configuration configuration, QoiEncoder encoder)
{
this.configuration = configuration;
this.encoder = encoder;
}
/// <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));
WriteHeader(image, stream);
WritePixels(image, stream);
WriteEndOfStream(stream);
stream.Flush();
}
private static 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 = image.PixelType.BitsPerPixel == 24 ? QoiChannels.Rgb : QoiChannels.Rgba;
// I need to check this, how do I check it with the pixel type or metadata of the original image?
const QoiColorSpace qoiColorSpace = 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 static void WritePixels<TPixel>(Image<TPixel> image, Stream stream) where TPixel : unmanaged, IPixel<TPixel>
{
// Start image encoding
Rgba32[] previouslySeenPixels = new Rgba32[64];
Rgba32 previousPixel = new(0, 0, 0, 255);
int pixelArrayPosition = GetArrayPosition(previousPixel);
previouslySeenPixels[pixelArrayPosition] = previousPixel;
Buffer2D<TPixel> pixels = image.Frames[0].PixelBuffer;
Rgba32 currentRgba32 = new();
for (int i = 0; i < pixels.Height; i++)
{
for (int j = 0; j < pixels.Width && i < pixels.Height; j++)
{
// We get the RGBA value from pixels
TPixel currentPixel = pixels[j, i];
currentPixel.ToRgba32(ref currentRgba32);
// 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
*/
byte repetitions = 0;
do
{
repetitions++;
j++;
if (j == pixels.Width)
{
j = 0;
i++;
}
if (i == pixels.Height)
{
break;
}
currentPixel = pixels[j, i];
currentPixel.ToRgba32(ref currentRgba32);
} while (currentRgba32.Equals(previousPixel) && repetitions < 62);
j--;
stream.WriteByte((byte)((byte)QoiChunkEnum.QOI_OP_RUN | (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
pixelArrayPosition = GetArrayPosition(currentRgba32);
if (previouslySeenPixels[pixelArrayPosition].Equals(currentPixel))
{
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;
sbyte diffRed = (sbyte)(currentRgba32.R - previousPixel.R),
diffGreen = (sbyte)(currentRgba32.G - previousPixel.G),
diffBlue = (sbyte)(currentRgba32.B - previousPixel.B);
// If so, we do a QOI_OP_DIFF
if (diffRed is > -3 and < 2 &&
diffGreen is > -3 and < 2 &&
diffBlue is > -3 and < 2 &&
currentRgba32.A == previousPixel.A)
{
// Bottom limit is -2, so we add 2 to make it equal to 0
byte dr = (byte)(diffRed + 2),
dg = (byte)(diffGreen + 2),
db = (byte)(diffBlue + 2),
valueToWrite = (byte)((byte)QoiChunkEnum.QOI_OP_DIFF | (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
sbyte diffRedGreen = (sbyte)(diffRed - diffGreen),
diffBlueGreen = (sbyte)(diffBlue - diffGreen);
if (diffGreen is > -33 and < 8 &&
diffRedGreen is > -9 and < 8 &&
diffBlueGreen is > -9 and < 8 &&
currentRgba32.A == previousPixel.A)
{
byte dr_dg = (byte)(diffRedGreen + 8),
db_dg = (byte)(diffBlueGreen + 8),
byteToWrite1 = (byte)((byte)QoiChunkEnum.QOI_OP_LUMA | (diffGreen + 32)),
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)QoiChunkEnum.QOI_OP_RGB);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
}
else
{
// else, we do a QOI_OP_RGBA
stream.WriteByte((byte)QoiChunkEnum.QOI_OP_RGBA);
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);
}
private static int GetArrayPosition(Rgba32 pixel)
=> ((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)) % 64;
}

33
tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs

@ -0,0 +1,33 @@
// 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;
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
[Trait("Format", "Qoi")]
[ValidateDisposedMemoryAllocations]
public class QoiEncoderTests
{
[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)]
private static void Encode<TPixel>(TestImageProvider<TPixel> provider) where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
using MemoryStream stream = new();
QoiEncoder encoder = new();
image.Save(stream, encoder);
stream.Position = 0;
using Image<TPixel> encodedImage = (Image<TPixel>)Image.Load(stream);
ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage);
}
}
Loading…
Cancel
Save