mirror of https://github.com/SixLabors/ImageSharp
Browse Source
-Also adding Decode method without TPixel value -Adding stream end check to decoder (we must discuss if it's necesarry or not) -formating general codeqoi
5 changed files with 282 additions and 4 deletions
@ -1,13 +1,19 @@ |
|||||
// Copyright (c) Six Labors.
|
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
|
||||
namespace SixLabors.ImageSharp.Formats.Qoi; |
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Image encoder for writing an image to a stream as a QOI image
|
||||
|
/// </summary>
|
||||
public class QoiEncoder : ImageEncoder |
public class QoiEncoder : ImageEncoder |
||||
{ |
{ |
||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
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); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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; |
||||
|
} |
||||
@ -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…
Reference in new issue