From 07e65973a3eae1b79d5ed5586cc6b98c22f6b1d7 Mon Sep 17 00:00:00 2001 From: LuisAlfredo92 <92luisalfredo@protonmail.com> Date: Wed, 28 Jun 2023 16:30:43 -0600 Subject: [PATCH] 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 --- src/ImageSharp/Formats/Qoi/QoiDecoder.cs | 6 +- src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs | 20 +- src/ImageSharp/Formats/Qoi/QoiEncoder.cs | 8 +- src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs | 219 ++++++++++++++++++ .../Formats/Qoi/QoiEncoderTests.cs | 33 +++ 5 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs create mode 100644 tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoder.cs b/src/ImageSharp/Formats/Qoi/QoiDecoder.cs index 2736f9df3b..a54095dfc6 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoder.cs +++ b/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(); + /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); @@ -23,11 +26,12 @@ internal class QoiDecoder : ImageDecoder return image; } + /// 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(options, stream, cancellationToken); } protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs index af07050558..279dbd2331 100644 --- a/src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs +++ b/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(BufferedReadStream stream, Buffer2D pixels) + private void ProcessPixels(Stream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { 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; diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs index e4aa0177bf..526a84524b 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs +++ b/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; +/// +/// Image encoder for writing an image to a stream as a QOI image +/// public class QoiEncoder : ImageEncoder { /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - throw new NotImplementedException(); + QoiEncoderCore encoder = new(image.GetConfiguration(), this); + encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs new file mode 100644 index 0000000000..463a9be7c0 --- /dev/null +++ b/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; + +/// +/// Image encoder for writing an image to a stream as a QOi image +/// +public class QoiEncoderCore : IImageEncoderInternals +{ + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The encoder with options. + /// + private readonly QoiEncoder encoder; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The encoder with options. + public QoiEncoderCore(Configuration configuration, QoiEncoder encoder) + { + this.configuration = configuration; + this.encoder = encoder; + } + + /// + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + 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 width = stackalloc byte[4]; + Span 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(Image image, Stream stream) where TPixel : unmanaged, IPixel + { + // Start image encoding + Rgba32[] previouslySeenPixels = new Rgba32[64]; + Rgba32 previousPixel = new(0, 0, 0, 255); + int pixelArrayPosition = GetArrayPosition(previousPixel); + previouslySeenPixels[pixelArrayPosition] = previousPixel; + + Buffer2D 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; +} diff --git a/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs new file mode 100644 index 0000000000..98331a8ffe --- /dev/null +++ b/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(TestImageProvider provider) where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + using MemoryStream stream = new(); + QoiEncoder encoder = new(); + image.Save(stream, encoder); + stream.Position = 0; + using Image encodedImage = (Image)Image.Load(stream); + ImageComparingUtils.CompareWithReferenceDecoder(provider, encodedImage); + } +}