mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
51 changed files with 1435 additions and 64 deletions
@ -1 +1 @@ |
|||||
Subproject commit 9a6cf00d9a3d482bb08211dd8309f4724a2735cb |
Subproject commit 353b9afe32a8000410312d17263407cd7bb82d19 |
||||
@ -0,0 +1,20 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Qoi; |
||||
|
using SixLabors.ImageSharp.Metadata; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Extension methods for the <see cref="ImageMetadata"/> type.
|
||||
|
/// </summary>
|
||||
|
public static partial class MetadataExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the qoi format specific metadata for the image.
|
||||
|
/// </summary>
|
||||
|
/// <param name="metadata">The metadata this method extends.</param>
|
||||
|
/// <returns>The <see cref="QoiMetadata"/>.</returns>
|
||||
|
public static QoiMetadata GetQoiMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(QoiFormat.Instance); |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides enumeration of available QOI color channels.
|
||||
|
/// </summary>
|
||||
|
public enum QoiChannels |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Each pixel is an R,G,B triple.
|
||||
|
/// </summary>
|
||||
|
Rgb = 3, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Each pixel is an R,G,B triple, followed by an alpha sample.
|
||||
|
/// </summary>
|
||||
|
Rgba = 4 |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enum that contains the operations that encoder and decoder must process, written
|
||||
|
/// in binary to be easier to compare them in the reference
|
||||
|
/// </summary>
|
||||
|
internal enum QoiChunk |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Indicates that the operation is QOI_OP_RGB where the RGB values are written
|
||||
|
/// in one byte each one after this marker
|
||||
|
/// </summary>
|
||||
|
QoiOpRgb = 0b11111110, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that the operation is QOI_OP_RGBA where the RGBA values are written
|
||||
|
/// in one byte each one after this marker
|
||||
|
/// </summary>
|
||||
|
QoiOpRgba = 0b11111111, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that the operation is QOI_OP_INDEX where one byte contains a 2-bit
|
||||
|
/// marker (0b00) followed by an index on the previously seen pixels array 0..63
|
||||
|
/// </summary>
|
||||
|
QoiOpIndex = 0b00000000, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that the operation is QOI_OP_DIFF where one byte contains a 2-bit
|
||||
|
/// marker (0b01) followed by 2-bit differences in red, green and blue channel
|
||||
|
/// with the previous pixel with a bias of 2 (-2..1)
|
||||
|
/// </summary>
|
||||
|
QoiOpDiff = 0b01000000, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that the operation is QOI_OP_LUMA where one byte contains a 2-bit
|
||||
|
/// marker (0b01) followed by a 6-bits number that indicates the difference of
|
||||
|
/// the green channel with the previous pixel. Then another byte that contains
|
||||
|
/// a 4-bit number that indicates the difference of the red channel minus the
|
||||
|
/// previous difference, and another 4-bit number that indicates the difference
|
||||
|
/// of the blue channel minus the green difference
|
||||
|
/// Example: 0b10[6-bits diff green] 0b[6-bits dr-dg][6-bits db-dg]
|
||||
|
/// dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g)
|
||||
|
/// db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g)
|
||||
|
/// </summary>
|
||||
|
QoiOpLuma = 0b10000000, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Indicates that the operation is QOI_OP_RUN where one byte contains a 2-bit
|
||||
|
/// marker (0b11) followed by a 6-bits number that indicates the times that the
|
||||
|
/// previous pixel is repeated
|
||||
|
/// </summary>
|
||||
|
QoiOpRun = 0b11000000 |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
// ReSharper disable InconsistentNaming
|
||||
|
// ReSharper disable IdentifierTypo
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enum for the different QOI color spaces.
|
||||
|
/// </summary>
|
||||
|
public enum QoiColorSpace |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// sRGB color space with linear alpha value
|
||||
|
/// </summary>
|
||||
|
SrgbWithLinearAlpha, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// All the values in the color space are linear
|
||||
|
/// </summary>
|
||||
|
AllChannelsLinear |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Registers the image encoders, decoders and mime type detectors for the qoi format.
|
||||
|
/// </summary>
|
||||
|
public sealed class QoiConfigurationModule : IImageFormatConfigurationModule |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public void Configure(Configuration configuration) |
||||
|
{ |
||||
|
configuration.ImageFormatsManager.SetDecoder(QoiFormat.Instance, QoiDecoder.Instance); |
||||
|
configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder()); |
||||
|
configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Text; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
internal static class QoiConstants |
||||
|
{ |
||||
|
private static readonly byte[] SMagic = Encoding.UTF8.GetBytes("qoif"); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the bytes that indicates the image is QOI
|
||||
|
/// </summary>
|
||||
|
public static ReadOnlySpan<byte> Magic => SMagic; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the list of mimetypes that equate to a QOI.
|
||||
|
/// See https://github.com/phoboslab/qoi/issues/167
|
||||
|
/// </summary>
|
||||
|
public static string[] MimeTypes { get; } = { "image/qoi", "image/x-qoi", "image/vnd.qoi" }; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the list of file extensions that equate to a QOI.
|
||||
|
/// </summary>
|
||||
|
public static string[] FileExtensions { get; } = { "qoi" }; |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
internal class QoiDecoder : ImageDecoder |
||||
|
{ |
||||
|
private QoiDecoder() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public static QoiDecoder Instance { get; } = new(); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
Guard.NotNull(options, nameof(options)); |
||||
|
Guard.NotNull(stream, nameof(stream)); |
||||
|
|
||||
|
QoiDecoderCore decoder = new(options); |
||||
|
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken); |
||||
|
|
||||
|
ScaleToTargetSize(options, image); |
||||
|
|
||||
|
return image; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
Guard.NotNull(options, nameof(options)); |
||||
|
Guard.NotNull(stream, nameof(stream)); |
||||
|
return this.Decode<Rgba32>(options, stream, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
Guard.NotNull(options, nameof(options)); |
||||
|
Guard.NotNull(stream, nameof(stream)); |
||||
|
return new QoiDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,291 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers; |
||||
|
using System.Buffers.Binary; |
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using SixLabors.ImageSharp.IO; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.Metadata; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
internal class QoiDecoderCore : IImageDecoderInternals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The global configuration.
|
||||
|
/// </summary>
|
||||
|
private readonly Configuration configuration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Used the manage memory allocations.
|
||||
|
/// </summary>
|
||||
|
private readonly MemoryAllocator memoryAllocator; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The QOI header.
|
||||
|
/// </summary>
|
||||
|
private QoiHeader header; |
||||
|
|
||||
|
public QoiDecoderCore(DecoderOptions options) |
||||
|
{ |
||||
|
this.Options = options; |
||||
|
this.configuration = options.Configuration; |
||||
|
this.memoryAllocator = this.configuration.MemoryAllocator; |
||||
|
} |
||||
|
|
||||
|
public DecoderOptions Options { get; } |
||||
|
|
||||
|
public Size Dimensions { get; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
// Process the header to get metadata
|
||||
|
this.ProcessHeader(stream); |
||||
|
|
||||
|
// Create Image object
|
||||
|
ImageMetadata metadata = new() |
||||
|
{ |
||||
|
DecodedImageFormat = QoiFormat.Instance, |
||||
|
HorizontalResolution = this.header.Width, |
||||
|
VerticalResolution = this.header.Height, |
||||
|
ResolutionUnits = PixelResolutionUnit.AspectRatio |
||||
|
}; |
||||
|
QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); |
||||
|
qoiMetadata.Channels = this.header.Channels; |
||||
|
qoiMetadata.ColorSpace = this.header.ColorSpace; |
||||
|
Image<TPixel> image = new(this.configuration, (int)this.header.Width, (int)this.header.Height, metadata); |
||||
|
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); |
||||
|
|
||||
|
this.ProcessPixels(stream, pixels); |
||||
|
|
||||
|
return image; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
this.ProcessHeader(stream); |
||||
|
PixelTypeInfo pixelType = new(8 * (int)this.header.Channels); |
||||
|
Size size = new((int)this.header.Width, (int)this.header.Height); |
||||
|
|
||||
|
ImageMetadata metadata = new(); |
||||
|
QoiMetadata qoiMetadata = metadata.GetQoiMetadata(); |
||||
|
qoiMetadata.Channels = this.header.Channels; |
||||
|
qoiMetadata.ColorSpace = this.header.ColorSpace; |
||||
|
|
||||
|
return new ImageInfo(pixelType, size, metadata); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Processes the 14-byte header to validate the image and save the metadata
|
||||
|
/// in <see cref="header"/>
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream where the bytes are being read</param>
|
||||
|
/// <exception cref="InvalidImageContentException">If the stream doesn't store a qoi image</exception>
|
||||
|
private void ProcessHeader(BufferedReadStream stream) |
||||
|
{ |
||||
|
Span<byte> magicBytes = stackalloc byte[4]; |
||||
|
Span<byte> widthBytes = stackalloc byte[4]; |
||||
|
Span<byte> heightBytes = stackalloc byte[4]; |
||||
|
|
||||
|
// Read magic bytes
|
||||
|
int read = stream.Read(magicBytes); |
||||
|
if (read != 4 || !magicBytes.SequenceEqual(QoiConstants.Magic.ToArray())) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
// If it's a qoi image, read the rest of properties
|
||||
|
read = stream.Read(widthBytes); |
||||
|
if (read != 4) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
read = stream.Read(heightBytes); |
||||
|
if (read != 4) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
// These numbers are in Big Endian so we have to reverse them to get the real number
|
||||
|
uint width = BinaryPrimitives.ReadUInt32BigEndian(widthBytes); |
||||
|
uint height = BinaryPrimitives.ReadUInt32BigEndian(heightBytes); |
||||
|
if (width == 0 || height == 0) |
||||
|
{ |
||||
|
throw new InvalidImageContentException( |
||||
|
$"The image has an invalid size: width = {width}, height = {height}"); |
||||
|
} |
||||
|
|
||||
|
int channels = stream.ReadByte(); |
||||
|
if (channels is -1 or (not 3 and not 4)) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
int colorSpace = stream.ReadByte(); |
||||
|
if (colorSpace is -1 or (not 0 and not 1)) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
this.header = new QoiHeader(width, height, (QoiChannels)channels, (QoiColorSpace)colorSpace); |
||||
|
} |
||||
|
|
||||
|
[DoesNotReturn] |
||||
|
private static void ThrowInvalidImageContentException() |
||||
|
=> throw new InvalidImageContentException("The image is not a valid QOI image."); |
||||
|
|
||||
|
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
using IMemoryOwner<Rgba32> previouslySeenPixelsBuffer = this.memoryAllocator.Allocate<Rgba32>(64, AllocationOptions.Clean); |
||||
|
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan(); |
||||
|
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
|
||||
|
int pixelArrayPosition = GetArrayPosition(previousPixel); |
||||
|
previouslySeenPixels[pixelArrayPosition] = previousPixel; |
||||
|
byte operationByte; |
||||
|
Rgba32 readPixel = default; |
||||
|
Span<byte> pixelBytes = MemoryMarshal.CreateSpan(ref Unsafe.As<Rgba32, byte>(ref readPixel), 4); |
||||
|
TPixel pixel = default; |
||||
|
|
||||
|
for (int i = 0; i < this.header.Height; i++) |
||||
|
{ |
||||
|
Span<TPixel> row = pixels.DangerousGetRowSpan(i); |
||||
|
for (int j = 0; j < row.Length; j++) |
||||
|
{ |
||||
|
operationByte = (byte)stream.ReadByte(); |
||||
|
switch ((QoiChunk)operationByte) |
||||
|
{ |
||||
|
// Reading one pixel with previous alpha intact
|
||||
|
case QoiChunk.QoiOpRgb: |
||||
|
if (stream.Read(pixelBytes[..3]) < 3) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
readPixel.A = previousPixel.A; |
||||
|
pixel.FromRgba32(readPixel); |
||||
|
pixelArrayPosition = GetArrayPosition(readPixel); |
||||
|
previouslySeenPixels[pixelArrayPosition] = readPixel; |
||||
|
break; |
||||
|
|
||||
|
// Reading one pixel with new alpha
|
||||
|
case QoiChunk.QoiOpRgba: |
||||
|
if (stream.Read(pixelBytes) < 4) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
pixel.FromRgba32(readPixel); |
||||
|
pixelArrayPosition = GetArrayPosition(readPixel); |
||||
|
previouslySeenPixels[pixelArrayPosition] = readPixel; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
switch ((QoiChunk)(operationByte & 0b11000000)) |
||||
|
{ |
||||
|
// Getting one pixel from previously seen pixels
|
||||
|
case QoiChunk.QoiOpIndex: |
||||
|
readPixel = previouslySeenPixels[operationByte]; |
||||
|
pixel.FromRgba32(readPixel); |
||||
|
break; |
||||
|
|
||||
|
// Get one pixel from the difference (-2..1) of the previous pixel
|
||||
|
case QoiChunk.QoiOpDiff: |
||||
|
int redDifference = (operationByte & 0b00110000) >> 4; |
||||
|
int greenDifference = (operationByte & 0b00001100) >> 2; |
||||
|
int blueDifference = operationByte & 0b00000011; |
||||
|
readPixel = previousPixel with |
||||
|
{ |
||||
|
R = (byte)Numerics.Modulo256(previousPixel.R + (redDifference - 2)), |
||||
|
G = (byte)Numerics.Modulo256(previousPixel.G + (greenDifference - 2)), |
||||
|
B = (byte)Numerics.Modulo256(previousPixel.B + (blueDifference - 2)) |
||||
|
}; |
||||
|
pixel.FromRgba32(readPixel); |
||||
|
pixelArrayPosition = GetArrayPosition(readPixel); |
||||
|
previouslySeenPixels[pixelArrayPosition] = readPixel; |
||||
|
break; |
||||
|
|
||||
|
// Get green difference in 6 bits and red and blue differences
|
||||
|
// depending on the green one
|
||||
|
case QoiChunk.QoiOpLuma: |
||||
|
int diffGreen = operationByte & 0b00111111; |
||||
|
int currentGreen = Numerics.Modulo256(previousPixel.G + (diffGreen - 32)); |
||||
|
int nextByte = stream.ReadByte(); |
||||
|
int diffRedDG = nextByte >> 4; |
||||
|
int diffBlueDG = nextByte & 0b00001111; |
||||
|
int currentRed = Numerics.Modulo256(diffRedDG - 8 + (diffGreen - 32) + previousPixel.R); |
||||
|
int currentBlue = Numerics.Modulo256(diffBlueDG - 8 + (diffGreen - 32) + previousPixel.B); |
||||
|
readPixel = previousPixel with { R = (byte)currentRed, B = (byte)currentBlue, G = (byte)currentGreen }; |
||||
|
pixel.FromRgba32(readPixel); |
||||
|
pixelArrayPosition = GetArrayPosition(readPixel); |
||||
|
previouslySeenPixels[pixelArrayPosition] = readPixel; |
||||
|
break; |
||||
|
|
||||
|
// Repeating the previous pixel 1..63 times
|
||||
|
case QoiChunk.QoiOpRun: |
||||
|
int repetitions = operationByte & 0b00111111; |
||||
|
if (repetitions is 62 or 63) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
|
||||
|
readPixel = previousPixel; |
||||
|
pixel.FromRgba32(readPixel); |
||||
|
for (int k = -1; k < repetitions; k++, j++) |
||||
|
{ |
||||
|
if (j == row.Length) |
||||
|
{ |
||||
|
j = 0; |
||||
|
i++; |
||||
|
row = pixels.DangerousGetRowSpan(i); |
||||
|
} |
||||
|
|
||||
|
row[j] = pixel; |
||||
|
} |
||||
|
|
||||
|
j--; |
||||
|
continue; |
||||
|
|
||||
|
default: |
||||
|
ThrowInvalidImageContentException(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
row[j] = pixel; |
||||
|
previousPixel = readPixel; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Check stream end
|
||||
|
for (int i = 0; i < 7; i++) |
||||
|
{ |
||||
|
if (stream.ReadByte() != 0) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (stream.ReadByte() != 1) |
||||
|
{ |
||||
|
ThrowInvalidImageContentException(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static int GetArrayPosition(Rgba32 pixel) |
||||
|
=> Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)); |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// 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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the color channels on the image that can be
|
||||
|
/// RGB or RGBA. This is purely informative. It doesn't
|
||||
|
/// change the way data chunks are encoded.
|
||||
|
/// </summary>
|
||||
|
public QoiChannels? Channels { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the color space of the image that can be sRGB with
|
||||
|
/// linear alpha or all channels linear. This is purely
|
||||
|
/// informative. It doesn't change the way data chunks are encoded.
|
||||
|
/// </summary>
|
||||
|
public QoiColorSpace? ColorSpace { get; init; } |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration()); |
||||
|
encoder.Encode(image, stream, cancellationToken); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,231 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers; |
||||
|
using System.Buffers.Binary; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
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>
|
||||
|
internal class QoiEncoderCore : IImageEncoderInternals |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The encoder with options
|
||||
|
/// </summary>
|
||||
|
private readonly QoiEncoder encoder; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Used the manage memory allocations.
|
||||
|
/// </summary>
|
||||
|
private readonly MemoryAllocator memoryAllocator; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The configuration instance for the encoding operation.
|
||||
|
/// </summary>
|
||||
|
private readonly Configuration configuration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="QoiEncoderCore"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="encoder">The encoder with options.</param>
|
||||
|
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
|
||||
|
/// <param name="configuration">The configuration of the Encoder.</param>
|
||||
|
public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration) |
||||
|
{ |
||||
|
this.encoder = encoder; |
||||
|
this.memoryAllocator = memoryAllocator; |
||||
|
this.configuration = configuration; |
||||
|
} |
||||
|
|
||||
|
/// <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)); |
||||
|
|
||||
|
this.WriteHeader(image, stream); |
||||
|
this.WritePixels(image, stream); |
||||
|
WriteEndOfStream(stream); |
||||
|
stream.Flush(); |
||||
|
} |
||||
|
|
||||
|
private 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 = this.encoder.Channels ?? QoiChannels.Rgba; |
||||
|
QoiColorSpace qoiColorSpace = this.encoder.ColorSpace ?? 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 void WritePixels<TPixel>(Image<TPixel> image, Stream stream) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
// Start image encoding
|
||||
|
using IMemoryOwner<Rgba32> previouslySeenPixelsBuffer = this.memoryAllocator.Allocate<Rgba32>(64, AllocationOptions.Clean); |
||||
|
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan(); |
||||
|
Rgba32 previousPixel = new(0, 0, 0, 255); |
||||
|
Rgba32 currentRgba32 = default; |
||||
|
Buffer2D<TPixel> pixels = image.Frames[0].PixelBuffer; |
||||
|
using IMemoryOwner<Rgba32> rgbaRowBuffer = this.memoryAllocator.Allocate<Rgba32>(pixels.Width); |
||||
|
Span<Rgba32> rgbaRow = rgbaRowBuffer.GetSpan(); |
||||
|
|
||||
|
for (int i = 0; i < pixels.Height; i++) |
||||
|
{ |
||||
|
Span<TPixel> row = pixels.DangerousGetRowSpan(i); |
||||
|
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow); |
||||
|
for (int j = 0; j < row.Length && i < pixels.Height; j++) |
||||
|
{ |
||||
|
// We get the RGBA value from pixels
|
||||
|
currentRgba32 = rgbaRow[j]; |
||||
|
|
||||
|
// 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 |
||||
|
*/ |
||||
|
int repetitions = 0; |
||||
|
do |
||||
|
{ |
||||
|
repetitions++; |
||||
|
j++; |
||||
|
if (j == row.Length) |
||||
|
{ |
||||
|
j = 0; |
||||
|
i++; |
||||
|
if (i == pixels.Height) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
row = pixels.DangerousGetRowSpan(i); |
||||
|
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow); |
||||
|
} |
||||
|
|
||||
|
currentRgba32 = rgbaRow[j]; |
||||
|
} |
||||
|
while (currentRgba32.Equals(previousPixel) && repetitions < 62); |
||||
|
|
||||
|
j--; |
||||
|
stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (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
|
||||
|
int pixelArrayPosition = GetArrayPosition(currentRgba32); |
||||
|
if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32)) |
||||
|
{ |
||||
|
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; |
||||
|
|
||||
|
int diffRed = currentRgba32.R - previousPixel.R; |
||||
|
int diffGreen = currentRgba32.G - previousPixel.G; |
||||
|
int diffBlue = currentRgba32.B - previousPixel.B; |
||||
|
|
||||
|
// If so, we do a QOI_OP_DIFF
|
||||
|
if (diffRed is >= -2 and <= 1 && |
||||
|
diffGreen is >= -2 and <= 1 && |
||||
|
diffBlue is >= -2 and <= 1 && |
||||
|
currentRgba32.A == previousPixel.A) |
||||
|
{ |
||||
|
// Bottom limit is -2, so we add 2 to make it equal to 0
|
||||
|
int dr = diffRed + 2; |
||||
|
int dg = diffGreen + 2; |
||||
|
int db = diffBlue + 2; |
||||
|
byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (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
|
||||
|
int diffRedGreen = diffRed - diffGreen; |
||||
|
int diffBlueGreen = diffBlue - diffGreen; |
||||
|
if (diffGreen is >= -32 and <= 31 && |
||||
|
diffRedGreen is >= -8 and <= 7 && |
||||
|
diffBlueGreen is >= -8 and <= 7 && |
||||
|
currentRgba32.A == previousPixel.A) |
||||
|
{ |
||||
|
int dr_dg = diffRedGreen + 8; |
||||
|
int db_dg = diffBlueGreen + 8; |
||||
|
byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32)); |
||||
|
byte 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)QoiChunk.QoiOpRgb); |
||||
|
stream.WriteByte(currentRgba32.R); |
||||
|
stream.WriteByte(currentRgba32.G); |
||||
|
stream.WriteByte(currentRgba32.B); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// else, we do a QOI_OP_RGBA
|
||||
|
stream.WriteByte((byte)QoiChunk.QoiOpRgba); |
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static int GetArrayPosition(Rgba32 pixel) |
||||
|
=> Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11)); |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Registers the image encoders, decoders and mime type detectors for the qoi format.
|
||||
|
/// </summary>
|
||||
|
public sealed class QoiFormat : IImageFormat<QoiMetadata> |
||||
|
{ |
||||
|
private QoiFormat() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the shared instance.
|
||||
|
/// </summary>
|
||||
|
public static QoiFormat Instance { get; } = new QoiFormat(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public string DefaultMimeType => "image/qoi"; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public string Name => "QOI"; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IEnumerable<string> MimeTypes => QoiConstants.MimeTypes; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IEnumerable<string> FileExtensions => QoiConstants.FileExtensions; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public QoiMetadata CreateDefaultFormatMetadata() => new(); |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Text; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents the qoi header chunk.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct QoiHeader |
||||
|
{ |
||||
|
public QoiHeader(uint width, uint height, QoiChannels channels, QoiColorSpace colorSpace) |
||||
|
{ |
||||
|
this.Width = width; |
||||
|
this.Height = height; |
||||
|
this.Channels = channels; |
||||
|
this.ColorSpace = colorSpace; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the magic bytes "qoif"
|
||||
|
/// </summary>
|
||||
|
public byte[] Magic { get; } = Encoding.UTF8.GetBytes("qoif"); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the image width in pixels (Big Endian)
|
||||
|
/// </summary>
|
||||
|
public uint Width { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the image height in pixels (Big Endian)
|
||||
|
/// </summary>
|
||||
|
public uint Height { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the color channels of the image. 3 = RGB, 4 = RGBA.
|
||||
|
/// </summary>
|
||||
|
public QoiChannels Channels { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
|
||||
|
/// </summary>
|
||||
|
public QoiColorSpace ColorSpace { get; } |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Detects qoi file headers
|
||||
|
/// </summary>
|
||||
|
public class QoiImageFormatDetector : IImageFormatDetector |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public int HeaderSize => 14; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format) |
||||
|
{ |
||||
|
format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null; |
||||
|
return format != null; |
||||
|
} |
||||
|
|
||||
|
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) |
||||
|
=> header.Length >= this.HeaderSize && QoiConstants.Magic.SequenceEqual(header[..4]); |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Qoi; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides Qoi specific metadata information for the image.
|
||||
|
/// </summary>
|
||||
|
public class QoiMetadata : IDeepCloneable |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
|
||||
|
/// </summary>
|
||||
|
public QoiMetadata() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="other">The metadata to create an instance from.</param>
|
||||
|
private QoiMetadata(QoiMetadata other) |
||||
|
{ |
||||
|
this.Channels = other.Channels; |
||||
|
this.ColorSpace = other.ColorSpace; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets color channels of the image. 3 = RGB, 4 = RGBA.
|
||||
|
/// </summary>
|
||||
|
public QoiChannels Channels { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
|
||||
|
/// </summary>
|
||||
|
public QoiColorSpace ColorSpace { get; set; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IDeepCloneable DeepClone() => new QoiMetadata(this); |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,135 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Qoi; |
||||
|
using SixLabors.ImageSharp.Formats; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Qoi; |
||||
|
|
||||
|
public class ImageExtensionsTest |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void SaveAsQoi_Path() |
||||
|
{ |
||||
|
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); |
||||
|
string file = Path.Combine(dir, "SaveAsQoi_Path.qoi"); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
image.SaveAsQoi(file); |
||||
|
} |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(file); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SaveAsQoiAsync_Path() |
||||
|
{ |
||||
|
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); |
||||
|
string file = Path.Combine(dir, "SaveAsQoiAsync_Path.qoi"); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
await image.SaveAsQoiAsync(file); |
||||
|
} |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(file); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SaveAsQoi_Path_Encoder() |
||||
|
{ |
||||
|
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); |
||||
|
string file = Path.Combine(dir, "SaveAsQoi_Path_Encoder.qoi"); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
image.SaveAsQoi(file, new QoiEncoder()); |
||||
|
} |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(file); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SaveAsQoiAsync_Path_Encoder() |
||||
|
{ |
||||
|
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); |
||||
|
string file = Path.Combine(dir, "SaveAsQoiAsync_Path_Encoder.qoi"); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
await image.SaveAsQoiAsync(file, new QoiEncoder()); |
||||
|
} |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(file); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SaveAsQoi_Stream() |
||||
|
{ |
||||
|
using MemoryStream memoryStream = new(); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
image.SaveAsQoi(memoryStream); |
||||
|
} |
||||
|
|
||||
|
memoryStream.Position = 0; |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(memoryStream); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SaveAsQoiAsync_StreamAsync() |
||||
|
{ |
||||
|
using MemoryStream memoryStream = new(); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
await image.SaveAsQoiAsync(memoryStream); |
||||
|
} |
||||
|
|
||||
|
memoryStream.Position = 0; |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(memoryStream); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SaveAsQoi_Stream_Encoder() |
||||
|
{ |
||||
|
using MemoryStream memoryStream = new(); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
image.SaveAsQoi(memoryStream, new QoiEncoder()); |
||||
|
} |
||||
|
|
||||
|
memoryStream.Position = 0; |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(memoryStream); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task SaveAsQoiAsync_Stream_Encoder() |
||||
|
{ |
||||
|
using MemoryStream memoryStream = new(); |
||||
|
|
||||
|
using (Image<L8> image = new(10, 10)) |
||||
|
{ |
||||
|
await image.SaveAsQoiAsync(memoryStream, new QoiEncoder()); |
||||
|
} |
||||
|
|
||||
|
memoryStream.Position = 0; |
||||
|
|
||||
|
IImageFormat format = Image.DetectFormat(memoryStream); |
||||
|
Assert.True(format is QoiFormat); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Qoi; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Qoi; |
||||
|
|
||||
|
[Trait("Format", "Qoi")] |
||||
|
[ValidateDisposedMemoryAllocations] |
||||
|
public class QoiDecoderTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(TestImages.Qoi.Dice, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.EdgeCase, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.Kodim10, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.Kodim23, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.QoiLogo, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.TestCard, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.TestCardRGBA, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[InlineData(TestImages.Qoi.Wikipedia008, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
public void Identify(string imagePath, QoiChannels channels, QoiColorSpace colorSpace) |
||||
|
{ |
||||
|
TestFile testFile = TestFile.Create(imagePath); |
||||
|
using MemoryStream stream = new(testFile.Bytes, false); |
||||
|
|
||||
|
ImageInfo imageInfo = Image.Identify(stream); |
||||
|
QoiMetadata qoiMetadata = imageInfo.Metadata.GetQoiMetadata(); |
||||
|
|
||||
|
Assert.NotNull(imageInfo); |
||||
|
Assert.Equal(imageInfo.Metadata.DecodedImageFormat, QoiFormat.Instance); |
||||
|
Assert.Equal(qoiMetadata.Channels, channels); |
||||
|
Assert.Equal(qoiMetadata.ColorSpace, colorSpace); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
public void Decode<TPixel>(TestImageProvider<TPixel> provider, QoiChannels channels, QoiColorSpace colorSpace) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
using Image<TPixel> image = provider.GetImage(); |
||||
|
QoiMetadata qoiMetadata = image.Metadata.GetQoiMetadata(); |
||||
|
image.DebugSave(provider); |
||||
|
|
||||
|
image.CompareToReferenceOutput(provider); |
||||
|
Assert.Equal(qoiMetadata.Channels, channels); |
||||
|
Assert.Equal(qoiMetadata.ColorSpace, colorSpace); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
// 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; |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Qoi; |
||||
|
|
||||
|
[Trait("Format", "Qoi")] |
||||
|
[ValidateDisposedMemoryAllocations] |
||||
|
public class QoiEncoderTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)] |
||||
|
public static void Encode<TPixel>(TestImageProvider<TPixel> provider, QoiChannels channels, QoiColorSpace colorSpace) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder()); |
||||
|
using MemoryStream stream = new(); |
||||
|
QoiEncoder encoder = new() |
||||
|
{ |
||||
|
Channels = channels, |
||||
|
ColorSpace = colorSpace |
||||
|
}; |
||||
|
image.Save(stream, encoder); |
||||
|
stream.Position = 0; |
||||
|
|
||||
|
using Image<TPixel> encodedImage = (Image<TPixel>)Image.Load(stream); |
||||
|
QoiMetadata qoiMetadata = encodedImage.Metadata.GetQoiMetadata(); |
||||
|
|
||||
|
ImageComparer.Exact.CompareImages(image, encodedImage); |
||||
|
Assert.Equal(qoiMetadata.Channels, channels); |
||||
|
Assert.Equal(qoiMetadata.ColorSpace, colorSpace); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:8e4a5cf4e80ed1e1106eceb3e873aecf7b8e0022dfe39aa4c0c64ffc41091f09 |
||||
|
size 243458 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:12c966382b318c58578e3823ac066c597ce1e16ce7c2315b0f9d66451803a082 |
||||
|
size 1245 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:ca18bd41b7d6db902e86c7a1be32ceb0989aaec0bf9fa94ca599887970b83e63 |
||||
|
size 598510 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:f6c7a229a652bfcaba998e713e169072475bea9bba35374be9219eb19c6ab42b |
||||
|
size 562295 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:593549012cf9573c457c4de9161c347f1ae81d80c057ea70b89fbb197bdd028f |
||||
|
size 16953 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:4ad1df5a4549a4860e00fbb53328208d4458e1961ae2fac290278c612432d1e7 |
||||
|
size 12299 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:ed62e82f1fed2bf16569298a61f792706a1b61e99026acefcbf8aeb0da6f6e08 |
||||
|
size 16075 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:ed7705c6ccb440f6bff77b0b9ac8275576d3f1c1fa4ecaa83ff80a72359e6f2f |
||||
|
size 1376202 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:6bd5d14cbbead348404511801d7a2bacab19174e9f4063b5d2cec96f28fd578e |
||||
|
size 300170 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:b05a622813eff15ce64f33ab76eee3f9d144f5cf24386e13ddf17c27f6310a01 |
||||
|
size 519653 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:3cae50b533fbc796171a0763c29a576eaac475d04b6a95fe46b02d440f609e11 |
||||
|
size 2114 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:e330cc81299a2641386f32bdf4b7070b8d5f8f2f76d899ced389b5a1469e65b0 |
||||
|
size 652383 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:d225e987dc07262be2acee5dee164b5f48d3a49dd0e03f426b3111b52f265548 |
||||
|
size 675251 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:e6519746939c2b6bc6776a65ce87b1dbd769069c2d2c11295453e9f35160ba57 |
||||
|
size 16488 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:de309646439d2e49c51d9921eb1faff9af4cb33f0019a24ccb57dce1ef00dbab |
||||
|
size 21857 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:b284ed810a892bca34e89a956b7f8bf21afae4826197a8f3eaef90e470e2149e |
||||
|
size 24167 |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:a289c12cd96cc3ff65fcafa1a6d55c5cace0095a45bc570ca1a4d8b79a20b4df |
||||
|
size 1521134 |
||||
Loading…
Reference in new issue